@better-auth/infra 0.1.9 → 0.1.11
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 +270 -450
- package/dist/index.mjs +273 -316
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1523,6 +1523,7 @@ const paths = [
|
|
|
1523
1523
|
"/email-otp/verify-email",
|
|
1524
1524
|
"/sign-in/email-otp",
|
|
1525
1525
|
"/sign-in/magic-link",
|
|
1526
|
+
"/sign-in/email",
|
|
1526
1527
|
"/forget-password/email-otp",
|
|
1527
1528
|
"/email-otp/reset-password",
|
|
1528
1529
|
"/email-otp/create-verification-otp",
|
|
@@ -1555,25 +1556,6 @@ const signIn = new Set(paths.slice(1, 12));
|
|
|
1555
1556
|
* @returns boolean
|
|
1556
1557
|
*/
|
|
1557
1558
|
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
1559
|
|
|
1578
1560
|
//#endregion
|
|
1579
1561
|
//#region src/validation/email.ts
|
|
@@ -1605,9 +1587,13 @@ const PLUS_ADDRESSING_DOMAINS = new Set([
|
|
|
1605
1587
|
* - Remove dots from Gmail-like providers (they ignore dots)
|
|
1606
1588
|
* - Remove plus addressing (user+tag@domain → user@domain)
|
|
1607
1589
|
* - Normalize googlemail.com to gmail.com
|
|
1590
|
+
*
|
|
1591
|
+
* @param email - Raw email to normalize
|
|
1592
|
+
* @param context - Auth context with getPlugin (for sentinel policy). Pass undefined when context unavailable (e.g. server, hooks).
|
|
1608
1593
|
*/
|
|
1609
|
-
function normalizeEmail(email) {
|
|
1594
|
+
function normalizeEmail(email, context) {
|
|
1610
1595
|
if (!email || typeof email !== "string") return email;
|
|
1596
|
+
if ((context.getPlugin?.("sentinel"))?.options?.emailValidation?.enabled === false) return email;
|
|
1611
1597
|
const trimmed = email.trim().toLowerCase();
|
|
1612
1598
|
const atIndex = trimmed.lastIndexOf("@");
|
|
1613
1599
|
if (atIndex === -1) return trimmed;
|
|
@@ -1700,45 +1686,42 @@ function isValidEmailFormatLocal(email) {
|
|
|
1700
1686
|
if (domain.length > 253) return false;
|
|
1701
1687
|
return true;
|
|
1702
1688
|
}
|
|
1703
|
-
const getEmail = (ctx) =>
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1689
|
+
const getEmail = (ctx) => {
|
|
1690
|
+
if (ctx.path === "/change-email") return {
|
|
1691
|
+
email: ctx.body?.newEmail,
|
|
1692
|
+
container: "body",
|
|
1693
|
+
field: "newEmail"
|
|
1694
|
+
};
|
|
1695
|
+
const body = ctx.body;
|
|
1696
|
+
const query = ctx.query;
|
|
1697
|
+
return {
|
|
1698
|
+
email: body?.email ?? query?.email,
|
|
1699
|
+
container: body ? "body" : "query",
|
|
1700
|
+
field: "email"
|
|
1701
|
+
};
|
|
1702
|
+
};
|
|
1707
1703
|
/**
|
|
1708
1704
|
* Create email normalization hook (shared between all configurations)
|
|
1709
1705
|
*/
|
|
1710
1706
|
function createEmailNormalizationHook() {
|
|
1711
1707
|
return {
|
|
1712
|
-
matcher:
|
|
1708
|
+
matcher: allEmail,
|
|
1713
1709
|
handler: createAuthMiddleware(async (ctx) => {
|
|
1714
|
-
const { email, container } = getEmail(ctx);
|
|
1710
|
+
const { email, container, field } = getEmail(ctx);
|
|
1715
1711
|
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
|
-
}
|
|
1712
|
+
const normalized = normalizeEmail(email, ctx.context);
|
|
1713
|
+
if (normalized === email) return;
|
|
1714
|
+
const data = container === "query" ? {
|
|
1715
|
+
...ctx.query,
|
|
1716
|
+
[field]: normalized
|
|
1717
|
+
} : {
|
|
1718
|
+
...ctx.body,
|
|
1719
|
+
[field]: normalized
|
|
1720
|
+
};
|
|
1721
|
+
return { context: {
|
|
1722
|
+
...ctx,
|
|
1723
|
+
[container]: data
|
|
1724
|
+
} };
|
|
1742
1725
|
})
|
|
1743
1726
|
};
|
|
1744
1727
|
}
|
|
@@ -1749,7 +1732,7 @@ function createEmailValidationHook(validator, onDisposableEmail) {
|
|
|
1749
1732
|
return {
|
|
1750
1733
|
matcher: allEmail,
|
|
1751
1734
|
handler: createAuthMiddleware(async (ctx) => {
|
|
1752
|
-
const
|
|
1735
|
+
const { email } = getEmail(ctx);
|
|
1753
1736
|
if (typeof email !== "string") return;
|
|
1754
1737
|
if (!isValidEmailFormatLocal(email)) throw new APIError$1("BAD_REQUEST", { message: "Invalid email" });
|
|
1755
1738
|
if (validator) {
|
|
@@ -1758,7 +1741,7 @@ function createEmailValidationHook(validator, onDisposableEmail) {
|
|
|
1758
1741
|
if (!policy?.enabled) return;
|
|
1759
1742
|
const action = policy.action;
|
|
1760
1743
|
if (!result.valid) {
|
|
1761
|
-
if ((result.disposable || result.reason === "no_mx_records" || result.reason === "blocklist") && onDisposableEmail) {
|
|
1744
|
+
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
1745
|
const ip = ctx.request?.headers?.get("x-forwarded-for")?.split(",")[0] || ctx.request?.headers?.get("cf-connecting-ip") || void 0;
|
|
1763
1746
|
onDisposableEmail({
|
|
1764
1747
|
email,
|
|
@@ -1770,7 +1753,7 @@ function createEmailValidationHook(validator, onDisposableEmail) {
|
|
|
1770
1753
|
});
|
|
1771
1754
|
}
|
|
1772
1755
|
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 === "
|
|
1756
|
+
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
1757
|
}
|
|
1775
1758
|
}
|
|
1776
1759
|
})
|
|
@@ -1809,12 +1792,13 @@ function createEmailHooks(options = {}) {
|
|
|
1809
1792
|
...defaultConfig
|
|
1810
1793
|
};
|
|
1811
1794
|
if (!emailConfig.enabled) return { before: [] };
|
|
1812
|
-
|
|
1795
|
+
const validator = useApi ? createEmailValidator({
|
|
1813
1796
|
apiUrl,
|
|
1814
1797
|
kvUrl,
|
|
1815
1798
|
apiKey,
|
|
1816
1799
|
defaultConfig: emailConfig
|
|
1817
|
-
}) : void 0
|
|
1800
|
+
}) : void 0;
|
|
1801
|
+
return { before: [createEmailNormalizationHook(), createEmailValidationHook(validator, onDisposableEmail)] };
|
|
1818
1802
|
}
|
|
1819
1803
|
/**
|
|
1820
1804
|
* Default email hooks using local validation only
|
|
@@ -2084,100 +2068,107 @@ const sentinel = (options) => {
|
|
|
2084
2068
|
return {
|
|
2085
2069
|
id: "sentinel",
|
|
2086
2070
|
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.");
|
|
2071
|
+
return { options: {
|
|
2072
|
+
emailValidation: opts.security?.emailValidation,
|
|
2073
|
+
databaseHooks: {
|
|
2074
|
+
user: { create: {
|
|
2075
|
+
async before(user, ctx) {
|
|
2076
|
+
if (!ctx) return;
|
|
2077
|
+
const visitorId = ctx.context.visitorId;
|
|
2078
|
+
if (visitorId && opts.security?.freeTrialAbuse?.enabled) {
|
|
2079
|
+
const abuseCheck = await securityService.checkFreeTrialAbuse(visitorId);
|
|
2080
|
+
if (abuseCheck.isAbuse && abuseCheck.action === "block") throw new APIError("FORBIDDEN", { message: "Account creation is not allowed from this device." });
|
|
2113
2081
|
}
|
|
2082
|
+
if (user.email && typeof user.email === "string") return { data: {
|
|
2083
|
+
...user,
|
|
2084
|
+
email: normalizeEmail(user.email, ctx.context)
|
|
2085
|
+
} };
|
|
2086
|
+
},
|
|
2087
|
+
async after(user, ctx) {
|
|
2088
|
+
if (!ctx) return;
|
|
2089
|
+
const visitorId = ctx.context.visitorId;
|
|
2090
|
+
if (visitorId && opts.security?.freeTrialAbuse?.enabled) await ctx.context.runInBackgroundOrAwait(securityService.trackFreeTrialSignup(visitorId, user.id));
|
|
2114
2091
|
}
|
|
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"
|
|
2092
|
+
} },
|
|
2093
|
+
session: { create: {
|
|
2094
|
+
async before(session, ctx) {
|
|
2095
|
+
if (!ctx) return;
|
|
2096
|
+
const visitorId = ctx.context.visitorId;
|
|
2097
|
+
const identification = ctx.context.identification;
|
|
2098
|
+
if (session.userId && identification?.location && visitorId) {
|
|
2099
|
+
const travelCheck = await securityService.checkImpossibleTravel(session.userId, identification.location, visitorId);
|
|
2100
|
+
if (travelCheck?.isImpossible) {
|
|
2101
|
+
if (travelCheck.action === "block") throw new APIError("FORBIDDEN", { message: "Login blocked due to suspicious location change." });
|
|
2102
|
+
if (travelCheck.action === "challenge" && travelCheck.challenge) throwChallengeError(travelCheck.challenge, "impossible_travel", "Unusual login location detected. Please complete a security check.");
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
},
|
|
2106
|
+
async after(session, ctx) {
|
|
2107
|
+
if (!ctx || !session.userId) return;
|
|
2108
|
+
const visitorId = ctx.context.visitorId;
|
|
2109
|
+
const identification = ctx.context.identification;
|
|
2110
|
+
let user = null;
|
|
2111
|
+
try {
|
|
2112
|
+
user = await ctx.context.adapter.findOne({
|
|
2113
|
+
model: "user",
|
|
2114
|
+
select: [
|
|
2115
|
+
"email",
|
|
2116
|
+
"name",
|
|
2117
|
+
"lastActiveAt"
|
|
2118
|
+
],
|
|
2119
|
+
where: [{
|
|
2120
|
+
field: "id",
|
|
2121
|
+
value: session.userId
|
|
2122
|
+
}]
|
|
2174
2123
|
});
|
|
2124
|
+
} catch (error) {
|
|
2125
|
+
logger.warn("[Sentinel] Failed to fetch user for security checks:", error);
|
|
2126
|
+
}
|
|
2127
|
+
if (visitorId) {
|
|
2128
|
+
if (await securityService.checkUnknownDevice(session.userId, visitorId) && user?.email) await ctx.context.runInBackgroundOrAwait(securityService.notifyUnknownDevice(session.userId, user.email, identification));
|
|
2129
|
+
}
|
|
2130
|
+
if (opts.security?.staleUsers?.enabled && user) {
|
|
2131
|
+
const staleCheck = await securityService.checkStaleUser(session.userId, user.lastActiveAt || null);
|
|
2132
|
+
if (staleCheck.isStale) {
|
|
2133
|
+
const staleOpts = opts.security.staleUsers;
|
|
2134
|
+
const notificationPromises = [];
|
|
2135
|
+
if (staleCheck.notifyUser && user.email) notificationPromises.push(securityService.notifyStaleAccountUser(user.email, user.name || null, staleCheck.daysSinceLastActive || 0, identification));
|
|
2136
|
+
if (staleCheck.notifyAdmin && staleOpts.adminEmail) notificationPromises.push(securityService.notifyStaleAccountAdmin(staleOpts.adminEmail, session.userId, user.email || "unknown", user.name || null, staleCheck.daysSinceLastActive || 0, identification));
|
|
2137
|
+
if (notificationPromises.length > 0) Promise.all(notificationPromises).catch((error) => {
|
|
2138
|
+
logger.error("[Sentinel] Failed to send stale account notifications:", error);
|
|
2139
|
+
});
|
|
2140
|
+
trackEvent({
|
|
2141
|
+
eventKey: session.userId,
|
|
2142
|
+
eventType: "security_stale_account",
|
|
2143
|
+
eventDisplayName: "Security: stale account reactivated",
|
|
2144
|
+
eventData: {
|
|
2145
|
+
action: staleCheck.action === "block" ? "blocked" : staleCheck.action === "challenge" ? "challenged" : "logged",
|
|
2146
|
+
reason: "stale_account_reactivation",
|
|
2147
|
+
userId: session.userId,
|
|
2148
|
+
daysSinceLastActive: staleCheck.daysSinceLastActive,
|
|
2149
|
+
staleDays: staleCheck.staleDays,
|
|
2150
|
+
lastActiveAt: staleCheck.lastActiveAt,
|
|
2151
|
+
notifyUser: staleCheck.notifyUser,
|
|
2152
|
+
notifyAdmin: staleCheck.notifyAdmin,
|
|
2153
|
+
detectionLabel: "Stale Account Reactivation",
|
|
2154
|
+
description: `Dormant account (inactive for ${staleCheck.daysSinceLastActive} days) became active`
|
|
2155
|
+
},
|
|
2156
|
+
ipAddress: identification?.ip || void 0,
|
|
2157
|
+
city: identification?.location?.city || void 0,
|
|
2158
|
+
country: identification?.location?.country?.name || void 0,
|
|
2159
|
+
countryCode: identification?.location?.country?.code || void 0
|
|
2160
|
+
});
|
|
2161
|
+
if (staleCheck.action === "block") throw new APIError("FORBIDDEN", {
|
|
2162
|
+
message: "This account has been inactive for an extended period. Please contact support to reactivate.",
|
|
2163
|
+
code: "STALE_ACCOUNT"
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2175
2166
|
}
|
|
2167
|
+
if (identification?.location) await ctx.context.runInBackgroundOrAwait(securityService.storeLastLocation(session.userId, identification.location));
|
|
2176
2168
|
}
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
} } };
|
|
2169
|
+
} }
|
|
2170
|
+
}
|
|
2171
|
+
} };
|
|
2181
2172
|
},
|
|
2182
2173
|
hooks: {
|
|
2183
2174
|
before: [
|
|
@@ -2633,7 +2624,7 @@ const initTeamEvents = (tracker) => {
|
|
|
2633
2624
|
//#region src/jwt.ts
|
|
2634
2625
|
/**
|
|
2635
2626
|
* Hash the given value
|
|
2636
|
-
* Note: Must match @infra/crypto hash()
|
|
2627
|
+
* Note: Must match @infra/utils/crypto hash()
|
|
2637
2628
|
* @param value - The value to hash
|
|
2638
2629
|
*/
|
|
2639
2630
|
async function hash(value) {
|
|
@@ -2765,7 +2756,7 @@ const getConfig = (options) => {
|
|
|
2765
2756
|
schema: plugin.schema,
|
|
2766
2757
|
options: sanitizePluginOptions(plugin.id, plugin.options)
|
|
2767
2758
|
};
|
|
2768
|
-
}),
|
|
2759
|
+
}) ?? [],
|
|
2769
2760
|
organization: {
|
|
2770
2761
|
sendInvitationEmailEnabled: !!organizationPlugin?.options?.sendInvitationEmail,
|
|
2771
2762
|
additionalFields: (() => {
|
|
@@ -2894,7 +2885,8 @@ async function getScimProviderOwner(ctx, organizationId, providerId) {
|
|
|
2894
2885
|
});
|
|
2895
2886
|
return (await ctx.context.adapter.findOne({
|
|
2896
2887
|
model: "scimProvider",
|
|
2897
|
-
where
|
|
2888
|
+
where,
|
|
2889
|
+
select: ["userId"]
|
|
2898
2890
|
}))?.userId ?? null;
|
|
2899
2891
|
}
|
|
2900
2892
|
function getScimEndpoint(baseUrl) {
|
|
@@ -3367,7 +3359,8 @@ const acceptInvitation = (options) => {
|
|
|
3367
3359
|
throw new APIError("BAD_REQUEST", { message: "This invitation has expired." });
|
|
3368
3360
|
}
|
|
3369
3361
|
}
|
|
3370
|
-
const
|
|
3362
|
+
const invitationEmail = normalizeEmail(invitation.email, ctx.context);
|
|
3363
|
+
const existingUser = await ctx.context.internalAdapter.findUserByEmail(invitationEmail).then((user$1) => user$1?.user);
|
|
3371
3364
|
if (existingUser) {
|
|
3372
3365
|
await $api("/api/internal/invitations/mark-accepted", {
|
|
3373
3366
|
method: "POST",
|
|
@@ -3381,7 +3374,7 @@ const acceptInvitation = (options) => {
|
|
|
3381
3374
|
user: existingUser
|
|
3382
3375
|
});
|
|
3383
3376
|
const redirectUrl$1 = invitation.redirectUrl || ctx.context.options.baseURL || "/";
|
|
3384
|
-
return ctx.redirect(redirectUrl$1);
|
|
3377
|
+
return ctx.redirect(redirectUrl$1.toString());
|
|
3385
3378
|
}
|
|
3386
3379
|
if (invitation.authMode === "auth") {
|
|
3387
3380
|
const platformUrl = options.apiUrl || INFRA_API_URL;
|
|
@@ -3392,7 +3385,7 @@ const acceptInvitation = (options) => {
|
|
|
3392
3385
|
return ctx.redirect(acceptPageUrl.toString());
|
|
3393
3386
|
}
|
|
3394
3387
|
const user = await ctx.context.internalAdapter.createUser({
|
|
3395
|
-
email:
|
|
3388
|
+
email: invitationEmail,
|
|
3396
3389
|
name: invitation.name || invitation.email.split("@")[0] || "",
|
|
3397
3390
|
emailVerified: true,
|
|
3398
3391
|
createdAt: /* @__PURE__ */ new Date(),
|
|
@@ -3410,7 +3403,7 @@ const acceptInvitation = (options) => {
|
|
|
3410
3403
|
user
|
|
3411
3404
|
});
|
|
3412
3405
|
const redirectUrl = invitation.redirectUrl || ctx.context.options.baseURL || "/";
|
|
3413
|
-
return ctx.redirect(redirectUrl);
|
|
3406
|
+
return ctx.redirect(redirectUrl.toString());
|
|
3414
3407
|
});
|
|
3415
3408
|
};
|
|
3416
3409
|
/**
|
|
@@ -3442,7 +3435,8 @@ const completeInvitation = (options) => {
|
|
|
3442
3435
|
if (error || !invitation) throw new APIError("BAD_REQUEST", { message: "Invalid or expired invitation." });
|
|
3443
3436
|
if (invitation.status !== "pending") throw new APIError("BAD_REQUEST", { message: `This invitation has already been ${invitation.status}.` });
|
|
3444
3437
|
if (!ctx.context) throw new APIError("BAD_REQUEST", { message: "Context is required" });
|
|
3445
|
-
const
|
|
3438
|
+
const invitationEmail = normalizeEmail(invitation.email, ctx.context);
|
|
3439
|
+
const existingUser = await ctx.context.internalAdapter.findUserByEmail(invitationEmail).then((user$1) => user$1?.user);
|
|
3446
3440
|
if (existingUser) {
|
|
3447
3441
|
await $api("/api/internal/invitations/mark-accepted", {
|
|
3448
3442
|
method: "POST",
|
|
@@ -3461,7 +3455,7 @@ const completeInvitation = (options) => {
|
|
|
3461
3455
|
};
|
|
3462
3456
|
}
|
|
3463
3457
|
const user = await ctx.context.internalAdapter.createUser({
|
|
3464
|
-
email:
|
|
3458
|
+
email: invitationEmail,
|
|
3465
3459
|
name: invitation.name || invitation.email.split("@")[0] || "",
|
|
3466
3460
|
emailVerified: true,
|
|
3467
3461
|
createdAt: /* @__PURE__ */ new Date(),
|
|
@@ -3509,7 +3503,8 @@ const checkUserExists = (_options) => {
|
|
|
3509
3503
|
}, async (ctx) => {
|
|
3510
3504
|
const { email } = ctx.body;
|
|
3511
3505
|
if (!ctx.request?.headers.get("Authorization")) throw new APIError("UNAUTHORIZED", { message: "Authorization required" });
|
|
3512
|
-
const
|
|
3506
|
+
const normalizedEmail = normalizeEmail(email, ctx.context);
|
|
3507
|
+
const existingUser = await ctx.context.internalAdapter.findUserByEmail(normalizedEmail).then((user) => user?.user);
|
|
3513
3508
|
return {
|
|
3514
3509
|
exists: !!existingUser,
|
|
3515
3510
|
userId: existingUser?.id || null
|
|
@@ -3719,7 +3714,7 @@ const deleteOrganizationLogDrain = (options) => {
|
|
|
3719
3714
|
body: z$1.object({ logDrainId: z$1.string() })
|
|
3720
3715
|
}, async (ctx) => {
|
|
3721
3716
|
const { organizationId } = ctx.context.payload;
|
|
3722
|
-
if (
|
|
3717
|
+
if (await ctx.context.adapter.count({
|
|
3723
3718
|
model: "orgLogDrain",
|
|
3724
3719
|
where: [{
|
|
3725
3720
|
field: "id",
|
|
@@ -3728,7 +3723,7 @@ const deleteOrganizationLogDrain = (options) => {
|
|
|
3728
3723
|
field: "organizationId",
|
|
3729
3724
|
value: organizationId
|
|
3730
3725
|
}]
|
|
3731
|
-
})) throw ctx.error("NOT_FOUND", { message: "Log drain not found" });
|
|
3726
|
+
}) === 0) throw ctx.error("NOT_FOUND", { message: "Log drain not found" });
|
|
3732
3727
|
await ctx.context.adapter.delete({
|
|
3733
3728
|
model: "orgLogDrain",
|
|
3734
3729
|
where: [{
|
|
@@ -3917,6 +3912,15 @@ const exportFactory = (input, options) => async (ctx) => {
|
|
|
3917
3912
|
|
|
3918
3913
|
//#endregion
|
|
3919
3914
|
//#region src/helper.ts
|
|
3915
|
+
/**
|
|
3916
|
+
* Checks whether a plugin is registered by its ID.
|
|
3917
|
+
* Prefers the native `hasPlugin` when available, otherwise
|
|
3918
|
+
* falls back to scanning `options.plugins`.
|
|
3919
|
+
*/
|
|
3920
|
+
function hasPlugin(context, pluginId) {
|
|
3921
|
+
if (typeof context.hasPlugin === "function") return context.hasPlugin(pluginId);
|
|
3922
|
+
return context.options.plugins?.some((p) => p.id === pluginId) ?? false;
|
|
3923
|
+
}
|
|
3920
3924
|
function* chunkArray(arr, options) {
|
|
3921
3925
|
const batchSize = options?.batchSize || 200;
|
|
3922
3926
|
for (let i = 0; i < arr.length; i += batchSize) yield arr.slice(i, i + batchSize);
|
|
@@ -4129,32 +4133,26 @@ const listOrganizations = (options) => {
|
|
|
4129
4133
|
sortBy: {
|
|
4130
4134
|
field: dbSortBy,
|
|
4131
4135
|
direction: sortOrder
|
|
4132
|
-
}
|
|
4136
|
+
},
|
|
4137
|
+
join: { member: { limit: 5 } }
|
|
4133
4138
|
}), needsInMemoryProcessing ? Promise.resolve(0) : ctx.context.adapter.count({
|
|
4134
4139
|
model: "organization",
|
|
4135
4140
|
where
|
|
4136
4141
|
})]);
|
|
4137
4142
|
const orgIds = organizations.map((o) => o.id);
|
|
4138
|
-
const
|
|
4143
|
+
const memberCounts = await withConcurrency(orgIds, (orgId) => ctx.context.adapter.count({
|
|
4139
4144
|
model: "member",
|
|
4140
4145
|
where: [{
|
|
4141
4146
|
field: "organizationId",
|
|
4142
|
-
value:
|
|
4143
|
-
operator: "in"
|
|
4147
|
+
value: orgId
|
|
4144
4148
|
}]
|
|
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
|
-
}
|
|
4149
|
+
}), { concurrency: 10 });
|
|
4150
|
+
const memberCountByOrg = new Map(orgIds.map((orgId, i) => [orgId, memberCounts[i]]));
|
|
4152
4151
|
let withCounts = organizations.map((organization) => {
|
|
4153
|
-
const
|
|
4152
|
+
const memberCount = memberCountByOrg.get(organization.id) ?? 0;
|
|
4154
4153
|
return {
|
|
4155
4154
|
...organization,
|
|
4156
|
-
|
|
4157
|
-
memberCount: orgMembers.length
|
|
4155
|
+
memberCount
|
|
4158
4156
|
};
|
|
4159
4157
|
});
|
|
4160
4158
|
if (filterMembers) {
|
|
@@ -4174,25 +4172,26 @@ const listOrganizations = (options) => {
|
|
|
4174
4172
|
const total = needsInMemoryProcessing ? withCounts.length : initialTotal;
|
|
4175
4173
|
if (needsInMemoryProcessing) withCounts = withCounts.slice(offset, offset + limit);
|
|
4176
4174
|
const allUserIds = /* @__PURE__ */ new Set();
|
|
4177
|
-
for (const organization of withCounts) for (const member of organization.
|
|
4175
|
+
for (const organization of withCounts) for (const member of organization.member) allUserIds.add(member.userId);
|
|
4178
4176
|
const users = allUserIds.size > 0 ? await ctx.context.adapter.findMany({
|
|
4179
4177
|
model: "user",
|
|
4180
4178
|
where: [{
|
|
4181
4179
|
field: "id",
|
|
4182
4180
|
value: Array.from(allUserIds),
|
|
4183
4181
|
operator: "in"
|
|
4184
|
-
}]
|
|
4182
|
+
}],
|
|
4183
|
+
limit: allUserIds.size
|
|
4185
4184
|
}) : [];
|
|
4186
4185
|
const userMap = new Map(users.map((u) => [u.id, u]));
|
|
4187
4186
|
return {
|
|
4188
4187
|
organizations: withCounts.map((organization) => {
|
|
4189
|
-
const members = organization.
|
|
4188
|
+
const members = organization.member.map((m) => userMap.get(m.userId)).filter((u) => u !== void 0).map((u) => ({
|
|
4190
4189
|
id: u.id,
|
|
4191
4190
|
name: u.name,
|
|
4192
4191
|
email: u.email,
|
|
4193
4192
|
image: u.image
|
|
4194
4193
|
}));
|
|
4195
|
-
const { _members, ...org } = organization;
|
|
4194
|
+
const { member: _members, ...org } = organization;
|
|
4196
4195
|
return {
|
|
4197
4196
|
...org,
|
|
4198
4197
|
members
|
|
@@ -4284,17 +4283,9 @@ const listOrganizationMembers = (options) => {
|
|
|
4284
4283
|
where: [{
|
|
4285
4284
|
field: "organizationId",
|
|
4286
4285
|
value: ctx.params.id
|
|
4287
|
-
}]
|
|
4286
|
+
}],
|
|
4287
|
+
join: { user: true }
|
|
4288
4288
|
});
|
|
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
4289
|
const invitations = await ctx.context.adapter.findMany({
|
|
4299
4290
|
model: "invitation",
|
|
4300
4291
|
where: [{
|
|
@@ -4303,22 +4294,17 @@ const listOrganizationMembers = (options) => {
|
|
|
4303
4294
|
}, {
|
|
4304
4295
|
field: "status",
|
|
4305
4296
|
value: "accepted"
|
|
4306
|
-
}]
|
|
4297
|
+
}],
|
|
4298
|
+
join: { user: true }
|
|
4307
4299
|
});
|
|
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]));
|
|
4300
|
+
const inviterById = /* @__PURE__ */ new Map();
|
|
4301
|
+
for (const inv of invitations) {
|
|
4302
|
+
const inviter = Array.isArray(inv.user) ? inv.user[0] : inv.user;
|
|
4303
|
+
if (inviter && inv.inviterId) inviterById.set(inv.inviterId, inviter);
|
|
4304
|
+
}
|
|
4319
4305
|
const invitationByEmail = new Map(invitations.map((i) => [i.email.toLowerCase(), i]));
|
|
4320
4306
|
return members.map((m) => {
|
|
4321
|
-
const user =
|
|
4307
|
+
const user = (Array.isArray(m.user) ? m.user[0] : m.user) ?? null;
|
|
4322
4308
|
const invitation = user ? invitationByEmail.get(user.email.toLowerCase()) : null;
|
|
4323
4309
|
const inviter = invitation ? inviterById.get(invitation.inviterId) : null;
|
|
4324
4310
|
return {
|
|
@@ -4351,7 +4337,7 @@ const listOrganizationInvitations = (options) => {
|
|
|
4351
4337
|
value: ctx.params.id
|
|
4352
4338
|
}]
|
|
4353
4339
|
});
|
|
4354
|
-
const emails = [...new Set(invitations.map((i) => i.email.
|
|
4340
|
+
const emails = [...new Set(invitations.map((i) => normalizeEmail(i.email, ctx.context)))];
|
|
4355
4341
|
const users = emails.length ? await ctx.context.adapter.findMany({
|
|
4356
4342
|
model: "user",
|
|
4357
4343
|
where: [{
|
|
@@ -4360,9 +4346,10 @@ const listOrganizationInvitations = (options) => {
|
|
|
4360
4346
|
operator: "in"
|
|
4361
4347
|
}]
|
|
4362
4348
|
}) : [];
|
|
4363
|
-
const userByEmail = new Map(users.map((u) => [u.email.
|
|
4349
|
+
const userByEmail = new Map(users.map((u) => [normalizeEmail(u.email, ctx.context), u]));
|
|
4364
4350
|
return invitations.map((invitation) => {
|
|
4365
|
-
const
|
|
4351
|
+
const invitationEmail = normalizeEmail(invitation.email, ctx.context);
|
|
4352
|
+
const user = userByEmail.get(invitationEmail);
|
|
4366
4353
|
return {
|
|
4367
4354
|
...invitation,
|
|
4368
4355
|
user: user ? {
|
|
@@ -4398,17 +4385,12 @@ const deleteOrganization = (options) => {
|
|
|
4398
4385
|
field: "createdAt",
|
|
4399
4386
|
direction: "asc"
|
|
4400
4387
|
},
|
|
4401
|
-
limit: 1
|
|
4388
|
+
limit: 1,
|
|
4389
|
+
join: { user: true }
|
|
4402
4390
|
});
|
|
4403
4391
|
if (owners.length === 0) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
|
|
4404
4392
|
const owner = owners[0];
|
|
4405
|
-
const deletedByUser =
|
|
4406
|
-
model: "user",
|
|
4407
|
-
where: [{
|
|
4408
|
-
field: "id",
|
|
4409
|
-
value: owner.userId
|
|
4410
|
-
}]
|
|
4411
|
-
});
|
|
4393
|
+
const deletedByUser = Array.isArray(owner.user) ? owner.user[0] : owner.user;
|
|
4412
4394
|
if (!deletedByUser) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
|
|
4413
4395
|
const organization = await ctx.context.adapter.findOne({
|
|
4414
4396
|
model: "organization",
|
|
@@ -4555,17 +4537,12 @@ const updateTeam = (options) => {
|
|
|
4555
4537
|
field: "createdAt",
|
|
4556
4538
|
direction: "asc"
|
|
4557
4539
|
},
|
|
4558
|
-
limit: 1
|
|
4540
|
+
limit: 1,
|
|
4541
|
+
join: { user: true }
|
|
4559
4542
|
});
|
|
4560
4543
|
if (owners.length === 0) throw ctx.error("NOT_FOUND", { message: "Owner not found" });
|
|
4561
4544
|
const owner = owners[0];
|
|
4562
|
-
const user =
|
|
4563
|
-
model: "user",
|
|
4564
|
-
where: [{
|
|
4565
|
-
field: "id",
|
|
4566
|
-
value: owner.userId
|
|
4567
|
-
}]
|
|
4568
|
-
});
|
|
4545
|
+
const user = Array.isArray(owner.user) ? owner.user[0] : owner.user;
|
|
4569
4546
|
if (!user) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
|
|
4570
4547
|
let updateData = { updatedAt: /* @__PURE__ */ new Date() };
|
|
4571
4548
|
if (ctx.body.name) updateData.name = ctx.body.name;
|
|
@@ -4649,17 +4626,12 @@ const deleteTeam = (options) => {
|
|
|
4649
4626
|
field: "createdAt",
|
|
4650
4627
|
direction: "asc"
|
|
4651
4628
|
},
|
|
4652
|
-
limit: 1
|
|
4629
|
+
limit: 1,
|
|
4630
|
+
join: { user: true }
|
|
4653
4631
|
});
|
|
4654
4632
|
if (owners.length === 0) throw ctx.error("NOT_FOUND", { message: "Owner not found" });
|
|
4655
4633
|
const owner = owners[0];
|
|
4656
|
-
const user =
|
|
4657
|
-
model: "user",
|
|
4658
|
-
where: [{
|
|
4659
|
-
field: "id",
|
|
4660
|
-
value: owner.userId
|
|
4661
|
-
}]
|
|
4662
|
-
});
|
|
4634
|
+
const user = Array.isArray(owner.user) ? owner.user[0] : owner.user;
|
|
4663
4635
|
if (!user) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
|
|
4664
4636
|
if (orgOptions?.organizationHooks?.beforeDeleteTeam) await orgOptions.organizationHooks.beforeDeleteTeam({
|
|
4665
4637
|
team,
|
|
@@ -4727,17 +4699,12 @@ const createTeam = (options) => {
|
|
|
4727
4699
|
field: "createdAt",
|
|
4728
4700
|
direction: "asc"
|
|
4729
4701
|
},
|
|
4730
|
-
limit: 1
|
|
4702
|
+
limit: 1,
|
|
4703
|
+
join: { user: true }
|
|
4731
4704
|
});
|
|
4732
4705
|
if (owners.length === 0) throw ctx.error("NOT_FOUND", { message: "Owner not found" });
|
|
4733
4706
|
const owner = owners[0];
|
|
4734
|
-
const user =
|
|
4735
|
-
model: "user",
|
|
4736
|
-
where: [{
|
|
4737
|
-
field: "id",
|
|
4738
|
-
value: owner.userId
|
|
4739
|
-
}]
|
|
4740
|
-
});
|
|
4707
|
+
const user = Array.isArray(owner.user) ? owner.user[0] : owner.user;
|
|
4741
4708
|
if (!user) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
|
|
4742
4709
|
let teamData = {
|
|
4743
4710
|
name: ctx.body.name,
|
|
@@ -4773,7 +4740,7 @@ const listTeamMembers = (options) => {
|
|
|
4773
4740
|
use: [jwtMiddleware(options)]
|
|
4774
4741
|
}, async (ctx) => {
|
|
4775
4742
|
try {
|
|
4776
|
-
if (
|
|
4743
|
+
if (await ctx.context.adapter.count({
|
|
4777
4744
|
model: "team",
|
|
4778
4745
|
where: [{
|
|
4779
4746
|
field: "id",
|
|
@@ -4782,22 +4749,16 @@ const listTeamMembers = (options) => {
|
|
|
4782
4749
|
field: "organizationId",
|
|
4783
4750
|
value: ctx.params.orgId
|
|
4784
4751
|
}]
|
|
4785
|
-
})) throw ctx.error("NOT_FOUND", { message: "Team not found" });
|
|
4786
|
-
|
|
4752
|
+
}) === 0) throw ctx.error("NOT_FOUND", { message: "Team not found" });
|
|
4753
|
+
return (await ctx.context.adapter.findMany({
|
|
4787
4754
|
model: "teamMember",
|
|
4788
4755
|
where: [{
|
|
4789
4756
|
field: "teamId",
|
|
4790
4757
|
value: ctx.params.teamId
|
|
4791
|
-
}]
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
const user =
|
|
4795
|
-
model: "user",
|
|
4796
|
-
where: [{
|
|
4797
|
-
field: "id",
|
|
4798
|
-
value: tm.userId
|
|
4799
|
-
}]
|
|
4800
|
-
});
|
|
4758
|
+
}],
|
|
4759
|
+
join: { user: true }
|
|
4760
|
+
})).map((tm) => {
|
|
4761
|
+
const user = Array.isArray(tm.user) ? tm.user[0] : tm.user;
|
|
4801
4762
|
return {
|
|
4802
4763
|
...tm,
|
|
4803
4764
|
user: user ? {
|
|
@@ -4807,7 +4768,7 @@ const listTeamMembers = (options) => {
|
|
|
4807
4768
|
image: user.image
|
|
4808
4769
|
} : null
|
|
4809
4770
|
};
|
|
4810
|
-
})
|
|
4771
|
+
});
|
|
4811
4772
|
} catch (e) {
|
|
4812
4773
|
ctx.context.logger.warn("[Dash] Failed to list team members:", e);
|
|
4813
4774
|
return [];
|
|
@@ -4865,7 +4826,7 @@ const addTeamMember = (options) => {
|
|
|
4865
4826
|
value: organizationId
|
|
4866
4827
|
}]
|
|
4867
4828
|
})) throw ctx.error("BAD_REQUEST", { message: "User is not a member of this organization" });
|
|
4868
|
-
if (await ctx.context.adapter.
|
|
4829
|
+
if (await ctx.context.adapter.count({
|
|
4869
4830
|
model: "teamMember",
|
|
4870
4831
|
where: [{
|
|
4871
4832
|
field: "teamId",
|
|
@@ -4874,7 +4835,7 @@ const addTeamMember = (options) => {
|
|
|
4874
4835
|
field: "userId",
|
|
4875
4836
|
value: ctx.body.userId
|
|
4876
4837
|
}]
|
|
4877
|
-
})) throw ctx.error("BAD_REQUEST", { message: "User is already a member of this team" });
|
|
4838
|
+
}) > 0) throw ctx.error("BAD_REQUEST", { message: "User is already a member of this team" });
|
|
4878
4839
|
if (orgOptions?.teams?.maximumMembersPerTeam) {
|
|
4879
4840
|
const teamMemberCount = await ctx.context.adapter.count({
|
|
4880
4841
|
model: "teamMember",
|
|
@@ -5023,13 +4984,13 @@ const createOrganization = (options) => {
|
|
|
5023
4984
|
});
|
|
5024
4985
|
if (!user) throw ctx.error("BAD_REQUEST", { message: "User not found" });
|
|
5025
4986
|
const orgOptions = organizationPlugin.options || {};
|
|
5026
|
-
if (await ctx.context.adapter.
|
|
4987
|
+
if (await ctx.context.adapter.count({
|
|
5027
4988
|
model: "organization",
|
|
5028
4989
|
where: [{
|
|
5029
4990
|
field: "slug",
|
|
5030
4991
|
value: ctx.body.slug
|
|
5031
4992
|
}]
|
|
5032
|
-
})) throw ctx.error("BAD_REQUEST", { message: "Organization already exists" });
|
|
4993
|
+
}) > 0) throw ctx.error("BAD_REQUEST", { message: "Organization already exists" });
|
|
5033
4994
|
let orgData = {
|
|
5034
4995
|
...ctx.body,
|
|
5035
4996
|
defaultTeamName: void 0
|
|
@@ -5178,14 +5139,15 @@ const updateOrganization = (options) => {
|
|
|
5178
5139
|
const { organizationId } = ctx.context.payload;
|
|
5179
5140
|
const orgOptions = ctx.context.getPlugin("organization")?.options || {};
|
|
5180
5141
|
if (ctx.body.slug) {
|
|
5181
|
-
const
|
|
5142
|
+
const orgWithSlug = await ctx.context.adapter.findOne({
|
|
5182
5143
|
model: "organization",
|
|
5183
5144
|
where: [{
|
|
5184
5145
|
field: "slug",
|
|
5185
5146
|
value: ctx.body.slug
|
|
5186
|
-
}]
|
|
5147
|
+
}],
|
|
5148
|
+
select: ["id"]
|
|
5187
5149
|
});
|
|
5188
|
-
if (
|
|
5150
|
+
if (orgWithSlug && orgWithSlug.id !== organizationId) throw ctx.error("BAD_REQUEST", { message: "Slug already exists" });
|
|
5189
5151
|
}
|
|
5190
5152
|
const owners = await ctx.context.adapter.findMany({
|
|
5191
5153
|
model: "member",
|
|
@@ -5200,17 +5162,12 @@ const updateOrganization = (options) => {
|
|
|
5200
5162
|
field: "createdAt",
|
|
5201
5163
|
direction: "asc"
|
|
5202
5164
|
},
|
|
5203
|
-
limit: 1
|
|
5165
|
+
limit: 1,
|
|
5166
|
+
join: { user: true }
|
|
5204
5167
|
});
|
|
5205
5168
|
if (owners.length === 0) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
|
|
5206
5169
|
const owner = owners[0];
|
|
5207
|
-
const updatedByUser =
|
|
5208
|
-
model: "user",
|
|
5209
|
-
where: [{
|
|
5210
|
-
field: "id",
|
|
5211
|
-
value: owner.userId
|
|
5212
|
-
}]
|
|
5213
|
-
});
|
|
5170
|
+
const updatedByUser = Array.isArray(owner.user) ? owner.user[0] : owner.user;
|
|
5214
5171
|
if (!updatedByUser) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
|
|
5215
5172
|
let updateData = { ...ctx.body };
|
|
5216
5173
|
if (typeof updateData.metadata === "string") try {
|
|
@@ -5472,10 +5429,11 @@ const inviteMember = (options) => {
|
|
|
5472
5429
|
}]
|
|
5473
5430
|
});
|
|
5474
5431
|
if (!invitedBy) throw ctx.error("BAD_REQUEST", { message: "Invited by user not found" });
|
|
5432
|
+
const invitationEmail = normalizeEmail(ctx.body.email, ctx.context);
|
|
5475
5433
|
return await organizationPlugin.endpoints.createInvitation({
|
|
5476
5434
|
headers: ctx.request?.headers ?? new Headers(),
|
|
5477
5435
|
body: {
|
|
5478
|
-
email:
|
|
5436
|
+
email: invitationEmail,
|
|
5479
5437
|
role: ctx.body.role,
|
|
5480
5438
|
organizationId
|
|
5481
5439
|
},
|
|
@@ -5497,11 +5455,12 @@ const checkUserByEmail = (options) => {
|
|
|
5497
5455
|
use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))]
|
|
5498
5456
|
}, async (ctx) => {
|
|
5499
5457
|
const { organizationId } = ctx.context.payload;
|
|
5458
|
+
const email = normalizeEmail(ctx.body.email, ctx.context);
|
|
5500
5459
|
const user = await ctx.context.adapter.findOne({
|
|
5501
5460
|
model: "user",
|
|
5502
5461
|
where: [{
|
|
5503
5462
|
field: "email",
|
|
5504
|
-
value:
|
|
5463
|
+
value: email
|
|
5505
5464
|
}]
|
|
5506
5465
|
});
|
|
5507
5466
|
if (!user) return {
|
|
@@ -6095,10 +6054,11 @@ const resendInvitation = (options) => {
|
|
|
6095
6054
|
});
|
|
6096
6055
|
if (!invitedByUser) throw ctx.error("BAD_REQUEST", { message: "Inviter user not found" });
|
|
6097
6056
|
if (!organizationPlugin.endpoints?.createInvitation) throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Organization plugin endpoints not available" });
|
|
6057
|
+
const invitationEmail = normalizeEmail(invitation.email, ctx.context);
|
|
6098
6058
|
await organizationPlugin.endpoints.createInvitation({
|
|
6099
6059
|
headers: ctx.request?.headers ?? new Headers(),
|
|
6100
6060
|
body: {
|
|
6101
|
-
email:
|
|
6061
|
+
email: invitationEmail,
|
|
6102
6062
|
role: invitation.role,
|
|
6103
6063
|
organizationId,
|
|
6104
6064
|
resend: true
|
|
@@ -6137,28 +6097,31 @@ const listAllSessions = (options) => {
|
|
|
6137
6097
|
}).optional()
|
|
6138
6098
|
}, async (ctx) => {
|
|
6139
6099
|
const sessionsCount = await ctx.context.adapter.count({ model: "session" });
|
|
6100
|
+
const limit = ctx.query?.limit || sessionsCount;
|
|
6101
|
+
const offset = ctx.query?.offset || 0;
|
|
6140
6102
|
const sessions = await ctx.context.adapter.findMany({
|
|
6141
6103
|
model: "session",
|
|
6142
|
-
limit
|
|
6143
|
-
offset
|
|
6104
|
+
limit,
|
|
6105
|
+
offset,
|
|
6144
6106
|
sortBy: {
|
|
6145
6107
|
field: "createdAt",
|
|
6146
6108
|
direction: "desc"
|
|
6147
|
-
}
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
}
|
|
6109
|
+
},
|
|
6110
|
+
join: { user: true }
|
|
6111
|
+
});
|
|
6112
|
+
const userMap = /* @__PURE__ */ new Map();
|
|
6113
|
+
for (const s of sessions) {
|
|
6114
|
+
const user = Array.isArray(s.user) ? s.user[0] : s.user;
|
|
6115
|
+
if (!user) continue;
|
|
6116
|
+
const { user: _u, ...sessionData } = s;
|
|
6117
|
+
const session = sessionData;
|
|
6118
|
+
if (!userMap.has(user.id)) userMap.set(user.id, {
|
|
6119
|
+
...user,
|
|
6120
|
+
sessions: []
|
|
6121
|
+
});
|
|
6122
|
+
userMap.get(user.id).sessions.push(session);
|
|
6123
|
+
}
|
|
6124
|
+
return Array.from(userMap.values());
|
|
6162
6125
|
});
|
|
6163
6126
|
};
|
|
6164
6127
|
const revokeSession = (options) => createAuthEndpoint("/dash/sessions/revoke", {
|
|
@@ -6267,7 +6230,7 @@ const getUsers = (options) => {
|
|
|
6267
6230
|
totalQuery,
|
|
6268
6231
|
onlineUsersQuery
|
|
6269
6232
|
]);
|
|
6270
|
-
const hasAdminPlugin = ctx.context
|
|
6233
|
+
const hasAdminPlugin = hasPlugin(ctx.context, "admin");
|
|
6271
6234
|
return {
|
|
6272
6235
|
users: users.map((user) => {
|
|
6273
6236
|
const u = user;
|
|
@@ -6292,7 +6255,7 @@ const exportUsers = (options) => {
|
|
|
6292
6255
|
use: [jwtMiddleware(options)],
|
|
6293
6256
|
query: getUsersQuerySchema
|
|
6294
6257
|
}, async (ctx) => {
|
|
6295
|
-
const hasAdminPlugin = ctx.context
|
|
6258
|
+
const hasAdminPlugin = hasPlugin(ctx.context, "admin");
|
|
6296
6259
|
return exportFactory({
|
|
6297
6260
|
model: "user",
|
|
6298
6261
|
limit: ctx.query?.limit,
|
|
@@ -6428,7 +6391,8 @@ const createUser = (options) => {
|
|
|
6428
6391
|
}).passthrough()
|
|
6429
6392
|
}, async (ctx) => {
|
|
6430
6393
|
const userData = ctx.body;
|
|
6431
|
-
|
|
6394
|
+
const email = normalizeEmail(userData.email, ctx.context);
|
|
6395
|
+
if (await ctx.context.internalAdapter.findUserByEmail(email)) throw new APIError("BAD_REQUEST", { message: "User with this email already exist" });
|
|
6432
6396
|
let password = null;
|
|
6433
6397
|
if (userData.generatePassword && !userData.password) password = generateId(12);
|
|
6434
6398
|
else if (userData.password && userData.password.trim() !== "") password = userData.password;
|
|
@@ -6449,6 +6413,7 @@ const createUser = (options) => {
|
|
|
6449
6413
|
}
|
|
6450
6414
|
const user = await ctx.context.internalAdapter.createUser({
|
|
6451
6415
|
...userData,
|
|
6416
|
+
email,
|
|
6452
6417
|
emailVerified: userData.emailVerified,
|
|
6453
6418
|
createdAt: /* @__PURE__ */ new Date(),
|
|
6454
6419
|
updatedAt: /* @__PURE__ */ new Date()
|
|
@@ -6562,7 +6527,7 @@ const getUserDetails = (options) => {
|
|
|
6562
6527
|
}, async (ctx) => {
|
|
6563
6528
|
const { userId } = ctx.context.payload;
|
|
6564
6529
|
const minimal = !!ctx.query?.minimal;
|
|
6565
|
-
const hasAdminPlugin = ctx.context
|
|
6530
|
+
const hasAdminPlugin = hasPlugin(ctx.context, "admin");
|
|
6566
6531
|
const user = await ctx.context.adapter.findOne({
|
|
6567
6532
|
model: "user",
|
|
6568
6533
|
where: [{
|
|
@@ -6632,47 +6597,39 @@ const getUserOrganizations = (options) => {
|
|
|
6632
6597
|
const isOrgEnabled = ctx.context.getPlugin("organization");
|
|
6633
6598
|
if (!isOrgEnabled) return { organizations: [] };
|
|
6634
6599
|
const isTeamEnabled = isOrgEnabled.options?.teams?.enabled;
|
|
6635
|
-
const [
|
|
6600
|
+
const [membersWithOrg, teamMembersWithTeam] = await Promise.all([ctx.context.adapter.findMany({
|
|
6636
6601
|
model: "member",
|
|
6637
6602
|
where: [{
|
|
6638
6603
|
field: "userId",
|
|
6639
6604
|
value: userId
|
|
6640
|
-
}]
|
|
6605
|
+
}],
|
|
6606
|
+
join: { organization: true }
|
|
6641
6607
|
}), isTeamEnabled ? ctx.context.adapter.findMany({
|
|
6642
6608
|
model: "teamMember",
|
|
6643
6609
|
where: [{
|
|
6644
6610
|
field: "userId",
|
|
6645
6611
|
value: userId
|
|
6646
|
-
}]
|
|
6612
|
+
}],
|
|
6613
|
+
join: { team: true }
|
|
6647
6614
|
}).catch((e) => {
|
|
6648
6615
|
ctx.context.logger.error("[Dash] Failed to fetch team members:", e);
|
|
6649
6616
|
return [];
|
|
6650
6617
|
}) : 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
|
-
})) };
|
|
6618
|
+
if (membersWithOrg.length === 0) return { organizations: [] };
|
|
6619
|
+
const teams = teamMembersWithTeam.map((tm) => Array.isArray(tm.team) ? tm.team[0] : tm.team).filter((t) => t != null);
|
|
6620
|
+
return { organizations: membersWithOrg.map((m) => {
|
|
6621
|
+
const organization = Array.isArray(m.organization) ? m.organization[0] : m.organization;
|
|
6622
|
+
if (!organization) return null;
|
|
6623
|
+
return {
|
|
6624
|
+
id: organization.id,
|
|
6625
|
+
name: organization.name,
|
|
6626
|
+
logo: organization.logo,
|
|
6627
|
+
createdAt: organization.createdAt,
|
|
6628
|
+
slug: organization.slug,
|
|
6629
|
+
role: m.role,
|
|
6630
|
+
teams: teams.filter((team) => team.organizationId === organization.id)
|
|
6631
|
+
};
|
|
6632
|
+
}).filter(Boolean) };
|
|
6676
6633
|
});
|
|
6677
6634
|
};
|
|
6678
6635
|
const updateUser = (options) => createAuthEndpoint("/dash/update-user", {
|
|
@@ -8051,4 +8008,4 @@ const dash = (options) => {
|
|
|
8051
8008
|
};
|
|
8052
8009
|
|
|
8053
8010
|
//#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 };
|
|
8011
|
+
export { CHALLENGE_TTL, DEFAULT_DIFFICULTY, EMAIL_TEMPLATES, SMS_TEMPLATES, USER_EVENT_TYPES, createEmailSender, createSMSSender, dash, decodePoWChallenge, encodePoWSolution, normalizeEmail, sendBulkEmails, sendEmail, sendSMS, sentinel, solvePoWChallenge, verifyPoWSolution };
|