@better-auth/infra 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +279 -450
- package/dist/index.mjs +336 -324
- package/package.json +11 -11
package/dist/index.mjs
CHANGED
|
@@ -815,7 +815,10 @@ const getOrganizationTriggerInfo = (user) => {
|
|
|
815
815
|
const initTrackEvents = (options) => {
|
|
816
816
|
const $fetch = createFetch({
|
|
817
817
|
baseURL: options.apiUrl,
|
|
818
|
-
headers: {
|
|
818
|
+
headers: {
|
|
819
|
+
"user-agent": "better-auth",
|
|
820
|
+
"x-api-key": options.apiKey
|
|
821
|
+
}
|
|
819
822
|
});
|
|
820
823
|
const trackEvent = (data) => {
|
|
821
824
|
const track = async () => {
|
|
@@ -1523,6 +1526,7 @@ const paths = [
|
|
|
1523
1526
|
"/email-otp/verify-email",
|
|
1524
1527
|
"/sign-in/email-otp",
|
|
1525
1528
|
"/sign-in/magic-link",
|
|
1529
|
+
"/sign-in/email",
|
|
1526
1530
|
"/forget-password/email-otp",
|
|
1527
1531
|
"/email-otp/reset-password",
|
|
1528
1532
|
"/email-otp/create-verification-otp",
|
|
@@ -1555,25 +1559,6 @@ const signIn = new Set(paths.slice(1, 12));
|
|
|
1555
1559
|
* @returns boolean
|
|
1556
1560
|
*/
|
|
1557
1561
|
const allEmail = ({ path }) => !!path && all.has(path);
|
|
1558
|
-
/**
|
|
1559
|
-
* Path is one of `[
|
|
1560
|
-
* '/email-otp/verify-email',
|
|
1561
|
-
* '/sign-in/email-otp',
|
|
1562
|
-
* '/sign-in/magic-link',
|
|
1563
|
-
* '/sign-in/email',
|
|
1564
|
-
* '/forget-password/email-otp',
|
|
1565
|
-
* '/email-otp/reset-password',
|
|
1566
|
-
* '/email-otp/create-verification-otp',
|
|
1567
|
-
* '/email-otp/get-verification-otp',
|
|
1568
|
-
* '/email-otp/send-verification-otp',
|
|
1569
|
-
* '/forget-password',
|
|
1570
|
-
* '/send-verification-email'
|
|
1571
|
-
* ]`.
|
|
1572
|
-
* @param context Request context
|
|
1573
|
-
* @param context.path Request path
|
|
1574
|
-
* @returns boolean
|
|
1575
|
-
*/
|
|
1576
|
-
const allEmailSignIn = ({ path }) => !!path && signIn.has(path);
|
|
1577
1562
|
|
|
1578
1563
|
//#endregion
|
|
1579
1564
|
//#region src/validation/email.ts
|
|
@@ -1605,9 +1590,13 @@ const PLUS_ADDRESSING_DOMAINS = new Set([
|
|
|
1605
1590
|
* - Remove dots from Gmail-like providers (they ignore dots)
|
|
1606
1591
|
* - Remove plus addressing (user+tag@domain → user@domain)
|
|
1607
1592
|
* - Normalize googlemail.com to gmail.com
|
|
1593
|
+
*
|
|
1594
|
+
* @param email - Raw email to normalize
|
|
1595
|
+
* @param context - Auth context with getPlugin (for sentinel policy). Pass undefined when context unavailable (e.g. server, hooks).
|
|
1608
1596
|
*/
|
|
1609
|
-
function normalizeEmail(email) {
|
|
1597
|
+
function normalizeEmail(email, context) {
|
|
1610
1598
|
if (!email || typeof email !== "string") return email;
|
|
1599
|
+
if ((context.getPlugin?.("sentinel"))?.options?.emailValidation?.enabled === false) return email;
|
|
1611
1600
|
const trimmed = email.trim().toLowerCase();
|
|
1612
1601
|
const atIndex = trimmed.lastIndexOf("@");
|
|
1613
1602
|
if (atIndex === -1) return trimmed;
|
|
@@ -1700,45 +1689,42 @@ function isValidEmailFormatLocal(email) {
|
|
|
1700
1689
|
if (domain.length > 253) return false;
|
|
1701
1690
|
return true;
|
|
1702
1691
|
}
|
|
1703
|
-
const getEmail = (ctx) =>
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1692
|
+
const getEmail = (ctx) => {
|
|
1693
|
+
if (ctx.path === "/change-email") return {
|
|
1694
|
+
email: ctx.body?.newEmail,
|
|
1695
|
+
container: "body",
|
|
1696
|
+
field: "newEmail"
|
|
1697
|
+
};
|
|
1698
|
+
const body = ctx.body;
|
|
1699
|
+
const query = ctx.query;
|
|
1700
|
+
return {
|
|
1701
|
+
email: body?.email ?? query?.email,
|
|
1702
|
+
container: body ? "body" : "query",
|
|
1703
|
+
field: "email"
|
|
1704
|
+
};
|
|
1705
|
+
};
|
|
1707
1706
|
/**
|
|
1708
1707
|
* Create email normalization hook (shared between all configurations)
|
|
1709
1708
|
*/
|
|
1710
1709
|
function createEmailNormalizationHook() {
|
|
1711
1710
|
return {
|
|
1712
|
-
matcher:
|
|
1711
|
+
matcher: allEmail,
|
|
1713
1712
|
handler: createAuthMiddleware(async (ctx) => {
|
|
1714
|
-
const { email, container } = getEmail(ctx);
|
|
1713
|
+
const { email, container, field } = getEmail(ctx);
|
|
1715
1714
|
if (typeof email !== "string") return;
|
|
1716
|
-
const
|
|
1717
|
-
if (
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
...ctx.query,
|
|
1730
|
-
email: user.email,
|
|
1731
|
-
normalizedEmail
|
|
1732
|
-
}
|
|
1733
|
-
} } : { context: {
|
|
1734
|
-
...ctx,
|
|
1735
|
-
body: {
|
|
1736
|
-
...ctx.body,
|
|
1737
|
-
email: user.email,
|
|
1738
|
-
normalizedEmail
|
|
1739
|
-
}
|
|
1740
|
-
} };
|
|
1741
|
-
}
|
|
1715
|
+
const normalized = normalizeEmail(email, ctx.context);
|
|
1716
|
+
if (normalized === email) return;
|
|
1717
|
+
const data = container === "query" ? {
|
|
1718
|
+
...ctx.query,
|
|
1719
|
+
[field]: normalized
|
|
1720
|
+
} : {
|
|
1721
|
+
...ctx.body,
|
|
1722
|
+
[field]: normalized
|
|
1723
|
+
};
|
|
1724
|
+
return { context: {
|
|
1725
|
+
...ctx,
|
|
1726
|
+
[container]: data
|
|
1727
|
+
} };
|
|
1742
1728
|
})
|
|
1743
1729
|
};
|
|
1744
1730
|
}
|
|
@@ -1749,7 +1735,7 @@ function createEmailValidationHook(validator, onDisposableEmail) {
|
|
|
1749
1735
|
return {
|
|
1750
1736
|
matcher: allEmail,
|
|
1751
1737
|
handler: createAuthMiddleware(async (ctx) => {
|
|
1752
|
-
const
|
|
1738
|
+
const { email } = getEmail(ctx);
|
|
1753
1739
|
if (typeof email !== "string") return;
|
|
1754
1740
|
if (!isValidEmailFormatLocal(email)) throw new APIError$1("BAD_REQUEST", { message: "Invalid email" });
|
|
1755
1741
|
if (validator) {
|
|
@@ -1758,7 +1744,7 @@ function createEmailValidationHook(validator, onDisposableEmail) {
|
|
|
1758
1744
|
if (!policy?.enabled) return;
|
|
1759
1745
|
const action = policy.action;
|
|
1760
1746
|
if (!result.valid) {
|
|
1761
|
-
if ((result.disposable || result.reason === "no_mx_records" || result.reason === "blocklist") && onDisposableEmail) {
|
|
1747
|
+
if ((result.disposable || result.reason === "no_mx_records" || result.reason === "blocklist" || result.reason === "known_invalid_email" || result.reason === "known_invalid_host" || result.reason === "reserved_tld" || result.reason === "provider_local_too_short") && onDisposableEmail) {
|
|
1762
1748
|
const ip = ctx.request?.headers?.get("x-forwarded-for")?.split(",")[0] || ctx.request?.headers?.get("cf-connecting-ip") || void 0;
|
|
1763
1749
|
onDisposableEmail({
|
|
1764
1750
|
email,
|
|
@@ -1770,7 +1756,7 @@ function createEmailValidationHook(validator, onDisposableEmail) {
|
|
|
1770
1756
|
});
|
|
1771
1757
|
}
|
|
1772
1758
|
if (action === "allow") return;
|
|
1773
|
-
throw new APIError$1("BAD_REQUEST", { message: result.reason === "no_mx_records" ? "This email domain cannot receive emails" : result.disposable || result.reason === "blocklist" ? "Disposable email addresses are not allowed" : result.reason === "
|
|
1759
|
+
throw new APIError$1("BAD_REQUEST", { message: result.reason === "no_mx_records" ? "This email domain cannot receive emails" : result.disposable || result.reason === "blocklist" ? "Disposable email addresses are not allowed" : result.reason === "known_invalid_email" || result.reason === "known_invalid_host" || result.reason === "reserved_tld" || result.reason === "provider_local_too_short" ? "This email address appears to be invalid" : "Invalid email" });
|
|
1774
1760
|
}
|
|
1775
1761
|
}
|
|
1776
1762
|
})
|
|
@@ -1809,12 +1795,13 @@ function createEmailHooks(options = {}) {
|
|
|
1809
1795
|
...defaultConfig
|
|
1810
1796
|
};
|
|
1811
1797
|
if (!emailConfig.enabled) return { before: [] };
|
|
1812
|
-
|
|
1798
|
+
const validator = useApi ? createEmailValidator({
|
|
1813
1799
|
apiUrl,
|
|
1814
1800
|
kvUrl,
|
|
1815
1801
|
apiKey,
|
|
1816
1802
|
defaultConfig: emailConfig
|
|
1817
|
-
}) : void 0
|
|
1803
|
+
}) : void 0;
|
|
1804
|
+
return { before: [createEmailNormalizationHook(), createEmailValidationHook(validator, onDisposableEmail)] };
|
|
1818
1805
|
}
|
|
1819
1806
|
/**
|
|
1820
1807
|
* Default email hooks using local validation only
|
|
@@ -2084,100 +2071,107 @@ const sentinel = (options) => {
|
|
|
2084
2071
|
return {
|
|
2085
2072
|
id: "sentinel",
|
|
2086
2073
|
init() {
|
|
2087
|
-
return { options: {
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
const
|
|
2094
|
-
if (
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
async after(user, ctx) {
|
|
2098
|
-
if (!ctx) return;
|
|
2099
|
-
const visitorId = ctx.context.visitorId;
|
|
2100
|
-
if (visitorId && opts.security?.freeTrialAbuse?.enabled) await ctx.context.runInBackgroundOrAwait(securityService.trackFreeTrialSignup(visitorId, user.id));
|
|
2101
|
-
}
|
|
2102
|
-
} },
|
|
2103
|
-
session: { create: {
|
|
2104
|
-
async before(session, ctx) {
|
|
2105
|
-
if (!ctx) return;
|
|
2106
|
-
const visitorId = ctx.context.visitorId;
|
|
2107
|
-
const identification = ctx.context.identification;
|
|
2108
|
-
if (session.userId && identification?.location && visitorId) {
|
|
2109
|
-
const travelCheck = await securityService.checkImpossibleTravel(session.userId, identification.location, visitorId);
|
|
2110
|
-
if (travelCheck?.isImpossible) {
|
|
2111
|
-
if (travelCheck.action === "block") throw new APIError("FORBIDDEN", { message: "Login blocked due to suspicious location change." });
|
|
2112
|
-
if (travelCheck.action === "challenge" && travelCheck.challenge) throwChallengeError(travelCheck.challenge, "impossible_travel", "Unusual login location detected. Please complete a security check.");
|
|
2074
|
+
return { options: {
|
|
2075
|
+
emailValidation: opts.security?.emailValidation,
|
|
2076
|
+
databaseHooks: {
|
|
2077
|
+
user: { create: {
|
|
2078
|
+
async before(user, ctx) {
|
|
2079
|
+
if (!ctx) return;
|
|
2080
|
+
const visitorId = ctx.context.visitorId;
|
|
2081
|
+
if (visitorId && opts.security?.freeTrialAbuse?.enabled) {
|
|
2082
|
+
const abuseCheck = await securityService.checkFreeTrialAbuse(visitorId);
|
|
2083
|
+
if (abuseCheck.isAbuse && abuseCheck.action === "block") throw new APIError("FORBIDDEN", { message: "Account creation is not allowed from this device." });
|
|
2113
2084
|
}
|
|
2085
|
+
if (user.email && typeof user.email === "string") return { data: {
|
|
2086
|
+
...user,
|
|
2087
|
+
email: normalizeEmail(user.email, ctx.context)
|
|
2088
|
+
} };
|
|
2089
|
+
},
|
|
2090
|
+
async after(user, ctx) {
|
|
2091
|
+
if (!ctx) return;
|
|
2092
|
+
const visitorId = ctx.context.visitorId;
|
|
2093
|
+
if (visitorId && opts.security?.freeTrialAbuse?.enabled) await ctx.context.runInBackgroundOrAwait(securityService.trackFreeTrialSignup(visitorId, user.id));
|
|
2114
2094
|
}
|
|
2115
|
-
},
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
"
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
if (staleCheck.notifyAdmin && staleOpts.adminEmail) notificationPromises.push(securityService.notifyStaleAccountAdmin(staleOpts.adminEmail, session.userId, user.email || "unknown", user.name || null, staleCheck.daysSinceLastActive || 0, identification));
|
|
2147
|
-
if (notificationPromises.length > 0) Promise.all(notificationPromises).catch((error) => {
|
|
2148
|
-
logger.error("[Sentinel] Failed to send stale account notifications:", error);
|
|
2149
|
-
});
|
|
2150
|
-
trackEvent({
|
|
2151
|
-
eventKey: session.userId,
|
|
2152
|
-
eventType: "security_stale_account",
|
|
2153
|
-
eventDisplayName: "Security: stale account reactivated",
|
|
2154
|
-
eventData: {
|
|
2155
|
-
action: staleCheck.action === "block" ? "blocked" : staleCheck.action === "challenge" ? "challenged" : "logged",
|
|
2156
|
-
reason: "stale_account_reactivation",
|
|
2157
|
-
userId: session.userId,
|
|
2158
|
-
daysSinceLastActive: staleCheck.daysSinceLastActive,
|
|
2159
|
-
staleDays: staleCheck.staleDays,
|
|
2160
|
-
lastActiveAt: staleCheck.lastActiveAt,
|
|
2161
|
-
notifyUser: staleCheck.notifyUser,
|
|
2162
|
-
notifyAdmin: staleCheck.notifyAdmin,
|
|
2163
|
-
detectionLabel: "Stale Account Reactivation",
|
|
2164
|
-
description: `Dormant account (inactive for ${staleCheck.daysSinceLastActive} days) became active`
|
|
2165
|
-
},
|
|
2166
|
-
ipAddress: identification?.ip || void 0,
|
|
2167
|
-
city: identification?.location?.city || void 0,
|
|
2168
|
-
country: identification?.location?.country?.name || void 0,
|
|
2169
|
-
countryCode: identification?.location?.country?.code || void 0
|
|
2170
|
-
});
|
|
2171
|
-
if (staleCheck.action === "block") throw new APIError("FORBIDDEN", {
|
|
2172
|
-
message: "This account has been inactive for an extended period. Please contact support to reactivate.",
|
|
2173
|
-
code: "STALE_ACCOUNT"
|
|
2095
|
+
} },
|
|
2096
|
+
session: { create: {
|
|
2097
|
+
async before(session, ctx) {
|
|
2098
|
+
if (!ctx) return;
|
|
2099
|
+
const visitorId = ctx.context.visitorId;
|
|
2100
|
+
const identification = ctx.context.identification;
|
|
2101
|
+
if (session.userId && identification?.location && visitorId) {
|
|
2102
|
+
const travelCheck = await securityService.checkImpossibleTravel(session.userId, identification.location, visitorId);
|
|
2103
|
+
if (travelCheck?.isImpossible) {
|
|
2104
|
+
if (travelCheck.action === "block") throw new APIError("FORBIDDEN", { message: "Login blocked due to suspicious location change." });
|
|
2105
|
+
if (travelCheck.action === "challenge" && travelCheck.challenge) throwChallengeError(travelCheck.challenge, "impossible_travel", "Unusual login location detected. Please complete a security check.");
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
},
|
|
2109
|
+
async after(session, ctx) {
|
|
2110
|
+
if (!ctx || !session.userId) return;
|
|
2111
|
+
const visitorId = ctx.context.visitorId;
|
|
2112
|
+
const identification = ctx.context.identification;
|
|
2113
|
+
let user = null;
|
|
2114
|
+
try {
|
|
2115
|
+
user = await ctx.context.adapter.findOne({
|
|
2116
|
+
model: "user",
|
|
2117
|
+
select: [
|
|
2118
|
+
"email",
|
|
2119
|
+
"name",
|
|
2120
|
+
"lastActiveAt"
|
|
2121
|
+
],
|
|
2122
|
+
where: [{
|
|
2123
|
+
field: "id",
|
|
2124
|
+
value: session.userId
|
|
2125
|
+
}]
|
|
2174
2126
|
});
|
|
2127
|
+
} catch (error) {
|
|
2128
|
+
logger.warn("[Sentinel] Failed to fetch user for security checks:", error);
|
|
2129
|
+
}
|
|
2130
|
+
if (visitorId) {
|
|
2131
|
+
if (await securityService.checkUnknownDevice(session.userId, visitorId) && user?.email) await ctx.context.runInBackgroundOrAwait(securityService.notifyUnknownDevice(session.userId, user.email, identification));
|
|
2132
|
+
}
|
|
2133
|
+
if (opts.security?.staleUsers?.enabled && user) {
|
|
2134
|
+
const staleCheck = await securityService.checkStaleUser(session.userId, user.lastActiveAt || null);
|
|
2135
|
+
if (staleCheck.isStale) {
|
|
2136
|
+
const staleOpts = opts.security.staleUsers;
|
|
2137
|
+
const notificationPromises = [];
|
|
2138
|
+
if (staleCheck.notifyUser && user.email) notificationPromises.push(securityService.notifyStaleAccountUser(user.email, user.name || null, staleCheck.daysSinceLastActive || 0, identification));
|
|
2139
|
+
if (staleCheck.notifyAdmin && staleOpts.adminEmail) notificationPromises.push(securityService.notifyStaleAccountAdmin(staleOpts.adminEmail, session.userId, user.email || "unknown", user.name || null, staleCheck.daysSinceLastActive || 0, identification));
|
|
2140
|
+
if (notificationPromises.length > 0) Promise.all(notificationPromises).catch((error) => {
|
|
2141
|
+
logger.error("[Sentinel] Failed to send stale account notifications:", error);
|
|
2142
|
+
});
|
|
2143
|
+
trackEvent({
|
|
2144
|
+
eventKey: session.userId,
|
|
2145
|
+
eventType: "security_stale_account",
|
|
2146
|
+
eventDisplayName: "Security: stale account reactivated",
|
|
2147
|
+
eventData: {
|
|
2148
|
+
action: staleCheck.action === "block" ? "blocked" : staleCheck.action === "challenge" ? "challenged" : "logged",
|
|
2149
|
+
reason: "stale_account_reactivation",
|
|
2150
|
+
userId: session.userId,
|
|
2151
|
+
daysSinceLastActive: staleCheck.daysSinceLastActive,
|
|
2152
|
+
staleDays: staleCheck.staleDays,
|
|
2153
|
+
lastActiveAt: staleCheck.lastActiveAt,
|
|
2154
|
+
notifyUser: staleCheck.notifyUser,
|
|
2155
|
+
notifyAdmin: staleCheck.notifyAdmin,
|
|
2156
|
+
detectionLabel: "Stale Account Reactivation",
|
|
2157
|
+
description: `Dormant account (inactive for ${staleCheck.daysSinceLastActive} days) became active`
|
|
2158
|
+
},
|
|
2159
|
+
ipAddress: identification?.ip || void 0,
|
|
2160
|
+
city: identification?.location?.city || void 0,
|
|
2161
|
+
country: identification?.location?.country?.name || void 0,
|
|
2162
|
+
countryCode: identification?.location?.country?.code || void 0
|
|
2163
|
+
});
|
|
2164
|
+
if (staleCheck.action === "block") throw new APIError("FORBIDDEN", {
|
|
2165
|
+
message: "This account has been inactive for an extended period. Please contact support to reactivate.",
|
|
2166
|
+
code: "STALE_ACCOUNT"
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2175
2169
|
}
|
|
2170
|
+
if (identification?.location) await ctx.context.runInBackgroundOrAwait(securityService.storeLastLocation(session.userId, identification.location));
|
|
2176
2171
|
}
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
} } };
|
|
2172
|
+
} }
|
|
2173
|
+
}
|
|
2174
|
+
} };
|
|
2181
2175
|
},
|
|
2182
2176
|
hooks: {
|
|
2183
2177
|
before: [
|
|
@@ -2633,7 +2627,7 @@ const initTeamEvents = (tracker) => {
|
|
|
2633
2627
|
//#region src/jwt.ts
|
|
2634
2628
|
/**
|
|
2635
2629
|
* Hash the given value
|
|
2636
|
-
* Note: Must match @infra/crypto hash()
|
|
2630
|
+
* Note: Must match @infra/utils/crypto hash()
|
|
2637
2631
|
* @param value - The value to hash
|
|
2638
2632
|
*/
|
|
2639
2633
|
async function hash(value) {
|
|
@@ -2687,13 +2681,16 @@ function isRecentlyIssued(payload) {
|
|
|
2687
2681
|
}
|
|
2688
2682
|
const jwtMiddleware = (options, schema, getJWT) => createAuthMiddleware(async (ctx) => {
|
|
2689
2683
|
const jwsFromHeader = getJWT ? await getJWT(ctx) : ctx.headers?.get("Authorization")?.split(" ")[1];
|
|
2690
|
-
if (!jwsFromHeader)
|
|
2684
|
+
if (!jwsFromHeader) {
|
|
2685
|
+
ctx.context.logger.warn("[Dash] JWT is missing from header");
|
|
2686
|
+
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2687
|
+
}
|
|
2691
2688
|
const { payload } = await jwtVerify(jwsFromHeader, await getJWKs(options.apiUrl), { maxTokenAge: "5m" }).catch((e) => {
|
|
2692
|
-
ctx.context.logger.
|
|
2689
|
+
ctx.context.logger.warn("[Dash] JWT verification failed:", e);
|
|
2693
2690
|
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2694
2691
|
});
|
|
2695
2692
|
if (!isRecentlyIssued(payload)) {
|
|
2696
|
-
|
|
2693
|
+
const { error, data } = await betterFetch("/api/auth/check-jti", {
|
|
2697
2694
|
baseURL: options.apiUrl,
|
|
2698
2695
|
method: "POST",
|
|
2699
2696
|
headers: { "x-api-key": options.apiKey },
|
|
@@ -2701,18 +2698,52 @@ const jwtMiddleware = (options, schema, getJWT) => createAuthMiddleware(async (c
|
|
|
2701
2698
|
jti: payload.jti,
|
|
2702
2699
|
expiresAt: payload.exp
|
|
2703
2700
|
}
|
|
2704
|
-
})
|
|
2701
|
+
});
|
|
2702
|
+
if (error || !data?.valid) {
|
|
2703
|
+
ctx.context.logger.warn("[Dash] JTI check failed with error", error, data?.valid);
|
|
2704
|
+
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2705
|
+
}
|
|
2705
2706
|
}
|
|
2706
2707
|
const apiKeyHash = payload.apiKeyHash;
|
|
2707
|
-
if (typeof apiKeyHash !== "string" || !options.apiKey)
|
|
2708
|
-
|
|
2708
|
+
if (typeof apiKeyHash !== "string" || !options.apiKey) {
|
|
2709
|
+
ctx.context.logger.warn("[Dash] API key hash is missing or invalid", {
|
|
2710
|
+
apiKeyHash,
|
|
2711
|
+
apiKey: options.apiKey ? "present" : "missing"
|
|
2712
|
+
});
|
|
2713
|
+
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2714
|
+
}
|
|
2715
|
+
const expectedHash = await hash(options.apiKey);
|
|
2716
|
+
if (apiKeyHash !== expectedHash) {
|
|
2717
|
+
ctx.context.logger.warn("[Dash] API key hash is invalid", apiKeyHash, expectedHash);
|
|
2718
|
+
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2719
|
+
}
|
|
2709
2720
|
if (schema) {
|
|
2710
2721
|
const parsed = schema.safeParse(payload);
|
|
2711
|
-
if (!parsed.success)
|
|
2722
|
+
if (!parsed.success) {
|
|
2723
|
+
ctx.context.logger.warn("[Dash] JWT payload is invalid", parsed.error);
|
|
2724
|
+
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2725
|
+
}
|
|
2712
2726
|
return { payload: parsed.data };
|
|
2713
2727
|
}
|
|
2714
2728
|
return { payload };
|
|
2715
2729
|
});
|
|
2730
|
+
/**
|
|
2731
|
+
* Lightweight JWT middleware for /dash/validate. Verifies JWT signature and
|
|
2732
|
+
* apiKeyHash only—no JTI check. Used during onboarding when the org doesn't
|
|
2733
|
+
* exist yet.
|
|
2734
|
+
*/
|
|
2735
|
+
const jwtValidateMiddleware = (options) => createAuthMiddleware(async (ctx) => {
|
|
2736
|
+
const jwsFromHeader = ctx.headers?.get("Authorization")?.split(" ")[1];
|
|
2737
|
+
if (!jwsFromHeader) throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2738
|
+
const { payload } = await jwtVerify(jwsFromHeader, await getJWKs(options.apiUrl), { maxTokenAge: "5m" }).catch((e) => {
|
|
2739
|
+
ctx.context.logger.error("[Dash] JWT verification failed:", e);
|
|
2740
|
+
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2741
|
+
});
|
|
2742
|
+
const apiKeyHash = payload.apiKeyHash;
|
|
2743
|
+
if (typeof apiKeyHash !== "string" || !options.apiKey) throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2744
|
+
if (apiKeyHash !== await hash(options.apiKey)) throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2745
|
+
return { payload };
|
|
2746
|
+
});
|
|
2716
2747
|
|
|
2717
2748
|
//#endregion
|
|
2718
2749
|
//#region src/routes/config.ts
|
|
@@ -2765,7 +2796,7 @@ const getConfig = (options) => {
|
|
|
2765
2796
|
schema: plugin.schema,
|
|
2766
2797
|
options: sanitizePluginOptions(plugin.id, plugin.options)
|
|
2767
2798
|
};
|
|
2768
|
-
}),
|
|
2799
|
+
}) ?? [],
|
|
2769
2800
|
organization: {
|
|
2770
2801
|
sendInvitationEmailEnabled: !!organizationPlugin?.options?.sendInvitationEmail,
|
|
2771
2802
|
additionalFields: (() => {
|
|
@@ -2894,7 +2925,8 @@ async function getScimProviderOwner(ctx, organizationId, providerId) {
|
|
|
2894
2925
|
});
|
|
2895
2926
|
return (await ctx.context.adapter.findOne({
|
|
2896
2927
|
model: "scimProvider",
|
|
2897
|
-
where
|
|
2928
|
+
where,
|
|
2929
|
+
select: ["userId"]
|
|
2898
2930
|
}))?.userId ?? null;
|
|
2899
2931
|
}
|
|
2900
2932
|
function getScimEndpoint(baseUrl) {
|
|
@@ -3367,7 +3399,8 @@ const acceptInvitation = (options) => {
|
|
|
3367
3399
|
throw new APIError("BAD_REQUEST", { message: "This invitation has expired." });
|
|
3368
3400
|
}
|
|
3369
3401
|
}
|
|
3370
|
-
const
|
|
3402
|
+
const invitationEmail = normalizeEmail(invitation.email, ctx.context);
|
|
3403
|
+
const existingUser = await ctx.context.internalAdapter.findUserByEmail(invitationEmail).then((user$1) => user$1?.user);
|
|
3371
3404
|
if (existingUser) {
|
|
3372
3405
|
await $api("/api/internal/invitations/mark-accepted", {
|
|
3373
3406
|
method: "POST",
|
|
@@ -3381,7 +3414,7 @@ const acceptInvitation = (options) => {
|
|
|
3381
3414
|
user: existingUser
|
|
3382
3415
|
});
|
|
3383
3416
|
const redirectUrl$1 = invitation.redirectUrl || ctx.context.options.baseURL || "/";
|
|
3384
|
-
return ctx.redirect(redirectUrl$1);
|
|
3417
|
+
return ctx.redirect(redirectUrl$1.toString());
|
|
3385
3418
|
}
|
|
3386
3419
|
if (invitation.authMode === "auth") {
|
|
3387
3420
|
const platformUrl = options.apiUrl || INFRA_API_URL;
|
|
@@ -3392,7 +3425,7 @@ const acceptInvitation = (options) => {
|
|
|
3392
3425
|
return ctx.redirect(acceptPageUrl.toString());
|
|
3393
3426
|
}
|
|
3394
3427
|
const user = await ctx.context.internalAdapter.createUser({
|
|
3395
|
-
email:
|
|
3428
|
+
email: invitationEmail,
|
|
3396
3429
|
name: invitation.name || invitation.email.split("@")[0] || "",
|
|
3397
3430
|
emailVerified: true,
|
|
3398
3431
|
createdAt: /* @__PURE__ */ new Date(),
|
|
@@ -3410,7 +3443,7 @@ const acceptInvitation = (options) => {
|
|
|
3410
3443
|
user
|
|
3411
3444
|
});
|
|
3412
3445
|
const redirectUrl = invitation.redirectUrl || ctx.context.options.baseURL || "/";
|
|
3413
|
-
return ctx.redirect(redirectUrl);
|
|
3446
|
+
return ctx.redirect(redirectUrl.toString());
|
|
3414
3447
|
});
|
|
3415
3448
|
};
|
|
3416
3449
|
/**
|
|
@@ -3442,7 +3475,8 @@ const completeInvitation = (options) => {
|
|
|
3442
3475
|
if (error || !invitation) throw new APIError("BAD_REQUEST", { message: "Invalid or expired invitation." });
|
|
3443
3476
|
if (invitation.status !== "pending") throw new APIError("BAD_REQUEST", { message: `This invitation has already been ${invitation.status}.` });
|
|
3444
3477
|
if (!ctx.context) throw new APIError("BAD_REQUEST", { message: "Context is required" });
|
|
3445
|
-
const
|
|
3478
|
+
const invitationEmail = normalizeEmail(invitation.email, ctx.context);
|
|
3479
|
+
const existingUser = await ctx.context.internalAdapter.findUserByEmail(invitationEmail).then((user$1) => user$1?.user);
|
|
3446
3480
|
if (existingUser) {
|
|
3447
3481
|
await $api("/api/internal/invitations/mark-accepted", {
|
|
3448
3482
|
method: "POST",
|
|
@@ -3461,7 +3495,7 @@ const completeInvitation = (options) => {
|
|
|
3461
3495
|
};
|
|
3462
3496
|
}
|
|
3463
3497
|
const user = await ctx.context.internalAdapter.createUser({
|
|
3464
|
-
email:
|
|
3498
|
+
email: invitationEmail,
|
|
3465
3499
|
name: invitation.name || invitation.email.split("@")[0] || "",
|
|
3466
3500
|
emailVerified: true,
|
|
3467
3501
|
createdAt: /* @__PURE__ */ new Date(),
|
|
@@ -3509,7 +3543,8 @@ const checkUserExists = (_options) => {
|
|
|
3509
3543
|
}, async (ctx) => {
|
|
3510
3544
|
const { email } = ctx.body;
|
|
3511
3545
|
if (!ctx.request?.headers.get("Authorization")) throw new APIError("UNAUTHORIZED", { message: "Authorization required" });
|
|
3512
|
-
const
|
|
3546
|
+
const normalizedEmail = normalizeEmail(email, ctx.context);
|
|
3547
|
+
const existingUser = await ctx.context.internalAdapter.findUserByEmail(normalizedEmail).then((user) => user?.user);
|
|
3513
3548
|
return {
|
|
3514
3549
|
exists: !!existingUser,
|
|
3515
3550
|
userId: existingUser?.id || null
|
|
@@ -3719,7 +3754,7 @@ const deleteOrganizationLogDrain = (options) => {
|
|
|
3719
3754
|
body: z$1.object({ logDrainId: z$1.string() })
|
|
3720
3755
|
}, async (ctx) => {
|
|
3721
3756
|
const { organizationId } = ctx.context.payload;
|
|
3722
|
-
if (
|
|
3757
|
+
if (await ctx.context.adapter.count({
|
|
3723
3758
|
model: "orgLogDrain",
|
|
3724
3759
|
where: [{
|
|
3725
3760
|
field: "id",
|
|
@@ -3728,7 +3763,7 @@ const deleteOrganizationLogDrain = (options) => {
|
|
|
3728
3763
|
field: "organizationId",
|
|
3729
3764
|
value: organizationId
|
|
3730
3765
|
}]
|
|
3731
|
-
})) throw ctx.error("NOT_FOUND", { message: "Log drain not found" });
|
|
3766
|
+
}) === 0) throw ctx.error("NOT_FOUND", { message: "Log drain not found" });
|
|
3732
3767
|
await ctx.context.adapter.delete({
|
|
3733
3768
|
model: "orgLogDrain",
|
|
3734
3769
|
where: [{
|
|
@@ -3917,6 +3952,15 @@ const exportFactory = (input, options) => async (ctx) => {
|
|
|
3917
3952
|
|
|
3918
3953
|
//#endregion
|
|
3919
3954
|
//#region src/helper.ts
|
|
3955
|
+
/**
|
|
3956
|
+
* Checks whether a plugin is registered by its ID.
|
|
3957
|
+
* Prefers the native `hasPlugin` when available, otherwise
|
|
3958
|
+
* falls back to scanning `options.plugins`.
|
|
3959
|
+
*/
|
|
3960
|
+
function hasPlugin(context, pluginId) {
|
|
3961
|
+
if (typeof context.hasPlugin === "function") return context.hasPlugin(pluginId);
|
|
3962
|
+
return context.options.plugins?.some((p) => p.id === pluginId) ?? false;
|
|
3963
|
+
}
|
|
3920
3964
|
function* chunkArray(arr, options) {
|
|
3921
3965
|
const batchSize = options?.batchSize || 200;
|
|
3922
3966
|
for (let i = 0; i < arr.length; i += batchSize) yield arr.slice(i, i + batchSize);
|
|
@@ -4129,32 +4173,26 @@ const listOrganizations = (options) => {
|
|
|
4129
4173
|
sortBy: {
|
|
4130
4174
|
field: dbSortBy,
|
|
4131
4175
|
direction: sortOrder
|
|
4132
|
-
}
|
|
4176
|
+
},
|
|
4177
|
+
join: { member: { limit: 5 } }
|
|
4133
4178
|
}), needsInMemoryProcessing ? Promise.resolve(0) : ctx.context.adapter.count({
|
|
4134
4179
|
model: "organization",
|
|
4135
4180
|
where
|
|
4136
4181
|
})]);
|
|
4137
4182
|
const orgIds = organizations.map((o) => o.id);
|
|
4138
|
-
const
|
|
4183
|
+
const memberCounts = await withConcurrency(orgIds, (orgId) => ctx.context.adapter.count({
|
|
4139
4184
|
model: "member",
|
|
4140
4185
|
where: [{
|
|
4141
4186
|
field: "organizationId",
|
|
4142
|
-
value:
|
|
4143
|
-
operator: "in"
|
|
4187
|
+
value: orgId
|
|
4144
4188
|
}]
|
|
4145
|
-
}) :
|
|
4146
|
-
const
|
|
4147
|
-
for (const m of allMembers) {
|
|
4148
|
-
const list = membersByOrg.get(m.organizationId) || [];
|
|
4149
|
-
list.push(m);
|
|
4150
|
-
membersByOrg.set(m.organizationId, list);
|
|
4151
|
-
}
|
|
4189
|
+
}), { concurrency: 10 });
|
|
4190
|
+
const memberCountByOrg = new Map(orgIds.map((orgId, i) => [orgId, memberCounts[i]]));
|
|
4152
4191
|
let withCounts = organizations.map((organization) => {
|
|
4153
|
-
const
|
|
4192
|
+
const memberCount = memberCountByOrg.get(organization.id) ?? 0;
|
|
4154
4193
|
return {
|
|
4155
4194
|
...organization,
|
|
4156
|
-
|
|
4157
|
-
memberCount: orgMembers.length
|
|
4195
|
+
memberCount
|
|
4158
4196
|
};
|
|
4159
4197
|
});
|
|
4160
4198
|
if (filterMembers) {
|
|
@@ -4174,25 +4212,26 @@ const listOrganizations = (options) => {
|
|
|
4174
4212
|
const total = needsInMemoryProcessing ? withCounts.length : initialTotal;
|
|
4175
4213
|
if (needsInMemoryProcessing) withCounts = withCounts.slice(offset, offset + limit);
|
|
4176
4214
|
const allUserIds = /* @__PURE__ */ new Set();
|
|
4177
|
-
for (const organization of withCounts) for (const member of organization.
|
|
4215
|
+
for (const organization of withCounts) for (const member of organization.member) allUserIds.add(member.userId);
|
|
4178
4216
|
const users = allUserIds.size > 0 ? await ctx.context.adapter.findMany({
|
|
4179
4217
|
model: "user",
|
|
4180
4218
|
where: [{
|
|
4181
4219
|
field: "id",
|
|
4182
4220
|
value: Array.from(allUserIds),
|
|
4183
4221
|
operator: "in"
|
|
4184
|
-
}]
|
|
4222
|
+
}],
|
|
4223
|
+
limit: allUserIds.size
|
|
4185
4224
|
}) : [];
|
|
4186
4225
|
const userMap = new Map(users.map((u) => [u.id, u]));
|
|
4187
4226
|
return {
|
|
4188
4227
|
organizations: withCounts.map((organization) => {
|
|
4189
|
-
const members = organization.
|
|
4228
|
+
const members = organization.member.map((m) => userMap.get(m.userId)).filter((u) => u !== void 0).map((u) => ({
|
|
4190
4229
|
id: u.id,
|
|
4191
4230
|
name: u.name,
|
|
4192
4231
|
email: u.email,
|
|
4193
4232
|
image: u.image
|
|
4194
4233
|
}));
|
|
4195
|
-
const { _members, ...org } = organization;
|
|
4234
|
+
const { member: _members, ...org } = organization;
|
|
4196
4235
|
return {
|
|
4197
4236
|
...org,
|
|
4198
4237
|
members
|
|
@@ -4284,17 +4323,9 @@ const listOrganizationMembers = (options) => {
|
|
|
4284
4323
|
where: [{
|
|
4285
4324
|
field: "organizationId",
|
|
4286
4325
|
value: ctx.params.id
|
|
4287
|
-
}]
|
|
4326
|
+
}],
|
|
4327
|
+
join: { user: true }
|
|
4288
4328
|
});
|
|
4289
|
-
const userIds = members.map((m) => m.userId);
|
|
4290
|
-
const users = userIds.length ? await ctx.context.adapter.findMany({
|
|
4291
|
-
model: "user",
|
|
4292
|
-
where: [{
|
|
4293
|
-
field: "id",
|
|
4294
|
-
value: userIds,
|
|
4295
|
-
operator: "in"
|
|
4296
|
-
}]
|
|
4297
|
-
}) : [];
|
|
4298
4329
|
const invitations = await ctx.context.adapter.findMany({
|
|
4299
4330
|
model: "invitation",
|
|
4300
4331
|
where: [{
|
|
@@ -4303,22 +4334,17 @@ const listOrganizationMembers = (options) => {
|
|
|
4303
4334
|
}, {
|
|
4304
4335
|
field: "status",
|
|
4305
4336
|
value: "accepted"
|
|
4306
|
-
}]
|
|
4337
|
+
}],
|
|
4338
|
+
join: { user: true }
|
|
4307
4339
|
});
|
|
4308
|
-
const
|
|
4309
|
-
const
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
value: inviterIds,
|
|
4314
|
-
operator: "in"
|
|
4315
|
-
}]
|
|
4316
|
-
}) : [];
|
|
4317
|
-
const inviterById = new Map(inviters.map((u) => [u.id, u]));
|
|
4318
|
-
const userById = new Map(users.map((u) => [u.id, u]));
|
|
4340
|
+
const inviterById = /* @__PURE__ */ new Map();
|
|
4341
|
+
for (const inv of invitations) {
|
|
4342
|
+
const inviter = Array.isArray(inv.user) ? inv.user[0] : inv.user;
|
|
4343
|
+
if (inviter && inv.inviterId) inviterById.set(inv.inviterId, inviter);
|
|
4344
|
+
}
|
|
4319
4345
|
const invitationByEmail = new Map(invitations.map((i) => [i.email.toLowerCase(), i]));
|
|
4320
4346
|
return members.map((m) => {
|
|
4321
|
-
const user =
|
|
4347
|
+
const user = (Array.isArray(m.user) ? m.user[0] : m.user) ?? null;
|
|
4322
4348
|
const invitation = user ? invitationByEmail.get(user.email.toLowerCase()) : null;
|
|
4323
4349
|
const inviter = invitation ? inviterById.get(invitation.inviterId) : null;
|
|
4324
4350
|
return {
|
|
@@ -4351,7 +4377,7 @@ const listOrganizationInvitations = (options) => {
|
|
|
4351
4377
|
value: ctx.params.id
|
|
4352
4378
|
}]
|
|
4353
4379
|
});
|
|
4354
|
-
const emails = [...new Set(invitations.map((i) => i.email.
|
|
4380
|
+
const emails = [...new Set(invitations.map((i) => normalizeEmail(i.email, ctx.context)))];
|
|
4355
4381
|
const users = emails.length ? await ctx.context.adapter.findMany({
|
|
4356
4382
|
model: "user",
|
|
4357
4383
|
where: [{
|
|
@@ -4360,9 +4386,10 @@ const listOrganizationInvitations = (options) => {
|
|
|
4360
4386
|
operator: "in"
|
|
4361
4387
|
}]
|
|
4362
4388
|
}) : [];
|
|
4363
|
-
const userByEmail = new Map(users.map((u) => [u.email.
|
|
4389
|
+
const userByEmail = new Map(users.map((u) => [normalizeEmail(u.email, ctx.context), u]));
|
|
4364
4390
|
return invitations.map((invitation) => {
|
|
4365
|
-
const
|
|
4391
|
+
const invitationEmail = normalizeEmail(invitation.email, ctx.context);
|
|
4392
|
+
const user = userByEmail.get(invitationEmail);
|
|
4366
4393
|
return {
|
|
4367
4394
|
...invitation,
|
|
4368
4395
|
user: user ? {
|
|
@@ -4398,17 +4425,12 @@ const deleteOrganization = (options) => {
|
|
|
4398
4425
|
field: "createdAt",
|
|
4399
4426
|
direction: "asc"
|
|
4400
4427
|
},
|
|
4401
|
-
limit: 1
|
|
4428
|
+
limit: 1,
|
|
4429
|
+
join: { user: true }
|
|
4402
4430
|
});
|
|
4403
4431
|
if (owners.length === 0) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
|
|
4404
4432
|
const owner = owners[0];
|
|
4405
|
-
const deletedByUser =
|
|
4406
|
-
model: "user",
|
|
4407
|
-
where: [{
|
|
4408
|
-
field: "id",
|
|
4409
|
-
value: owner.userId
|
|
4410
|
-
}]
|
|
4411
|
-
});
|
|
4433
|
+
const deletedByUser = Array.isArray(owner.user) ? owner.user[0] : owner.user;
|
|
4412
4434
|
if (!deletedByUser) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
|
|
4413
4435
|
const organization = await ctx.context.adapter.findOne({
|
|
4414
4436
|
model: "organization",
|
|
@@ -4555,17 +4577,12 @@ const updateTeam = (options) => {
|
|
|
4555
4577
|
field: "createdAt",
|
|
4556
4578
|
direction: "asc"
|
|
4557
4579
|
},
|
|
4558
|
-
limit: 1
|
|
4580
|
+
limit: 1,
|
|
4581
|
+
join: { user: true }
|
|
4559
4582
|
});
|
|
4560
4583
|
if (owners.length === 0) throw ctx.error("NOT_FOUND", { message: "Owner not found" });
|
|
4561
4584
|
const owner = owners[0];
|
|
4562
|
-
const user =
|
|
4563
|
-
model: "user",
|
|
4564
|
-
where: [{
|
|
4565
|
-
field: "id",
|
|
4566
|
-
value: owner.userId
|
|
4567
|
-
}]
|
|
4568
|
-
});
|
|
4585
|
+
const user = Array.isArray(owner.user) ? owner.user[0] : owner.user;
|
|
4569
4586
|
if (!user) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
|
|
4570
4587
|
let updateData = { updatedAt: /* @__PURE__ */ new Date() };
|
|
4571
4588
|
if (ctx.body.name) updateData.name = ctx.body.name;
|
|
@@ -4649,17 +4666,12 @@ const deleteTeam = (options) => {
|
|
|
4649
4666
|
field: "createdAt",
|
|
4650
4667
|
direction: "asc"
|
|
4651
4668
|
},
|
|
4652
|
-
limit: 1
|
|
4669
|
+
limit: 1,
|
|
4670
|
+
join: { user: true }
|
|
4653
4671
|
});
|
|
4654
4672
|
if (owners.length === 0) throw ctx.error("NOT_FOUND", { message: "Owner not found" });
|
|
4655
4673
|
const owner = owners[0];
|
|
4656
|
-
const user =
|
|
4657
|
-
model: "user",
|
|
4658
|
-
where: [{
|
|
4659
|
-
field: "id",
|
|
4660
|
-
value: owner.userId
|
|
4661
|
-
}]
|
|
4662
|
-
});
|
|
4674
|
+
const user = Array.isArray(owner.user) ? owner.user[0] : owner.user;
|
|
4663
4675
|
if (!user) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
|
|
4664
4676
|
if (orgOptions?.organizationHooks?.beforeDeleteTeam) await orgOptions.organizationHooks.beforeDeleteTeam({
|
|
4665
4677
|
team,
|
|
@@ -4727,17 +4739,12 @@ const createTeam = (options) => {
|
|
|
4727
4739
|
field: "createdAt",
|
|
4728
4740
|
direction: "asc"
|
|
4729
4741
|
},
|
|
4730
|
-
limit: 1
|
|
4742
|
+
limit: 1,
|
|
4743
|
+
join: { user: true }
|
|
4731
4744
|
});
|
|
4732
4745
|
if (owners.length === 0) throw ctx.error("NOT_FOUND", { message: "Owner not found" });
|
|
4733
4746
|
const owner = owners[0];
|
|
4734
|
-
const user =
|
|
4735
|
-
model: "user",
|
|
4736
|
-
where: [{
|
|
4737
|
-
field: "id",
|
|
4738
|
-
value: owner.userId
|
|
4739
|
-
}]
|
|
4740
|
-
});
|
|
4747
|
+
const user = Array.isArray(owner.user) ? owner.user[0] : owner.user;
|
|
4741
4748
|
if (!user) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
|
|
4742
4749
|
let teamData = {
|
|
4743
4750
|
name: ctx.body.name,
|
|
@@ -4773,7 +4780,7 @@ const listTeamMembers = (options) => {
|
|
|
4773
4780
|
use: [jwtMiddleware(options)]
|
|
4774
4781
|
}, async (ctx) => {
|
|
4775
4782
|
try {
|
|
4776
|
-
if (
|
|
4783
|
+
if (await ctx.context.adapter.count({
|
|
4777
4784
|
model: "team",
|
|
4778
4785
|
where: [{
|
|
4779
4786
|
field: "id",
|
|
@@ -4782,22 +4789,16 @@ const listTeamMembers = (options) => {
|
|
|
4782
4789
|
field: "organizationId",
|
|
4783
4790
|
value: ctx.params.orgId
|
|
4784
4791
|
}]
|
|
4785
|
-
})) throw ctx.error("NOT_FOUND", { message: "Team not found" });
|
|
4786
|
-
|
|
4792
|
+
}) === 0) throw ctx.error("NOT_FOUND", { message: "Team not found" });
|
|
4793
|
+
return (await ctx.context.adapter.findMany({
|
|
4787
4794
|
model: "teamMember",
|
|
4788
4795
|
where: [{
|
|
4789
4796
|
field: "teamId",
|
|
4790
4797
|
value: ctx.params.teamId
|
|
4791
|
-
}]
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
const user =
|
|
4795
|
-
model: "user",
|
|
4796
|
-
where: [{
|
|
4797
|
-
field: "id",
|
|
4798
|
-
value: tm.userId
|
|
4799
|
-
}]
|
|
4800
|
-
});
|
|
4798
|
+
}],
|
|
4799
|
+
join: { user: true }
|
|
4800
|
+
})).map((tm) => {
|
|
4801
|
+
const user = Array.isArray(tm.user) ? tm.user[0] : tm.user;
|
|
4801
4802
|
return {
|
|
4802
4803
|
...tm,
|
|
4803
4804
|
user: user ? {
|
|
@@ -4807,7 +4808,7 @@ const listTeamMembers = (options) => {
|
|
|
4807
4808
|
image: user.image
|
|
4808
4809
|
} : null
|
|
4809
4810
|
};
|
|
4810
|
-
})
|
|
4811
|
+
});
|
|
4811
4812
|
} catch (e) {
|
|
4812
4813
|
ctx.context.logger.warn("[Dash] Failed to list team members:", e);
|
|
4813
4814
|
return [];
|
|
@@ -4865,7 +4866,7 @@ const addTeamMember = (options) => {
|
|
|
4865
4866
|
value: organizationId
|
|
4866
4867
|
}]
|
|
4867
4868
|
})) throw ctx.error("BAD_REQUEST", { message: "User is not a member of this organization" });
|
|
4868
|
-
if (await ctx.context.adapter.
|
|
4869
|
+
if (await ctx.context.adapter.count({
|
|
4869
4870
|
model: "teamMember",
|
|
4870
4871
|
where: [{
|
|
4871
4872
|
field: "teamId",
|
|
@@ -4874,7 +4875,7 @@ const addTeamMember = (options) => {
|
|
|
4874
4875
|
field: "userId",
|
|
4875
4876
|
value: ctx.body.userId
|
|
4876
4877
|
}]
|
|
4877
|
-
})) throw ctx.error("BAD_REQUEST", { message: "User is already a member of this team" });
|
|
4878
|
+
}) > 0) throw ctx.error("BAD_REQUEST", { message: "User is already a member of this team" });
|
|
4878
4879
|
if (orgOptions?.teams?.maximumMembersPerTeam) {
|
|
4879
4880
|
const teamMemberCount = await ctx.context.adapter.count({
|
|
4880
4881
|
model: "teamMember",
|
|
@@ -5023,13 +5024,13 @@ const createOrganization = (options) => {
|
|
|
5023
5024
|
});
|
|
5024
5025
|
if (!user) throw ctx.error("BAD_REQUEST", { message: "User not found" });
|
|
5025
5026
|
const orgOptions = organizationPlugin.options || {};
|
|
5026
|
-
if (await ctx.context.adapter.
|
|
5027
|
+
if (await ctx.context.adapter.count({
|
|
5027
5028
|
model: "organization",
|
|
5028
5029
|
where: [{
|
|
5029
5030
|
field: "slug",
|
|
5030
5031
|
value: ctx.body.slug
|
|
5031
5032
|
}]
|
|
5032
|
-
})) throw ctx.error("BAD_REQUEST", { message: "Organization already exists" });
|
|
5033
|
+
}) > 0) throw ctx.error("BAD_REQUEST", { message: "Organization already exists" });
|
|
5033
5034
|
let orgData = {
|
|
5034
5035
|
...ctx.body,
|
|
5035
5036
|
defaultTeamName: void 0
|
|
@@ -5178,14 +5179,15 @@ const updateOrganization = (options) => {
|
|
|
5178
5179
|
const { organizationId } = ctx.context.payload;
|
|
5179
5180
|
const orgOptions = ctx.context.getPlugin("organization")?.options || {};
|
|
5180
5181
|
if (ctx.body.slug) {
|
|
5181
|
-
const
|
|
5182
|
+
const orgWithSlug = await ctx.context.adapter.findOne({
|
|
5182
5183
|
model: "organization",
|
|
5183
5184
|
where: [{
|
|
5184
5185
|
field: "slug",
|
|
5185
5186
|
value: ctx.body.slug
|
|
5186
|
-
}]
|
|
5187
|
+
}],
|
|
5188
|
+
select: ["id"]
|
|
5187
5189
|
});
|
|
5188
|
-
if (
|
|
5190
|
+
if (orgWithSlug && orgWithSlug.id !== organizationId) throw ctx.error("BAD_REQUEST", { message: "Slug already exists" });
|
|
5189
5191
|
}
|
|
5190
5192
|
const owners = await ctx.context.adapter.findMany({
|
|
5191
5193
|
model: "member",
|
|
@@ -5200,17 +5202,12 @@ const updateOrganization = (options) => {
|
|
|
5200
5202
|
field: "createdAt",
|
|
5201
5203
|
direction: "asc"
|
|
5202
5204
|
},
|
|
5203
|
-
limit: 1
|
|
5205
|
+
limit: 1,
|
|
5206
|
+
join: { user: true }
|
|
5204
5207
|
});
|
|
5205
5208
|
if (owners.length === 0) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
|
|
5206
5209
|
const owner = owners[0];
|
|
5207
|
-
const updatedByUser =
|
|
5208
|
-
model: "user",
|
|
5209
|
-
where: [{
|
|
5210
|
-
field: "id",
|
|
5211
|
-
value: owner.userId
|
|
5212
|
-
}]
|
|
5213
|
-
});
|
|
5210
|
+
const updatedByUser = Array.isArray(owner.user) ? owner.user[0] : owner.user;
|
|
5214
5211
|
if (!updatedByUser) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
|
|
5215
5212
|
let updateData = { ...ctx.body };
|
|
5216
5213
|
if (typeof updateData.metadata === "string") try {
|
|
@@ -5472,10 +5469,11 @@ const inviteMember = (options) => {
|
|
|
5472
5469
|
}]
|
|
5473
5470
|
});
|
|
5474
5471
|
if (!invitedBy) throw ctx.error("BAD_REQUEST", { message: "Invited by user not found" });
|
|
5472
|
+
const invitationEmail = normalizeEmail(ctx.body.email, ctx.context);
|
|
5475
5473
|
return await organizationPlugin.endpoints.createInvitation({
|
|
5476
5474
|
headers: ctx.request?.headers ?? new Headers(),
|
|
5477
5475
|
body: {
|
|
5478
|
-
email:
|
|
5476
|
+
email: invitationEmail,
|
|
5479
5477
|
role: ctx.body.role,
|
|
5480
5478
|
organizationId
|
|
5481
5479
|
},
|
|
@@ -5497,11 +5495,12 @@ const checkUserByEmail = (options) => {
|
|
|
5497
5495
|
use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))]
|
|
5498
5496
|
}, async (ctx) => {
|
|
5499
5497
|
const { organizationId } = ctx.context.payload;
|
|
5498
|
+
const email = normalizeEmail(ctx.body.email, ctx.context);
|
|
5500
5499
|
const user = await ctx.context.adapter.findOne({
|
|
5501
5500
|
model: "user",
|
|
5502
5501
|
where: [{
|
|
5503
5502
|
field: "email",
|
|
5504
|
-
value:
|
|
5503
|
+
value: email
|
|
5505
5504
|
}]
|
|
5506
5505
|
});
|
|
5507
5506
|
if (!user) return {
|
|
@@ -6095,10 +6094,11 @@ const resendInvitation = (options) => {
|
|
|
6095
6094
|
});
|
|
6096
6095
|
if (!invitedByUser) throw ctx.error("BAD_REQUEST", { message: "Inviter user not found" });
|
|
6097
6096
|
if (!organizationPlugin.endpoints?.createInvitation) throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Organization plugin endpoints not available" });
|
|
6097
|
+
const invitationEmail = normalizeEmail(invitation.email, ctx.context);
|
|
6098
6098
|
await organizationPlugin.endpoints.createInvitation({
|
|
6099
6099
|
headers: ctx.request?.headers ?? new Headers(),
|
|
6100
6100
|
body: {
|
|
6101
|
-
email:
|
|
6101
|
+
email: invitationEmail,
|
|
6102
6102
|
role: invitation.role,
|
|
6103
6103
|
organizationId,
|
|
6104
6104
|
resend: true
|
|
@@ -6137,28 +6137,31 @@ const listAllSessions = (options) => {
|
|
|
6137
6137
|
}).optional()
|
|
6138
6138
|
}, async (ctx) => {
|
|
6139
6139
|
const sessionsCount = await ctx.context.adapter.count({ model: "session" });
|
|
6140
|
+
const limit = ctx.query?.limit || sessionsCount;
|
|
6141
|
+
const offset = ctx.query?.offset || 0;
|
|
6140
6142
|
const sessions = await ctx.context.adapter.findMany({
|
|
6141
6143
|
model: "session",
|
|
6142
|
-
limit
|
|
6143
|
-
offset
|
|
6144
|
+
limit,
|
|
6145
|
+
offset,
|
|
6144
6146
|
sortBy: {
|
|
6145
6147
|
field: "createdAt",
|
|
6146
6148
|
direction: "desc"
|
|
6147
|
-
}
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
}
|
|
6149
|
+
},
|
|
6150
|
+
join: { user: true }
|
|
6151
|
+
});
|
|
6152
|
+
const userMap = /* @__PURE__ */ new Map();
|
|
6153
|
+
for (const s of sessions) {
|
|
6154
|
+
const user = Array.isArray(s.user) ? s.user[0] : s.user;
|
|
6155
|
+
if (!user) continue;
|
|
6156
|
+
const { user: _u, ...sessionData } = s;
|
|
6157
|
+
const session = sessionData;
|
|
6158
|
+
if (!userMap.has(user.id)) userMap.set(user.id, {
|
|
6159
|
+
...user,
|
|
6160
|
+
sessions: []
|
|
6161
|
+
});
|
|
6162
|
+
userMap.get(user.id).sessions.push(session);
|
|
6163
|
+
}
|
|
6164
|
+
return Array.from(userMap.values());
|
|
6162
6165
|
});
|
|
6163
6166
|
};
|
|
6164
6167
|
const revokeSession = (options) => createAuthEndpoint("/dash/sessions/revoke", {
|
|
@@ -6267,7 +6270,7 @@ const getUsers = (options) => {
|
|
|
6267
6270
|
totalQuery,
|
|
6268
6271
|
onlineUsersQuery
|
|
6269
6272
|
]);
|
|
6270
|
-
const hasAdminPlugin = ctx.context
|
|
6273
|
+
const hasAdminPlugin = hasPlugin(ctx.context, "admin");
|
|
6271
6274
|
return {
|
|
6272
6275
|
users: users.map((user) => {
|
|
6273
6276
|
const u = user;
|
|
@@ -6292,7 +6295,7 @@ const exportUsers = (options) => {
|
|
|
6292
6295
|
use: [jwtMiddleware(options)],
|
|
6293
6296
|
query: getUsersQuerySchema
|
|
6294
6297
|
}, async (ctx) => {
|
|
6295
|
-
const hasAdminPlugin = ctx.context
|
|
6298
|
+
const hasAdminPlugin = hasPlugin(ctx.context, "admin");
|
|
6296
6299
|
return exportFactory({
|
|
6297
6300
|
model: "user",
|
|
6298
6301
|
limit: ctx.query?.limit,
|
|
@@ -6428,7 +6431,8 @@ const createUser = (options) => {
|
|
|
6428
6431
|
}).passthrough()
|
|
6429
6432
|
}, async (ctx) => {
|
|
6430
6433
|
const userData = ctx.body;
|
|
6431
|
-
|
|
6434
|
+
const email = normalizeEmail(userData.email, ctx.context);
|
|
6435
|
+
if (await ctx.context.internalAdapter.findUserByEmail(email)) throw new APIError("BAD_REQUEST", { message: "User with this email already exist" });
|
|
6432
6436
|
let password = null;
|
|
6433
6437
|
if (userData.generatePassword && !userData.password) password = generateId(12);
|
|
6434
6438
|
else if (userData.password && userData.password.trim() !== "") password = userData.password;
|
|
@@ -6449,6 +6453,7 @@ const createUser = (options) => {
|
|
|
6449
6453
|
}
|
|
6450
6454
|
const user = await ctx.context.internalAdapter.createUser({
|
|
6451
6455
|
...userData,
|
|
6456
|
+
email,
|
|
6452
6457
|
emailVerified: userData.emailVerified,
|
|
6453
6458
|
createdAt: /* @__PURE__ */ new Date(),
|
|
6454
6459
|
updatedAt: /* @__PURE__ */ new Date()
|
|
@@ -6562,7 +6567,7 @@ const getUserDetails = (options) => {
|
|
|
6562
6567
|
}, async (ctx) => {
|
|
6563
6568
|
const { userId } = ctx.context.payload;
|
|
6564
6569
|
const minimal = !!ctx.query?.minimal;
|
|
6565
|
-
const hasAdminPlugin = ctx.context
|
|
6570
|
+
const hasAdminPlugin = hasPlugin(ctx.context, "admin");
|
|
6566
6571
|
const user = await ctx.context.adapter.findOne({
|
|
6567
6572
|
model: "user",
|
|
6568
6573
|
where: [{
|
|
@@ -6632,47 +6637,39 @@ const getUserOrganizations = (options) => {
|
|
|
6632
6637
|
const isOrgEnabled = ctx.context.getPlugin("organization");
|
|
6633
6638
|
if (!isOrgEnabled) return { organizations: [] };
|
|
6634
6639
|
const isTeamEnabled = isOrgEnabled.options?.teams?.enabled;
|
|
6635
|
-
const [
|
|
6640
|
+
const [membersWithOrg, teamMembersWithTeam] = await Promise.all([ctx.context.adapter.findMany({
|
|
6636
6641
|
model: "member",
|
|
6637
6642
|
where: [{
|
|
6638
6643
|
field: "userId",
|
|
6639
6644
|
value: userId
|
|
6640
|
-
}]
|
|
6645
|
+
}],
|
|
6646
|
+
join: { organization: true }
|
|
6641
6647
|
}), isTeamEnabled ? ctx.context.adapter.findMany({
|
|
6642
6648
|
model: "teamMember",
|
|
6643
6649
|
where: [{
|
|
6644
6650
|
field: "userId",
|
|
6645
6651
|
value: userId
|
|
6646
|
-
}]
|
|
6652
|
+
}],
|
|
6653
|
+
join: { team: true }
|
|
6647
6654
|
}).catch((e) => {
|
|
6648
6655
|
ctx.context.logger.error("[Dash] Failed to fetch team members:", e);
|
|
6649
6656
|
return [];
|
|
6650
6657
|
}) : Promise.resolve([])]);
|
|
6651
|
-
if (
|
|
6652
|
-
const
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
}) : Promise.resolve([])]);
|
|
6667
|
-
return { organizations: organizations.map((organization) => ({
|
|
6668
|
-
id: organization.id,
|
|
6669
|
-
name: organization.name,
|
|
6670
|
-
logo: organization.logo,
|
|
6671
|
-
createdAt: organization.createdAt,
|
|
6672
|
-
slug: organization.slug,
|
|
6673
|
-
role: member.find((m) => m.organizationId === organization.id)?.role,
|
|
6674
|
-
teams: teams.filter((team) => team.organizationId === organization.id)
|
|
6675
|
-
})) };
|
|
6658
|
+
if (membersWithOrg.length === 0) return { organizations: [] };
|
|
6659
|
+
const teams = teamMembersWithTeam.map((tm) => Array.isArray(tm.team) ? tm.team[0] : tm.team).filter((t) => t != null);
|
|
6660
|
+
return { organizations: membersWithOrg.map((m) => {
|
|
6661
|
+
const organization = Array.isArray(m.organization) ? m.organization[0] : m.organization;
|
|
6662
|
+
if (!organization) return null;
|
|
6663
|
+
return {
|
|
6664
|
+
id: organization.id,
|
|
6665
|
+
name: organization.name,
|
|
6666
|
+
logo: organization.logo,
|
|
6667
|
+
createdAt: organization.createdAt,
|
|
6668
|
+
slug: organization.slug,
|
|
6669
|
+
role: m.role,
|
|
6670
|
+
teams: teams.filter((team) => team.organizationId === organization.id)
|
|
6671
|
+
};
|
|
6672
|
+
}).filter(Boolean) };
|
|
6676
6673
|
});
|
|
6677
6674
|
};
|
|
6678
6675
|
const updateUser = (options) => createAuthEndpoint("/dash/update-user", {
|
|
@@ -7462,6 +7459,20 @@ const generateBackupCodes = (options) => createAuthEndpoint("/dash/generate-back
|
|
|
7462
7459
|
return { backupCodes: newBackupCodes };
|
|
7463
7460
|
});
|
|
7464
7461
|
|
|
7462
|
+
//#endregion
|
|
7463
|
+
//#region src/routes/validate.ts
|
|
7464
|
+
/**
|
|
7465
|
+
* Lightweight endpoint to verify API key ownership during onboarding
|
|
7466
|
+
*/
|
|
7467
|
+
const getValidate = (options) => {
|
|
7468
|
+
return createAuthEndpoint("/dash/validate", {
|
|
7469
|
+
method: "GET",
|
|
7470
|
+
use: [jwtValidateMiddleware(options)]
|
|
7471
|
+
}, async () => {
|
|
7472
|
+
return { valid: true };
|
|
7473
|
+
});
|
|
7474
|
+
};
|
|
7475
|
+
|
|
7465
7476
|
//#endregion
|
|
7466
7477
|
//#region src/pow.ts
|
|
7467
7478
|
/** Default difficulty in bits (18 = ~500ms solve time) */
|
|
@@ -7965,6 +7976,7 @@ const dash = (options) => {
|
|
|
7965
7976
|
},
|
|
7966
7977
|
endpoints: {
|
|
7967
7978
|
getDashConfig: getConfig(opts),
|
|
7979
|
+
getDashValidate: getValidate(opts),
|
|
7968
7980
|
getDashUsers: getUsers(opts),
|
|
7969
7981
|
exportDashUsers: exportUsers(opts),
|
|
7970
7982
|
getOnlineUsersCount: getOnlineUsersCount(opts),
|
|
@@ -8051,4 +8063,4 @@ const dash = (options) => {
|
|
|
8051
8063
|
};
|
|
8052
8064
|
|
|
8053
8065
|
//#endregion
|
|
8054
|
-
export { CHALLENGE_TTL, DEFAULT_DIFFICULTY, EMAIL_TEMPLATES, SMS_TEMPLATES, USER_EVENT_TYPES, createEmailSender, createSMSSender, dash, decodePoWChallenge, encodePoWSolution, sendBulkEmails, sendEmail, sendSMS, sentinel, solvePoWChallenge, verifyPoWSolution };
|
|
8066
|
+
export { CHALLENGE_TTL, DEFAULT_DIFFICULTY, EMAIL_TEMPLATES, SMS_TEMPLATES, USER_EVENT_TYPES, createEmailSender, createSMSSender, dash, decodePoWChallenge, encodePoWSolution, normalizeEmail, sendBulkEmails, sendEmail, sendSMS, sentinel, solvePoWChallenge, verifyPoWSolution };
|