@better-auth/infra 0.3.4 → 0.3.5
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/CHANGELOG.md +15 -0
- package/dist/client.mjs +2 -2
- package/dist/{constants-D81vWTXm.mjs → constants-COVOGExF.mjs} +1 -1
- package/dist/{crypto-BlYEkWgT.mjs → crypto-DvC9lT5e.mjs} +1 -1
- package/dist/email.mjs +1 -1
- package/dist/index.d.mts +5 -1
- package/dist/index.mjs +473 -398
- package/dist/native.mjs +2 -2
- package/dist/{pow-retry-BN8NVM3m.mjs → pow-retry-DEaL-FIC.mjs} +1 -1
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as INFRA_USER_AGENT, n as INFRA_API_URL, o as PLUGIN_VERSION, r as INFRA_KV_URL } from "./constants-
|
|
2
|
-
import { a as createAPI, o as createKV, r as hmacSha256Hex } from "./crypto-
|
|
1
|
+
import { i as INFRA_USER_AGENT, n as INFRA_API_URL, o as PLUGIN_VERSION, r as INFRA_KV_URL } from "./constants-COVOGExF.mjs";
|
|
2
|
+
import { a as createAPI, o as createKV, r as hmacSha256Hex } from "./crypto-DvC9lT5e.mjs";
|
|
3
3
|
import { EMAIL_TEMPLATES, createEmailSender, sendBulkEmails, sendEmail } from "./email.mjs";
|
|
4
4
|
import { getCurrentAuthContext } from "@better-auth/core/context";
|
|
5
5
|
import { APIError, generateId, getAuthTables, logger } from "better-auth";
|
|
@@ -148,6 +148,15 @@ const USER_EVENT_TYPES = {
|
|
|
148
148
|
};
|
|
149
149
|
//#endregion
|
|
150
150
|
//#region src/events/core/adapter.ts
|
|
151
|
+
function resolveUserFromContext(userId, ctx) {
|
|
152
|
+
for (const candidate of [ctx.context.session?.user, ctx.context.newSession?.user]) if (candidate?.id === userId) return {
|
|
153
|
+
id: candidate.id,
|
|
154
|
+
name: candidate.name,
|
|
155
|
+
email: candidate.email ?? null,
|
|
156
|
+
..."lastActiveAt" in candidate && candidate.lastActiveAt !== void 0 ? { lastActiveAt: candidate.lastActiveAt } : {}
|
|
157
|
+
};
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
151
160
|
const getUserByEmail = async (email, ctx) => {
|
|
152
161
|
let user = null;
|
|
153
162
|
try {
|
|
@@ -168,25 +177,26 @@ const getUserByEmail = async (email, ctx) => {
|
|
|
168
177
|
}
|
|
169
178
|
return user;
|
|
170
179
|
};
|
|
171
|
-
async function getUserById(userId, ctx) {
|
|
172
|
-
|
|
180
|
+
async function getUserById(userId, ctx, options) {
|
|
181
|
+
const select = [
|
|
182
|
+
"id",
|
|
183
|
+
"name",
|
|
184
|
+
"email"
|
|
185
|
+
];
|
|
186
|
+
if (options?.includeLastActiveAt) select.push("lastActiveAt");
|
|
173
187
|
try {
|
|
174
|
-
|
|
188
|
+
return await ctx.context.adapter.findOne({
|
|
175
189
|
model: "user",
|
|
176
|
-
select
|
|
177
|
-
"id",
|
|
178
|
-
"name",
|
|
179
|
-
"email"
|
|
180
|
-
],
|
|
190
|
+
select,
|
|
181
191
|
where: [{
|
|
182
192
|
field: "id",
|
|
183
193
|
value: userId
|
|
184
194
|
}]
|
|
185
|
-
});
|
|
195
|
+
}) ?? null;
|
|
186
196
|
} catch (error) {
|
|
187
197
|
logger.debug("[Dash] Failed to fetch user info:", error);
|
|
198
|
+
return null;
|
|
188
199
|
}
|
|
189
|
-
return user;
|
|
190
200
|
}
|
|
191
201
|
//#endregion
|
|
192
202
|
//#region src/events/core/events-account.ts
|
|
@@ -293,6 +303,8 @@ const matchesAnyRoute = (path, routes) => {
|
|
|
293
303
|
const cleanPath = stripQuery(path);
|
|
294
304
|
return routes.some((route) => routeToRegex(route).test(cleanPath));
|
|
295
305
|
};
|
|
306
|
+
/** True for `/dash` and any `/dash/*` handler (dashboard admin API). */
|
|
307
|
+
const isDashRoute = (path) => matchesAnyRoute(path, ["/dash"]);
|
|
296
308
|
//#endregion
|
|
297
309
|
//#region src/events/oauth-callback-provider.ts
|
|
298
310
|
/** Provider id from social (`/callback/:id`) or generic OAuth (`/oauth2/callback/:providerId`). */
|
|
@@ -379,10 +391,10 @@ async function resolveOAuthUser(providerId, ctx) {
|
|
|
379
391
|
//#region src/events/core/events-session.ts
|
|
380
392
|
const initSessionEvents = (tracker) => {
|
|
381
393
|
const { trackEvent } = tracker;
|
|
382
|
-
const trackUserSignedIn = (session, trigger, ctx, location) => {
|
|
394
|
+
const trackUserSignedIn = (session, trigger, ctx, location, knownUser) => {
|
|
383
395
|
const track = async () => {
|
|
384
396
|
const loginMethod = session.loginMethod ?? "unknown";
|
|
385
|
-
const user = await getUserById(session.userId, ctx);
|
|
397
|
+
const user = knownUser ?? await getUserById(session.userId, ctx);
|
|
386
398
|
trackEvent({
|
|
387
399
|
eventKey: session.userId,
|
|
388
400
|
eventType: EVENT_TYPES.USER_SIGNED_IN,
|
|
@@ -405,10 +417,10 @@ const initSessionEvents = (tracker) => {
|
|
|
405
417
|
};
|
|
406
418
|
ctx.context.runInBackground(track());
|
|
407
419
|
};
|
|
408
|
-
const trackUserSignedOut = (session, trigger, ctx, location) => {
|
|
420
|
+
const trackUserSignedOut = (session, trigger, ctx, location, knownUser) => {
|
|
409
421
|
const track = async () => {
|
|
410
422
|
const loginMethod = session.loginMethod ?? "unknown";
|
|
411
|
-
const user = await getUserById(session.userId, ctx);
|
|
423
|
+
const user = knownUser ?? await getUserById(session.userId, ctx);
|
|
412
424
|
trackEvent({
|
|
413
425
|
eventKey: session.userId,
|
|
414
426
|
eventType: EVENT_TYPES.USER_SIGNED_OUT,
|
|
@@ -431,10 +443,10 @@ const initSessionEvents = (tracker) => {
|
|
|
431
443
|
};
|
|
432
444
|
ctx.context.runInBackground(track());
|
|
433
445
|
};
|
|
434
|
-
const trackSessionRevoked = (session, trigger, ctx, location) => {
|
|
446
|
+
const trackSessionRevoked = (session, trigger, ctx, location, knownUser) => {
|
|
435
447
|
const track = async () => {
|
|
436
448
|
const loginMethod = session.loginMethod ?? "unknown";
|
|
437
|
-
const user = await getUserById(session.userId, ctx);
|
|
449
|
+
const user = knownUser ?? await getUserById(session.userId, ctx);
|
|
438
450
|
trackEvent({
|
|
439
451
|
eventKey: session.userId,
|
|
440
452
|
eventType: EVENT_TYPES.SESSION_REVOKED,
|
|
@@ -457,9 +469,9 @@ const initSessionEvents = (tracker) => {
|
|
|
457
469
|
};
|
|
458
470
|
ctx.context.runInBackground(track());
|
|
459
471
|
};
|
|
460
|
-
const trackSessionRevokedAll = (session, trigger, ctx, location) => {
|
|
472
|
+
const trackSessionRevokedAll = (session, trigger, ctx, location, knownUser) => {
|
|
461
473
|
const track = async () => {
|
|
462
|
-
const user = await getUserById(session.userId, ctx);
|
|
474
|
+
const user = knownUser ?? await getUserById(session.userId, ctx);
|
|
463
475
|
trackEvent({
|
|
464
476
|
eventKey: session.userId,
|
|
465
477
|
eventType: EVENT_TYPES.ALL_SESSIONS_REVOKED,
|
|
@@ -479,11 +491,11 @@ const initSessionEvents = (tracker) => {
|
|
|
479
491
|
};
|
|
480
492
|
ctx.context.runInBackground(track());
|
|
481
493
|
};
|
|
482
|
-
const trackUserImpersonated = (session, trigger, ctx, location) => {
|
|
494
|
+
const trackUserImpersonated = (session, trigger, ctx, location, knownUser, knownImpersonator) => {
|
|
483
495
|
const track = async () => {
|
|
484
496
|
const loginMethod = session.loginMethod ?? "unknown";
|
|
485
|
-
const user = await getUserById(session.userId, ctx);
|
|
486
|
-
const impersonator = session.impersonatedBy ? await getUserById(session.impersonatedBy, ctx) : null;
|
|
497
|
+
const user = knownUser ?? await getUserById(session.userId, ctx);
|
|
498
|
+
const impersonator = knownImpersonator ?? (session.impersonatedBy ? await getUserById(session.impersonatedBy, ctx) : null);
|
|
487
499
|
trackEvent({
|
|
488
500
|
eventKey: session.userId,
|
|
489
501
|
eventType: EVENT_TYPES.USER_IMPERSONATED,
|
|
@@ -508,11 +520,11 @@ const initSessionEvents = (tracker) => {
|
|
|
508
520
|
};
|
|
509
521
|
ctx.context.runInBackground(track());
|
|
510
522
|
};
|
|
511
|
-
const trackUserImpersonationStop = (session, trigger, ctx, location) => {
|
|
523
|
+
const trackUserImpersonationStop = (session, trigger, ctx, location, knownUser, knownImpersonator) => {
|
|
512
524
|
const track = async () => {
|
|
513
525
|
const loginMethod = session.loginMethod ?? "unknown";
|
|
514
|
-
const user = await getUserById(session.userId, ctx);
|
|
515
|
-
const impersonator = session.impersonatedBy ? await getUserById(session.impersonatedBy, ctx) : null;
|
|
526
|
+
const user = knownUser ?? await getUserById(session.userId, ctx);
|
|
527
|
+
const impersonator = knownImpersonator ?? (session.impersonatedBy ? await getUserById(session.impersonatedBy, ctx) : null);
|
|
516
528
|
trackEvent({
|
|
517
529
|
eventKey: session.userId,
|
|
518
530
|
eventType: EVENT_TYPES.USER_IMPERSONATED_STOPPED,
|
|
@@ -537,10 +549,10 @@ const initSessionEvents = (tracker) => {
|
|
|
537
549
|
};
|
|
538
550
|
ctx.context.runInBackground(track());
|
|
539
551
|
};
|
|
540
|
-
const trackSessionCreated = (session, trigger, ctx, location) => {
|
|
552
|
+
const trackSessionCreated = (session, trigger, ctx, location, knownUser) => {
|
|
541
553
|
const track = async () => {
|
|
542
554
|
const loginMethod = session.loginMethod ?? "unknown";
|
|
543
|
-
const user = await getUserById(session.userId, ctx);
|
|
555
|
+
const user = knownUser ?? await getUserById(session.userId, ctx);
|
|
544
556
|
trackEvent({
|
|
545
557
|
eventKey: session.userId,
|
|
546
558
|
eventType: EVENT_TYPES.SESSION_CREATED,
|
|
@@ -1033,12 +1045,19 @@ function resolveUntrustedVisitorId(visitorId, ip, headerVisitorId) {
|
|
|
1033
1045
|
*
|
|
1034
1046
|
* @param $kv — KV client from {@link createKV} for the same plugin options (one instance per plugin).
|
|
1035
1047
|
*/
|
|
1036
|
-
function createIdentificationMiddleware($kv) {
|
|
1048
|
+
function createIdentificationMiddleware($kv, options) {
|
|
1037
1049
|
return createAuthMiddleware(async (ctx) => {
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1050
|
+
const skipIdentification = options?.skipIdentification?.(ctx) ?? false;
|
|
1051
|
+
let headerVisitorId = null;
|
|
1052
|
+
let requestId = null;
|
|
1053
|
+
if (!skipIdentification) {
|
|
1054
|
+
const extracted = extractIdentificationHeaders(ctx.request);
|
|
1055
|
+
headerVisitorId = extracted.visitorId;
|
|
1056
|
+
requestId = extracted.requestId ?? ctx.getCookie("__infra-rid") ?? null;
|
|
1057
|
+
}
|
|
1040
1058
|
ctx.context.requestId = requestId;
|
|
1041
|
-
if (
|
|
1059
|
+
if (skipIdentification) ctx.context.identification = null;
|
|
1060
|
+
else if (requestId) {
|
|
1042
1061
|
if (ctx.context.identification === void 0) ctx.context.identification = await getIdentification(requestId, $kv) ?? null;
|
|
1043
1062
|
} else ctx.context.identification = null;
|
|
1044
1063
|
const identification = ctx.context.identification;
|
|
@@ -1064,7 +1083,7 @@ function createIdentificationMiddleware($kv) {
|
|
|
1064
1083
|
else location = void 0;
|
|
1065
1084
|
ctx.context.location = location;
|
|
1066
1085
|
ctx.context.ip = resolveSecurityIp(identification, location);
|
|
1067
|
-
ctx.context.untrustedVisitorId = resolveUntrustedVisitorId(visitorId, ctx.context.ip, headerVisitorId);
|
|
1086
|
+
ctx.context.untrustedVisitorId = resolveUntrustedVisitorId(visitorId, ctx.context.ip, skipIdentification ? null : headerVisitorId);
|
|
1068
1087
|
});
|
|
1069
1088
|
}
|
|
1070
1089
|
/**
|
|
@@ -2210,6 +2229,13 @@ const sentinel = (options) => {
|
|
|
2210
2229
|
create: {
|
|
2211
2230
|
async before(user, ctx) {
|
|
2212
2231
|
if (!ctx) return;
|
|
2232
|
+
if (isDashRoute(ctx.path)) {
|
|
2233
|
+
if (user.email && typeof user.email === "string" && isEmailNormalizationEnabled(opts.security)) return { data: {
|
|
2234
|
+
...user,
|
|
2235
|
+
email: normalizeEmail(user.email, ctx.context)
|
|
2236
|
+
} };
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2213
2239
|
const visitorId = ctx.context.visitorId;
|
|
2214
2240
|
if (opts.security?.freeTrialAbuse?.enabled) {
|
|
2215
2241
|
if (!visitorId) {
|
|
@@ -2237,7 +2263,7 @@ const sentinel = (options) => {
|
|
|
2237
2263
|
} };
|
|
2238
2264
|
},
|
|
2239
2265
|
async after(user, ctx) {
|
|
2240
|
-
if (!ctx) return;
|
|
2266
|
+
if (!ctx || isDashRoute(ctx.path)) return;
|
|
2241
2267
|
const visitorId = ctx.context.visitorId;
|
|
2242
2268
|
const reservationId = ctx.context.freeTrialReservationId;
|
|
2243
2269
|
if (visitorId && reservationId && opts.security?.freeTrialAbuse?.enabled) await ctx.context.runInBackgroundOrAwait(securityService.confirmFreeTrialSignup(visitorId, reservationId, user.id, ctx.context.requestId ?? null));
|
|
@@ -2253,7 +2279,7 @@ const sentinel = (options) => {
|
|
|
2253
2279
|
},
|
|
2254
2280
|
session: { create: {
|
|
2255
2281
|
async before(session, ctx) {
|
|
2256
|
-
if (!ctx) return;
|
|
2282
|
+
if (!ctx || isDashRoute(ctx.path)) return;
|
|
2257
2283
|
const visitorId = ctx.context.visitorId;
|
|
2258
2284
|
const identification = ctx.context.identification;
|
|
2259
2285
|
if (session.userId && identification?.location && visitorId) {
|
|
@@ -2286,24 +2312,12 @@ const sentinel = (options) => {
|
|
|
2286
2312
|
}
|
|
2287
2313
|
},
|
|
2288
2314
|
async after(session, ctx) {
|
|
2289
|
-
if (!ctx || !session.userId) return;
|
|
2315
|
+
if (!ctx || !session.userId || isDashRoute(ctx.path)) return;
|
|
2290
2316
|
const visitorId = ctx.context.visitorId;
|
|
2291
2317
|
const identification = ctx.context.identification;
|
|
2292
2318
|
let user = null;
|
|
2293
|
-
const userSelect = [
|
|
2294
|
-
"email",
|
|
2295
|
-
"name",
|
|
2296
|
-
...activityTrackingEnabled ? ["lastActiveAt"] : []
|
|
2297
|
-
];
|
|
2298
2319
|
try {
|
|
2299
|
-
user = await ctx
|
|
2300
|
-
model: "user",
|
|
2301
|
-
select: [...userSelect],
|
|
2302
|
-
where: [{
|
|
2303
|
-
field: "id",
|
|
2304
|
-
value: session.userId
|
|
2305
|
-
}]
|
|
2306
|
-
});
|
|
2320
|
+
user = await getUserById(session.userId, ctx, { includeLastActiveAt: activityTrackingEnabled });
|
|
2307
2321
|
} catch (error) {
|
|
2308
2322
|
logger.warn("[Sentinel] Failed to fetch user for security checks:", error);
|
|
2309
2323
|
}
|
|
@@ -2349,12 +2363,12 @@ const sentinel = (options) => {
|
|
|
2349
2363
|
before: [
|
|
2350
2364
|
{
|
|
2351
2365
|
matcher: (ctx) => ctx.request?.method !== "GET",
|
|
2352
|
-
handler: createIdentificationMiddleware($kv)
|
|
2366
|
+
handler: createIdentificationMiddleware($kv, { skipIdentification: (ctx) => isDashRoute(ctx.path) })
|
|
2353
2367
|
},
|
|
2354
2368
|
...emailHooks.before,
|
|
2355
2369
|
...phoneValidationHooks.before,
|
|
2356
2370
|
{
|
|
2357
|
-
matcher: (ctx) => ctx.request?.method !== "GET",
|
|
2371
|
+
matcher: (ctx) => ctx.request?.method !== "GET" && !isDashRoute(ctx.path),
|
|
2358
2372
|
handler: createAuthMiddleware(async (ctx) => {
|
|
2359
2373
|
const visitorId = ctx.context.visitorId;
|
|
2360
2374
|
const identification = ctx.context.identification;
|
|
@@ -2448,7 +2462,7 @@ const sentinel = (options) => {
|
|
|
2448
2462
|
}
|
|
2449
2463
|
],
|
|
2450
2464
|
after: [{
|
|
2451
|
-
matcher: (ctx) => ctx.request?.method !== "GET",
|
|
2465
|
+
matcher: (ctx) => ctx.request?.method !== "GET" && !isDashRoute(ctx.path),
|
|
2452
2466
|
handler: createAuthMiddleware(async (ctx) => {
|
|
2453
2467
|
const untrustedVisitorId = ctx.context.untrustedVisitorId;
|
|
2454
2468
|
const ip = ctx.context.ip;
|
|
@@ -3332,7 +3346,7 @@ async function buildSyntheticSession(ctx, ownerUserId) {
|
|
|
3332
3346
|
}
|
|
3333
3347
|
//#endregion
|
|
3334
3348
|
//#region src/routes/directory-sync/index.ts
|
|
3335
|
-
async function
|
|
3349
|
+
async function getLegacyScimProviderOwnerUserId(ctx, organizationId, providerId) {
|
|
3336
3350
|
const where = [{
|
|
3337
3351
|
field: "organizationId",
|
|
3338
3352
|
value: organizationId
|
|
@@ -3347,6 +3361,44 @@ async function getScimProviderOwner(ctx, organizationId, providerId) {
|
|
|
3347
3361
|
select: ["userId"]
|
|
3348
3362
|
}))?.userId ?? null;
|
|
3349
3363
|
}
|
|
3364
|
+
function parseMemberRoles(role) {
|
|
3365
|
+
return role.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
3366
|
+
}
|
|
3367
|
+
function getScimManagementRoles(ctx) {
|
|
3368
|
+
const creatorRole = ctx.context.getPlugin("organization")?.options?.creatorRole ?? "owner";
|
|
3369
|
+
return Array.from(new Set(["admin", creatorRole]));
|
|
3370
|
+
}
|
|
3371
|
+
async function findOrganizationScimManagerUserId(ctx, organizationId) {
|
|
3372
|
+
const requiredRoles = getScimManagementRoles(ctx);
|
|
3373
|
+
const members = await ctx.context.adapter.findMany({
|
|
3374
|
+
model: "member",
|
|
3375
|
+
where: [{
|
|
3376
|
+
field: "organizationId",
|
|
3377
|
+
value: organizationId
|
|
3378
|
+
}],
|
|
3379
|
+
select: ["userId", "role"]
|
|
3380
|
+
});
|
|
3381
|
+
for (const member of members) if (parseMemberRoles(member.role).some((role) => requiredRoles.includes(role))) return member.userId;
|
|
3382
|
+
return null;
|
|
3383
|
+
}
|
|
3384
|
+
async function organizationHasScimProviders(ctx, organizationId) {
|
|
3385
|
+
return await ctx.context.adapter.findOne({
|
|
3386
|
+
model: "scimProvider",
|
|
3387
|
+
where: [{
|
|
3388
|
+
field: "organizationId",
|
|
3389
|
+
value: organizationId
|
|
3390
|
+
}],
|
|
3391
|
+
select: ["id"]
|
|
3392
|
+
}) != null;
|
|
3393
|
+
}
|
|
3394
|
+
/** User id for synthetic SCIM management sessions (list/delete/regenerate). */
|
|
3395
|
+
async function resolveScimManagementUserId(ctx, organizationId, providerId) {
|
|
3396
|
+
if (providerId) {
|
|
3397
|
+
if (!await scimProviderExists(ctx, organizationId, providerId)) return null;
|
|
3398
|
+
} else if (!await organizationHasScimProviders(ctx, organizationId)) return null;
|
|
3399
|
+
if (isVersionAtLeast(ctx.context.version, "1.7.0")) return findOrganizationScimManagerUserId(ctx, organizationId);
|
|
3400
|
+
return getLegacyScimProviderOwnerUserId(ctx, organizationId, providerId);
|
|
3401
|
+
}
|
|
3350
3402
|
async function scimProviderExists(ctx, organizationId, providerId) {
|
|
3351
3403
|
return await ctx.context.adapter.findOne({
|
|
3352
3404
|
model: "scimProvider",
|
|
@@ -3391,14 +3443,14 @@ const listOrganizationDirectories = (options) => {
|
|
|
3391
3443
|
ctx.context.logger.warn("[Dash] SCIM plugin not available or provider ownership disabled, returning empty directories list", { organizationId });
|
|
3392
3444
|
return [];
|
|
3393
3445
|
}
|
|
3394
|
-
const
|
|
3395
|
-
if (!
|
|
3396
|
-
ctx.context.logger.warn("[Dash] No SCIM provider
|
|
3446
|
+
const managerUserId = await resolveScimManagementUserId(ctx, organizationId);
|
|
3447
|
+
if (!managerUserId) {
|
|
3448
|
+
ctx.context.logger.warn("[Dash] No SCIM provider or organization manager found, returning empty directories list", { organizationId });
|
|
3397
3449
|
return [];
|
|
3398
3450
|
}
|
|
3399
3451
|
try {
|
|
3400
3452
|
const { providers } = await scimPlugin.endpoints.listSCIMProviderConnections({
|
|
3401
|
-
context: await buildSyntheticSession(ctx,
|
|
3453
|
+
context: await buildSyntheticSession(ctx, managerUserId),
|
|
3402
3454
|
headers: ctx.request?.headers ?? new Headers()
|
|
3403
3455
|
});
|
|
3404
3456
|
return providers.filter((p) => p.organizationId === organizationId).map((provider) => ({
|
|
@@ -3463,11 +3515,14 @@ const deleteOrganizationDirectory = (options) => {
|
|
|
3463
3515
|
if (!scimPlugin?.endpoints.deleteSCIMProviderConnection || !isScimDirectorySyncEnabled(scimPlugin, ctx.context.version)) throw ctx.error("BAD_REQUEST", { message: "SCIM plugin is not enabled or does not support this feature" });
|
|
3464
3516
|
const { organizationId } = ctx.context.payload;
|
|
3465
3517
|
const { providerId } = ctx.body;
|
|
3466
|
-
const
|
|
3467
|
-
if (!
|
|
3518
|
+
const managerUserId = await resolveScimManagementUserId(ctx, organizationId, providerId);
|
|
3519
|
+
if (!managerUserId) throw ctx.error("NOT_FOUND", { message: "Directory sync connection not found or missing owner" });
|
|
3468
3520
|
await scimPlugin.endpoints.deleteSCIMProviderConnection({
|
|
3469
|
-
body: {
|
|
3470
|
-
|
|
3521
|
+
body: {
|
|
3522
|
+
providerId,
|
|
3523
|
+
organizationId
|
|
3524
|
+
},
|
|
3525
|
+
context: await buildSyntheticSession(ctx, managerUserId),
|
|
3471
3526
|
headers: ctx.request?.headers ?? new Headers()
|
|
3472
3527
|
});
|
|
3473
3528
|
return { success: true };
|
|
@@ -3484,14 +3539,14 @@ const regenerateDirectoryToken = (options) => {
|
|
|
3484
3539
|
if (!scimPlugin?.endpoints.generateSCIMToken || !isScimDirectorySyncEnabled(scimPlugin, ctx.context.version)) throw ctx.error("BAD_REQUEST", { message: "SCIM plugin is not enabled or does not support this feature" });
|
|
3485
3540
|
const { organizationId } = ctx.context.payload;
|
|
3486
3541
|
const { providerId } = ctx.body;
|
|
3487
|
-
const
|
|
3488
|
-
if (!
|
|
3542
|
+
const managerUserId = await resolveScimManagementUserId(ctx, organizationId, providerId);
|
|
3543
|
+
if (!managerUserId) throw ctx.error("NOT_FOUND", { message: "Directory sync connection not found or missing owner" });
|
|
3489
3544
|
const { scimToken } = await scimPlugin.endpoints.generateSCIMToken({
|
|
3490
3545
|
body: {
|
|
3491
3546
|
providerId,
|
|
3492
3547
|
organizationId
|
|
3493
3548
|
},
|
|
3494
|
-
context: await buildSyntheticSession(ctx,
|
|
3549
|
+
context: await buildSyntheticSession(ctx, managerUserId),
|
|
3495
3550
|
headers: ctx.request?.headers ?? new Headers()
|
|
3496
3551
|
});
|
|
3497
3552
|
if (!scimToken) throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Failed to regenerate directory sync token" });
|
|
@@ -4251,6 +4306,13 @@ const checkUserExists = (options) => {
|
|
|
4251
4306
|
});
|
|
4252
4307
|
};
|
|
4253
4308
|
//#endregion
|
|
4309
|
+
//#region src/db-types.ts
|
|
4310
|
+
/** Unwrap a joined relation that may be a single item or an array. */
|
|
4311
|
+
function joinedRelation(value) {
|
|
4312
|
+
if (value == null) return null;
|
|
4313
|
+
return (Array.isArray(value) ? value[0] : value) ?? null;
|
|
4314
|
+
}
|
|
4315
|
+
//#endregion
|
|
4254
4316
|
//#region src/routes/organizations/roles.ts
|
|
4255
4317
|
const DEFAULT_ORG_MEMBER_ROLES = [
|
|
4256
4318
|
"member",
|
|
@@ -4268,6 +4330,14 @@ function validateOrganizationMemberRole(ctx, role, orgOptions) {
|
|
|
4268
4330
|
if (!allowedRoles.includes(role)) throw ctx.error("BAD_REQUEST", { message: `Invalid role. Allowed roles: ${allowedRoles.join(", ")}` });
|
|
4269
4331
|
}
|
|
4270
4332
|
//#endregion
|
|
4333
|
+
//#region src/routes/organizations/types.ts
|
|
4334
|
+
const ORGANIZATION_USER_PREVIEW_SELECT = [
|
|
4335
|
+
"id",
|
|
4336
|
+
"name",
|
|
4337
|
+
"email",
|
|
4338
|
+
"image"
|
|
4339
|
+
];
|
|
4340
|
+
//#endregion
|
|
4271
4341
|
//#region src/routes/organizations/invitations.ts
|
|
4272
4342
|
const listOrganizationInvitations = (options) => {
|
|
4273
4343
|
return createAuthEndpoint("/dash/organization/:id/invitations", {
|
|
@@ -4293,7 +4363,9 @@ const listOrganizationInvitations = (options) => {
|
|
|
4293
4363
|
field: "email",
|
|
4294
4364
|
value: emails,
|
|
4295
4365
|
operator: "in"
|
|
4296
|
-
}]
|
|
4366
|
+
}],
|
|
4367
|
+
select: [...ORGANIZATION_USER_PREVIEW_SELECT],
|
|
4368
|
+
limit: emails.length
|
|
4297
4369
|
}) : [];
|
|
4298
4370
|
const userByEmail = new Map(users.filter((u) => u.email != null).map((u) => [normalizeEmail(u.email, ctx.context), u]));
|
|
4299
4371
|
return invitations.map((invitation) => {
|
|
@@ -4305,7 +4377,7 @@ const listOrganizationInvitations = (options) => {
|
|
|
4305
4377
|
id: user.id,
|
|
4306
4378
|
name: user.name,
|
|
4307
4379
|
email: user.email ?? void 0,
|
|
4308
|
-
image: user.image
|
|
4380
|
+
image: user.image ?? null
|
|
4309
4381
|
} : null
|
|
4310
4382
|
};
|
|
4311
4383
|
});
|
|
@@ -4369,7 +4441,8 @@ const checkUserByEmail = (options) => {
|
|
|
4369
4441
|
where: [{
|
|
4370
4442
|
field: "email",
|
|
4371
4443
|
value: email
|
|
4372
|
-
}]
|
|
4444
|
+
}],
|
|
4445
|
+
select: [...ORGANIZATION_USER_PREVIEW_SELECT]
|
|
4373
4446
|
});
|
|
4374
4447
|
if (!user) return {
|
|
4375
4448
|
exists: false,
|
|
@@ -4410,7 +4483,7 @@ const cancelInvitation = (options) => {
|
|
|
4410
4483
|
const orgOptions = requireOrganizationPlugin(ctx).options || {};
|
|
4411
4484
|
const { invitationId, organizationId } = ctx.context.payload;
|
|
4412
4485
|
if (ctx.body.invitationId !== invitationId) throw ctx.error("BAD_REQUEST", { message: "Invitation ID mismatch" });
|
|
4413
|
-
const
|
|
4486
|
+
const invitationRow = await ctx.context.adapter.findOne({
|
|
4414
4487
|
model: "invitation",
|
|
4415
4488
|
where: [{
|
|
4416
4489
|
field: "id",
|
|
@@ -4418,24 +4491,17 @@ const cancelInvitation = (options) => {
|
|
|
4418
4491
|
}, {
|
|
4419
4492
|
field: "organizationId",
|
|
4420
4493
|
value: organizationId
|
|
4421
|
-
}]
|
|
4494
|
+
}],
|
|
4495
|
+
join: {
|
|
4496
|
+
user: true,
|
|
4497
|
+
organization: true
|
|
4498
|
+
}
|
|
4422
4499
|
});
|
|
4423
|
-
if (!
|
|
4424
|
-
const
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
value: invitation.inviterId
|
|
4429
|
-
}]
|
|
4430
|
-
}), ctx.context.adapter.findOne({
|
|
4431
|
-
model: "organization",
|
|
4432
|
-
where: [{
|
|
4433
|
-
field: "id",
|
|
4434
|
-
value: organizationId
|
|
4435
|
-
}]
|
|
4436
|
-
})]);
|
|
4437
|
-
if (!user) throw ctx.error("NOT_FOUND", { message: "Inviter user not found" });
|
|
4438
|
-
if (!organization) throw ctx.error("NOT_FOUND", { message: "Organization not found" });
|
|
4500
|
+
if (!invitationRow) throw ctx.error("NOT_FOUND", { message: "Invitation not found" });
|
|
4501
|
+
const user = joinedRelation(invitationRow.user);
|
|
4502
|
+
const organization = joinedRelation(invitationRow.organization);
|
|
4503
|
+
if (!user || !organization) throw ctx.error("NOT_FOUND", { message: "Inviter not found or is not associated with this invitation" });
|
|
4504
|
+
const invitation = invitationRow;
|
|
4439
4505
|
if (orgOptions?.organizationHooks?.beforeCancelInvitation) await orgOptions.organizationHooks.beforeCancelInvitation({
|
|
4440
4506
|
invitation,
|
|
4441
4507
|
organization,
|
|
@@ -4475,7 +4541,7 @@ const resendInvitation = (options) => {
|
|
|
4475
4541
|
const organizationPlugin = requireOrganizationPlugin(ctx);
|
|
4476
4542
|
const { invitationId, organizationId } = ctx.context.payload;
|
|
4477
4543
|
if (ctx.body.invitationId !== invitationId) throw ctx.error("BAD_REQUEST", { message: "Invitation ID mismatch" });
|
|
4478
|
-
const
|
|
4544
|
+
const invitationRow = await ctx.context.adapter.findOne({
|
|
4479
4545
|
model: "invitation",
|
|
4480
4546
|
where: [{
|
|
4481
4547
|
field: "id",
|
|
@@ -4483,18 +4549,14 @@ const resendInvitation = (options) => {
|
|
|
4483
4549
|
}, {
|
|
4484
4550
|
field: "organizationId",
|
|
4485
4551
|
value: organizationId
|
|
4486
|
-
}]
|
|
4552
|
+
}],
|
|
4553
|
+
join: { user: true }
|
|
4487
4554
|
});
|
|
4488
|
-
if (!
|
|
4555
|
+
if (!invitationRow) throw ctx.error("NOT_FOUND", { message: "Invitation not found" });
|
|
4556
|
+
const invitation = invitationRow;
|
|
4489
4557
|
if (invitation.status !== "pending") throw ctx.error("BAD_REQUEST", { message: "Only pending invitations can be resent" });
|
|
4490
|
-
const invitedByUser =
|
|
4491
|
-
|
|
4492
|
-
where: [{
|
|
4493
|
-
field: "id",
|
|
4494
|
-
value: invitation.inviterId
|
|
4495
|
-
}]
|
|
4496
|
-
});
|
|
4497
|
-
if (!invitedByUser) throw ctx.error("BAD_REQUEST", { message: "Inviter user not found" });
|
|
4558
|
+
const invitedByUser = joinedRelation(invitationRow.user);
|
|
4559
|
+
if (!invitedByUser) throw ctx.error("BAD_REQUEST", { message: "Inviter not found or is not associated with this invitation" });
|
|
4498
4560
|
if (!organizationPlugin.endpoints?.createInvitation) throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Organization plugin endpoints not available" });
|
|
4499
4561
|
const invitationEmail = normalizeEmail(invitation.email, ctx.context);
|
|
4500
4562
|
await organizationPlugin.endpoints.createInvitation({
|
|
@@ -4529,15 +4591,14 @@ const listOrganizationMembers = (options) => {
|
|
|
4529
4591
|
return [];
|
|
4530
4592
|
}
|
|
4531
4593
|
const organizationId = tryDecode(ctx.params.id);
|
|
4532
|
-
const members = await ctx.context.adapter.findMany({
|
|
4594
|
+
const [members, invitations] = await Promise.all([ctx.context.adapter.findMany({
|
|
4533
4595
|
model: "member",
|
|
4534
4596
|
where: [{
|
|
4535
4597
|
field: "organizationId",
|
|
4536
4598
|
value: organizationId
|
|
4537
4599
|
}],
|
|
4538
4600
|
join: { user: true }
|
|
4539
|
-
})
|
|
4540
|
-
const invitations = await ctx.context.adapter.findMany({
|
|
4601
|
+
}), ctx.context.adapter.findMany({
|
|
4541
4602
|
model: "invitation",
|
|
4542
4603
|
where: [{
|
|
4543
4604
|
field: "organizationId",
|
|
@@ -4547,16 +4608,16 @@ const listOrganizationMembers = (options) => {
|
|
|
4547
4608
|
value: "accepted"
|
|
4548
4609
|
}],
|
|
4549
4610
|
join: { user: true }
|
|
4550
|
-
});
|
|
4611
|
+
})]);
|
|
4551
4612
|
const inviterById = /* @__PURE__ */ new Map();
|
|
4552
4613
|
for (const inv of invitations) {
|
|
4553
|
-
const inviter =
|
|
4614
|
+
const inviter = joinedRelation(inv.user);
|
|
4554
4615
|
if (inviter && inv.inviterId) inviterById.set(inv.inviterId, inviter);
|
|
4555
4616
|
}
|
|
4556
4617
|
const invitationByEmail = new Map(invitations.map((i) => [i.email.toLowerCase(), i]));
|
|
4557
4618
|
const result = [];
|
|
4558
4619
|
for (const m of members) {
|
|
4559
|
-
const joinedUser =
|
|
4620
|
+
const joinedUser = joinedRelation(m.user);
|
|
4560
4621
|
if (!joinedUser) continue;
|
|
4561
4622
|
const invitation = joinedUser.email ? invitationByEmail.get(joinedUser.email.toLowerCase()) : void 0;
|
|
4562
4623
|
const inviter = invitation ? inviterById.get(invitation.inviterId) : void 0;
|
|
@@ -4646,7 +4707,7 @@ const removeMember = (options) => {
|
|
|
4646
4707
|
}, async (ctx) => {
|
|
4647
4708
|
const { organizationId } = ctx.context.payload;
|
|
4648
4709
|
const orgOptions = requireOrganizationPlugin(ctx).options || {};
|
|
4649
|
-
const
|
|
4710
|
+
const memberRow = await ctx.context.adapter.findOne({
|
|
4650
4711
|
model: "member",
|
|
4651
4712
|
where: [{
|
|
4652
4713
|
field: "id",
|
|
@@ -4654,24 +4715,17 @@ const removeMember = (options) => {
|
|
|
4654
4715
|
}, {
|
|
4655
4716
|
field: "organizationId",
|
|
4656
4717
|
value: organizationId
|
|
4657
|
-
}]
|
|
4718
|
+
}],
|
|
4719
|
+
join: {
|
|
4720
|
+
user: true,
|
|
4721
|
+
organization: true
|
|
4722
|
+
}
|
|
4658
4723
|
});
|
|
4659
|
-
if (!
|
|
4660
|
-
const
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
value: member.userId
|
|
4665
|
-
}]
|
|
4666
|
-
}), ctx.context.adapter.findOne({
|
|
4667
|
-
model: "organization",
|
|
4668
|
-
where: [{
|
|
4669
|
-
field: "id",
|
|
4670
|
-
value: organizationId
|
|
4671
|
-
}]
|
|
4672
|
-
})]);
|
|
4673
|
-
if (!user) throw ctx.error("NOT_FOUND", { message: "User not found" });
|
|
4674
|
-
if (!organization) throw ctx.error("NOT_FOUND", { message: "Organization not found" });
|
|
4724
|
+
if (!memberRow) throw ctx.error("NOT_FOUND", { message: "Member not found" });
|
|
4725
|
+
const user = joinedRelation(memberRow.user);
|
|
4726
|
+
const organization = joinedRelation(memberRow.organization);
|
|
4727
|
+
if (!user || !organization) throw ctx.error("NOT_FOUND", { message: "User not found or is not associated with this member" });
|
|
4728
|
+
const member = memberRow;
|
|
4675
4729
|
if (orgOptions?.organizationHooks?.beforeRemoveMember) await orgOptions.organizationHooks.beforeRemoveMember({
|
|
4676
4730
|
member,
|
|
4677
4731
|
user,
|
|
@@ -4727,7 +4781,7 @@ const updateMemberRole = (options) => {
|
|
|
4727
4781
|
const { organizationId } = ctx.context.payload;
|
|
4728
4782
|
const orgOptions = requireOrganizationPlugin(ctx).options || {};
|
|
4729
4783
|
validateOrganizationMemberRole(ctx, ctx.body.role, orgOptions);
|
|
4730
|
-
const
|
|
4784
|
+
const memberRow = await ctx.context.adapter.findOne({
|
|
4731
4785
|
model: "member",
|
|
4732
4786
|
where: [{
|
|
4733
4787
|
field: "id",
|
|
@@ -4735,24 +4789,17 @@ const updateMemberRole = (options) => {
|
|
|
4735
4789
|
}, {
|
|
4736
4790
|
field: "organizationId",
|
|
4737
4791
|
value: organizationId
|
|
4738
|
-
}]
|
|
4792
|
+
}],
|
|
4793
|
+
join: {
|
|
4794
|
+
user: true,
|
|
4795
|
+
organization: true
|
|
4796
|
+
}
|
|
4739
4797
|
});
|
|
4740
|
-
if (!
|
|
4741
|
-
const
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
value: existingMember.userId
|
|
4746
|
-
}]
|
|
4747
|
-
}), ctx.context.adapter.findOne({
|
|
4748
|
-
model: "organization",
|
|
4749
|
-
where: [{
|
|
4750
|
-
field: "id",
|
|
4751
|
-
value: organizationId
|
|
4752
|
-
}]
|
|
4753
|
-
})]);
|
|
4754
|
-
if (!user) throw ctx.error("NOT_FOUND", { message: "User not found" });
|
|
4755
|
-
if (!organization) throw ctx.error("NOT_FOUND", { message: "Organization not found" });
|
|
4798
|
+
if (!memberRow) throw ctx.error("NOT_FOUND", { message: "Member not found" });
|
|
4799
|
+
const user = joinedRelation(memberRow.user);
|
|
4800
|
+
const organization = joinedRelation(memberRow.organization);
|
|
4801
|
+
if (!user || !organization) throw ctx.error("NOT_FOUND", { message: "User not found or is not associated with this member" });
|
|
4802
|
+
const existingMember = memberRow;
|
|
4756
4803
|
const previousRole = existingMember.role;
|
|
4757
4804
|
let newRole = ctx.body.role;
|
|
4758
4805
|
if (orgOptions?.organizationHooks?.beforeUpdateMemberRole) {
|
|
@@ -5001,6 +5048,39 @@ function validateWritableOrganizationUpdateFields(data, orgOptions) {
|
|
|
5001
5048
|
}
|
|
5002
5049
|
//#endregion
|
|
5003
5050
|
//#region src/routes/organizations/organization.ts
|
|
5051
|
+
const MEMBER_COUNT_BATCH_SIZE = 1e4;
|
|
5052
|
+
const MEMBER_COUNT_ORG_CHUNK_SIZE = 200;
|
|
5053
|
+
async function countMembersForOrganizationIdChunk(ctx, orgIdChunk) {
|
|
5054
|
+
const counts = /* @__PURE__ */ new Map();
|
|
5055
|
+
for (const orgId of orgIdChunk) counts.set(orgId, 0);
|
|
5056
|
+
let offset = 0;
|
|
5057
|
+
while (true) {
|
|
5058
|
+
const batch = await ctx.context.adapter.findMany({
|
|
5059
|
+
model: "member",
|
|
5060
|
+
where: [{
|
|
5061
|
+
field: "organizationId",
|
|
5062
|
+
value: orgIdChunk,
|
|
5063
|
+
operator: "in"
|
|
5064
|
+
}],
|
|
5065
|
+
select: ["organizationId"],
|
|
5066
|
+
limit: MEMBER_COUNT_BATCH_SIZE,
|
|
5067
|
+
offset
|
|
5068
|
+
});
|
|
5069
|
+
for (const row of batch) counts.set(row.organizationId, (counts.get(row.organizationId) ?? 0) + 1);
|
|
5070
|
+
if (batch.length < MEMBER_COUNT_BATCH_SIZE) break;
|
|
5071
|
+
offset += MEMBER_COUNT_BATCH_SIZE;
|
|
5072
|
+
}
|
|
5073
|
+
return counts;
|
|
5074
|
+
}
|
|
5075
|
+
/** Count members per org in batched reads instead of one count query per org. */
|
|
5076
|
+
async function countMembersByOrganizationIds(ctx, orgIds) {
|
|
5077
|
+
const counts = /* @__PURE__ */ new Map();
|
|
5078
|
+
for (const orgId of orgIds) counts.set(orgId, 0);
|
|
5079
|
+
if (orgIds.length === 0) return counts;
|
|
5080
|
+
const chunkMaps = await withConcurrency(chunkArray(orgIds, { batchSize: MEMBER_COUNT_ORG_CHUNK_SIZE }), (orgIdChunk) => countMembersForOrganizationIdChunk(ctx, orgIdChunk), { concurrency: 5 });
|
|
5081
|
+
for (const chunkMap of chunkMaps) for (const [orgId, count] of chunkMap) counts.set(orgId, count);
|
|
5082
|
+
return counts;
|
|
5083
|
+
}
|
|
5004
5084
|
const listOrganizations = (options) => {
|
|
5005
5085
|
return createAuthEndpoint("/dash/list-organizations", {
|
|
5006
5086
|
method: "GET",
|
|
@@ -5082,15 +5162,7 @@ const listOrganizations = (options) => {
|
|
|
5082
5162
|
model: "organization",
|
|
5083
5163
|
where
|
|
5084
5164
|
})]);
|
|
5085
|
-
const
|
|
5086
|
-
const memberCounts = await withConcurrency(orgIds, (orgId) => ctx.context.adapter.count({
|
|
5087
|
-
model: "member",
|
|
5088
|
-
where: [{
|
|
5089
|
-
field: "organizationId",
|
|
5090
|
-
value: orgId
|
|
5091
|
-
}]
|
|
5092
|
-
}), { concurrency: 10 });
|
|
5093
|
-
const memberCountByOrg = new Map(orgIds.map((orgId, i) => [orgId, memberCounts[i]]));
|
|
5165
|
+
const memberCountByOrg = await countMembersByOrganizationIds(ctx, organizations.map((o) => o.id));
|
|
5094
5166
|
let withCounts = organizations.map((organization) => {
|
|
5095
5167
|
const memberCount = memberCountByOrg.get(organization.id) ?? 0;
|
|
5096
5168
|
return {
|
|
@@ -5123,6 +5195,7 @@ const listOrganizations = (options) => {
|
|
|
5123
5195
|
value: Array.from(allUserIds),
|
|
5124
5196
|
operator: "in"
|
|
5125
5197
|
}],
|
|
5198
|
+
select: [...ORGANIZATION_USER_PREVIEW_SELECT],
|
|
5126
5199
|
limit: allUserIds.size
|
|
5127
5200
|
}) : [];
|
|
5128
5201
|
const userMap = new Map(users.map((u) => [u.id, u]));
|
|
@@ -5229,7 +5302,7 @@ const deleteOrganization = (options) => {
|
|
|
5229
5302
|
const { organizationId: bodyOrganizationId } = ctx.body;
|
|
5230
5303
|
const orgOptions = requireOrganizationPlugin(ctx).options || {};
|
|
5231
5304
|
if (organizationId !== bodyOrganizationId) throw ctx.error("BAD_REQUEST", { message: "Organization ID mismatch" });
|
|
5232
|
-
const
|
|
5305
|
+
const owner = (await ctx.context.adapter.findMany({
|
|
5233
5306
|
model: "member",
|
|
5234
5307
|
where: [{
|
|
5235
5308
|
field: "organizationId",
|
|
@@ -5243,20 +5316,14 @@ const deleteOrganization = (options) => {
|
|
|
5243
5316
|
direction: "asc"
|
|
5244
5317
|
},
|
|
5245
5318
|
limit: 1,
|
|
5246
|
-
join: {
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
const organization =
|
|
5253
|
-
|
|
5254
|
-
where: [{
|
|
5255
|
-
field: "id",
|
|
5256
|
-
value: organizationId
|
|
5257
|
-
}]
|
|
5258
|
-
});
|
|
5259
|
-
if (!organization) throw ctx.error("NOT_FOUND", { message: "Organization not found" });
|
|
5319
|
+
join: {
|
|
5320
|
+
user: true,
|
|
5321
|
+
organization: true
|
|
5322
|
+
}
|
|
5323
|
+
}))[0];
|
|
5324
|
+
const deletedByUser = owner ? joinedRelation(owner.user) : null;
|
|
5325
|
+
const organization = owner ? joinedRelation(owner.organization) : null;
|
|
5326
|
+
if (!owner || !deletedByUser || !organization) throw ctx.error("NOT_FOUND", { message: "Organization owner not found" });
|
|
5260
5327
|
if (orgOptions?.organizationHooks?.beforeDeleteOrganization) await orgOptions.organizationHooks.beforeDeleteOrganization({
|
|
5261
5328
|
organization,
|
|
5262
5329
|
user: deletedByUser
|
|
@@ -5297,7 +5364,8 @@ const deleteManyOrganizations = (options) => {
|
|
|
5297
5364
|
});
|
|
5298
5365
|
const remainingOrgs = await ctx.context.adapter.findMany({
|
|
5299
5366
|
model: "organization",
|
|
5300
|
-
where
|
|
5367
|
+
where,
|
|
5368
|
+
select: ["id"]
|
|
5301
5369
|
});
|
|
5302
5370
|
for (const id of chunk) if (!remainingOrgs.some((u) => u.id === id)) deletedOrgIds.add(id);
|
|
5303
5371
|
else skippedOrgIds.add(id);
|
|
@@ -5483,18 +5551,7 @@ const updateOrganization = (options) => {
|
|
|
5483
5551
|
}, async (ctx) => {
|
|
5484
5552
|
const { organizationId } = ctx.context.payload;
|
|
5485
5553
|
const orgOptions = requireOrganizationPlugin(ctx).options || {};
|
|
5486
|
-
|
|
5487
|
-
const orgWithSlug = await ctx.context.adapter.findOne({
|
|
5488
|
-
model: "organization",
|
|
5489
|
-
where: [{
|
|
5490
|
-
field: "slug",
|
|
5491
|
-
value: ctx.body.slug
|
|
5492
|
-
}],
|
|
5493
|
-
select: ["id"]
|
|
5494
|
-
});
|
|
5495
|
-
if (orgWithSlug && orgWithSlug.id !== organizationId) throw ctx.error("BAD_REQUEST", { message: "Slug already exists" });
|
|
5496
|
-
}
|
|
5497
|
-
const owners = await ctx.context.adapter.findMany({
|
|
5554
|
+
const ownersPromise = ctx.context.adapter.findMany({
|
|
5498
5555
|
model: "member",
|
|
5499
5556
|
where: [{
|
|
5500
5557
|
field: "organizationId",
|
|
@@ -5508,12 +5565,23 @@ const updateOrganization = (options) => {
|
|
|
5508
5565
|
direction: "asc"
|
|
5509
5566
|
},
|
|
5510
5567
|
limit: 1,
|
|
5511
|
-
join: {
|
|
5568
|
+
join: {
|
|
5569
|
+
user: true,
|
|
5570
|
+
organization: true
|
|
5571
|
+
}
|
|
5512
5572
|
});
|
|
5513
|
-
|
|
5573
|
+
const [orgWithSlug, owners] = await Promise.all([ctx.body.slug ? ctx.context.adapter.findOne({
|
|
5574
|
+
model: "organization",
|
|
5575
|
+
where: [{
|
|
5576
|
+
field: "slug",
|
|
5577
|
+
value: ctx.body.slug
|
|
5578
|
+
}],
|
|
5579
|
+
select: ["id"]
|
|
5580
|
+
}) : Promise.resolve(null), ownersPromise]);
|
|
5581
|
+
if (orgWithSlug && orgWithSlug.id !== organizationId) throw ctx.error("BAD_REQUEST", { message: "Slug already exists" });
|
|
5514
5582
|
const owner = owners[0];
|
|
5515
|
-
const updatedByUser =
|
|
5516
|
-
if (!updatedByUser) throw ctx.error("NOT_FOUND", { message: "
|
|
5583
|
+
const updatedByUser = owner ? joinedRelation(owner.user) : null;
|
|
5584
|
+
if (!owner || !updatedByUser || !joinedRelation(owner.organization)) throw ctx.error("NOT_FOUND", { message: "Organization owner not found" });
|
|
5517
5585
|
let updateData = pickWritableOrganizationFields(ctx.body, orgOptions, "update");
|
|
5518
5586
|
try {
|
|
5519
5587
|
validateWritableOrganizationUpdateFields(updateData, orgOptions);
|
|
@@ -5571,14 +5639,13 @@ const listOrganizationTeams = (options) => {
|
|
|
5571
5639
|
}
|
|
5572
5640
|
const organizationId = tryDecode(ctx.params.id);
|
|
5573
5641
|
try {
|
|
5574
|
-
|
|
5642
|
+
return await withConcurrency(await ctx.context.adapter.findMany({
|
|
5575
5643
|
model: "team",
|
|
5576
5644
|
where: [{
|
|
5577
5645
|
field: "organizationId",
|
|
5578
5646
|
value: organizationId
|
|
5579
5647
|
}]
|
|
5580
|
-
})
|
|
5581
|
-
return await Promise.all(teams.map(async (team) => {
|
|
5648
|
+
}), async (team) => {
|
|
5582
5649
|
let memberCount = 0;
|
|
5583
5650
|
try {
|
|
5584
5651
|
memberCount = await ctx.context.adapter.count({
|
|
@@ -5596,7 +5663,7 @@ const listOrganizationTeams = (options) => {
|
|
|
5596
5663
|
...team,
|
|
5597
5664
|
memberCount
|
|
5598
5665
|
};
|
|
5599
|
-
})
|
|
5666
|
+
}, { concurrency: 5 });
|
|
5600
5667
|
} catch (error) {
|
|
5601
5668
|
ctx.context.logger.warn("[Dash] Failed to list organization teams:", error);
|
|
5602
5669
|
return [];
|
|
@@ -5614,7 +5681,7 @@ const updateTeam = (options) => {
|
|
|
5614
5681
|
}, async (ctx) => {
|
|
5615
5682
|
const { organizationId } = ctx.context.payload;
|
|
5616
5683
|
const orgOptions = requireTeamsEnabled(ctx);
|
|
5617
|
-
const
|
|
5684
|
+
const teamRow = await ctx.context.adapter.findOne({
|
|
5618
5685
|
model: "team",
|
|
5619
5686
|
where: [{
|
|
5620
5687
|
field: "id",
|
|
@@ -5622,18 +5689,13 @@ const updateTeam = (options) => {
|
|
|
5622
5689
|
}, {
|
|
5623
5690
|
field: "organizationId",
|
|
5624
5691
|
value: organizationId
|
|
5625
|
-
}]
|
|
5626
|
-
|
|
5627
|
-
if (!existingTeam) throw ctx.error("NOT_FOUND", { message: "Team not found" });
|
|
5628
|
-
const organization = await ctx.context.adapter.findOne({
|
|
5629
|
-
model: "organization",
|
|
5630
|
-
where: [{
|
|
5631
|
-
field: "id",
|
|
5632
|
-
value: organizationId
|
|
5633
|
-
}]
|
|
5692
|
+
}],
|
|
5693
|
+
join: { organization: true }
|
|
5634
5694
|
});
|
|
5635
|
-
|
|
5636
|
-
|
|
5695
|
+
const organization = joinedRelation(teamRow?.organization);
|
|
5696
|
+
if (!teamRow || !organization) throw ctx.error("NOT_FOUND", { message: "Team not found or does not belong to this organization" });
|
|
5697
|
+
const existingTeam = teamRow;
|
|
5698
|
+
const owner = (await ctx.context.adapter.findMany({
|
|
5637
5699
|
model: "member",
|
|
5638
5700
|
where: [{
|
|
5639
5701
|
field: "organizationId",
|
|
@@ -5648,11 +5710,9 @@ const updateTeam = (options) => {
|
|
|
5648
5710
|
},
|
|
5649
5711
|
limit: 1,
|
|
5650
5712
|
join: { user: true }
|
|
5651
|
-
});
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
const user = Array.isArray(owner.user) ? owner.user[0] : owner.user;
|
|
5655
|
-
if (!user) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
|
|
5713
|
+
}))[0];
|
|
5714
|
+
const user = joinedRelation(owner?.user);
|
|
5715
|
+
if (!owner || !user) throw ctx.error("NOT_FOUND", { message: "Organization owner not found" });
|
|
5656
5716
|
let updateData = { updatedAt: /* @__PURE__ */ new Date() };
|
|
5657
5717
|
if (ctx.body.name) updateData.name = ctx.body.name;
|
|
5658
5718
|
if (orgOptions?.organizationHooks?.beforeUpdateTeam) {
|
|
@@ -5692,7 +5752,7 @@ const deleteTeam = (options) => {
|
|
|
5692
5752
|
}, async (ctx) => {
|
|
5693
5753
|
const { organizationId } = ctx.context.payload;
|
|
5694
5754
|
const orgOptions = requireTeamsEnabled(ctx);
|
|
5695
|
-
const
|
|
5755
|
+
const teamRow = await ctx.context.adapter.findOne({
|
|
5696
5756
|
model: "team",
|
|
5697
5757
|
where: [{
|
|
5698
5758
|
field: "id",
|
|
@@ -5700,17 +5760,12 @@ const deleteTeam = (options) => {
|
|
|
5700
5760
|
}, {
|
|
5701
5761
|
field: "organizationId",
|
|
5702
5762
|
value: organizationId
|
|
5703
|
-
}]
|
|
5704
|
-
|
|
5705
|
-
if (!team) throw ctx.error("NOT_FOUND", { message: "Team not found" });
|
|
5706
|
-
const organization = await ctx.context.adapter.findOne({
|
|
5707
|
-
model: "organization",
|
|
5708
|
-
where: [{
|
|
5709
|
-
field: "id",
|
|
5710
|
-
value: organizationId
|
|
5711
|
-
}]
|
|
5763
|
+
}],
|
|
5764
|
+
join: { organization: true }
|
|
5712
5765
|
});
|
|
5713
|
-
|
|
5766
|
+
const organization = joinedRelation(teamRow?.organization);
|
|
5767
|
+
if (!teamRow || !organization) throw ctx.error("NOT_FOUND", { message: "Team not found or does not belong to this organization" });
|
|
5768
|
+
const team = teamRow;
|
|
5714
5769
|
if (orgOptions?.teams?.allowRemovingAllTeams === false) {
|
|
5715
5770
|
if (await ctx.context.adapter.count({
|
|
5716
5771
|
model: "team",
|
|
@@ -5720,7 +5775,7 @@ const deleteTeam = (options) => {
|
|
|
5720
5775
|
}]
|
|
5721
5776
|
}) <= 1) throw ctx.error("BAD_REQUEST", { message: "Cannot remove the last team in the organization" });
|
|
5722
5777
|
}
|
|
5723
|
-
const
|
|
5778
|
+
const owner = (await ctx.context.adapter.findMany({
|
|
5724
5779
|
model: "member",
|
|
5725
5780
|
where: [{
|
|
5726
5781
|
field: "organizationId",
|
|
@@ -5735,11 +5790,9 @@ const deleteTeam = (options) => {
|
|
|
5735
5790
|
},
|
|
5736
5791
|
limit: 1,
|
|
5737
5792
|
join: { user: true }
|
|
5738
|
-
});
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
const user = Array.isArray(owner.user) ? owner.user[0] : owner.user;
|
|
5742
|
-
if (!user) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
|
|
5793
|
+
}))[0];
|
|
5794
|
+
const user = joinedRelation(owner?.user);
|
|
5795
|
+
if (!owner || !user) throw ctx.error("NOT_FOUND", { message: "Organization owner not found" });
|
|
5743
5796
|
if (orgOptions?.organizationHooks?.beforeDeleteTeam) await orgOptions.organizationHooks.beforeDeleteTeam({
|
|
5744
5797
|
team,
|
|
5745
5798
|
user,
|
|
@@ -5768,14 +5821,28 @@ const createTeam = (options) => {
|
|
|
5768
5821
|
}, async (ctx) => {
|
|
5769
5822
|
const { organizationId } = ctx.context.payload;
|
|
5770
5823
|
const orgOptions = requireTeamsEnabled(ctx);
|
|
5771
|
-
const
|
|
5772
|
-
model: "
|
|
5824
|
+
const owner = (await ctx.context.adapter.findMany({
|
|
5825
|
+
model: "member",
|
|
5773
5826
|
where: [{
|
|
5774
|
-
field: "
|
|
5827
|
+
field: "organizationId",
|
|
5775
5828
|
value: organizationId
|
|
5776
|
-
}
|
|
5777
|
-
|
|
5778
|
-
|
|
5829
|
+
}, {
|
|
5830
|
+
field: "role",
|
|
5831
|
+
value: "owner"
|
|
5832
|
+
}],
|
|
5833
|
+
sortBy: {
|
|
5834
|
+
field: "createdAt",
|
|
5835
|
+
direction: "asc"
|
|
5836
|
+
},
|
|
5837
|
+
limit: 1,
|
|
5838
|
+
join: {
|
|
5839
|
+
user: true,
|
|
5840
|
+
organization: true
|
|
5841
|
+
}
|
|
5842
|
+
}))[0];
|
|
5843
|
+
const user = joinedRelation(owner?.user);
|
|
5844
|
+
const organization = joinedRelation(owner?.organization);
|
|
5845
|
+
if (!owner || !user || !organization) throw ctx.error("NOT_FOUND", { message: "Organization owner not found" });
|
|
5779
5846
|
if (orgOptions?.teams?.maximumTeams) {
|
|
5780
5847
|
const teamsCount = await ctx.context.adapter.count({
|
|
5781
5848
|
model: "team",
|
|
@@ -5790,26 +5857,6 @@ const createTeam = (options) => {
|
|
|
5790
5857
|
}) : orgOptions.teams.maximumTeams;
|
|
5791
5858
|
if (teamsCount >= maxTeams) throw ctx.error("BAD_REQUEST", { message: `Maximum number of teams (${maxTeams}) reached for this organization` });
|
|
5792
5859
|
}
|
|
5793
|
-
const owners = await ctx.context.adapter.findMany({
|
|
5794
|
-
model: "member",
|
|
5795
|
-
where: [{
|
|
5796
|
-
field: "organizationId",
|
|
5797
|
-
value: organizationId
|
|
5798
|
-
}, {
|
|
5799
|
-
field: "role",
|
|
5800
|
-
value: "owner"
|
|
5801
|
-
}],
|
|
5802
|
-
sortBy: {
|
|
5803
|
-
field: "createdAt",
|
|
5804
|
-
direction: "asc"
|
|
5805
|
-
},
|
|
5806
|
-
limit: 1,
|
|
5807
|
-
join: { user: true }
|
|
5808
|
-
});
|
|
5809
|
-
if (owners.length === 0) throw ctx.error("NOT_FOUND", { message: "Owner not found" });
|
|
5810
|
-
const owner = owners[0];
|
|
5811
|
-
const user = Array.isArray(owner.user) ? owner.user[0] : owner.user;
|
|
5812
|
-
if (!user) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
|
|
5813
5860
|
let teamData = {
|
|
5814
5861
|
name: ctx.body.name,
|
|
5815
5862
|
organizationId,
|
|
@@ -5868,7 +5915,7 @@ const listTeamMembers = (options) => {
|
|
|
5868
5915
|
}],
|
|
5869
5916
|
join: { user: true }
|
|
5870
5917
|
})).map((tm) => {
|
|
5871
|
-
const user =
|
|
5918
|
+
const user = joinedRelation(tm.user);
|
|
5872
5919
|
return {
|
|
5873
5920
|
...tm,
|
|
5874
5921
|
user: user ? {
|
|
@@ -5896,7 +5943,7 @@ const addTeamMember = (options) => {
|
|
|
5896
5943
|
}, async (ctx) => {
|
|
5897
5944
|
const { organizationId } = ctx.context.payload;
|
|
5898
5945
|
const orgOptions = requireTeamsEnabled(ctx);
|
|
5899
|
-
const
|
|
5946
|
+
const teamRow = await ctx.context.adapter.findOne({
|
|
5900
5947
|
model: "team",
|
|
5901
5948
|
where: [{
|
|
5902
5949
|
field: "id",
|
|
@@ -5904,26 +5951,13 @@ const addTeamMember = (options) => {
|
|
|
5904
5951
|
}, {
|
|
5905
5952
|
field: "organizationId",
|
|
5906
5953
|
value: organizationId
|
|
5907
|
-
}]
|
|
5908
|
-
|
|
5909
|
-
if (!team) throw ctx.error("NOT_FOUND", { message: "Team not found" });
|
|
5910
|
-
const organization = await ctx.context.adapter.findOne({
|
|
5911
|
-
model: "organization",
|
|
5912
|
-
where: [{
|
|
5913
|
-
field: "id",
|
|
5914
|
-
value: organizationId
|
|
5915
|
-
}]
|
|
5916
|
-
});
|
|
5917
|
-
if (!organization) throw ctx.error("NOT_FOUND", { message: "Organization not found" });
|
|
5918
|
-
const user = await ctx.context.adapter.findOne({
|
|
5919
|
-
model: "user",
|
|
5920
|
-
where: [{
|
|
5921
|
-
field: "id",
|
|
5922
|
-
value: ctx.body.userId
|
|
5923
|
-
}]
|
|
5954
|
+
}],
|
|
5955
|
+
join: { organization: true }
|
|
5924
5956
|
});
|
|
5925
|
-
|
|
5926
|
-
if (!
|
|
5957
|
+
const organization = joinedRelation(teamRow?.organization);
|
|
5958
|
+
if (!teamRow || !organization) throw ctx.error("NOT_FOUND", { message: "Team not found or does not belong to this organization" });
|
|
5959
|
+
const team = teamRow;
|
|
5960
|
+
const orgMember = await ctx.context.adapter.findOne({
|
|
5927
5961
|
model: "member",
|
|
5928
5962
|
where: [{
|
|
5929
5963
|
field: "userId",
|
|
@@ -5931,8 +5965,11 @@ const addTeamMember = (options) => {
|
|
|
5931
5965
|
}, {
|
|
5932
5966
|
field: "organizationId",
|
|
5933
5967
|
value: organizationId
|
|
5934
|
-
}]
|
|
5935
|
-
|
|
5968
|
+
}],
|
|
5969
|
+
join: { user: true }
|
|
5970
|
+
});
|
|
5971
|
+
const user = joinedRelation(orgMember?.user);
|
|
5972
|
+
if (!orgMember || !user) throw ctx.error("BAD_REQUEST", { message: "User not found or is not a member of this organization" });
|
|
5936
5973
|
if (await ctx.context.adapter.count({
|
|
5937
5974
|
model: "teamMember",
|
|
5938
5975
|
where: [{
|
|
@@ -5999,7 +6036,7 @@ const removeTeamMember = (options) => {
|
|
|
5999
6036
|
}, async (ctx) => {
|
|
6000
6037
|
const { organizationId } = ctx.context.payload;
|
|
6001
6038
|
const orgOptions = requireTeamsEnabled(ctx);
|
|
6002
|
-
const
|
|
6039
|
+
const teamRow = await ctx.context.adapter.findOne({
|
|
6003
6040
|
model: "team",
|
|
6004
6041
|
where: [{
|
|
6005
6042
|
field: "id",
|
|
@@ -6007,26 +6044,13 @@ const removeTeamMember = (options) => {
|
|
|
6007
6044
|
}, {
|
|
6008
6045
|
field: "organizationId",
|
|
6009
6046
|
value: organizationId
|
|
6010
|
-
}]
|
|
6011
|
-
|
|
6012
|
-
if (!team) throw ctx.error("NOT_FOUND", { message: "Team not found" });
|
|
6013
|
-
const organization = await ctx.context.adapter.findOne({
|
|
6014
|
-
model: "organization",
|
|
6015
|
-
where: [{
|
|
6016
|
-
field: "id",
|
|
6017
|
-
value: organizationId
|
|
6018
|
-
}]
|
|
6019
|
-
});
|
|
6020
|
-
if (!organization) throw ctx.error("NOT_FOUND", { message: "Organization not found" });
|
|
6021
|
-
const user = await ctx.context.adapter.findOne({
|
|
6022
|
-
model: "user",
|
|
6023
|
-
where: [{
|
|
6024
|
-
field: "id",
|
|
6025
|
-
value: ctx.body.userId
|
|
6026
|
-
}]
|
|
6047
|
+
}],
|
|
6048
|
+
join: { organization: true }
|
|
6027
6049
|
});
|
|
6028
|
-
|
|
6029
|
-
|
|
6050
|
+
const organization = joinedRelation(teamRow?.organization);
|
|
6051
|
+
if (!teamRow || !organization) throw ctx.error("NOT_FOUND", { message: "Team not found or does not belong to this organization" });
|
|
6052
|
+
const team = teamRow;
|
|
6053
|
+
const teamMemberRow = await ctx.context.adapter.findOne({
|
|
6030
6054
|
model: "teamMember",
|
|
6031
6055
|
where: [{
|
|
6032
6056
|
field: "teamId",
|
|
@@ -6034,9 +6058,12 @@ const removeTeamMember = (options) => {
|
|
|
6034
6058
|
}, {
|
|
6035
6059
|
field: "userId",
|
|
6036
6060
|
value: ctx.body.userId
|
|
6037
|
-
}]
|
|
6061
|
+
}],
|
|
6062
|
+
join: { user: true }
|
|
6038
6063
|
});
|
|
6039
|
-
|
|
6064
|
+
const user = joinedRelation(teamMemberRow?.user);
|
|
6065
|
+
if (!teamMemberRow || !user) throw ctx.error("NOT_FOUND", { message: "User not found or is not a member of this team" });
|
|
6066
|
+
const teamMember = teamMemberRow;
|
|
6040
6067
|
if (orgOptions?.organizationHooks?.beforeRemoveTeamMember) await orgOptions.organizationHooks.beforeRemoveTeamMember({
|
|
6041
6068
|
teamMember,
|
|
6042
6069
|
team,
|
|
@@ -6967,7 +6994,8 @@ async function resolveTwoFactorStatus(context, userId, user) {
|
|
|
6967
6994
|
where: [{
|
|
6968
6995
|
field: "userId",
|
|
6969
6996
|
value: userId
|
|
6970
|
-
}]
|
|
6997
|
+
}],
|
|
6998
|
+
select: ["id"]
|
|
6971
6999
|
}) ? "pending" : "disabled";
|
|
6972
7000
|
}
|
|
6973
7001
|
//#endregion
|
|
@@ -7280,7 +7308,8 @@ const deleteManyUsers = (options) => {
|
|
|
7280
7308
|
});
|
|
7281
7309
|
const remainingUsers = await ctx.context.adapter.findMany({
|
|
7282
7310
|
model: "user",
|
|
7283
|
-
where
|
|
7311
|
+
where,
|
|
7312
|
+
select: ["id"]
|
|
7284
7313
|
});
|
|
7285
7314
|
for (const id of chunk) if (!remainingUsers.some((u) => u.id === id)) deletedUserIds.add(id);
|
|
7286
7315
|
else skippedUserIds.add(id);
|
|
@@ -7470,35 +7499,72 @@ const unlinkAccount = (options) => {
|
|
|
7470
7499
|
return { success: true };
|
|
7471
7500
|
});
|
|
7472
7501
|
};
|
|
7502
|
+
const getUserDetailsJwtSchema = z$1.object({
|
|
7503
|
+
userId: z$1.string(),
|
|
7504
|
+
sessionOnly: z$1.boolean().optional(),
|
|
7505
|
+
accountOnly: z$1.boolean().optional()
|
|
7506
|
+
});
|
|
7473
7507
|
const getUserDetails = (options) => {
|
|
7474
7508
|
return createAuthEndpoint("/dash/user", {
|
|
7475
7509
|
method: "GET",
|
|
7476
|
-
use: [jwtMiddleware(options,
|
|
7510
|
+
use: [jwtMiddleware(options, getUserDetailsJwtSchema)],
|
|
7477
7511
|
query: z$1.object({ minimal: z$1.boolean().or(z$1.string().transform((val) => val === "true")).optional() }).optional()
|
|
7478
7512
|
}, async (ctx) => {
|
|
7479
|
-
const { userId } = ctx.context.payload;
|
|
7513
|
+
const { userId, sessionOnly, accountOnly } = ctx.context.payload;
|
|
7480
7514
|
const minimal = !!ctx.query?.minimal;
|
|
7515
|
+
if (sessionOnly && accountOnly) throw ctx.error("BAD_REQUEST", { message: "Cannot use sessionOnly and accountOnly together" });
|
|
7481
7516
|
const hasAdminPlugin = hasPlugin(ctx.context, "admin");
|
|
7482
7517
|
const secondaryStorageOnly = isSessionInSecondaryStorageOnly(ctx.context);
|
|
7518
|
+
const join = (() => {
|
|
7519
|
+
if (minimal) return void 0;
|
|
7520
|
+
if (sessionOnly) return secondaryStorageOnly ? void 0 : { session: true };
|
|
7521
|
+
if (accountOnly) return { account: true };
|
|
7522
|
+
return {
|
|
7523
|
+
account: true,
|
|
7524
|
+
...secondaryStorageOnly ? {} : { session: true }
|
|
7525
|
+
};
|
|
7526
|
+
})();
|
|
7483
7527
|
const user = await ctx.context.adapter.findOne({
|
|
7484
7528
|
model: "user",
|
|
7485
7529
|
where: [{
|
|
7486
7530
|
field: "id",
|
|
7487
7531
|
value: userId
|
|
7488
7532
|
}],
|
|
7489
|
-
...
|
|
7490
|
-
account: true,
|
|
7491
|
-
...secondaryStorageOnly ? {} : { session: true }
|
|
7492
|
-
} }
|
|
7533
|
+
...join ? { join } : {}
|
|
7493
7534
|
});
|
|
7494
7535
|
if (!user) throw ctx.error("NOT_FOUND", { message: "User not found" });
|
|
7536
|
+
const banFields = {
|
|
7537
|
+
banned: hasAdminPlugin ? user.banned ?? false : false,
|
|
7538
|
+
banReason: hasAdminPlugin ? user.banReason ?? null : null,
|
|
7539
|
+
banExpires: hasAdminPlugin ? user.banExpires ?? null : null
|
|
7540
|
+
};
|
|
7541
|
+
const lastActiveAtFromUser = user.lastActiveAt ?? null;
|
|
7542
|
+
if (sessionOnly) {
|
|
7543
|
+
const sessions = secondaryStorageOnly ? normalizeDashSessions(await ctx.context.internalAdapter.listSessions(userId)) : user.session || [];
|
|
7544
|
+
const { session: _session, account: _account, ...userWithoutRelations } = user;
|
|
7545
|
+
return {
|
|
7546
|
+
...userWithoutRelations,
|
|
7547
|
+
...banFields,
|
|
7548
|
+
lastActiveAt: lastActiveAtFromUser,
|
|
7549
|
+
session: sanitizeSessions(sessions),
|
|
7550
|
+
account: []
|
|
7551
|
+
};
|
|
7552
|
+
}
|
|
7553
|
+
if (accountOnly) {
|
|
7554
|
+
const { session: _session, account, ...userWithoutRelations } = user;
|
|
7555
|
+
return {
|
|
7556
|
+
...userWithoutRelations,
|
|
7557
|
+
...banFields,
|
|
7558
|
+
lastActiveAt: lastActiveAtFromUser,
|
|
7559
|
+
account: account || [],
|
|
7560
|
+
session: []
|
|
7561
|
+
};
|
|
7562
|
+
}
|
|
7495
7563
|
const twoFactorStatus = await resolveTwoFactorStatus(ctx.context, userId, user);
|
|
7496
7564
|
if (minimal) return {
|
|
7497
7565
|
...user,
|
|
7498
|
-
lastActiveAt:
|
|
7499
|
-
|
|
7500
|
-
banReason: hasAdminPlugin ? user.banReason ?? null : null,
|
|
7501
|
-
banExpires: hasAdminPlugin ? user.banExpires ?? null : null,
|
|
7566
|
+
lastActiveAt: lastActiveAtFromUser,
|
|
7567
|
+
...banFields,
|
|
7502
7568
|
account: [],
|
|
7503
7569
|
session: [],
|
|
7504
7570
|
twoFactorStatus
|
|
@@ -7538,9 +7604,7 @@ const getUserDetails = (options) => {
|
|
|
7538
7604
|
...userWithoutSessions,
|
|
7539
7605
|
session: sanitizeSessions(sessions),
|
|
7540
7606
|
lastActiveAt,
|
|
7541
|
-
|
|
7542
|
-
banReason: hasAdminPlugin ? user.banReason ?? null : null,
|
|
7543
|
-
banExpires: hasAdminPlugin ? user.banExpires ?? null : null,
|
|
7607
|
+
...banFields,
|
|
7544
7608
|
twoFactorStatus
|
|
7545
7609
|
};
|
|
7546
7610
|
});
|
|
@@ -7645,7 +7709,7 @@ async function countUniqueActiveUsers(ctx, range, options) {
|
|
|
7645
7709
|
break;
|
|
7646
7710
|
}
|
|
7647
7711
|
}
|
|
7648
|
-
}, { concurrency:
|
|
7712
|
+
}, { concurrency: 5 });
|
|
7649
7713
|
return activeUserIds.size;
|
|
7650
7714
|
}
|
|
7651
7715
|
const sessions = await ctx.context.adapter.findMany({
|
|
@@ -7857,8 +7921,8 @@ const getUserGraphData = (options) => createAuthEndpoint("/dash/user-graph-data"
|
|
|
7857
7921
|
label
|
|
7858
7922
|
});
|
|
7859
7923
|
}
|
|
7860
|
-
const
|
|
7861
|
-
ctx.context.adapter.count({
|
|
7924
|
+
const results = await withConcurrency(intervalData.flatMap((interval) => [
|
|
7925
|
+
() => ctx.context.adapter.count({
|
|
7862
7926
|
model: "user",
|
|
7863
7927
|
where: [{
|
|
7864
7928
|
field: "createdAt",
|
|
@@ -7866,7 +7930,7 @@ const getUserGraphData = (options) => createAuthEndpoint("/dash/user-graph-data"
|
|
|
7866
7930
|
value: interval.endDate
|
|
7867
7931
|
}]
|
|
7868
7932
|
}),
|
|
7869
|
-
ctx.context.adapter.count({
|
|
7933
|
+
() => ctx.context.adapter.count({
|
|
7870
7934
|
model: "user",
|
|
7871
7935
|
where: [{
|
|
7872
7936
|
field: "createdAt",
|
|
@@ -7878,15 +7942,14 @@ const getUserGraphData = (options) => createAuthEndpoint("/dash/user-graph-data"
|
|
|
7878
7942
|
value: interval.endDate
|
|
7879
7943
|
}]
|
|
7880
7944
|
}),
|
|
7881
|
-
countUniqueActiveUsers(ctx, {
|
|
7945
|
+
() => countUniqueActiveUsers(ctx, {
|
|
7882
7946
|
from: interval.startDate,
|
|
7883
7947
|
to: interval.endDate
|
|
7884
7948
|
}, {
|
|
7885
7949
|
storeInSecondaryStorageOnly,
|
|
7886
7950
|
activityTrackingEnabled
|
|
7887
7951
|
})
|
|
7888
|
-
]);
|
|
7889
|
-
const results = await Promise.all(allQueries);
|
|
7952
|
+
]), (runQuery) => runQuery(), { concurrency: 5 });
|
|
7890
7953
|
return {
|
|
7891
7954
|
data: intervalData.map((interval, idx) => ({
|
|
7892
7955
|
date: typeof interval.endDate === "object" ? interval.endDate : interval.endDate,
|
|
@@ -7948,41 +8011,19 @@ const getUserRetentionData = (options) => createAuthEndpoint("/dash/user-retenti
|
|
|
7948
8011
|
const activeStart = period === "daily" ? startOfUtcDay(now) : period === "weekly" ? startOfUtcWeek(now) : startOfUtcMonth(now);
|
|
7949
8012
|
const activeEnd = period === "daily" ? addUtcDays(activeStart, 1) : period === "weekly" ? addUtcDays(activeStart, 7) : addUtcMonths(activeStart, 1);
|
|
7950
8013
|
const prefix = period === "daily" ? "D" : period === "weekly" ? "W" : "M";
|
|
7951
|
-
const
|
|
7952
|
-
|
|
8014
|
+
const activeStartIso = activeStart.toISOString();
|
|
8015
|
+
const activeEndIso = activeEnd.toISOString();
|
|
8016
|
+
const intervalMeta = Array.from({ length: horizons }, (_, index) => {
|
|
8017
|
+
const n = index + 1;
|
|
7953
8018
|
const cohortStart = period === "daily" ? addUtcDays(activeStart, -n) : period === "weekly" ? addUtcDays(activeStart, -7 * n) : addUtcMonths(activeStart, -n);
|
|
7954
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
}, {
|
|
7963
|
-
field: "createdAt",
|
|
7964
|
-
operator: "lt",
|
|
7965
|
-
value: cohortEnd
|
|
7966
|
-
}]
|
|
7967
|
-
});
|
|
7968
|
-
const cohortSize = cohortUsers.length;
|
|
7969
|
-
if (cohortSize === 0) {
|
|
7970
|
-
data.push({
|
|
7971
|
-
n,
|
|
7972
|
-
label: `${prefix}${n}`,
|
|
7973
|
-
cohortStart: cohortStart.toISOString(),
|
|
7974
|
-
cohortEnd: cohortEnd.toISOString(),
|
|
7975
|
-
activeStart: activeStart.toISOString(),
|
|
7976
|
-
activeEnd: activeEnd.toISOString(),
|
|
7977
|
-
cohortSize: 0,
|
|
7978
|
-
retained: 0,
|
|
7979
|
-
retentionRate: 0
|
|
7980
|
-
});
|
|
7981
|
-
continue;
|
|
7982
|
-
}
|
|
7983
|
-
const cohortUserIds = cohortUsers.map((u) => u.id);
|
|
7984
|
-
let retained;
|
|
7985
|
-
if (activityTrackingEnabled) retained = await ctx.context.adapter.count({
|
|
8019
|
+
return {
|
|
8020
|
+
n,
|
|
8021
|
+
cohortStart,
|
|
8022
|
+
cohortEnd: period === "daily" ? addUtcDays(cohortStart, 1) : period === "weekly" ? addUtcDays(cohortStart, 7) : addUtcMonths(cohortStart, 1)
|
|
8023
|
+
};
|
|
8024
|
+
});
|
|
8025
|
+
const countRetainedUsers = async (cohortUserIds) => {
|
|
8026
|
+
if (activityTrackingEnabled) return ctx.context.adapter.count({
|
|
7986
8027
|
model: "user",
|
|
7987
8028
|
where: [
|
|
7988
8029
|
{
|
|
@@ -8005,7 +8046,7 @@ const getUserRetentionData = (options) => createAuthEndpoint("/dash/user-retenti
|
|
|
8005
8046
|
ctx.context.logger.warn("[Dash] Failed to count retained users by lastActiveAt:", error);
|
|
8006
8047
|
return 0;
|
|
8007
8048
|
});
|
|
8008
|
-
|
|
8049
|
+
if (secondaryOnly) {
|
|
8009
8050
|
const activeStartMs = activeStart.getTime();
|
|
8010
8051
|
const activeEndMs = activeEnd.getTime();
|
|
8011
8052
|
const retainedUsers = /* @__PURE__ */ new Set();
|
|
@@ -8018,50 +8059,78 @@ const getUserRetentionData = (options) => createAuthEndpoint("/dash/user-retenti
|
|
|
8018
8059
|
break;
|
|
8019
8060
|
}
|
|
8020
8061
|
}
|
|
8021
|
-
}, { concurrency:
|
|
8022
|
-
|
|
8023
|
-
} else {
|
|
8024
|
-
const sessionsInActiveWindow = await ctx.context.adapter.findMany({
|
|
8025
|
-
model: "session",
|
|
8026
|
-
select: ["userId"],
|
|
8027
|
-
where: [
|
|
8028
|
-
{
|
|
8029
|
-
field: "userId",
|
|
8030
|
-
operator: "in",
|
|
8031
|
-
value: cohortUserIds
|
|
8032
|
-
},
|
|
8033
|
-
{
|
|
8034
|
-
field: "updatedAt",
|
|
8035
|
-
operator: "gte",
|
|
8036
|
-
value: activeStart
|
|
8037
|
-
},
|
|
8038
|
-
{
|
|
8039
|
-
field: "updatedAt",
|
|
8040
|
-
operator: "lt",
|
|
8041
|
-
value: activeEnd
|
|
8042
|
-
}
|
|
8043
|
-
]
|
|
8044
|
-
}).catch((error) => {
|
|
8045
|
-
ctx.context.logger.warn("[Dash] Failed to fetch cohort sessions:", error);
|
|
8046
|
-
return [];
|
|
8047
|
-
});
|
|
8048
|
-
retained = new Set(sessionsInActiveWindow.map((s) => s.userId)).size;
|
|
8062
|
+
}, { concurrency: 5 });
|
|
8063
|
+
return retainedUsers.size;
|
|
8049
8064
|
}
|
|
8050
|
-
const
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8065
|
+
const sessionsInActiveWindow = await ctx.context.adapter.findMany({
|
|
8066
|
+
model: "session",
|
|
8067
|
+
select: ["userId"],
|
|
8068
|
+
where: [
|
|
8069
|
+
{
|
|
8070
|
+
field: "userId",
|
|
8071
|
+
operator: "in",
|
|
8072
|
+
value: cohortUserIds
|
|
8073
|
+
},
|
|
8074
|
+
{
|
|
8075
|
+
field: "updatedAt",
|
|
8076
|
+
operator: "gte",
|
|
8077
|
+
value: activeStart
|
|
8078
|
+
},
|
|
8079
|
+
{
|
|
8080
|
+
field: "updatedAt",
|
|
8081
|
+
operator: "lt",
|
|
8082
|
+
value: activeEnd
|
|
8083
|
+
}
|
|
8084
|
+
]
|
|
8085
|
+
}).catch((error) => {
|
|
8086
|
+
ctx.context.logger.warn("[Dash] Failed to fetch cohort sessions:", error);
|
|
8087
|
+
return [];
|
|
8061
8088
|
});
|
|
8062
|
-
|
|
8089
|
+
return new Set(sessionsInActiveWindow.map((s) => s.userId)).size;
|
|
8090
|
+
};
|
|
8063
8091
|
return {
|
|
8064
|
-
data,
|
|
8092
|
+
data: await withConcurrency(intervalMeta, async ({ n, cohortStart, cohortEnd }) => {
|
|
8093
|
+
const cohortStartIso = cohortStart.toISOString();
|
|
8094
|
+
const cohortEndIso = cohortEnd.toISOString();
|
|
8095
|
+
const cohortUsers = await ctx.context.adapter.findMany({
|
|
8096
|
+
model: "user",
|
|
8097
|
+
select: ["id"],
|
|
8098
|
+
where: [{
|
|
8099
|
+
field: "createdAt",
|
|
8100
|
+
operator: "gte",
|
|
8101
|
+
value: cohortStart
|
|
8102
|
+
}, {
|
|
8103
|
+
field: "createdAt",
|
|
8104
|
+
operator: "lt",
|
|
8105
|
+
value: cohortEnd
|
|
8106
|
+
}]
|
|
8107
|
+
});
|
|
8108
|
+
const cohortSize = cohortUsers.length;
|
|
8109
|
+
if (cohortSize === 0) return {
|
|
8110
|
+
n,
|
|
8111
|
+
label: `${prefix}${n}`,
|
|
8112
|
+
cohortStart: cohortStartIso,
|
|
8113
|
+
cohortEnd: cohortEndIso,
|
|
8114
|
+
activeStart: activeStartIso,
|
|
8115
|
+
activeEnd: activeEndIso,
|
|
8116
|
+
cohortSize: 0,
|
|
8117
|
+
retained: 0,
|
|
8118
|
+
retentionRate: 0
|
|
8119
|
+
};
|
|
8120
|
+
const retained = await countRetainedUsers(cohortUsers.map((u) => u.id));
|
|
8121
|
+
const retentionRate = Math.round(retained / cohortSize * 100 * 10) / 10;
|
|
8122
|
+
return {
|
|
8123
|
+
n,
|
|
8124
|
+
label: `${prefix}${n}`,
|
|
8125
|
+
cohortStart: cohortStartIso,
|
|
8126
|
+
cohortEnd: cohortEndIso,
|
|
8127
|
+
activeStart: activeStartIso,
|
|
8128
|
+
activeEnd: activeEndIso,
|
|
8129
|
+
cohortSize,
|
|
8130
|
+
retained,
|
|
8131
|
+
retentionRate
|
|
8132
|
+
};
|
|
8133
|
+
}, { concurrency: 5 }),
|
|
8065
8134
|
period
|
|
8066
8135
|
};
|
|
8067
8136
|
});
|
|
@@ -8611,6 +8680,7 @@ const dash = (options) => {
|
|
|
8611
8680
|
country: location?.country,
|
|
8612
8681
|
countryCode: location?.countryCode
|
|
8613
8682
|
};
|
|
8683
|
+
const eventUser = resolveUserFromContext(session.userId, ctx);
|
|
8614
8684
|
let trigger = null;
|
|
8615
8685
|
if (matchesAnyRoute(ctx.path, [
|
|
8616
8686
|
routes.SIGN_IN,
|
|
@@ -8619,18 +8689,19 @@ const dash = (options) => {
|
|
|
8619
8689
|
routes.SIGN_IN_OAUTH_CALLBACK
|
|
8620
8690
|
])) {
|
|
8621
8691
|
trigger = getTriggerInfo(ctx, session.userId, enrichedSession);
|
|
8622
|
-
trackUserSignedIn(enrichedSession, trigger, ctx, location);
|
|
8692
|
+
trackUserSignedIn(enrichedSession, trigger, ctx, location, eventUser);
|
|
8623
8693
|
} else trigger = getTriggerInfo(ctx, session.userId);
|
|
8624
|
-
trackSessionCreated(enrichedSession, trigger, ctx, location);
|
|
8694
|
+
trackSessionCreated(enrichedSession, trigger, ctx, location, eventUser);
|
|
8625
8695
|
if ("impersonatedBy" in session && session.impersonatedBy) {
|
|
8626
8696
|
trigger = {
|
|
8627
8697
|
...trigger,
|
|
8628
8698
|
triggeredBy: session.impersonatedBy
|
|
8629
8699
|
};
|
|
8630
|
-
|
|
8700
|
+
const knownImpersonator = resolveUserFromContext(session.impersonatedBy, ctx);
|
|
8701
|
+
trackUserImpersonated(enrichedSession, trigger, ctx, location, eventUser, knownImpersonator);
|
|
8631
8702
|
}
|
|
8632
8703
|
if (opts.activityTracking?.enabled) try {
|
|
8633
|
-
await ctx.context.adapter.
|
|
8704
|
+
await ctx.context.adapter.updateMany({
|
|
8634
8705
|
model: "user",
|
|
8635
8706
|
where: [{
|
|
8636
8707
|
field: "id",
|
|
@@ -8656,6 +8727,7 @@ const dash = (options) => {
|
|
|
8656
8727
|
countryCode: location?.countryCode
|
|
8657
8728
|
};
|
|
8658
8729
|
const trigger = getTriggerInfo(ctx, session.userId);
|
|
8730
|
+
const eventUser = resolveUserFromContext(session.userId, ctx);
|
|
8659
8731
|
if (matchesAnyRoute(ctx.path, [
|
|
8660
8732
|
routes.REVOKE_ALL_SESSIONS,
|
|
8661
8733
|
routes.ADMIN_REVOKE_USER_SESSIONS,
|
|
@@ -8663,12 +8735,12 @@ const dash = (options) => {
|
|
|
8663
8735
|
routes.DASH_BAN_USER
|
|
8664
8736
|
])) {
|
|
8665
8737
|
if (!processedBulkOperationContexts.has(ctx)) {
|
|
8666
|
-
trackSessionRevokedAll(enrichedSession, trigger, ctx, location);
|
|
8738
|
+
trackSessionRevokedAll(enrichedSession, trigger, ctx, location, eventUser);
|
|
8667
8739
|
processedBulkOperationContexts.add(ctx);
|
|
8668
8740
|
}
|
|
8669
|
-
} else if (matchesAnyRoute(path, [routes.SIGN_OUT])) trackUserSignedOut(enrichedSession, trigger, ctx, location);
|
|
8670
|
-
else trackSessionRevoked(enrichedSession, trigger, ctx, location);
|
|
8671
|
-
if ("impersonatedBy" in session && session.impersonatedBy) trackUserImpersonationStop(enrichedSession, trigger, ctx, location);
|
|
8741
|
+
} else if (matchesAnyRoute(path, [routes.SIGN_OUT])) trackUserSignedOut(enrichedSession, trigger, ctx, location, eventUser);
|
|
8742
|
+
else trackSessionRevoked(enrichedSession, trigger, ctx, location, eventUser);
|
|
8743
|
+
if ("impersonatedBy" in session && session.impersonatedBy) trackUserImpersonationStop(enrichedSession, trigger, ctx, location, eventUser, resolveUserFromContext(session.impersonatedBy, ctx));
|
|
8672
8744
|
} }
|
|
8673
8745
|
},
|
|
8674
8746
|
account: {
|
|
@@ -8685,13 +8757,14 @@ const dash = (options) => {
|
|
|
8685
8757
|
const path = ctx.path;
|
|
8686
8758
|
const trigger = getTriggerInfo(ctx, account.userId);
|
|
8687
8759
|
const location = ctx.context.location;
|
|
8688
|
-
if (matchesAnyRoute(path, [
|
|
8760
|
+
if (!matchesAnyRoute(path, [
|
|
8689
8761
|
routes.CHANGE_PASSWORD,
|
|
8690
8762
|
routes.SET_PASSWORD,
|
|
8691
8763
|
routes.RESET_PASSWORD,
|
|
8692
8764
|
routes.ADMIN_SET_PASSWORD,
|
|
8693
8765
|
routes.DASH_SET_PASSWORD
|
|
8694
|
-
]))
|
|
8766
|
+
])) return;
|
|
8767
|
+
trackAccountPasswordChange(account, trigger, ctx, location);
|
|
8695
8768
|
} },
|
|
8696
8769
|
delete: { async after(account, _ctx) {
|
|
8697
8770
|
const ctx = _ctx;
|
|
@@ -8736,7 +8809,7 @@ const dash = (options) => {
|
|
|
8736
8809
|
routes.DASH_COMPLETE_INVITATION_SOCIAL
|
|
8737
8810
|
]);
|
|
8738
8811
|
},
|
|
8739
|
-
handler: createIdentificationMiddleware($kv)
|
|
8812
|
+
handler: createIdentificationMiddleware($kv, { skipIdentification: (ctx) => isDashRoute(ctx.path) })
|
|
8740
8813
|
}],
|
|
8741
8814
|
after: [{
|
|
8742
8815
|
matcher: (ctx) => {
|
|
@@ -8779,13 +8852,15 @@ const dash = (options) => {
|
|
|
8779
8852
|
if (lastUpdate) {
|
|
8780
8853
|
if (now - new Date(lastUpdate).getTime() < activityUpdateInterval) return;
|
|
8781
8854
|
}
|
|
8782
|
-
ctx.context.adapter.
|
|
8855
|
+
ctx.context.adapter.updateMany({
|
|
8783
8856
|
model: "user",
|
|
8784
8857
|
where: [{
|
|
8785
8858
|
field: "id",
|
|
8786
8859
|
value: userId
|
|
8787
8860
|
}],
|
|
8788
8861
|
update: { lastActiveAt: /* @__PURE__ */ new Date() }
|
|
8862
|
+
}).catch((error) => {
|
|
8863
|
+
logger.warn("[Dash] Failed to update user activity", error);
|
|
8789
8864
|
});
|
|
8790
8865
|
}),
|
|
8791
8866
|
matcher: (ctx) => ctx.request?.method !== "GET"
|