@better-auth/infra 0.1.7 → 0.1.9-beta.1
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/client.d.mts +5 -5
- package/dist/client.mjs +6 -2
- package/dist/constants-DWl1utFw.mjs +18 -0
- package/dist/email.d.mts +53 -1
- package/dist/email.mjs +182 -2
- package/dist/index.d.mts +531 -264
- package/dist/index.mjs +816 -263
- package/package.json +18 -17
- package/dist/email-BGxJ96Ky.mjs +0 -128
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as INFRA_KV_URL, r as KV_TIMEOUT_MS, t as INFRA_API_URL } from "./constants-DWl1utFw.mjs";
|
|
2
|
+
import { EMAIL_TEMPLATES, createEmailSender, sendBulkEmails, sendEmail } from "./email.mjs";
|
|
2
3
|
import { APIError, generateId, getAuthTables, logger, parseState } from "better-auth";
|
|
3
4
|
import { env } from "@better-auth/core/env";
|
|
4
5
|
import { APIError as APIError$1, createAuthEndpoint, createAuthMiddleware, requestPasswordReset, sendVerificationEmailFn, sessionMiddleware } from "better-auth/api";
|
|
@@ -9,8 +10,6 @@ import z$1, { z } from "zod";
|
|
|
9
10
|
import { setSessionCookie } from "better-auth/cookies";
|
|
10
11
|
import { DEFAULT_MAX_SAML_METADATA_SIZE, DigestAlgorithm, DiscoveryError, SignatureAlgorithm, discoverOIDCConfig } from "@better-auth/sso";
|
|
11
12
|
|
|
12
|
-
export * from "better-call"
|
|
13
|
-
|
|
14
13
|
//#region src/options.ts
|
|
15
14
|
function resolveConnectionOptions(options) {
|
|
16
15
|
return {
|
|
@@ -141,11 +140,11 @@ function backgroundTask(task) {
|
|
|
141
140
|
try {
|
|
142
141
|
result = task();
|
|
143
142
|
} catch (error) {
|
|
144
|
-
logger.debug("
|
|
143
|
+
logger.debug("[Dash] Background operation failed: ", error);
|
|
145
144
|
return;
|
|
146
145
|
}
|
|
147
146
|
Promise.resolve(result).catch((error) => {
|
|
148
|
-
logger.debug("
|
|
147
|
+
logger.debug("[Dash] Background operation failed: ", error);
|
|
149
148
|
});
|
|
150
149
|
}
|
|
151
150
|
|
|
@@ -167,7 +166,7 @@ const getUserByEmail = async (email, ctx) => {
|
|
|
167
166
|
}]
|
|
168
167
|
});
|
|
169
168
|
} catch (error) {
|
|
170
|
-
logger.debug("
|
|
169
|
+
logger.debug("[Dash] Failed to fetch user info:", error);
|
|
171
170
|
}
|
|
172
171
|
return user;
|
|
173
172
|
};
|
|
@@ -186,7 +185,9 @@ async function getUserById(userId, ctx) {
|
|
|
186
185
|
value: userId
|
|
187
186
|
}]
|
|
188
187
|
});
|
|
189
|
-
} catch {
|
|
188
|
+
} catch (error) {
|
|
189
|
+
logger.debug("[Dash] Failed to fetch user info:", error);
|
|
190
|
+
}
|
|
190
191
|
return user;
|
|
191
192
|
}
|
|
192
193
|
const getUserByIdToken = async (providerId, idToken, ctx) => {
|
|
@@ -195,7 +196,7 @@ const getUserByIdToken = async (providerId, idToken, ctx) => {
|
|
|
195
196
|
if (provider) try {
|
|
196
197
|
user = await provider.getUserInfo(idToken);
|
|
197
198
|
} catch (error) {
|
|
198
|
-
logger.debug("
|
|
199
|
+
logger.debug("[Dash] Failed to fetch user info:", error);
|
|
199
200
|
}
|
|
200
201
|
return user;
|
|
201
202
|
};
|
|
@@ -213,7 +214,7 @@ const getUserByAuthorizationCode = async (providerId, ctx) => {
|
|
|
213
214
|
});
|
|
214
215
|
userInfo = await provider.getUserInfo({ ...tokens }).then((res) => res?.user);
|
|
215
216
|
} catch (error) {
|
|
216
|
-
logger.debug("
|
|
217
|
+
logger.debug("[Dash] Failed to fetch user info:", error);
|
|
217
218
|
}
|
|
218
219
|
return userInfo;
|
|
219
220
|
};
|
|
@@ -306,7 +307,7 @@ const initAccountEvents = (tracker) => {
|
|
|
306
307
|
const stripQuery = (value) => value.split("?")[0] || value;
|
|
307
308
|
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
308
309
|
const routeToRegex = (route) => {
|
|
309
|
-
const pattern = escapeRegex(stripQuery(route)).replace(
|
|
310
|
+
const pattern = escapeRegex(stripQuery(route)).replace(/\/:([^/]+)/g, "/[^/]+");
|
|
310
311
|
return /* @__PURE__ */ new RegExp(`${pattern}(?:$|[/?])`);
|
|
311
312
|
};
|
|
312
313
|
const matchesAnyRoute = (path, routes$1) => {
|
|
@@ -805,7 +806,7 @@ const initVerificationEvents = (tracker) => {
|
|
|
805
806
|
//#endregion
|
|
806
807
|
//#region src/events/triggers.ts
|
|
807
808
|
const getTriggerInfo = (ctx, userId, session) => {
|
|
808
|
-
const sessionUserId = session?.userId ?? ctx.context.session?.session.userId ??
|
|
809
|
+
const sessionUserId = session?.userId ?? ctx.context.session?.session.userId ?? userId;
|
|
809
810
|
return {
|
|
810
811
|
triggeredBy: sessionUserId,
|
|
811
812
|
triggerContext: sessionUserId === userId ? "user" : matchesAnyRoute(ctx.path, [routes.ADMIN_ROUTE]) ? "admin" : matchesAnyRoute(ctx.path, [routes.DASH_ROUTE]) ? "dashboard" : sessionUserId === UNKNOWN_USER ? "user" : "unknown"
|
|
@@ -863,6 +864,7 @@ const initTrackEvents = (options) => {
|
|
|
863
864
|
* Fetches identification data from the durable-kv service
|
|
864
865
|
* when a request includes an X-Request-Id header.
|
|
865
866
|
*/
|
|
867
|
+
const IDENTIFICATION_COOKIE_NAME = "__infra-rid";
|
|
866
868
|
const identificationCache = /* @__PURE__ */ new Map();
|
|
867
869
|
const CACHE_TTL_MS = 6e4;
|
|
868
870
|
const CACHE_MAX_SIZE = 1e3;
|
|
@@ -892,7 +894,8 @@ async function getIdentification(requestId, apiKey, kvUrl) {
|
|
|
892
894
|
for (let attempt = 0; attempt <= maxRetries; attempt++) try {
|
|
893
895
|
const response = await fetch(`${baseUrl}/identify/${requestId}`, {
|
|
894
896
|
method: "GET",
|
|
895
|
-
headers: { "x-api-key": apiKey }
|
|
897
|
+
headers: { "x-api-key": apiKey },
|
|
898
|
+
signal: AbortSignal.timeout(KV_TIMEOUT_MS)
|
|
896
899
|
});
|
|
897
900
|
if (response.ok) {
|
|
898
901
|
const data = await response.json();
|
|
@@ -938,7 +941,8 @@ function extractIdentificationHeaders(request) {
|
|
|
938
941
|
*/
|
|
939
942
|
function createIdentificationMiddleware(options) {
|
|
940
943
|
return createAuthMiddleware(async (ctx) => {
|
|
941
|
-
const { visitorId, requestId } = extractIdentificationHeaders(ctx.request);
|
|
944
|
+
const { visitorId, requestId: headerRequestId } = extractIdentificationHeaders(ctx.request);
|
|
945
|
+
const requestId = headerRequestId ?? ctx.getCookie(IDENTIFICATION_COOKIE_NAME) ?? null;
|
|
942
946
|
ctx.context.visitorId = visitorId;
|
|
943
947
|
ctx.context.requestId = requestId;
|
|
944
948
|
if (requestId) ctx.context.identification = ctx.context.identification ?? await getIdentification(requestId, options.apiKey, options.kvUrl) ?? null;
|
|
@@ -1020,7 +1024,8 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1020
1024
|
const resolvedApiUrl = apiUrl || INFRA_API_URL;
|
|
1021
1025
|
const $api = createFetch({
|
|
1022
1026
|
baseURL: resolvedApiUrl,
|
|
1023
|
-
headers: { "x-api-key": apiKey }
|
|
1027
|
+
headers: { "x-api-key": apiKey },
|
|
1028
|
+
throw: true
|
|
1024
1029
|
});
|
|
1025
1030
|
const emailSender = createEmailSender({
|
|
1026
1031
|
apiUrl: resolvedApiUrl,
|
|
@@ -1036,14 +1041,14 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1036
1041
|
return {
|
|
1037
1042
|
async checkSecurity(request) {
|
|
1038
1043
|
try {
|
|
1039
|
-
const
|
|
1044
|
+
const data = await $api("/security/check", {
|
|
1040
1045
|
method: "POST",
|
|
1041
1046
|
body: {
|
|
1042
1047
|
...request,
|
|
1043
1048
|
config: options
|
|
1044
1049
|
}
|
|
1045
1050
|
});
|
|
1046
|
-
if (data
|
|
1051
|
+
if (data.action !== "allow") logEvent({
|
|
1047
1052
|
type: this.mapReasonToEventType(data.reason),
|
|
1048
1053
|
userId: null,
|
|
1049
1054
|
visitorId: request.visitorId,
|
|
@@ -1052,7 +1057,7 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1052
1057
|
details: data.details || { reason: data.reason },
|
|
1053
1058
|
action: data.action === "block" ? "blocked" : "challenged"
|
|
1054
1059
|
});
|
|
1055
|
-
return data
|
|
1060
|
+
return data;
|
|
1056
1061
|
} catch (error) {
|
|
1057
1062
|
logger.error("[Dash] Security check failed:", error);
|
|
1058
1063
|
return { action: "allow" };
|
|
@@ -1070,7 +1075,7 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1070
1075
|
},
|
|
1071
1076
|
async trackFailedAttempt(identifier, visitorId, password, ip) {
|
|
1072
1077
|
try {
|
|
1073
|
-
const
|
|
1078
|
+
const data = await $api("/security/track-failed-login", {
|
|
1074
1079
|
method: "POST",
|
|
1075
1080
|
body: {
|
|
1076
1081
|
identifier,
|
|
@@ -1080,7 +1085,7 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1080
1085
|
config: options
|
|
1081
1086
|
}
|
|
1082
1087
|
});
|
|
1083
|
-
if (data
|
|
1088
|
+
if (data.blocked || data.challenged) logEvent({
|
|
1084
1089
|
type: "credential_stuffing",
|
|
1085
1090
|
userId: null,
|
|
1086
1091
|
visitorId,
|
|
@@ -1089,7 +1094,7 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1089
1094
|
details: data.details || { reason: data.reason },
|
|
1090
1095
|
action: data.blocked ? "blocked" : "challenged"
|
|
1091
1096
|
});
|
|
1092
|
-
return data
|
|
1097
|
+
return data;
|
|
1093
1098
|
} catch (error) {
|
|
1094
1099
|
logger.error("[Dash] Track failed attempt error:", error);
|
|
1095
1100
|
return { blocked: false };
|
|
@@ -1107,26 +1112,23 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1107
1112
|
},
|
|
1108
1113
|
async isBlocked(visitorId) {
|
|
1109
1114
|
try {
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1115
|
+
return (await $api(`/security/is-blocked?visitorId=${encodeURIComponent(visitorId)}`, { method: "GET" })).blocked ?? false;
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
logger.warn("[Dash] Security is-blocked check failed:", error);
|
|
1113
1118
|
return false;
|
|
1114
1119
|
}
|
|
1115
1120
|
},
|
|
1116
1121
|
async verifyPoWSolution(visitorId, solution) {
|
|
1117
1122
|
try {
|
|
1118
|
-
|
|
1123
|
+
return await $api("/security/pow/verify", {
|
|
1119
1124
|
method: "POST",
|
|
1120
1125
|
body: {
|
|
1121
1126
|
visitorId,
|
|
1122
1127
|
solution
|
|
1123
1128
|
}
|
|
1124
1129
|
});
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
reason: "unknown"
|
|
1128
|
-
};
|
|
1129
|
-
} catch {
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
logger.warn("[Dash] PoW verify failed:", error);
|
|
1130
1132
|
return {
|
|
1131
1133
|
valid: false,
|
|
1132
1134
|
reason: "error"
|
|
@@ -1135,22 +1137,22 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1135
1137
|
},
|
|
1136
1138
|
async generateChallenge(visitorId) {
|
|
1137
1139
|
try {
|
|
1138
|
-
|
|
1140
|
+
return (await $api("/security/pow/generate", {
|
|
1139
1141
|
method: "POST",
|
|
1140
1142
|
body: {
|
|
1141
1143
|
visitorId,
|
|
1142
1144
|
difficulty: options.challengeDifficulty
|
|
1143
1145
|
}
|
|
1144
|
-
});
|
|
1145
|
-
|
|
1146
|
-
|
|
1146
|
+
})).challenge || "";
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
logger.warn("[Dash] PoW generate challenge failed:", error);
|
|
1147
1149
|
return "";
|
|
1148
1150
|
}
|
|
1149
1151
|
},
|
|
1150
1152
|
async checkImpossibleTravel(userId, currentLocation, visitorId) {
|
|
1151
1153
|
if (!options.impossibleTravel?.enabled || !currentLocation) return null;
|
|
1152
1154
|
try {
|
|
1153
|
-
const
|
|
1155
|
+
const data = await $api("/security/impossible-travel", {
|
|
1154
1156
|
method: "POST",
|
|
1155
1157
|
body: {
|
|
1156
1158
|
userId,
|
|
@@ -1159,7 +1161,7 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1159
1161
|
config: options
|
|
1160
1162
|
}
|
|
1161
1163
|
});
|
|
1162
|
-
if (data
|
|
1164
|
+
if (data.isImpossible) {
|
|
1163
1165
|
const actionTaken = data.action === "block" ? "blocked" : data.action === "challenge" ? "challenged" : "logged";
|
|
1164
1166
|
logEvent({
|
|
1165
1167
|
type: "impossible_travel",
|
|
@@ -1177,8 +1179,9 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1177
1179
|
action: actionTaken
|
|
1178
1180
|
});
|
|
1179
1181
|
}
|
|
1180
|
-
return data
|
|
1181
|
-
} catch {
|
|
1182
|
+
return data;
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
logger.warn("[Dash] Impossible travel check failed:", error);
|
|
1182
1185
|
return null;
|
|
1183
1186
|
}
|
|
1184
1187
|
},
|
|
@@ -1204,14 +1207,14 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1204
1207
|
action: "log"
|
|
1205
1208
|
};
|
|
1206
1209
|
try {
|
|
1207
|
-
const
|
|
1210
|
+
const data = await $api("/security/free-trial-abuse/check", {
|
|
1208
1211
|
method: "POST",
|
|
1209
1212
|
body: {
|
|
1210
1213
|
visitorId,
|
|
1211
1214
|
config: options
|
|
1212
1215
|
}
|
|
1213
1216
|
});
|
|
1214
|
-
if (data
|
|
1217
|
+
if (data.isAbuse) logEvent({
|
|
1215
1218
|
type: "free_trial_abuse",
|
|
1216
1219
|
userId: null,
|
|
1217
1220
|
visitorId,
|
|
@@ -1223,13 +1226,9 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1223
1226
|
},
|
|
1224
1227
|
action: data.action === "block" ? "blocked" : "logged"
|
|
1225
1228
|
});
|
|
1226
|
-
return data
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
maxAccounts: 0,
|
|
1230
|
-
action: "log"
|
|
1231
|
-
};
|
|
1232
|
-
} catch {
|
|
1229
|
+
return data;
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
logger.warn("[Dash] Free trial abuse check failed:", error);
|
|
1233
1232
|
return {
|
|
1234
1233
|
isAbuse: false,
|
|
1235
1234
|
accountCount: 0,
|
|
@@ -1254,17 +1253,17 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1254
1253
|
},
|
|
1255
1254
|
async checkCompromisedPassword(password) {
|
|
1256
1255
|
try {
|
|
1257
|
-
const hash = await sha1Hash(password);
|
|
1258
|
-
const prefix = hash.substring(0, 5);
|
|
1259
|
-
const suffix = hash.substring(5);
|
|
1260
|
-
const
|
|
1256
|
+
const hash$1 = await sha1Hash(password);
|
|
1257
|
+
const prefix = hash$1.substring(0, 5);
|
|
1258
|
+
const suffix = hash$1.substring(5);
|
|
1259
|
+
const data = await $api("/security/breached-passwords", {
|
|
1261
1260
|
method: "POST",
|
|
1262
1261
|
body: {
|
|
1263
1262
|
passwordPrefix: prefix,
|
|
1264
1263
|
config: options
|
|
1265
1264
|
}
|
|
1266
1265
|
});
|
|
1267
|
-
if (!data
|
|
1266
|
+
if (!data.enabled) return { compromised: false };
|
|
1268
1267
|
const breachCount = (data.suffixes || {})[suffix] || 0;
|
|
1269
1268
|
const minBreachCount = data.minBreachCount ?? 1;
|
|
1270
1269
|
const action = data.action || "block";
|
|
@@ -1291,7 +1290,7 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1291
1290
|
async checkStaleUser(userId, lastActiveAt) {
|
|
1292
1291
|
if (!options.staleUsers?.enabled) return { isStale: false };
|
|
1293
1292
|
try {
|
|
1294
|
-
const
|
|
1293
|
+
const data = await $api("/security/stale-user", {
|
|
1295
1294
|
method: "POST",
|
|
1296
1295
|
body: {
|
|
1297
1296
|
userId,
|
|
@@ -1299,7 +1298,7 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1299
1298
|
config: options
|
|
1300
1299
|
}
|
|
1301
1300
|
});
|
|
1302
|
-
if (data
|
|
1301
|
+
if (data.isStale) logEvent({
|
|
1303
1302
|
type: "stale_account_reactivation",
|
|
1304
1303
|
userId,
|
|
1305
1304
|
visitorId: null,
|
|
@@ -1314,7 +1313,7 @@ function createSecurityClient(apiUrl, apiKey, options, onSecurityEvent) {
|
|
|
1314
1313
|
},
|
|
1315
1314
|
action: data.action === "block" ? "blocked" : data.action === "challenge" ? "challenged" : "logged"
|
|
1316
1315
|
});
|
|
1317
|
-
return data
|
|
1316
|
+
return data;
|
|
1318
1317
|
} catch (error) {
|
|
1319
1318
|
logger.error("[Dash] Stale user check error:", error);
|
|
1320
1319
|
return { isStale: false };
|
|
@@ -1496,8 +1495,13 @@ async function handleSecurityVerdict(verdict, ctx, trackEvent, securityService)
|
|
|
1496
1495
|
const reason = verdict.reason || "unknown";
|
|
1497
1496
|
const confidence = 1;
|
|
1498
1497
|
if (verdict.action === "challenge" && ctx.visitorId) {
|
|
1498
|
+
const challenge = verdict.challenge || await securityService.generateChallenge(ctx.visitorId);
|
|
1499
|
+
if (!challenge?.trim()) {
|
|
1500
|
+
logger.warn("[Sentinel] Could not generate PoW challenge (service may be unavailable). Falling back to allow.");
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1499
1503
|
trackEvent(buildEventData(ctx, "challenged", reason, confidence, verdict.details));
|
|
1500
|
-
throwChallengeError(
|
|
1504
|
+
throwChallengeError(challenge, reason, "Please complete a security check to continue.");
|
|
1501
1505
|
} else if (verdict.action === "block") {
|
|
1502
1506
|
trackEvent(buildEventData(ctx, "blocked", reason, confidence, verdict.details));
|
|
1503
1507
|
throw new APIError("FORBIDDEN", { message: ERROR_MESSAGES[reason] || "Access denied." });
|
|
@@ -1633,7 +1637,8 @@ function createEmailValidator(options = {}) {
|
|
|
1633
1637
|
});
|
|
1634
1638
|
const $kv = createFetch({
|
|
1635
1639
|
baseURL: kvUrl,
|
|
1636
|
-
headers: { "x-api-key": apiKey }
|
|
1640
|
+
headers: { "x-api-key": apiKey },
|
|
1641
|
+
timeout: KV_TIMEOUT_MS
|
|
1637
1642
|
});
|
|
1638
1643
|
/**
|
|
1639
1644
|
* Fetch and resolve email validity policy from API with caching
|
|
@@ -2124,12 +2129,19 @@ const sentinel = (options) => {
|
|
|
2124
2129
|
try {
|
|
2125
2130
|
user = await hookCtx.context.adapter.findOne({
|
|
2126
2131
|
model: "user",
|
|
2132
|
+
select: [
|
|
2133
|
+
"email",
|
|
2134
|
+
"name",
|
|
2135
|
+
"lastActiveAt"
|
|
2136
|
+
],
|
|
2127
2137
|
where: [{
|
|
2128
2138
|
field: "id",
|
|
2129
2139
|
value: session.userId
|
|
2130
2140
|
}]
|
|
2131
2141
|
});
|
|
2132
|
-
} catch {
|
|
2142
|
+
} catch (error) {
|
|
2143
|
+
logger.warn("[Sentinel] Failed to fetch user for security checks:", error);
|
|
2144
|
+
}
|
|
2133
2145
|
if (visitorId) {
|
|
2134
2146
|
if (await securityService.checkUnknownDevice(session.userId, visitorId) && user?.email) backgroundTask(() => securityService.notifyUnknownDevice(session.userId, user.email, identification));
|
|
2135
2147
|
}
|
|
@@ -2628,16 +2640,49 @@ const initTeamEvents = (tracker) => {
|
|
|
2628
2640
|
//#endregion
|
|
2629
2641
|
//#region src/jwt.ts
|
|
2630
2642
|
/**
|
|
2643
|
+
* Hash the given value
|
|
2644
|
+
* Note: Must match @infra/crypto hash()
|
|
2645
|
+
* @param value - The value to hash
|
|
2646
|
+
*/
|
|
2647
|
+
async function hash(value) {
|
|
2648
|
+
const data = new TextEncoder().encode(value);
|
|
2649
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
2650
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
2651
|
+
return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2652
|
+
}
|
|
2653
|
+
/**
|
|
2631
2654
|
* Skip JTI check for JWTs issued within this many seconds.
|
|
2632
2655
|
* This is safe because replay attacks require time for interception.
|
|
2633
2656
|
* A freshly issued token is almost certainly legitimate.
|
|
2634
2657
|
*/
|
|
2635
2658
|
const JTI_CHECK_GRACE_PERIOD_SECONDS = 30;
|
|
2636
|
-
|
|
2659
|
+
const JWKS_CACHE_TTL_MS = 900 * 1e3;
|
|
2660
|
+
const jwksCache = /* @__PURE__ */ new Map();
|
|
2661
|
+
const inflightRequests = /* @__PURE__ */ new Map();
|
|
2662
|
+
async function fetchJWKS(apiUrl) {
|
|
2637
2663
|
const jwks = await betterFetch(`${apiUrl}/api/auth/jwks`);
|
|
2638
2664
|
if (!jwks.data) throw new APIError$1("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2665
|
+
jwksCache.set(apiUrl, {
|
|
2666
|
+
data: jwks.data,
|
|
2667
|
+
expiresAt: Date.now() + JWKS_CACHE_TTL_MS
|
|
2668
|
+
});
|
|
2639
2669
|
return createLocalJWKSet(jwks.data);
|
|
2640
2670
|
}
|
|
2671
|
+
async function prefetchJWKS(apiUrl) {
|
|
2672
|
+
const fetchPromise = fetchJWKS(apiUrl);
|
|
2673
|
+
inflightRequests.set(apiUrl, fetchPromise);
|
|
2674
|
+
fetchPromise.finally(() => inflightRequests.delete(apiUrl));
|
|
2675
|
+
return fetchPromise;
|
|
2676
|
+
}
|
|
2677
|
+
async function getJWKs(apiUrl) {
|
|
2678
|
+
const cached = jwksCache.get(apiUrl);
|
|
2679
|
+
if (cached && Date.now() < cached.expiresAt) return createLocalJWKSet(cached.data);
|
|
2680
|
+
if (cached) {
|
|
2681
|
+
if (!inflightRequests.has(apiUrl)) prefetchJWKS(apiUrl);
|
|
2682
|
+
return createLocalJWKSet(cached.data);
|
|
2683
|
+
}
|
|
2684
|
+
return inflightRequests.get(apiUrl) ?? prefetchJWKS(apiUrl);
|
|
2685
|
+
}
|
|
2641
2686
|
/**
|
|
2642
2687
|
* Check if JWT is recently issued and can skip JTI verification.
|
|
2643
2688
|
* JWTs issued within the grace period are trusted without JTI check
|
|
@@ -2651,19 +2696,24 @@ function isRecentlyIssued(payload) {
|
|
|
2651
2696
|
const jwtMiddleware = (options, schema, getJWT) => createAuthMiddleware(async (ctx) => {
|
|
2652
2697
|
const jwsFromHeader = getJWT ? await getJWT(ctx) : ctx.headers?.get("Authorization")?.split(" ")[1];
|
|
2653
2698
|
if (!jwsFromHeader) throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2654
|
-
const { payload } = await jwtVerify(jwsFromHeader, await getJWKs(options.apiUrl), { maxTokenAge: "5m" }).catch(() => {
|
|
2699
|
+
const { payload } = await jwtVerify(jwsFromHeader, await getJWKs(options.apiUrl), { maxTokenAge: "5m" }).catch((e) => {
|
|
2700
|
+
ctx.context.logger.error("[Dash] JWT verification failed:", e);
|
|
2655
2701
|
throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2656
2702
|
});
|
|
2657
2703
|
if (!isRecentlyIssued(payload)) {
|
|
2658
2704
|
if (!(await betterFetch("/api/auth/check-jti", {
|
|
2659
2705
|
baseURL: options.apiUrl,
|
|
2660
2706
|
method: "POST",
|
|
2707
|
+
headers: { "x-api-key": options.apiKey },
|
|
2661
2708
|
body: {
|
|
2662
2709
|
jti: payload.jti,
|
|
2663
2710
|
expiresAt: payload.exp
|
|
2664
2711
|
}
|
|
2665
2712
|
})).data?.valid) throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2666
2713
|
}
|
|
2714
|
+
const apiKeyHash = payload.apiKeyHash;
|
|
2715
|
+
if (typeof apiKeyHash !== "string" || !options.apiKey) throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2716
|
+
if (apiKeyHash !== await hash(options.apiKey)) throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
2667
2717
|
if (schema) {
|
|
2668
2718
|
const parsed = schema.safeParse(payload);
|
|
2669
2719
|
if (!parsed.success) throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
|
|
@@ -2674,6 +2724,33 @@ const jwtMiddleware = (options, schema, getJWT) => createAuthMiddleware(async (c
|
|
|
2674
2724
|
|
|
2675
2725
|
//#endregion
|
|
2676
2726
|
//#region src/routes/config.ts
|
|
2727
|
+
const PLUGIN_OPTIONS_EXCLUDE_KEYS = { stripe: new Set(["stripeClient"]) };
|
|
2728
|
+
function isPlainSerializable(value) {
|
|
2729
|
+
if (value === null || typeof value !== "object") return true;
|
|
2730
|
+
if (Array.isArray(value)) return true;
|
|
2731
|
+
if (value instanceof Date) return true;
|
|
2732
|
+
const constructor = Object.getPrototypeOf(value)?.constructor;
|
|
2733
|
+
if (constructor && constructor.name !== "Object" && constructor.name !== "Array") return false;
|
|
2734
|
+
return true;
|
|
2735
|
+
}
|
|
2736
|
+
function sanitizePluginOptions(pluginId, options, seen = /* @__PURE__ */ new WeakSet()) {
|
|
2737
|
+
if (options === null || options === void 0) return options;
|
|
2738
|
+
if (typeof options === "function") return void 0;
|
|
2739
|
+
if (typeof options !== "object") return options;
|
|
2740
|
+
if (seen.has(options)) return void 0;
|
|
2741
|
+
seen.add(options);
|
|
2742
|
+
const excludeKeys = PLUGIN_OPTIONS_EXCLUDE_KEYS[pluginId];
|
|
2743
|
+
if (Array.isArray(options)) return options.map((item) => sanitizePluginOptions(pluginId, item, seen)).filter((item) => item !== void 0);
|
|
2744
|
+
const result = {};
|
|
2745
|
+
for (const [key, value] of Object.entries(options)) {
|
|
2746
|
+
if (excludeKeys?.has(key)) continue;
|
|
2747
|
+
if (typeof value === "function") continue;
|
|
2748
|
+
if (value !== null && typeof value === "object" && !isPlainSerializable(value)) continue;
|
|
2749
|
+
const sanitized = sanitizePluginOptions(pluginId, value, seen);
|
|
2750
|
+
if (sanitized !== void 0) result[key] = sanitized;
|
|
2751
|
+
}
|
|
2752
|
+
return result;
|
|
2753
|
+
}
|
|
2677
2754
|
function estimateEntropy(str) {
|
|
2678
2755
|
const unique = new Set(str).size;
|
|
2679
2756
|
if (unique === 0) return 0;
|
|
@@ -2685,6 +2762,7 @@ const getConfig = (options) => {
|
|
|
2685
2762
|
use: [jwtMiddleware(options)]
|
|
2686
2763
|
}, async (ctx) => {
|
|
2687
2764
|
const advancedOptions = ctx.context.options.advanced;
|
|
2765
|
+
const organizationPlugin = ctx.context.getPlugin("organization");
|
|
2688
2766
|
return {
|
|
2689
2767
|
version: ctx.context.version || null,
|
|
2690
2768
|
socialProviders: Object.keys(ctx.context.options.socialProviders || {}),
|
|
@@ -2693,13 +2771,13 @@ const getConfig = (options) => {
|
|
|
2693
2771
|
return {
|
|
2694
2772
|
id: plugin.id,
|
|
2695
2773
|
schema: plugin.schema,
|
|
2696
|
-
options: plugin.options
|
|
2774
|
+
options: sanitizePluginOptions(plugin.id, plugin.options)
|
|
2697
2775
|
};
|
|
2698
2776
|
}),
|
|
2699
2777
|
organization: {
|
|
2700
|
-
sendInvitationEmailEnabled: !!
|
|
2778
|
+
sendInvitationEmailEnabled: !!organizationPlugin?.options?.sendInvitationEmail,
|
|
2701
2779
|
additionalFields: (() => {
|
|
2702
|
-
const orgAdditionalFields =
|
|
2780
|
+
const orgAdditionalFields = organizationPlugin?.options?.schema?.organization?.additionalFields || {};
|
|
2703
2781
|
return Object.keys(orgAdditionalFields).map((field) => {
|
|
2704
2782
|
const fieldType = orgAdditionalFields[field];
|
|
2705
2783
|
return {
|
|
@@ -2853,7 +2931,8 @@ const listOrganizationDirectories = (options) => {
|
|
|
2853
2931
|
providerId: provider.providerId,
|
|
2854
2932
|
scimEndpoint: getScimEndpoint(ctx.context.baseURL)
|
|
2855
2933
|
}));
|
|
2856
|
-
} catch {
|
|
2934
|
+
} catch (error) {
|
|
2935
|
+
ctx.context.logger.warn("[Dash] Failed to list SCIM provider connections:", error);
|
|
2857
2936
|
return [];
|
|
2858
2937
|
}
|
|
2859
2938
|
});
|
|
@@ -3036,7 +3115,10 @@ const getUserEvents = (options) => {
|
|
|
3036
3115
|
offset: offset.toString()
|
|
3037
3116
|
}
|
|
3038
3117
|
});
|
|
3039
|
-
if (error || !data)
|
|
3118
|
+
if (error || !data) {
|
|
3119
|
+
ctx.context.logger.error("[Dash] Failed to fetch events:", error);
|
|
3120
|
+
throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Failed to fetch events" });
|
|
3121
|
+
}
|
|
3040
3122
|
let events = data.events.map(transformEvent);
|
|
3041
3123
|
if (ctx.query?.eventType) events = events.filter((event) => event.eventType === ctx.query?.eventType);
|
|
3042
3124
|
return {
|
|
@@ -3118,7 +3200,10 @@ const getAuditLogs = (options) => {
|
|
|
3118
3200
|
offset: offset.toString()
|
|
3119
3201
|
}
|
|
3120
3202
|
});
|
|
3121
|
-
if (error || !data)
|
|
3203
|
+
if (error || !data) {
|
|
3204
|
+
ctx.context.logger.error("[Dash] Failed to fetch organization audit logs:", error);
|
|
3205
|
+
throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Failed to fetch organization audit logs" });
|
|
3206
|
+
}
|
|
3122
3207
|
rawEvents = data.events;
|
|
3123
3208
|
total = data.total;
|
|
3124
3209
|
responseLimit = data.limit;
|
|
@@ -3132,7 +3217,10 @@ const getAuditLogs = (options) => {
|
|
|
3132
3217
|
offset: offset.toString()
|
|
3133
3218
|
}
|
|
3134
3219
|
});
|
|
3135
|
-
if (error || !data)
|
|
3220
|
+
if (error || !data) {
|
|
3221
|
+
ctx.context.logger.error("[Dash] Failed to fetch user audit logs:", error);
|
|
3222
|
+
throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Failed to fetch user audit logs" });
|
|
3223
|
+
}
|
|
3136
3224
|
rawEvents = data.events;
|
|
3137
3225
|
total = data.total;
|
|
3138
3226
|
responseLimit = data.limit;
|
|
@@ -3503,7 +3591,8 @@ const listOrganizationLogDrains = (options) => {
|
|
|
3503
3591
|
...drain,
|
|
3504
3592
|
config: maskSensitiveConfig(drain.config, drain.destinationType)
|
|
3505
3593
|
}));
|
|
3506
|
-
} catch {
|
|
3594
|
+
} catch (error) {
|
|
3595
|
+
ctx.context.logger.warn("[Dash] Failed to list log drains:", error);
|
|
3507
3596
|
return [];
|
|
3508
3597
|
}
|
|
3509
3598
|
});
|
|
@@ -3765,6 +3854,7 @@ const testOrganizationLogDrain = (options) => {
|
|
|
3765
3854
|
}
|
|
3766
3855
|
return { success: true };
|
|
3767
3856
|
} catch (error) {
|
|
3857
|
+
ctx.context.logger.warn("[Dash] Log drain test failed:", error);
|
|
3768
3858
|
return {
|
|
3769
3859
|
success: false,
|
|
3770
3860
|
error: error instanceof Error ? error.message : "Unknown error"
|
|
@@ -3773,6 +3863,89 @@ const testOrganizationLogDrain = (options) => {
|
|
|
3773
3863
|
});
|
|
3774
3864
|
};
|
|
3775
3865
|
|
|
3866
|
+
//#endregion
|
|
3867
|
+
//#region src/export-factory.ts
|
|
3868
|
+
const exportFactory = (input, options) => async (ctx) => {
|
|
3869
|
+
const batchSize = options?.batchSize || 1e4;
|
|
3870
|
+
const staleMs = options?.staleMs || 300 * 1e3;
|
|
3871
|
+
const enabledFields = options?.enabledFields || [];
|
|
3872
|
+
const userLimit = input.limit;
|
|
3873
|
+
const userOffset = input.offset || 0;
|
|
3874
|
+
const abortController = new AbortController();
|
|
3875
|
+
let page = 0;
|
|
3876
|
+
let totalExported = 0;
|
|
3877
|
+
let staleTimeout;
|
|
3878
|
+
const resetTimeout = () => {
|
|
3879
|
+
clearTimeout(staleTimeout);
|
|
3880
|
+
staleTimeout = setTimeout(() => {
|
|
3881
|
+
abortController.abort();
|
|
3882
|
+
}, staleMs);
|
|
3883
|
+
};
|
|
3884
|
+
const stream = new ReadableStream({ async start(controller) {
|
|
3885
|
+
const start = performance.now();
|
|
3886
|
+
resetTimeout();
|
|
3887
|
+
try {
|
|
3888
|
+
while (true) {
|
|
3889
|
+
let effectiveBatchSize = batchSize;
|
|
3890
|
+
if (userLimit !== void 0) {
|
|
3891
|
+
const remaining = userLimit - totalExported;
|
|
3892
|
+
if (remaining <= 0) break;
|
|
3893
|
+
effectiveBatchSize = Math.min(batchSize, remaining);
|
|
3894
|
+
}
|
|
3895
|
+
const batch = await ctx.context.adapter.findMany({
|
|
3896
|
+
...input,
|
|
3897
|
+
limit: effectiveBatchSize,
|
|
3898
|
+
offset: userOffset + page * batchSize
|
|
3899
|
+
});
|
|
3900
|
+
resetTimeout();
|
|
3901
|
+
if (batch.length === 0) {
|
|
3902
|
+
if (page === 0) throw new APIError("FAILED_DEPENDENCY", { message: "Nothing found to export" });
|
|
3903
|
+
break;
|
|
3904
|
+
}
|
|
3905
|
+
let processedBatch = batch;
|
|
3906
|
+
if (enabledFields.length > 0) processedBatch = batch.map((item) => Object.fromEntries(enabledFields.map((key) => [key, item?.[key] ?? null])));
|
|
3907
|
+
const chunk = processedBatch.map((u) => options?.processRow ? options.processRow(u) : u).filter((v) => v !== void 0).map((u) => JSON.stringify(u)).join("\n") + "\n";
|
|
3908
|
+
controller.enqueue(new TextEncoder().encode(chunk));
|
|
3909
|
+
totalExported += batch.length;
|
|
3910
|
+
page++;
|
|
3911
|
+
if (userLimit !== void 0 && totalExported >= userLimit) break;
|
|
3912
|
+
resetTimeout();
|
|
3913
|
+
}
|
|
3914
|
+
} catch (err) {
|
|
3915
|
+
if (!abortController.signal.aborted) console.error("Export stream failed:", err);
|
|
3916
|
+
} finally {
|
|
3917
|
+
clearTimeout(staleTimeout);
|
|
3918
|
+
controller.close();
|
|
3919
|
+
const end = performance.now();
|
|
3920
|
+
console.log(`Export streamed ${totalExported} records in ${Math.round((end - start) / 1e3)}s` + (abortController.signal.aborted ? " (stale timeout)" : ""));
|
|
3921
|
+
}
|
|
3922
|
+
} });
|
|
3923
|
+
return new Response(stream, { headers: { "Content-Type": "application/x-ndjson" } });
|
|
3924
|
+
};
|
|
3925
|
+
|
|
3926
|
+
//#endregion
|
|
3927
|
+
//#region src/helper.ts
|
|
3928
|
+
function* chunkArray(arr, options) {
|
|
3929
|
+
const batchSize = options?.batchSize || 200;
|
|
3930
|
+
for (let i = 0; i < arr.length; i += batchSize) yield arr.slice(i, i + batchSize);
|
|
3931
|
+
}
|
|
3932
|
+
async function withConcurrency(items, fn, options) {
|
|
3933
|
+
const concurrency = options?.concurrency || 5;
|
|
3934
|
+
const results = [];
|
|
3935
|
+
const executing = [];
|
|
3936
|
+
const run = async () => {
|
|
3937
|
+
const batchResults = await Promise.all(executing);
|
|
3938
|
+
results.push(...batchResults);
|
|
3939
|
+
executing.length = 0;
|
|
3940
|
+
};
|
|
3941
|
+
for (const item of items) {
|
|
3942
|
+
executing.push(fn(item));
|
|
3943
|
+
if (executing.length >= concurrency) await run();
|
|
3944
|
+
}
|
|
3945
|
+
if (executing.length > 0) await run();
|
|
3946
|
+
return results;
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3776
3949
|
//#endregion
|
|
3777
3950
|
//#region src/validation/ssrf.ts
|
|
3778
3951
|
/**
|
|
@@ -3865,9 +4038,7 @@ function buildSessionContext(userId, user) {
|
|
|
3865
4038
|
} };
|
|
3866
4039
|
}
|
|
3867
4040
|
function getSSOPlugin(ctx) {
|
|
3868
|
-
|
|
3869
|
-
if (!plugin || !("endpoints" in plugin)) return null;
|
|
3870
|
-
return plugin;
|
|
4041
|
+
return ctx.context.getPlugin("sso");
|
|
3871
4042
|
}
|
|
3872
4043
|
|
|
3873
4044
|
//#endregion
|
|
@@ -3916,12 +4087,19 @@ const listOrganizations = (options) => {
|
|
|
3916
4087
|
"members"
|
|
3917
4088
|
]).optional(),
|
|
3918
4089
|
sortOrder: z$1.enum(["asc", "desc"]).optional(),
|
|
4090
|
+
filterMembers: z$1.enum([
|
|
4091
|
+
"abandoned",
|
|
4092
|
+
"eq1",
|
|
4093
|
+
"gt1",
|
|
4094
|
+
"gt5",
|
|
4095
|
+
"gt10"
|
|
4096
|
+
]).optional(),
|
|
3919
4097
|
search: z$1.string().optional(),
|
|
3920
4098
|
startDate: z$1.date().or(z$1.string().transform((val) => new Date(val))).optional(),
|
|
3921
4099
|
endDate: z$1.date().or(z$1.string().transform((val) => new Date(val))).optional()
|
|
3922
4100
|
}).optional()
|
|
3923
4101
|
}, async (ctx) => {
|
|
3924
|
-
const { limit = 10, offset = 0, sortBy = "createdAt", sortOrder = "desc", search } = ctx.query || {};
|
|
4102
|
+
const { limit = 10, offset = 0, sortBy = "createdAt", sortOrder = "desc", search, filterMembers } = ctx.query || {};
|
|
3925
4103
|
const where = [];
|
|
3926
4104
|
if (search && search.trim().length > 0) {
|
|
3927
4105
|
const searchTerm = search.trim();
|
|
@@ -3947,26 +4125,64 @@ const listOrganizations = (options) => {
|
|
|
3947
4125
|
value: ctx.query.endDate,
|
|
3948
4126
|
operator: "lte"
|
|
3949
4127
|
});
|
|
4128
|
+
const needsInMemoryProcessing = sortBy === "members" || !!filterMembers;
|
|
4129
|
+
const dbSortBy = sortBy === "members" ? "createdAt" : sortBy;
|
|
3950
4130
|
const [organizations, initialTotal] = await Promise.all([ctx.context.adapter.findMany({
|
|
3951
4131
|
model: "organization",
|
|
3952
4132
|
where,
|
|
3953
|
-
|
|
3954
|
-
|
|
4133
|
+
...needsInMemoryProcessing ? {} : {
|
|
4134
|
+
limit,
|
|
4135
|
+
offset
|
|
4136
|
+
},
|
|
3955
4137
|
sortBy: {
|
|
3956
|
-
field:
|
|
4138
|
+
field: dbSortBy,
|
|
3957
4139
|
direction: sortOrder
|
|
3958
|
-
}
|
|
3959
|
-
|
|
3960
|
-
}), ctx.context.adapter.count({
|
|
4140
|
+
}
|
|
4141
|
+
}), needsInMemoryProcessing ? Promise.resolve(0) : ctx.context.adapter.count({
|
|
3961
4142
|
model: "organization",
|
|
3962
4143
|
where
|
|
3963
4144
|
})]);
|
|
3964
|
-
const
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
4145
|
+
const orgIds = organizations.map((o) => o.id);
|
|
4146
|
+
const allMembers = orgIds.length > 0 ? await ctx.context.adapter.findMany({
|
|
4147
|
+
model: "member",
|
|
4148
|
+
where: [{
|
|
4149
|
+
field: "organizationId",
|
|
4150
|
+
value: orgIds,
|
|
4151
|
+
operator: "in"
|
|
4152
|
+
}]
|
|
4153
|
+
}) : [];
|
|
4154
|
+
const membersByOrg = /* @__PURE__ */ new Map();
|
|
4155
|
+
for (const m of allMembers) {
|
|
4156
|
+
const list = membersByOrg.get(m.organizationId) || [];
|
|
4157
|
+
list.push(m);
|
|
4158
|
+
membersByOrg.set(m.organizationId, list);
|
|
4159
|
+
}
|
|
4160
|
+
let withCounts = organizations.map((organization) => {
|
|
4161
|
+
const orgMembers = membersByOrg.get(organization.id) || [];
|
|
4162
|
+
return {
|
|
4163
|
+
...organization,
|
|
4164
|
+
_members: orgMembers,
|
|
4165
|
+
memberCount: orgMembers.length
|
|
4166
|
+
};
|
|
4167
|
+
});
|
|
4168
|
+
if (filterMembers) {
|
|
4169
|
+
const predicate = {
|
|
4170
|
+
abandoned: (c) => c === 0,
|
|
4171
|
+
eq1: (c) => c === 1,
|
|
4172
|
+
gt1: (c) => c > 1,
|
|
4173
|
+
gt5: (c) => c > 5,
|
|
4174
|
+
gt10: (c) => c > 10
|
|
4175
|
+
}[filterMembers];
|
|
4176
|
+
if (predicate) withCounts = withCounts.filter((o) => predicate(o.memberCount));
|
|
4177
|
+
}
|
|
4178
|
+
if (sortBy === "members") {
|
|
4179
|
+
const dir = sortOrder === "asc" ? 1 : -1;
|
|
4180
|
+
withCounts.sort((a, b) => (a.memberCount - b.memberCount) * dir);
|
|
4181
|
+
}
|
|
4182
|
+
const total = needsInMemoryProcessing ? withCounts.length : initialTotal;
|
|
4183
|
+
if (needsInMemoryProcessing) withCounts = withCounts.slice(offset, offset + limit);
|
|
3968
4184
|
const allUserIds = /* @__PURE__ */ new Set();
|
|
3969
|
-
for (const organization of withCounts) for (const member of organization.
|
|
4185
|
+
for (const organization of withCounts) for (const member of organization._members.slice(0, 5)) allUserIds.add(member.userId);
|
|
3970
4186
|
const users = allUserIds.size > 0 ? await ctx.context.adapter.findMany({
|
|
3971
4187
|
model: "user",
|
|
3972
4188
|
where: [{
|
|
@@ -3978,29 +4194,63 @@ const listOrganizations = (options) => {
|
|
|
3978
4194
|
const userMap = new Map(users.map((u) => [u.id, u]));
|
|
3979
4195
|
return {
|
|
3980
4196
|
organizations: withCounts.map((organization) => {
|
|
3981
|
-
const members = organization.
|
|
4197
|
+
const members = organization._members.slice(0, 5).map((m) => userMap.get(m.userId)).filter((u) => u !== void 0).map((u) => ({
|
|
3982
4198
|
id: u.id,
|
|
3983
4199
|
name: u.name,
|
|
3984
4200
|
email: u.email,
|
|
3985
4201
|
image: u.image
|
|
3986
4202
|
}));
|
|
4203
|
+
const { _members, ...org } = organization;
|
|
3987
4204
|
return {
|
|
3988
|
-
...
|
|
4205
|
+
...org,
|
|
3989
4206
|
members
|
|
3990
4207
|
};
|
|
3991
4208
|
}),
|
|
3992
|
-
total
|
|
4209
|
+
total,
|
|
3993
4210
|
offset,
|
|
3994
4211
|
limit
|
|
3995
4212
|
};
|
|
3996
4213
|
});
|
|
3997
4214
|
};
|
|
4215
|
+
function parseWhereClause$1(val) {
|
|
4216
|
+
if (!val) return [];
|
|
4217
|
+
const parsed = JSON.parse(val);
|
|
4218
|
+
if (!Array.isArray(parsed)) return [];
|
|
4219
|
+
return parsed;
|
|
4220
|
+
}
|
|
4221
|
+
const exportOrganizationsQuerySchema = z$1.object({
|
|
4222
|
+
limit: z$1.number().or(z$1.string().transform(Number)).optional(),
|
|
4223
|
+
offset: z$1.number().or(z$1.string().transform(Number)).optional(),
|
|
4224
|
+
sortBy: z$1.string().optional(),
|
|
4225
|
+
sortOrder: z$1.enum(["asc", "desc"]).optional(),
|
|
4226
|
+
where: z$1.string().transform(parseWhereClause$1).optional()
|
|
4227
|
+
}).optional();
|
|
4228
|
+
const exportOrganizations = (options) => {
|
|
4229
|
+
return createAuthEndpoint("/dash/export-organizations", {
|
|
4230
|
+
method: "GET",
|
|
4231
|
+
use: [jwtMiddleware(options)],
|
|
4232
|
+
query: exportOrganizationsQuerySchema
|
|
4233
|
+
}, async (ctx) => {
|
|
4234
|
+
const sortBy = ctx.query?.sortBy || "createdAt";
|
|
4235
|
+
const sortOrder = ctx.query?.sortOrder || "desc";
|
|
4236
|
+
return exportFactory({
|
|
4237
|
+
model: "organization",
|
|
4238
|
+
limit: ctx.query?.limit,
|
|
4239
|
+
offset: ctx.query?.offset ?? 0,
|
|
4240
|
+
sortBy: {
|
|
4241
|
+
field: sortBy,
|
|
4242
|
+
direction: sortOrder
|
|
4243
|
+
},
|
|
4244
|
+
where: ctx.query?.where
|
|
4245
|
+
})(ctx);
|
|
4246
|
+
});
|
|
4247
|
+
};
|
|
3998
4248
|
const getOrganizationOptions = (options) => {
|
|
3999
4249
|
return createAuthEndpoint("/dash/organization/options", {
|
|
4000
4250
|
method: "GET",
|
|
4001
4251
|
use: [jwtMiddleware(options)]
|
|
4002
4252
|
}, async (ctx) => {
|
|
4003
|
-
const organizationPlugin = ctx.context.
|
|
4253
|
+
const organizationPlugin = ctx.context.getPlugin("organization");
|
|
4004
4254
|
if (!organizationPlugin) return { teamsEnabled: false };
|
|
4005
4255
|
return { teamsEnabled: organizationPlugin.options?.teams?.enabled && organizationPlugin.options.teams.defaultTeam?.enabled !== false };
|
|
4006
4256
|
});
|
|
@@ -4141,7 +4391,7 @@ const deleteOrganization = (options) => {
|
|
|
4141
4391
|
}, async (ctx) => {
|
|
4142
4392
|
const { organizationId } = ctx.context.payload;
|
|
4143
4393
|
const { organizationId: bodyOrganizationId } = ctx.body;
|
|
4144
|
-
const orgOptions =
|
|
4394
|
+
const orgOptions = ctx.context.getPlugin("organization")?.options || {};
|
|
4145
4395
|
if (organizationId !== bodyOrganizationId) throw ctx.error("BAD_REQUEST", { message: "Organization ID mismatch" });
|
|
4146
4396
|
const owners = await ctx.context.adapter.findMany({
|
|
4147
4397
|
model: "member",
|
|
@@ -4194,6 +4444,41 @@ const deleteOrganization = (options) => {
|
|
|
4194
4444
|
return { success: true };
|
|
4195
4445
|
});
|
|
4196
4446
|
};
|
|
4447
|
+
const deleteManyOrganizations = (options) => {
|
|
4448
|
+
return createAuthEndpoint("/dash/organization/delete-many", {
|
|
4449
|
+
method: "POST",
|
|
4450
|
+
use: [jwtMiddleware(options, z$1.object({ organizationIds: z$1.string().array() }))]
|
|
4451
|
+
}, async (ctx) => {
|
|
4452
|
+
const { organizationIds } = ctx.context.payload;
|
|
4453
|
+
const deletedOrgIds = /* @__PURE__ */ new Set();
|
|
4454
|
+
const skippedOrgIds = /* @__PURE__ */ new Set();
|
|
4455
|
+
const start = performance.now();
|
|
4456
|
+
await withConcurrency(chunkArray(organizationIds), async (chunk) => {
|
|
4457
|
+
const where = [{
|
|
4458
|
+
field: "id",
|
|
4459
|
+
value: chunk,
|
|
4460
|
+
operator: "in"
|
|
4461
|
+
}];
|
|
4462
|
+
await ctx.context.adapter.deleteMany({
|
|
4463
|
+
model: "organization",
|
|
4464
|
+
where
|
|
4465
|
+
});
|
|
4466
|
+
const remainingOrgs = await ctx.context.adapter.findMany({
|
|
4467
|
+
model: "organization",
|
|
4468
|
+
where
|
|
4469
|
+
});
|
|
4470
|
+
for (const id of chunk) if (!remainingOrgs.some((u) => u.id === id)) deletedOrgIds.add(id);
|
|
4471
|
+
else skippedOrgIds.add(id);
|
|
4472
|
+
});
|
|
4473
|
+
const end = performance.now();
|
|
4474
|
+
console.log(`Time taken to bulk delete ${deletedOrgIds.size} organizations: ${Math.round((end - start) / 1e3)}s`, skippedOrgIds.size > 0 ? `Failed: ${skippedOrgIds.size}` : "");
|
|
4475
|
+
return {
|
|
4476
|
+
success: deletedOrgIds.size > 0,
|
|
4477
|
+
deletedOrgIds: Array.from(deletedOrgIds),
|
|
4478
|
+
skippedOrgIds: Array.from(skippedOrgIds)
|
|
4479
|
+
};
|
|
4480
|
+
});
|
|
4481
|
+
};
|
|
4197
4482
|
const listOrganizationTeams = (options) => {
|
|
4198
4483
|
return createAuthEndpoint("/dash/organization/:id/teams", {
|
|
4199
4484
|
method: "GET",
|
|
@@ -4217,7 +4502,8 @@ const listOrganizationTeams = (options) => {
|
|
|
4217
4502
|
value: team.id
|
|
4218
4503
|
}]
|
|
4219
4504
|
});
|
|
4220
|
-
} catch {
|
|
4505
|
+
} catch (error) {
|
|
4506
|
+
ctx.context.logger.warn("[Dash] Failed to count team members:", error);
|
|
4221
4507
|
memberCount = 0;
|
|
4222
4508
|
}
|
|
4223
4509
|
return {
|
|
@@ -4225,7 +4511,8 @@ const listOrganizationTeams = (options) => {
|
|
|
4225
4511
|
memberCount
|
|
4226
4512
|
};
|
|
4227
4513
|
}));
|
|
4228
|
-
} catch {
|
|
4514
|
+
} catch (error) {
|
|
4515
|
+
ctx.context.logger.warn("[Dash] Failed to list organization teams:", error);
|
|
4229
4516
|
return [];
|
|
4230
4517
|
}
|
|
4231
4518
|
});
|
|
@@ -4240,7 +4527,7 @@ const updateTeam = (options) => {
|
|
|
4240
4527
|
})
|
|
4241
4528
|
}, async (ctx) => {
|
|
4242
4529
|
const { organizationId } = ctx.context.payload;
|
|
4243
|
-
const organizationPlugin = ctx.context.
|
|
4530
|
+
const organizationPlugin = ctx.context.getPlugin("organization");
|
|
4244
4531
|
if (!organizationPlugin) throw ctx.error("BAD_REQUEST", { message: "Organization plugin not enabled" });
|
|
4245
4532
|
const orgOptions = organizationPlugin.options || {};
|
|
4246
4533
|
if (!orgOptions?.teams?.enabled) throw ctx.error("BAD_REQUEST", { message: "Teams are not enabled" });
|
|
@@ -4325,7 +4612,7 @@ const deleteTeam = (options) => {
|
|
|
4325
4612
|
body: z$1.object({ teamId: z$1.string() })
|
|
4326
4613
|
}, async (ctx) => {
|
|
4327
4614
|
const { organizationId } = ctx.context.payload;
|
|
4328
|
-
const organizationPlugin = ctx.context.
|
|
4615
|
+
const organizationPlugin = ctx.context.getPlugin("organization");
|
|
4329
4616
|
if (!organizationPlugin) throw ctx.error("BAD_REQUEST", { message: "Organization plugin not enabled" });
|
|
4330
4617
|
const orgOptions = organizationPlugin.options || {};
|
|
4331
4618
|
if (!orgOptions?.teams?.enabled) throw ctx.error("BAD_REQUEST", { message: "Teams are not enabled" });
|
|
@@ -4409,7 +4696,7 @@ const createTeam = (options) => {
|
|
|
4409
4696
|
body: z$1.object({ name: z$1.string() })
|
|
4410
4697
|
}, async (ctx) => {
|
|
4411
4698
|
const { organizationId } = ctx.context.payload;
|
|
4412
|
-
const organizationPlugin = ctx.context.
|
|
4699
|
+
const organizationPlugin = ctx.context.getPlugin("organization");
|
|
4413
4700
|
if (!organizationPlugin) throw ctx.error("BAD_REQUEST", { message: "Organization plugin not enabled" });
|
|
4414
4701
|
const orgOptions = organizationPlugin.options || {};
|
|
4415
4702
|
if (!orgOptions?.teams?.enabled) throw ctx.error("BAD_REQUEST", { message: "Teams are not enabled" });
|
|
@@ -4429,7 +4716,10 @@ const createTeam = (options) => {
|
|
|
4429
4716
|
value: organizationId
|
|
4430
4717
|
}]
|
|
4431
4718
|
});
|
|
4432
|
-
const maxTeams = typeof orgOptions.teams.maximumTeams === "function" ? await orgOptions.teams.maximumTeams({
|
|
4719
|
+
const maxTeams = typeof orgOptions.teams.maximumTeams === "function" ? await orgOptions.teams.maximumTeams({
|
|
4720
|
+
organizationId,
|
|
4721
|
+
session: null
|
|
4722
|
+
}) : orgOptions.teams.maximumTeams;
|
|
4433
4723
|
if (teamsCount >= maxTeams) throw ctx.error("BAD_REQUEST", { message: `Maximum number of teams (${maxTeams}) reached for this organization` });
|
|
4434
4724
|
}
|
|
4435
4725
|
const owners = await ctx.context.adapter.findMany({
|
|
@@ -4526,7 +4816,8 @@ const listTeamMembers = (options) => {
|
|
|
4526
4816
|
} : null
|
|
4527
4817
|
};
|
|
4528
4818
|
}));
|
|
4529
|
-
} catch (
|
|
4819
|
+
} catch (e) {
|
|
4820
|
+
ctx.context.logger.warn("[Dash] Failed to list team members:", e);
|
|
4530
4821
|
return [];
|
|
4531
4822
|
}
|
|
4532
4823
|
});
|
|
@@ -4541,7 +4832,7 @@ const addTeamMember = (options) => {
|
|
|
4541
4832
|
})
|
|
4542
4833
|
}, async (ctx) => {
|
|
4543
4834
|
const { organizationId } = ctx.context.payload;
|
|
4544
|
-
const organizationPlugin = ctx.context.
|
|
4835
|
+
const organizationPlugin = ctx.context.getPlugin("organization");
|
|
4545
4836
|
if (!organizationPlugin) throw ctx.error("BAD_REQUEST", { message: "Organization plugin not enabled" });
|
|
4546
4837
|
const orgOptions = organizationPlugin.options || {};
|
|
4547
4838
|
if (!orgOptions?.teams?.enabled) throw ctx.error("BAD_REQUEST", { message: "Teams are not enabled" });
|
|
@@ -4602,7 +4893,8 @@ const addTeamMember = (options) => {
|
|
|
4602
4893
|
});
|
|
4603
4894
|
const maxMembers = typeof orgOptions.teams.maximumMembersPerTeam === "function" ? await orgOptions.teams.maximumMembersPerTeam({
|
|
4604
4895
|
teamId: ctx.body.teamId,
|
|
4605
|
-
organizationId
|
|
4896
|
+
organizationId,
|
|
4897
|
+
session: null
|
|
4606
4898
|
}) : orgOptions.teams.maximumMembersPerTeam;
|
|
4607
4899
|
if (teamMemberCount >= maxMembers) throw ctx.error("BAD_REQUEST", { message: `Maximum number of team members (${maxMembers}) reached for this team` });
|
|
4608
4900
|
}
|
|
@@ -4646,7 +4938,7 @@ const removeTeamMember = (options) => {
|
|
|
4646
4938
|
})
|
|
4647
4939
|
}, async (ctx) => {
|
|
4648
4940
|
const { organizationId } = ctx.context.payload;
|
|
4649
|
-
const organizationPlugin = ctx.context.
|
|
4941
|
+
const organizationPlugin = ctx.context.getPlugin("organization");
|
|
4650
4942
|
if (!organizationPlugin) throw ctx.error("BAD_REQUEST", { message: "Organization plugin not enabled" });
|
|
4651
4943
|
const orgOptions = organizationPlugin.options || {};
|
|
4652
4944
|
if (!orgOptions?.teams?.enabled) throw ctx.error("BAD_REQUEST", { message: "Teams are not enabled" });
|
|
@@ -4727,7 +5019,7 @@ const createOrganization = (options) => {
|
|
|
4727
5019
|
defaultTeamName: z$1.string().optional()
|
|
4728
5020
|
}).passthrough()
|
|
4729
5021
|
}, async (ctx) => {
|
|
4730
|
-
const organizationPlugin = ctx.context.
|
|
5022
|
+
const organizationPlugin = ctx.context.getPlugin("organization");
|
|
4731
5023
|
if (!organizationPlugin) throw ctx.error("BAD_REQUEST", { message: "Organization plugin not enabled" });
|
|
4732
5024
|
const { userId } = ctx.context.payload;
|
|
4733
5025
|
const user = await ctx.context.adapter.findOne({
|
|
@@ -4892,7 +5184,7 @@ const updateOrganization = (options) => {
|
|
|
4892
5184
|
}).passthrough()
|
|
4893
5185
|
}, async (ctx) => {
|
|
4894
5186
|
const { organizationId } = ctx.context.payload;
|
|
4895
|
-
const orgOptions =
|
|
5187
|
+
const orgOptions = ctx.context.getPlugin("organization")?.options || {};
|
|
4896
5188
|
if (ctx.body.slug) {
|
|
4897
5189
|
const existingOrg = await ctx.context.adapter.findOne({
|
|
4898
5190
|
model: "organization",
|
|
@@ -4929,10 +5221,16 @@ const updateOrganization = (options) => {
|
|
|
4929
5221
|
});
|
|
4930
5222
|
if (!updatedByUser) throw ctx.error("NOT_FOUND", { message: "Owner user not found" });
|
|
4931
5223
|
let updateData = { ...ctx.body };
|
|
5224
|
+
if (typeof updateData.metadata === "string") try {
|
|
5225
|
+
updateData.metadata = updateData.metadata === "" ? void 0 : JSON.parse(updateData.metadata);
|
|
5226
|
+
} catch {
|
|
5227
|
+
throw ctx.error("BAD_REQUEST", { message: "Invalid metadata: must be valid JSON" });
|
|
5228
|
+
}
|
|
4932
5229
|
if (orgOptions?.organizationHooks?.beforeUpdateOrganization) {
|
|
4933
5230
|
const response = await orgOptions.organizationHooks.beforeUpdateOrganization({
|
|
4934
5231
|
organization: updateData,
|
|
4935
|
-
user: updatedByUser
|
|
5232
|
+
user: updatedByUser,
|
|
5233
|
+
member: owner
|
|
4936
5234
|
});
|
|
4937
5235
|
if (response && typeof response === "object" && "data" in response) updateData = {
|
|
4938
5236
|
...updateData,
|
|
@@ -4945,11 +5243,16 @@ const updateOrganization = (options) => {
|
|
|
4945
5243
|
field: "id",
|
|
4946
5244
|
value: organizationId
|
|
4947
5245
|
}],
|
|
4948
|
-
update:
|
|
5246
|
+
update: {
|
|
5247
|
+
...updateData,
|
|
5248
|
+
metadata: typeof updateData.metadata === "object" ? JSON.stringify(updateData.metadata) : updateData.metadata
|
|
5249
|
+
}
|
|
4949
5250
|
});
|
|
5251
|
+
if (!organization) throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Failed to update organization" });
|
|
4950
5252
|
if (orgOptions?.organizationHooks?.afterUpdateOrganization) await orgOptions.organizationHooks.afterUpdateOrganization({
|
|
4951
5253
|
organization,
|
|
4952
|
-
user: updatedByUser
|
|
5254
|
+
user: updatedByUser,
|
|
5255
|
+
member: owner
|
|
4953
5256
|
});
|
|
4954
5257
|
return organization;
|
|
4955
5258
|
});
|
|
@@ -4964,7 +5267,7 @@ const addMember = (options) => {
|
|
|
4964
5267
|
})
|
|
4965
5268
|
}, async (ctx) => {
|
|
4966
5269
|
const { organizationId } = ctx.context.payload;
|
|
4967
|
-
const orgOptions =
|
|
5270
|
+
const orgOptions = ctx.context.getPlugin("organization")?.options || {};
|
|
4968
5271
|
const organization = await ctx.context.adapter.findOne({
|
|
4969
5272
|
model: "organization",
|
|
4970
5273
|
where: [{
|
|
@@ -5017,7 +5320,7 @@ const removeMember = (options) => {
|
|
|
5017
5320
|
body: z$1.object({ memberId: z$1.string() })
|
|
5018
5321
|
}, async (ctx) => {
|
|
5019
5322
|
const { organizationId } = ctx.context.payload;
|
|
5020
|
-
const orgOptions =
|
|
5323
|
+
const orgOptions = ctx.context.getPlugin("organization")?.options || {};
|
|
5021
5324
|
const member = await ctx.context.adapter.findOne({
|
|
5022
5325
|
model: "member",
|
|
5023
5326
|
where: [{
|
|
@@ -5097,7 +5400,7 @@ const updateMemberRole = (options) => {
|
|
|
5097
5400
|
})
|
|
5098
5401
|
}, async (ctx) => {
|
|
5099
5402
|
const { organizationId } = ctx.context.payload;
|
|
5100
|
-
const orgOptions =
|
|
5403
|
+
const orgOptions = ctx.context.getPlugin("organization")?.options || {};
|
|
5101
5404
|
const existingMember = await ctx.context.adapter.findOne({
|
|
5102
5405
|
model: "member",
|
|
5103
5406
|
where: [{
|
|
@@ -5131,7 +5434,7 @@ const updateMemberRole = (options) => {
|
|
|
5131
5434
|
member: existingMember,
|
|
5132
5435
|
user,
|
|
5133
5436
|
organization,
|
|
5134
|
-
|
|
5437
|
+
newRole
|
|
5135
5438
|
});
|
|
5136
5439
|
if (response && typeof response === "object" && "data" in response) newRole = response.data.role || newRole;
|
|
5137
5440
|
}
|
|
@@ -5143,6 +5446,7 @@ const updateMemberRole = (options) => {
|
|
|
5143
5446
|
}],
|
|
5144
5447
|
update: { role: newRole }
|
|
5145
5448
|
});
|
|
5449
|
+
if (!member) throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Failed to update member role" });
|
|
5146
5450
|
if (orgOptions?.organizationHooks?.afterUpdateMemberRole) await orgOptions.organizationHooks.afterUpdateMemberRole({
|
|
5147
5451
|
member,
|
|
5148
5452
|
user,
|
|
@@ -5166,8 +5470,8 @@ const inviteMember = (options) => {
|
|
|
5166
5470
|
}))]
|
|
5167
5471
|
}, async (ctx) => {
|
|
5168
5472
|
const { organizationId } = ctx.context.payload;
|
|
5169
|
-
const organizationPlugin = ctx.context.
|
|
5170
|
-
if (!organizationPlugin
|
|
5473
|
+
const organizationPlugin = ctx.context.getPlugin("organization");
|
|
5474
|
+
if (!organizationPlugin?.options?.sendInvitationEmail) throw ctx.error("BAD_REQUEST", { message: "Invitation email is not enabled" });
|
|
5171
5475
|
const invitedBy = await ctx.context.adapter.findOne({
|
|
5172
5476
|
model: "user",
|
|
5173
5477
|
where: [{
|
|
@@ -5177,7 +5481,7 @@ const inviteMember = (options) => {
|
|
|
5177
5481
|
});
|
|
5178
5482
|
if (!invitedBy) throw ctx.error("BAD_REQUEST", { message: "Invited by user not found" });
|
|
5179
5483
|
return await organizationPlugin.endpoints.createInvitation({
|
|
5180
|
-
headers: ctx.request?.headers,
|
|
5484
|
+
headers: ctx.request?.headers ?? new Headers(),
|
|
5181
5485
|
body: {
|
|
5182
5486
|
email: ctx.body.email,
|
|
5183
5487
|
role: ctx.body.role,
|
|
@@ -5244,7 +5548,7 @@ const cancelInvitation = (options) => {
|
|
|
5244
5548
|
}))],
|
|
5245
5549
|
body: z$1.object({ invitationId: z$1.string() })
|
|
5246
5550
|
}, async (ctx) => {
|
|
5247
|
-
const organizationPlugin = ctx.context.
|
|
5551
|
+
const organizationPlugin = ctx.context.getPlugin("organization");
|
|
5248
5552
|
if (!organizationPlugin) throw ctx.error("BAD_REQUEST", { message: "Organization plugin is not enabled" });
|
|
5249
5553
|
const orgOptions = organizationPlugin.options || {};
|
|
5250
5554
|
const { invitationId, organizationId } = ctx.context.payload;
|
|
@@ -5337,7 +5641,7 @@ async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
|
|
|
5337
5641
|
if (!metadataResponse.ok) throw ctx.error("BAD_REQUEST", { message: `Failed to fetch IdP metadata from URL: ${metadataResponse.status} ${metadataResponse.statusText}` });
|
|
5338
5642
|
idpMetadataXml = await metadataResponse.text();
|
|
5339
5643
|
} catch (e) {
|
|
5340
|
-
ctx.context.logger.error("Failed to fetch IdP metadata from URL",
|
|
5644
|
+
ctx.context.logger.error("[Dash] Failed to fetch IdP metadata from URL:", e);
|
|
5341
5645
|
throw ctx.error("BAD_REQUEST", { message: "Failed to fetch IdP metadata from URL" });
|
|
5342
5646
|
}
|
|
5343
5647
|
}
|
|
@@ -5351,10 +5655,7 @@ async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
|
|
|
5351
5655
|
const warnings = validateSAMLMetadataAlgorithms(idpMetadataXml);
|
|
5352
5656
|
if (warnings.length > 0) {
|
|
5353
5657
|
metadataAlgorithmWarnings.push(...warnings);
|
|
5354
|
-
ctx.context.logger.warn("SAML IdP metadata uses deprecated algorithms",
|
|
5355
|
-
providerId,
|
|
5356
|
-
warnings
|
|
5357
|
-
});
|
|
5658
|
+
ctx.context.logger.warn("[Dash] SAML IdP metadata uses deprecated algorithms:", providerId, warnings);
|
|
5358
5659
|
}
|
|
5359
5660
|
}
|
|
5360
5661
|
const idpMetadata = idpMetadataXml ? { metadata: idpMetadataXml } : {
|
|
@@ -5370,8 +5671,8 @@ async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
|
|
|
5370
5671
|
callbackUrl: `${baseURL}/sso/saml2/sp/acs/${providerId}`,
|
|
5371
5672
|
idpMetadata,
|
|
5372
5673
|
spMetadata: {},
|
|
5373
|
-
|
|
5374
|
-
|
|
5674
|
+
entryPoint: samlConfig.entryPoint ?? "",
|
|
5675
|
+
cert: samlConfig.cert ?? "",
|
|
5375
5676
|
...samlConfig.mapping ? { mapping: samlConfig.mapping } : {}
|
|
5376
5677
|
},
|
|
5377
5678
|
...metadataAlgorithmWarnings.length > 0 ? { warnings: metadataAlgorithmWarnings } : {}
|
|
@@ -5417,20 +5718,13 @@ async function resolveOIDCConfig(oidcConfig, domain, ctx) {
|
|
|
5417
5718
|
};
|
|
5418
5719
|
} catch (e) {
|
|
5419
5720
|
if (e instanceof DiscoveryError) {
|
|
5420
|
-
ctx.context.logger.error("OIDC discovery failed",
|
|
5421
|
-
code: e.code,
|
|
5422
|
-
message: e.message,
|
|
5423
|
-
details: e.details
|
|
5424
|
-
});
|
|
5721
|
+
ctx.context.logger.error("[Dash] OIDC discovery failed:", e);
|
|
5425
5722
|
throw ctx.error("BAD_REQUEST", {
|
|
5426
5723
|
message: `OIDC discovery failed: ${e.message}`,
|
|
5427
5724
|
code: e.code
|
|
5428
5725
|
});
|
|
5429
5726
|
}
|
|
5430
|
-
ctx.context.logger.error("OIDC discovery failed",
|
|
5431
|
-
issuer,
|
|
5432
|
-
error: e
|
|
5433
|
-
});
|
|
5727
|
+
ctx.context.logger.error("[Dash] OIDC discovery failed:", e);
|
|
5434
5728
|
throw ctx.error("BAD_REQUEST", {
|
|
5435
5729
|
message: `OIDC discovery failed: Unable to discover configuration from ${issuer}`,
|
|
5436
5730
|
code: "OIDC_DISCOVERY_FAILED"
|
|
@@ -5448,7 +5742,7 @@ const listOrganizationSsoProviders = (options) => {
|
|
|
5448
5742
|
use: [jwtMiddleware(options, z$1.object({ organizationId: z$1.string() }))]
|
|
5449
5743
|
}, async (ctx) => {
|
|
5450
5744
|
requireOrganizationAccess(ctx);
|
|
5451
|
-
if (!ctx.context.
|
|
5745
|
+
if (!ctx.context.getPlugin("sso")) return [];
|
|
5452
5746
|
try {
|
|
5453
5747
|
return await ctx.context.adapter.findMany({
|
|
5454
5748
|
model: "ssoProvider",
|
|
@@ -5457,7 +5751,8 @@ const listOrganizationSsoProviders = (options) => {
|
|
|
5457
5751
|
value: ctx.params.id
|
|
5458
5752
|
}]
|
|
5459
5753
|
});
|
|
5460
|
-
} catch {
|
|
5754
|
+
} catch (error) {
|
|
5755
|
+
ctx.context.logger.warn("[Dash] Failed to list SSO providers:", error);
|
|
5461
5756
|
return [];
|
|
5462
5757
|
}
|
|
5463
5758
|
});
|
|
@@ -5477,7 +5772,7 @@ const createSsoProvider = (options) => {
|
|
|
5477
5772
|
}, async (ctx) => {
|
|
5478
5773
|
requireOrganizationAccess(ctx);
|
|
5479
5774
|
const ssoPlugin = getSSOPlugin(ctx);
|
|
5480
|
-
if (!ssoPlugin) throw ctx.error("BAD_REQUEST", { message: "SSO plugin is not enabled" });
|
|
5775
|
+
if (!ssoPlugin?.endpoints?.registerSSOProvider) throw ctx.error("BAD_REQUEST", { message: "SSO plugin is not enabled or feature is not supported in your plugin version" });
|
|
5481
5776
|
const organizationId = ctx.params.id;
|
|
5482
5777
|
const { providerId, domain, protocol, samlConfig, oidcConfig, userId } = ctx.body;
|
|
5483
5778
|
const registerBody = {
|
|
@@ -5508,17 +5803,18 @@ const createSsoProvider = (options) => {
|
|
|
5508
5803
|
return {
|
|
5509
5804
|
success: true,
|
|
5510
5805
|
provider: {
|
|
5511
|
-
id: result.
|
|
5806
|
+
id: result.providerId,
|
|
5512
5807
|
providerId: result.providerId || providerId,
|
|
5513
5808
|
domain: result.domain || domain
|
|
5514
5809
|
},
|
|
5515
5810
|
domainVerification: {
|
|
5516
5811
|
txtRecordName: `better-auth-token-${providerId}`,
|
|
5517
|
-
verificationToken: result.domainVerificationToken
|
|
5812
|
+
verificationToken: result.domainVerificationToken ?? null
|
|
5518
5813
|
}
|
|
5519
5814
|
};
|
|
5520
5815
|
} catch (e) {
|
|
5521
5816
|
if (e instanceof APIError$1) throw e;
|
|
5817
|
+
ctx.context.logger.error("[Dash] Failed to create SSO provider:", e);
|
|
5522
5818
|
throw ctx.error("BAD_REQUEST", { message: e instanceof Error ? e.message : "Failed to create SSO provider" });
|
|
5523
5819
|
}
|
|
5524
5820
|
});
|
|
@@ -5537,7 +5833,7 @@ const updateSsoProvider = (options) => {
|
|
|
5537
5833
|
}, async (ctx) => {
|
|
5538
5834
|
requireOrganizationAccess(ctx);
|
|
5539
5835
|
const ssoPlugin = getSSOPlugin(ctx);
|
|
5540
|
-
if (!ssoPlugin) throw ctx.error("BAD_REQUEST", { message: "SSO plugin is not enabled" });
|
|
5836
|
+
if (!ssoPlugin?.endpoints?.updateSSOProvider) throw ctx.error("BAD_REQUEST", { message: "SSO plugin is not enabled or feature is not supported in your plugin version" });
|
|
5541
5837
|
const organizationId = ctx.params.id;
|
|
5542
5838
|
const { providerId, domain, protocol, samlConfig, oidcConfig } = ctx.body;
|
|
5543
5839
|
const existingProvider = await ctx.context.adapter.findOne({
|
|
@@ -5591,13 +5887,14 @@ const updateSsoProvider = (options) => {
|
|
|
5591
5887
|
return {
|
|
5592
5888
|
success: true,
|
|
5593
5889
|
provider: {
|
|
5594
|
-
id:
|
|
5595
|
-
providerId: result.providerId
|
|
5596
|
-
domain: result.domain
|
|
5890
|
+
id: existingProvider.id,
|
|
5891
|
+
providerId: result.providerId ?? existingProvider.providerId,
|
|
5892
|
+
domain: result.domain ?? domain
|
|
5597
5893
|
}
|
|
5598
5894
|
};
|
|
5599
5895
|
} catch (e) {
|
|
5600
5896
|
if (e instanceof APIError$1) throw e;
|
|
5897
|
+
ctx.context.logger.error("[Dash] Failed to update SSO provider:", e);
|
|
5601
5898
|
throw ctx.error("BAD_REQUEST", { message: e instanceof Error ? e.message : "Failed to update SSO provider" });
|
|
5602
5899
|
}
|
|
5603
5900
|
});
|
|
@@ -5610,7 +5907,7 @@ const requestSsoVerificationToken = (options) => {
|
|
|
5610
5907
|
}, async (ctx) => {
|
|
5611
5908
|
requireOrganizationAccess(ctx);
|
|
5612
5909
|
const ssoPlugin = getSSOPlugin(ctx);
|
|
5613
|
-
if (!ssoPlugin || !ssoPlugin.options?.domainVerification?.enabled) throw ctx.error("BAD_REQUEST", { message: "SSO plugin with domain verification is not enabled" });
|
|
5910
|
+
if (!ssoPlugin?.endpoints?.requestDomainVerification || !ssoPlugin.options?.domainVerification?.enabled) throw ctx.error("BAD_REQUEST", { message: "SSO plugin with domain verification is not enabled or feature is not supported in your plugin version" });
|
|
5614
5911
|
const organizationId = ctx.params.id;
|
|
5615
5912
|
const { providerId } = ctx.body;
|
|
5616
5913
|
const provider = await ctx.context.adapter.findOne({
|
|
@@ -5642,6 +5939,7 @@ const requestSsoVerificationToken = (options) => {
|
|
|
5642
5939
|
};
|
|
5643
5940
|
} catch (e) {
|
|
5644
5941
|
if (e instanceof APIError$1) throw e;
|
|
5942
|
+
ctx.context.logger.error("[Dash] Failed to request verification token:", e);
|
|
5645
5943
|
throw ctx.error("BAD_REQUEST", { message: e instanceof Error ? e.message : "Failed to request verification token" });
|
|
5646
5944
|
}
|
|
5647
5945
|
});
|
|
@@ -5654,7 +5952,7 @@ const verifySsoProviderDomain = (options) => {
|
|
|
5654
5952
|
}, async (ctx) => {
|
|
5655
5953
|
requireOrganizationAccess(ctx);
|
|
5656
5954
|
const ssoPlugin = getSSOPlugin(ctx);
|
|
5657
|
-
if (!ssoPlugin || !ssoPlugin.options?.domainVerification?.enabled) throw ctx.error("BAD_REQUEST", { message: "SSO plugin with domain verification is not enabled" });
|
|
5955
|
+
if (!ssoPlugin?.endpoints?.verifyDomain || !ssoPlugin.options?.domainVerification?.enabled) throw ctx.error("BAD_REQUEST", { message: "SSO plugin with domain verification is not enabled or feature is not supported in your plugin version" });
|
|
5658
5956
|
const organizationId = ctx.params.id;
|
|
5659
5957
|
const { providerId } = ctx.body;
|
|
5660
5958
|
const provider = await ctx.context.adapter.findOne({
|
|
@@ -5704,7 +6002,7 @@ const deleteSsoProvider = (options) => {
|
|
|
5704
6002
|
}, async (ctx) => {
|
|
5705
6003
|
requireOrganizationAccess(ctx);
|
|
5706
6004
|
const ssoPlugin = getSSOPlugin(ctx);
|
|
5707
|
-
if (!ssoPlugin) throw ctx.error("BAD_REQUEST", { message: "SSO plugin is not enabled" });
|
|
6005
|
+
if (!ssoPlugin?.endpoints?.deleteSSOProvider) throw ctx.error("BAD_REQUEST", { message: "SSO plugin is not enabled or feature is not supported in your plugin version" });
|
|
5708
6006
|
const organizationId = ctx.params.id;
|
|
5709
6007
|
const { providerId } = ctx.body;
|
|
5710
6008
|
const provider = await ctx.context.adapter.findOne({
|
|
@@ -5732,6 +6030,7 @@ const deleteSsoProvider = (options) => {
|
|
|
5732
6030
|
};
|
|
5733
6031
|
} catch (e) {
|
|
5734
6032
|
if (e instanceof APIError$1) throw e;
|
|
6033
|
+
ctx.context.logger.error("[Dash] Failed to delete SSO provider:", e);
|
|
5735
6034
|
throw ctx.error("BAD_REQUEST", { message: e instanceof Error ? e.message : "Failed to delete SSO provider" });
|
|
5736
6035
|
}
|
|
5737
6036
|
});
|
|
@@ -5783,7 +6082,7 @@ const resendInvitation = (options) => {
|
|
|
5783
6082
|
}))],
|
|
5784
6083
|
body: z$1.object({ invitationId: z$1.string() })
|
|
5785
6084
|
}, async (ctx) => {
|
|
5786
|
-
const organizationPlugin = ctx.context.
|
|
6085
|
+
const organizationPlugin = ctx.context.getPlugin("organization");
|
|
5787
6086
|
if (!organizationPlugin) throw ctx.error("BAD_REQUEST", { message: "Organization plugin is not enabled" });
|
|
5788
6087
|
const { invitationId, organizationId } = ctx.context.payload;
|
|
5789
6088
|
const invitation = await ctx.context.adapter.findOne({
|
|
@@ -5805,7 +6104,7 @@ const resendInvitation = (options) => {
|
|
|
5805
6104
|
if (!invitedByUser) throw ctx.error("BAD_REQUEST", { message: "Inviter user not found" });
|
|
5806
6105
|
if (!organizationPlugin.endpoints?.createInvitation) throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Organization plugin endpoints not available" });
|
|
5807
6106
|
await organizationPlugin.endpoints.createInvitation({
|
|
5808
|
-
headers: ctx.request?.headers,
|
|
6107
|
+
headers: ctx.request?.headers ?? new Headers(),
|
|
5809
6108
|
body: {
|
|
5810
6109
|
email: invitation.email,
|
|
5811
6110
|
role: invitation.role,
|
|
@@ -5901,27 +6200,41 @@ const revokeAllSessions = (options) => createAuthEndpoint("/dash/sessions/revoke
|
|
|
5901
6200
|
await ctx.context.internalAdapter.deleteSessions(userId);
|
|
5902
6201
|
return ctx.json({ success: true });
|
|
5903
6202
|
});
|
|
6203
|
+
const revokeManySessions = (options) => createAuthEndpoint("/dash/sessions/revoke-many", {
|
|
6204
|
+
method: "POST",
|
|
6205
|
+
use: [jwtMiddleware(options, z$1.object({ userIds: z$1.string().array() }))]
|
|
6206
|
+
}, async (ctx) => {
|
|
6207
|
+
const { userIds } = ctx.context.payload;
|
|
6208
|
+
await withConcurrency(chunkArray(userIds, { batchSize: 50 }), async (chunk) => {
|
|
6209
|
+
for (const userId of chunk) await ctx.context.internalAdapter.deleteSessions(userId);
|
|
6210
|
+
}, { concurrency: 3 });
|
|
6211
|
+
return ctx.json({
|
|
6212
|
+
success: true,
|
|
6213
|
+
revokedCount: userIds.length
|
|
6214
|
+
});
|
|
6215
|
+
});
|
|
5904
6216
|
|
|
5905
6217
|
//#endregion
|
|
5906
6218
|
//#region src/routes/users.ts
|
|
6219
|
+
function parseWhereClause(val) {
|
|
6220
|
+
if (!val) return [];
|
|
6221
|
+
const parsed = JSON.parse(val);
|
|
6222
|
+
if (!Array.isArray(parsed)) return [];
|
|
6223
|
+
return parsed;
|
|
6224
|
+
}
|
|
6225
|
+
const getUsersQuerySchema = z$1.object({
|
|
6226
|
+
limit: z$1.number().or(z$1.string().transform(Number)).optional(),
|
|
6227
|
+
offset: z$1.number().or(z$1.string().transform(Number)).optional(),
|
|
6228
|
+
sortBy: z$1.string().optional(),
|
|
6229
|
+
sortOrder: z$1.enum(["asc", "desc"]).optional(),
|
|
6230
|
+
where: z$1.string().transform(parseWhereClause).optional(),
|
|
6231
|
+
countWhere: z$1.string().transform(parseWhereClause).optional()
|
|
6232
|
+
}).optional();
|
|
5907
6233
|
const getUsers = (options) => {
|
|
5908
6234
|
return createAuthEndpoint("/dash/list-users", {
|
|
5909
6235
|
method: "GET",
|
|
5910
6236
|
use: [jwtMiddleware(options)],
|
|
5911
|
-
query:
|
|
5912
|
-
limit: z$1.number().or(z$1.string().transform(Number)).optional(),
|
|
5913
|
-
offset: z$1.number().or(z$1.string().transform(Number)).optional(),
|
|
5914
|
-
sortBy: z$1.string().optional(),
|
|
5915
|
-
sortOrder: z$1.enum(["asc", "desc"]).optional(),
|
|
5916
|
-
where: z$1.string().transform((val) => {
|
|
5917
|
-
if (!val) return [];
|
|
5918
|
-
return JSON.parse(val);
|
|
5919
|
-
}).optional(),
|
|
5920
|
-
countWhere: z$1.string().transform((val) => {
|
|
5921
|
-
if (!val) return [];
|
|
5922
|
-
return JSON.parse(val);
|
|
5923
|
-
}).optional()
|
|
5924
|
-
}).optional()
|
|
6237
|
+
query: getUsersQuerySchema
|
|
5925
6238
|
}, async (ctx) => {
|
|
5926
6239
|
const activityTrackingEnabled = (() => {
|
|
5927
6240
|
if (!options.activityTracking?.enabled) return false;
|
|
@@ -5930,6 +6243,8 @@ const getUsers = (options) => {
|
|
|
5930
6243
|
if (fields.lastActiveAt.type !== "date") return false;
|
|
5931
6244
|
return true;
|
|
5932
6245
|
})();
|
|
6246
|
+
const where = ctx.query?.where?.length ? ctx.query.where : void 0;
|
|
6247
|
+
const countWhere = ctx.query?.countWhere?.length ? ctx.query.countWhere : void 0;
|
|
5933
6248
|
const userQuery = ctx.context.adapter.findMany({
|
|
5934
6249
|
model: "user",
|
|
5935
6250
|
limit: ctx.query?.limit || 10,
|
|
@@ -5938,11 +6253,11 @@ const getUsers = (options) => {
|
|
|
5938
6253
|
field: ctx.query?.sortBy || "createdAt",
|
|
5939
6254
|
direction: ctx.query?.sortOrder || "desc"
|
|
5940
6255
|
},
|
|
5941
|
-
where
|
|
6256
|
+
where
|
|
5942
6257
|
});
|
|
5943
6258
|
const totalQuery = ctx.context.adapter.count({
|
|
5944
6259
|
model: "user",
|
|
5945
|
-
where:
|
|
6260
|
+
where: countWhere
|
|
5946
6261
|
});
|
|
5947
6262
|
const onlineUsersQuery = activityTrackingEnabled ? ctx.context.adapter.count({
|
|
5948
6263
|
model: "user",
|
|
@@ -5951,13 +6266,16 @@ const getUsers = (options) => {
|
|
|
5951
6266
|
value: /* @__PURE__ */ new Date(Date.now() - 1e3 * 60 * 2),
|
|
5952
6267
|
operator: "gte"
|
|
5953
6268
|
}]
|
|
6269
|
+
}).catch((e) => {
|
|
6270
|
+
ctx.context.logger.error("[Dash] Failed to count online users:", e);
|
|
6271
|
+
return 0;
|
|
5954
6272
|
}) : Promise.resolve(0);
|
|
5955
6273
|
const [users, total, onlineUsers] = await Promise.all([
|
|
5956
6274
|
userQuery,
|
|
5957
6275
|
totalQuery,
|
|
5958
6276
|
onlineUsersQuery
|
|
5959
6277
|
]);
|
|
5960
|
-
const hasAdminPlugin = ctx.context.
|
|
6278
|
+
const hasAdminPlugin = ctx.context.hasPlugin("admin");
|
|
5961
6279
|
return {
|
|
5962
6280
|
users: users.map((user) => {
|
|
5963
6281
|
const u = user;
|
|
@@ -5976,6 +6294,30 @@ const getUsers = (options) => {
|
|
|
5976
6294
|
};
|
|
5977
6295
|
});
|
|
5978
6296
|
};
|
|
6297
|
+
const exportUsers = (options) => {
|
|
6298
|
+
return createAuthEndpoint("/dash/export-users", {
|
|
6299
|
+
method: "GET",
|
|
6300
|
+
use: [jwtMiddleware(options)],
|
|
6301
|
+
query: getUsersQuerySchema
|
|
6302
|
+
}, async (ctx) => {
|
|
6303
|
+
const hasAdminPlugin = ctx.context.hasPlugin("admin");
|
|
6304
|
+
return exportFactory({
|
|
6305
|
+
model: "user",
|
|
6306
|
+
limit: ctx.query?.limit,
|
|
6307
|
+
offset: ctx.query?.offset ? ctx.query.offset : 0,
|
|
6308
|
+
sortBy: {
|
|
6309
|
+
field: ctx.query?.sortBy || "createdAt",
|
|
6310
|
+
direction: ctx.query?.sortOrder || "desc"
|
|
6311
|
+
},
|
|
6312
|
+
where: ctx.query?.where
|
|
6313
|
+
}, { processRow: (u) => ({
|
|
6314
|
+
...u,
|
|
6315
|
+
banned: hasAdminPlugin ? u.banned ?? false : false,
|
|
6316
|
+
banReason: hasAdminPlugin ? u.banReason ?? null : null,
|
|
6317
|
+
banExpires: hasAdminPlugin ? u.banExpires ?? null : null
|
|
6318
|
+
}) })(ctx);
|
|
6319
|
+
});
|
|
6320
|
+
};
|
|
5979
6321
|
const getOnlineUsersCount = (options) => {
|
|
5980
6322
|
return createAuthEndpoint("/dash/online-users-count", {
|
|
5981
6323
|
method: "GET",
|
|
@@ -6006,11 +6348,46 @@ const deleteUser = (options) => {
|
|
|
6006
6348
|
}]
|
|
6007
6349
|
});
|
|
6008
6350
|
} catch (e) {
|
|
6009
|
-
logger.error(e);
|
|
6351
|
+
ctx.context.logger.error("[Dash] Failed to delete user:", e);
|
|
6010
6352
|
throw ctx.error("INTERNAL_SERVER_ERROR", { message: "Internal server error" });
|
|
6011
6353
|
}
|
|
6012
6354
|
});
|
|
6013
6355
|
};
|
|
6356
|
+
const deleteManyUsers = (options) => {
|
|
6357
|
+
return createAuthEndpoint("/dash/delete-many-users", {
|
|
6358
|
+
method: "POST",
|
|
6359
|
+
use: [jwtMiddleware(options, z$1.object({ userIds: z$1.string().array() }))]
|
|
6360
|
+
}, async (ctx) => {
|
|
6361
|
+
const { userIds } = ctx.context.payload;
|
|
6362
|
+
const deletedUserIds = /* @__PURE__ */ new Set();
|
|
6363
|
+
const skippedUserIds = /* @__PURE__ */ new Set();
|
|
6364
|
+
const start = performance.now();
|
|
6365
|
+
await withConcurrency(chunkArray(userIds), async (chunk) => {
|
|
6366
|
+
const where = [{
|
|
6367
|
+
field: "id",
|
|
6368
|
+
value: chunk,
|
|
6369
|
+
operator: "in"
|
|
6370
|
+
}];
|
|
6371
|
+
await ctx.context.adapter.deleteMany({
|
|
6372
|
+
model: "user",
|
|
6373
|
+
where
|
|
6374
|
+
});
|
|
6375
|
+
const remainingUsers = await ctx.context.adapter.findMany({
|
|
6376
|
+
model: "user",
|
|
6377
|
+
where
|
|
6378
|
+
});
|
|
6379
|
+
for (const id of chunk) if (!remainingUsers.some((u) => u.id === id)) deletedUserIds.add(id);
|
|
6380
|
+
else skippedUserIds.add(id);
|
|
6381
|
+
});
|
|
6382
|
+
const end = performance.now();
|
|
6383
|
+
console.log(`Time taken to bulk delete ${deletedUserIds.size} users: ${(end - start) / 1e3}s`, skippedUserIds.size > 0 ? `Failed: ${skippedUserIds.size}` : "");
|
|
6384
|
+
return ctx.json({
|
|
6385
|
+
success: deletedUserIds.size > 0,
|
|
6386
|
+
skippedUserIds: Array.from(skippedUserIds),
|
|
6387
|
+
deletedUserIds: Array.from(deletedUserIds)
|
|
6388
|
+
});
|
|
6389
|
+
});
|
|
6390
|
+
};
|
|
6014
6391
|
const impersonateUser = (options) => {
|
|
6015
6392
|
return createAuthEndpoint("/dash/impersonate-user", {
|
|
6016
6393
|
method: "GET",
|
|
@@ -6096,10 +6473,10 @@ const createUser = (options) => {
|
|
|
6096
6473
|
const organizationId = ctx.context.payload?.organizationId || userData.organizationId;
|
|
6097
6474
|
const organizationRole = ctx.context.payload?.organizationRole || userData.organizationRole;
|
|
6098
6475
|
if (organizationId) {
|
|
6099
|
-
const organizationPlugin = ctx.context.
|
|
6476
|
+
const organizationPlugin = ctx.context.getPlugin("organization");
|
|
6100
6477
|
if (organizationPlugin) {
|
|
6101
|
-
const orgOptions = organizationPlugin
|
|
6102
|
-
const role = organizationRole ||
|
|
6478
|
+
const orgOptions = organizationPlugin.options || {};
|
|
6479
|
+
const role = organizationRole || "member";
|
|
6103
6480
|
const organization = await ctx.context.adapter.findOne({
|
|
6104
6481
|
model: "organization",
|
|
6105
6482
|
where: [{
|
|
@@ -6188,22 +6565,33 @@ const unlinkAccount = (options) => {
|
|
|
6188
6565
|
const getUserDetails = (options) => {
|
|
6189
6566
|
return createAuthEndpoint("/dash/user", {
|
|
6190
6567
|
method: "GET",
|
|
6191
|
-
use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
|
|
6568
|
+
use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))],
|
|
6569
|
+
query: z$1.object({ minimal: z$1.boolean().or(z$1.string().transform((val) => val === "true")).optional() }).optional()
|
|
6192
6570
|
}, async (ctx) => {
|
|
6193
6571
|
const { userId } = ctx.context.payload;
|
|
6194
|
-
const
|
|
6572
|
+
const minimal = !!ctx.query?.minimal;
|
|
6573
|
+
const hasAdminPlugin = ctx.context.hasPlugin("admin");
|
|
6195
6574
|
const user = await ctx.context.adapter.findOne({
|
|
6196
6575
|
model: "user",
|
|
6197
6576
|
where: [{
|
|
6198
6577
|
field: "id",
|
|
6199
6578
|
value: userId
|
|
6200
6579
|
}],
|
|
6201
|
-
join: {
|
|
6580
|
+
...minimal ? {} : { join: {
|
|
6202
6581
|
account: true,
|
|
6203
6582
|
session: true
|
|
6204
|
-
}
|
|
6583
|
+
} }
|
|
6205
6584
|
});
|
|
6206
6585
|
if (!user) throw ctx.error("NOT_FOUND", { message: "User not found" });
|
|
6586
|
+
if (minimal) return {
|
|
6587
|
+
...user,
|
|
6588
|
+
lastActiveAt: user.lastActiveAt ?? null,
|
|
6589
|
+
banned: hasAdminPlugin ? user.banned ?? false : false,
|
|
6590
|
+
banReason: hasAdminPlugin ? user.banReason ?? null : null,
|
|
6591
|
+
banExpires: hasAdminPlugin ? user.banExpires ?? null : null,
|
|
6592
|
+
account: [],
|
|
6593
|
+
session: []
|
|
6594
|
+
};
|
|
6207
6595
|
const activityTrackingEnabled = !!options.activityTracking?.enabled;
|
|
6208
6596
|
const sessions = user.session || [];
|
|
6209
6597
|
let lastActiveAt = null;
|
|
@@ -6220,13 +6608,18 @@ const getUserDetails = (options) => {
|
|
|
6220
6608
|
shouldUpdateLastActiveAt = true;
|
|
6221
6609
|
}
|
|
6222
6610
|
}
|
|
6223
|
-
if (shouldUpdateLastActiveAt && lastActiveAt)
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6611
|
+
if (shouldUpdateLastActiveAt && lastActiveAt) {
|
|
6612
|
+
const updateActivity = async () => {
|
|
6613
|
+
try {
|
|
6614
|
+
await ctx.context.internalAdapter.updateUser(userId, {
|
|
6615
|
+
lastActiveAt,
|
|
6616
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
6617
|
+
});
|
|
6618
|
+
} catch (error) {
|
|
6619
|
+
ctx.context.logger.error("[Dash] Failed to update user lastActiveAt:", error);
|
|
6620
|
+
}
|
|
6621
|
+
};
|
|
6622
|
+
updateActivity();
|
|
6230
6623
|
}
|
|
6231
6624
|
}
|
|
6232
6625
|
return {
|
|
@@ -6244,43 +6637,41 @@ const getUserOrganizations = (options) => {
|
|
|
6244
6637
|
use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
|
|
6245
6638
|
}, async (ctx) => {
|
|
6246
6639
|
const { userId } = ctx.context.payload;
|
|
6247
|
-
const isOrgEnabled = ctx.context.
|
|
6640
|
+
const isOrgEnabled = ctx.context.getPlugin("organization");
|
|
6248
6641
|
if (!isOrgEnabled) return { organizations: [] };
|
|
6249
|
-
const
|
|
6642
|
+
const isTeamEnabled = isOrgEnabled.options?.teams?.enabled;
|
|
6643
|
+
const [member, teamMembers] = await Promise.all([ctx.context.adapter.findMany({
|
|
6250
6644
|
model: "member",
|
|
6251
6645
|
where: [{
|
|
6252
6646
|
field: "userId",
|
|
6253
6647
|
value: userId
|
|
6254
6648
|
}]
|
|
6255
|
-
})
|
|
6256
|
-
if (member.length === 0) return { organizations: [] };
|
|
6257
|
-
const organizations = await ctx.context.adapter.findMany({
|
|
6258
|
-
model: "organization",
|
|
6259
|
-
where: [{
|
|
6260
|
-
field: "id",
|
|
6261
|
-
value: member.map((m) => m.organizationId),
|
|
6262
|
-
operator: "in"
|
|
6263
|
-
}]
|
|
6264
|
-
});
|
|
6265
|
-
const isTeamEnabled = isOrgEnabled.options?.teams?.enabled;
|
|
6266
|
-
const teamMembers = isTeamEnabled ? await ctx.context.adapter.findMany({
|
|
6649
|
+
}), isTeamEnabled ? ctx.context.adapter.findMany({
|
|
6267
6650
|
model: "teamMember",
|
|
6268
6651
|
where: [{
|
|
6269
6652
|
field: "userId",
|
|
6270
6653
|
value: userId
|
|
6271
6654
|
}]
|
|
6272
6655
|
}).catch((e) => {
|
|
6273
|
-
ctx.context.logger.error(e);
|
|
6656
|
+
ctx.context.logger.error("[Dash] Failed to fetch team members:", e);
|
|
6274
6657
|
return [];
|
|
6275
|
-
}) : [];
|
|
6276
|
-
|
|
6658
|
+
}) : Promise.resolve([])]);
|
|
6659
|
+
if (member.length === 0) return { organizations: [] };
|
|
6660
|
+
const [organizations, teams] = await Promise.all([ctx.context.adapter.findMany({
|
|
6661
|
+
model: "organization",
|
|
6662
|
+
where: [{
|
|
6663
|
+
field: "id",
|
|
6664
|
+
value: member.map((m) => m.organizationId),
|
|
6665
|
+
operator: "in"
|
|
6666
|
+
}]
|
|
6667
|
+
}), isTeamEnabled && teamMembers.length > 0 ? ctx.context.adapter.findMany({
|
|
6277
6668
|
model: "team",
|
|
6278
6669
|
where: [{
|
|
6279
6670
|
field: "id",
|
|
6280
6671
|
value: teamMembers.map((tm) => tm.teamId),
|
|
6281
6672
|
operator: "in"
|
|
6282
6673
|
}]
|
|
6283
|
-
}) : [];
|
|
6674
|
+
}) : Promise.resolve([])]);
|
|
6284
6675
|
return { organizations: organizations.map((organization) => ({
|
|
6285
6676
|
id: organization.id,
|
|
6286
6677
|
name: organization.name,
|
|
@@ -6314,9 +6705,25 @@ const updateUser = (options) => createAuthEndpoint("/dash/update-user", {
|
|
|
6314
6705
|
if (!user) throw new APIError("NOT_FOUND", { message: "User not found" });
|
|
6315
6706
|
return user;
|
|
6316
6707
|
});
|
|
6317
|
-
async function countUniqueActiveUsers(adapter,
|
|
6708
|
+
async function countUniqueActiveUsers(adapter, range, activityTrackingEnabled) {
|
|
6709
|
+
const field = activityTrackingEnabled ? "lastActiveAt" : "updatedAt";
|
|
6710
|
+
const where = [{
|
|
6711
|
+
field,
|
|
6712
|
+
operator: "gte",
|
|
6713
|
+
value: range.from
|
|
6714
|
+
}];
|
|
6715
|
+
if (range.to !== void 0) where.push({
|
|
6716
|
+
field,
|
|
6717
|
+
operator: "lt",
|
|
6718
|
+
value: range.to
|
|
6719
|
+
});
|
|
6720
|
+
if (activityTrackingEnabled) return adapter.count({
|
|
6721
|
+
model: "user",
|
|
6722
|
+
where
|
|
6723
|
+
});
|
|
6318
6724
|
const sessions = await adapter.findMany({
|
|
6319
6725
|
model: "session",
|
|
6726
|
+
select: ["userId"],
|
|
6320
6727
|
where
|
|
6321
6728
|
});
|
|
6322
6729
|
return new Set(sessions.map((s) => s.userId)).size;
|
|
@@ -6332,6 +6739,7 @@ const getUserStats = (options) => createAuthEndpoint("/dash/user-stats", {
|
|
|
6332
6739
|
const twoWeeksAgo = /* @__PURE__ */ new Date(now.getTime() - 336 * 60 * 60 * 1e3);
|
|
6333
6740
|
const oneMonthAgo = /* @__PURE__ */ new Date(now.getTime() - 720 * 60 * 60 * 1e3);
|
|
6334
6741
|
const twoMonthsAgo = /* @__PURE__ */ new Date(now.getTime() - 1440 * 60 * 60 * 1e3);
|
|
6742
|
+
const activityTrackingEnabled = !!options.activityTracking?.enabled;
|
|
6335
6743
|
const [dailyCount, previousDailyCount, weeklyCount, previousWeeklyCount, monthlyCount, previousMonthlyCount, totalCount, dailyActiveCount, previousDailyActiveCount, weeklyActiveCount, previousWeeklyActiveCount, monthlyActiveCount, previousMonthlyActiveCount] = await Promise.all([
|
|
6336
6744
|
ctx.context.adapter.count({
|
|
6337
6745
|
model: "user",
|
|
@@ -6394,48 +6802,21 @@ const getUserStats = (options) => createAuthEndpoint("/dash/user-stats", {
|
|
|
6394
6802
|
}]
|
|
6395
6803
|
}),
|
|
6396
6804
|
ctx.context.adapter.count({ model: "user" }),
|
|
6397
|
-
countUniqueActiveUsers(ctx.context.adapter,
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
}
|
|
6402
|
-
countUniqueActiveUsers(ctx.context.adapter,
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
},
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
field: "updatedAt",
|
|
6413
|
-
operator: "gte",
|
|
6414
|
-
value: oneWeekAgo
|
|
6415
|
-
}]),
|
|
6416
|
-
countUniqueActiveUsers(ctx.context.adapter, [{
|
|
6417
|
-
field: "updatedAt",
|
|
6418
|
-
operator: "gte",
|
|
6419
|
-
value: twoWeeksAgo
|
|
6420
|
-
}, {
|
|
6421
|
-
field: "updatedAt",
|
|
6422
|
-
operator: "lt",
|
|
6423
|
-
value: oneWeekAgo
|
|
6424
|
-
}]),
|
|
6425
|
-
countUniqueActiveUsers(ctx.context.adapter, [{
|
|
6426
|
-
field: "updatedAt",
|
|
6427
|
-
operator: "gte",
|
|
6428
|
-
value: oneMonthAgo
|
|
6429
|
-
}]),
|
|
6430
|
-
countUniqueActiveUsers(ctx.context.adapter, [{
|
|
6431
|
-
field: "updatedAt",
|
|
6432
|
-
operator: "gte",
|
|
6433
|
-
value: twoMonthsAgo
|
|
6434
|
-
}, {
|
|
6435
|
-
field: "updatedAt",
|
|
6436
|
-
operator: "lt",
|
|
6437
|
-
value: oneMonthAgo
|
|
6438
|
-
}])
|
|
6805
|
+
countUniqueActiveUsers(ctx.context.adapter, { from: oneDayAgo }, activityTrackingEnabled),
|
|
6806
|
+
countUniqueActiveUsers(ctx.context.adapter, {
|
|
6807
|
+
from: twoDaysAgo,
|
|
6808
|
+
to: oneDayAgo
|
|
6809
|
+
}, activityTrackingEnabled),
|
|
6810
|
+
countUniqueActiveUsers(ctx.context.adapter, { from: oneWeekAgo }, activityTrackingEnabled),
|
|
6811
|
+
countUniqueActiveUsers(ctx.context.adapter, {
|
|
6812
|
+
from: twoWeeksAgo,
|
|
6813
|
+
to: oneWeekAgo
|
|
6814
|
+
}, activityTrackingEnabled),
|
|
6815
|
+
countUniqueActiveUsers(ctx.context.adapter, { from: oneMonthAgo }, activityTrackingEnabled),
|
|
6816
|
+
countUniqueActiveUsers(ctx.context.adapter, {
|
|
6817
|
+
from: twoMonthsAgo,
|
|
6818
|
+
to: oneMonthAgo
|
|
6819
|
+
}, activityTrackingEnabled)
|
|
6439
6820
|
]);
|
|
6440
6821
|
const calculatePercentage = (current, previous) => {
|
|
6441
6822
|
if (previous === 0) return current > 0 ? 100 : 0;
|
|
@@ -6482,6 +6863,7 @@ const getUserGraphData = (options) => createAuthEndpoint("/dash/user-graph-data"
|
|
|
6482
6863
|
}, async (ctx) => {
|
|
6483
6864
|
const { period } = ctx.query;
|
|
6484
6865
|
const now = /* @__PURE__ */ new Date();
|
|
6866
|
+
const activityTrackingEnabled = !!options.activityTracking?.enabled;
|
|
6485
6867
|
const intervals = period === "daily" ? 7 : period === "weekly" ? 8 : 6;
|
|
6486
6868
|
const msPerInterval = period === "daily" ? 1440 * 60 * 1e3 : period === "weekly" ? 10080 * 60 * 1e3 : 720 * 60 * 60 * 1e3;
|
|
6487
6869
|
const intervalData = [];
|
|
@@ -6522,15 +6904,10 @@ const getUserGraphData = (options) => createAuthEndpoint("/dash/user-graph-data"
|
|
|
6522
6904
|
value: interval.endDate
|
|
6523
6905
|
}]
|
|
6524
6906
|
}),
|
|
6525
|
-
countUniqueActiveUsers(ctx.context.adapter,
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
}, {
|
|
6530
|
-
field: "updatedAt",
|
|
6531
|
-
operator: "lte",
|
|
6532
|
-
value: interval.endDate
|
|
6533
|
-
}])
|
|
6907
|
+
countUniqueActiveUsers(ctx.context.adapter, {
|
|
6908
|
+
from: interval.startDate,
|
|
6909
|
+
to: interval.endDate
|
|
6910
|
+
}, activityTrackingEnabled)
|
|
6534
6911
|
]);
|
|
6535
6912
|
const results = await Promise.all(allQueries);
|
|
6536
6913
|
return {
|
|
@@ -6565,9 +6942,10 @@ const getUserRetentionData = (options) => createAuthEndpoint("/dash/user-retenti
|
|
|
6565
6942
|
* M-N retention:
|
|
6566
6943
|
* users created during Month(-N) who are active during "this month"
|
|
6567
6944
|
*
|
|
6568
|
-
* Active
|
|
6945
|
+
* Active: user.lastActiveAt when activity tracking enabled, else session.updatedAt.
|
|
6569
6946
|
*/
|
|
6570
6947
|
const { period } = ctx.query;
|
|
6948
|
+
const activityTrackingEnabled = !!options.activityTracking?.enabled;
|
|
6571
6949
|
const now = /* @__PURE__ */ new Date();
|
|
6572
6950
|
const startOfUtcDay = (d) => new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
|
6573
6951
|
const startOfUtcMonth = (d) => new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1));
|
|
@@ -6598,6 +6976,7 @@ const getUserRetentionData = (options) => createAuthEndpoint("/dash/user-retenti
|
|
|
6598
6976
|
const cohortEnd = period === "daily" ? addUtcDays(cohortStart, 1) : period === "weekly" ? addUtcDays(cohortStart, 7) : addUtcMonths(cohortStart, 1);
|
|
6599
6977
|
const cohortUsers = await ctx.context.adapter.findMany({
|
|
6600
6978
|
model: "user",
|
|
6979
|
+
select: ["id"],
|
|
6601
6980
|
where: [{
|
|
6602
6981
|
field: "createdAt",
|
|
6603
6982
|
operator: "gte",
|
|
@@ -6624,27 +7003,57 @@ const getUserRetentionData = (options) => createAuthEndpoint("/dash/user-retenti
|
|
|
6624
7003
|
continue;
|
|
6625
7004
|
}
|
|
6626
7005
|
const cohortUserIds = cohortUsers.map((u) => u.id);
|
|
6627
|
-
|
|
6628
|
-
|
|
7006
|
+
let retained;
|
|
7007
|
+
if (activityTrackingEnabled) retained = await ctx.context.adapter.count({
|
|
7008
|
+
model: "user",
|
|
6629
7009
|
where: [
|
|
6630
7010
|
{
|
|
6631
|
-
field: "
|
|
7011
|
+
field: "id",
|
|
6632
7012
|
operator: "in",
|
|
6633
7013
|
value: cohortUserIds
|
|
6634
7014
|
},
|
|
6635
7015
|
{
|
|
6636
|
-
field: "
|
|
7016
|
+
field: "lastActiveAt",
|
|
6637
7017
|
operator: "gte",
|
|
6638
7018
|
value: activeStart
|
|
6639
7019
|
},
|
|
6640
7020
|
{
|
|
6641
|
-
field: "
|
|
7021
|
+
field: "lastActiveAt",
|
|
6642
7022
|
operator: "lt",
|
|
6643
7023
|
value: activeEnd
|
|
6644
7024
|
}
|
|
6645
7025
|
]
|
|
6646
|
-
}).catch(() =>
|
|
6647
|
-
|
|
7026
|
+
}).catch((error) => {
|
|
7027
|
+
ctx.context.logger.warn("[Dash] Failed to count retained users by lastActiveAt:", error);
|
|
7028
|
+
return 0;
|
|
7029
|
+
});
|
|
7030
|
+
else {
|
|
7031
|
+
const sessionsInActiveWindow = await ctx.context.adapter.findMany({
|
|
7032
|
+
model: "session",
|
|
7033
|
+
select: ["userId"],
|
|
7034
|
+
where: [
|
|
7035
|
+
{
|
|
7036
|
+
field: "userId",
|
|
7037
|
+
operator: "in",
|
|
7038
|
+
value: cohortUserIds
|
|
7039
|
+
},
|
|
7040
|
+
{
|
|
7041
|
+
field: "updatedAt",
|
|
7042
|
+
operator: "gte",
|
|
7043
|
+
value: activeStart
|
|
7044
|
+
},
|
|
7045
|
+
{
|
|
7046
|
+
field: "updatedAt",
|
|
7047
|
+
operator: "lt",
|
|
7048
|
+
value: activeEnd
|
|
7049
|
+
}
|
|
7050
|
+
]
|
|
7051
|
+
}).catch((error) => {
|
|
7052
|
+
ctx.context.logger.warn("[Dash] Failed to fetch cohort sessions:", error);
|
|
7053
|
+
return [];
|
|
7054
|
+
});
|
|
7055
|
+
retained = new Set(sessionsInActiveWindow.map((s) => s.userId)).size;
|
|
7056
|
+
}
|
|
6648
7057
|
const retentionRate = cohortSize > 0 ? Math.round(retained / cohortSize * 100 * 10) / 10 : 0;
|
|
6649
7058
|
data.push({
|
|
6650
7059
|
n,
|
|
@@ -6696,6 +7105,57 @@ const banUser = (options) => createAuthEndpoint("/dash/ban-user", {
|
|
|
6696
7105
|
});
|
|
6697
7106
|
return { success: true };
|
|
6698
7107
|
});
|
|
7108
|
+
const banManyUsers = (options) => {
|
|
7109
|
+
return createAuthEndpoint("/dash/ban-many-users", {
|
|
7110
|
+
method: "POST",
|
|
7111
|
+
use: [jwtMiddleware(options, z$1.object({ userIds: z$1.string().array() }))],
|
|
7112
|
+
body: z$1.object({
|
|
7113
|
+
banReason: z$1.string().optional(),
|
|
7114
|
+
banExpires: z$1.number().optional()
|
|
7115
|
+
})
|
|
7116
|
+
}, async (ctx) => {
|
|
7117
|
+
const { userIds } = ctx.context.payload;
|
|
7118
|
+
const { banReason, banExpires } = ctx.body;
|
|
7119
|
+
const start = performance.now();
|
|
7120
|
+
await withConcurrency(chunkArray(userIds, { batchSize: 50 }), async (chunk) => {
|
|
7121
|
+
await ctx.context.adapter.updateMany({
|
|
7122
|
+
model: "user",
|
|
7123
|
+
where: [{
|
|
7124
|
+
field: "id",
|
|
7125
|
+
value: chunk,
|
|
7126
|
+
operator: "in"
|
|
7127
|
+
}],
|
|
7128
|
+
update: {
|
|
7129
|
+
banned: true,
|
|
7130
|
+
banReason: banReason || null,
|
|
7131
|
+
banExpires: banExpires ? new Date(banExpires) : null
|
|
7132
|
+
}
|
|
7133
|
+
});
|
|
7134
|
+
}, { concurrency: 3 });
|
|
7135
|
+
const skippedUserIds = (await ctx.context.adapter.findMany({
|
|
7136
|
+
model: "user",
|
|
7137
|
+
where: [{
|
|
7138
|
+
field: "id",
|
|
7139
|
+
value: userIds,
|
|
7140
|
+
operator: "in",
|
|
7141
|
+
connector: "AND"
|
|
7142
|
+
}, {
|
|
7143
|
+
field: "banned",
|
|
7144
|
+
value: false,
|
|
7145
|
+
operator: "eq"
|
|
7146
|
+
}],
|
|
7147
|
+
select: ["id"]
|
|
7148
|
+
})).map((u) => u.id);
|
|
7149
|
+
const bannedUserIds = userIds.filter((id) => !skippedUserIds.includes(id));
|
|
7150
|
+
const end = performance.now();
|
|
7151
|
+
console.log(`Time taken to ban ${bannedUserIds.length} users: ${(end - start) / 1e3}s`, skippedUserIds.length > 0 ? `Skipped: ${skippedUserIds.length}` : "");
|
|
7152
|
+
return ctx.json({
|
|
7153
|
+
success: bannedUserIds.length > 0,
|
|
7154
|
+
bannedUserIds,
|
|
7155
|
+
skippedUserIds
|
|
7156
|
+
});
|
|
7157
|
+
});
|
|
7158
|
+
};
|
|
6699
7159
|
const unbanUser = (options) => createAuthEndpoint("/dash/unban-user", {
|
|
6700
7160
|
method: "POST",
|
|
6701
7161
|
use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
|
|
@@ -6730,6 +7190,63 @@ const sendVerificationEmail = (options) => createAuthEndpoint("/dash/send-verifi
|
|
|
6730
7190
|
}, user);
|
|
6731
7191
|
return { success: true };
|
|
6732
7192
|
});
|
|
7193
|
+
const sendManyVerificationEmails = (options) => {
|
|
7194
|
+
return createAuthEndpoint("/dash/send-many-verification-emails", {
|
|
7195
|
+
method: "POST",
|
|
7196
|
+
use: [jwtMiddleware(options, z$1.object({ userIds: z$1.string().array() }))],
|
|
7197
|
+
body: z$1.object({ callbackUrl: z$1.string().url() })
|
|
7198
|
+
}, async (ctx) => {
|
|
7199
|
+
if (!ctx.context.options.emailVerification?.sendVerificationEmail) throw ctx.error("BAD_REQUEST", { message: "Email verification is not enabled" });
|
|
7200
|
+
const { userIds } = ctx.context.payload;
|
|
7201
|
+
const { callbackUrl } = ctx.body;
|
|
7202
|
+
const sentEmailUserIds = /* @__PURE__ */ new Set();
|
|
7203
|
+
const skippedEmailUserIds = /* @__PURE__ */ new Set();
|
|
7204
|
+
const start = performance.now();
|
|
7205
|
+
await withConcurrency(chunkArray(userIds, { batchSize: 20 }), async (chunk) => {
|
|
7206
|
+
const users = await ctx.context.adapter.findMany({
|
|
7207
|
+
model: "user",
|
|
7208
|
+
where: [{
|
|
7209
|
+
field: "id",
|
|
7210
|
+
value: chunk,
|
|
7211
|
+
operator: "in"
|
|
7212
|
+
}, {
|
|
7213
|
+
field: "emailVerified",
|
|
7214
|
+
value: false
|
|
7215
|
+
}]
|
|
7216
|
+
});
|
|
7217
|
+
if (chunk.length - users.length > 0) for (const id of chunk.filter((id$1) => !users.some((u) => u.id === id$1))) skippedEmailUserIds.add(id);
|
|
7218
|
+
for (const result of await Promise.allSettled(users.map(async (user) => {
|
|
7219
|
+
try {
|
|
7220
|
+
await sendVerificationEmailFn({
|
|
7221
|
+
...ctx,
|
|
7222
|
+
body: {
|
|
7223
|
+
...ctx.body,
|
|
7224
|
+
callbackURL: callbackUrl
|
|
7225
|
+
}
|
|
7226
|
+
}, user);
|
|
7227
|
+
} catch {
|
|
7228
|
+
return {
|
|
7229
|
+
success: false,
|
|
7230
|
+
id: user.id
|
|
7231
|
+
};
|
|
7232
|
+
}
|
|
7233
|
+
return {
|
|
7234
|
+
success: true,
|
|
7235
|
+
id: user.id
|
|
7236
|
+
};
|
|
7237
|
+
}))) if (result.status === "fulfilled") if (result.value.success) sentEmailUserIds.add(result.value.id);
|
|
7238
|
+
else skippedEmailUserIds.add(result.value.id);
|
|
7239
|
+
else for (const { id } of users) skippedEmailUserIds.add(id);
|
|
7240
|
+
}, { concurrency: 2 });
|
|
7241
|
+
const end = performance.now();
|
|
7242
|
+
console.log(`Time taken to send verification emails to ${sentEmailUserIds.size} users: ${Math.round((end - start) / 1e3)}s`, skippedEmailUserIds.size > 0 ? `Skipped: ${skippedEmailUserIds.size}` : "");
|
|
7243
|
+
return ctx.json({
|
|
7244
|
+
success: sentEmailUserIds.size > 0,
|
|
7245
|
+
sentEmailUserIds: Array.from(sentEmailUserIds),
|
|
7246
|
+
skippedEmailUserIds: Array.from(skippedEmailUserIds)
|
|
7247
|
+
});
|
|
7248
|
+
});
|
|
7249
|
+
};
|
|
6733
7250
|
const sendResetPasswordEmail = (options) => createAuthEndpoint("/dash/send-reset-password-email", {
|
|
6734
7251
|
method: "POST",
|
|
6735
7252
|
use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))],
|
|
@@ -6748,6 +7265,11 @@ const getUserMapData = (options) => createAuthEndpoint("/dash/user-map-data", {
|
|
|
6748
7265
|
}, async (ctx) => {
|
|
6749
7266
|
const sessions = await ctx.context.adapter.findMany({
|
|
6750
7267
|
model: "session",
|
|
7268
|
+
select: [
|
|
7269
|
+
"country",
|
|
7270
|
+
"city",
|
|
7271
|
+
"userId"
|
|
7272
|
+
],
|
|
6751
7273
|
limit: 1e4
|
|
6752
7274
|
});
|
|
6753
7275
|
const countryMap = /* @__PURE__ */ new Map();
|
|
@@ -6790,7 +7312,7 @@ const enableTwoFactor = (options) => createAuthEndpoint("/dash/enable-two-factor
|
|
|
6790
7312
|
use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
|
|
6791
7313
|
}, async (ctx) => {
|
|
6792
7314
|
const { userId } = ctx.context.payload;
|
|
6793
|
-
const twoFactorPlugin = ctx.context.
|
|
7315
|
+
const twoFactorPlugin = ctx.context.getPlugin("two-factor");
|
|
6794
7316
|
if (!twoFactorPlugin) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication plugin is not enabled" });
|
|
6795
7317
|
if (await ctx.context.adapter.findOne({
|
|
6796
7318
|
model: "twoFactor",
|
|
@@ -6841,7 +7363,7 @@ const viewTwoFactorTotpUri = (options) => createAuthEndpoint("/dash/view-two-fac
|
|
|
6841
7363
|
use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
|
|
6842
7364
|
}, async (ctx) => {
|
|
6843
7365
|
const { userId } = ctx.context.payload;
|
|
6844
|
-
const twoFactorPlugin = ctx.context.
|
|
7366
|
+
const twoFactorPlugin = ctx.context.getPlugin("two-factor");
|
|
6845
7367
|
if (!twoFactorPlugin) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication plugin is not enabled" });
|
|
6846
7368
|
const twoFactorRecord = await ctx.context.adapter.findOne({
|
|
6847
7369
|
model: "twoFactor",
|
|
@@ -6901,7 +7423,7 @@ const disableTwoFactor = (options) => createAuthEndpoint("/dash/disable-two-fact
|
|
|
6901
7423
|
use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
|
|
6902
7424
|
}, async (ctx) => {
|
|
6903
7425
|
const { userId } = ctx.context.payload;
|
|
6904
|
-
if (!ctx.context.
|
|
7426
|
+
if (!ctx.context.getPlugin("two-factor")) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication is not enabled" });
|
|
6905
7427
|
await ctx.context.adapter.delete({
|
|
6906
7428
|
model: "twoFactor",
|
|
6907
7429
|
where: [{
|
|
@@ -6965,12 +7487,12 @@ async function sha256(message) {
|
|
|
6965
7487
|
/**
|
|
6966
7488
|
* Check if a hash has the required number of leading zero bits
|
|
6967
7489
|
*/
|
|
6968
|
-
function hasLeadingZeroBits(hash, bits) {
|
|
7490
|
+
function hasLeadingZeroBits(hash$1, bits) {
|
|
6969
7491
|
const fullHexChars = Math.floor(bits / 4);
|
|
6970
7492
|
const remainingBits = bits % 4;
|
|
6971
|
-
for (let i = 0; i < fullHexChars; i++) if (hash[i] !== "0") return false;
|
|
6972
|
-
if (remainingBits > 0 && fullHexChars < hash.length) {
|
|
6973
|
-
if (parseInt(hash[fullHexChars], 16) > (1 << 4 - remainingBits) - 1) return false;
|
|
7493
|
+
for (let i = 0; i < fullHexChars; i++) if (hash$1[i] !== "0") return false;
|
|
7494
|
+
if (remainingBits > 0 && fullHexChars < hash$1.length) {
|
|
7495
|
+
if (parseInt(hash$1[fullHexChars], 16) > (1 << 4 - remainingBits) - 1) return false;
|
|
6974
7496
|
}
|
|
6975
7497
|
return true;
|
|
6976
7498
|
}
|
|
@@ -7069,6 +7591,7 @@ function createSMSSender(config) {
|
|
|
7069
7591
|
messageId: (await response.json()).messageId
|
|
7070
7592
|
};
|
|
7071
7593
|
} catch (error) {
|
|
7594
|
+
logger.warn("[Dash] SMS send failed:", error);
|
|
7072
7595
|
return {
|
|
7073
7596
|
success: false,
|
|
7074
7597
|
error: error instanceof Error ? error.message : "Failed to send SMS"
|
|
@@ -7127,7 +7650,7 @@ const dash = (options) => {
|
|
|
7127
7650
|
return {
|
|
7128
7651
|
id: "dash",
|
|
7129
7652
|
init(ctx) {
|
|
7130
|
-
const organizationPlugin = ctx.
|
|
7653
|
+
const organizationPlugin = ctx.getPlugin("organization");
|
|
7131
7654
|
if (organizationPlugin) {
|
|
7132
7655
|
const instrumentOrganizationHooks = (organizationPluginOptions) => {
|
|
7133
7656
|
const organizationHooks = organizationPluginOptions.organizationHooks = organizationPluginOptions.organizationHooks ?? {};
|
|
@@ -7217,7 +7740,7 @@ const dash = (options) => {
|
|
|
7217
7740
|
};
|
|
7218
7741
|
};
|
|
7219
7742
|
instrumentOrganizationHooks(organizationPlugin.options = organizationPlugin.options ?? {});
|
|
7220
|
-
} else logger.debug("Organization plugin not active. Skipping instrumentation");
|
|
7743
|
+
} else logger.debug("[Dash] Organization plugin not active. Skipping instrumentation");
|
|
7221
7744
|
return { options: {
|
|
7222
7745
|
databaseHooks: {
|
|
7223
7746
|
user: {
|
|
@@ -7269,8 +7792,10 @@ const dash = (options) => {
|
|
|
7269
7792
|
const ctx$1 = _ctx;
|
|
7270
7793
|
if (!ctx$1 || !session.userId) return;
|
|
7271
7794
|
const location = ctx$1.context.location;
|
|
7795
|
+
const loginMethod = getLoginMethod(ctx$1) ?? void 0;
|
|
7272
7796
|
const enrichedSession = {
|
|
7273
7797
|
...session,
|
|
7798
|
+
loginMethod,
|
|
7274
7799
|
ipAddress: location?.ipAddress,
|
|
7275
7800
|
city: location?.city,
|
|
7276
7801
|
country: location?.country,
|
|
@@ -7303,7 +7828,9 @@ const dash = (options) => {
|
|
|
7303
7828
|
}],
|
|
7304
7829
|
update: { lastActiveAt: /* @__PURE__ */ new Date() }
|
|
7305
7830
|
});
|
|
7306
|
-
} catch {
|
|
7831
|
+
} catch (error) {
|
|
7832
|
+
logger.warn("[Dash] Failed to update user activity", error);
|
|
7833
|
+
}
|
|
7307
7834
|
}
|
|
7308
7835
|
},
|
|
7309
7836
|
delete: { async after(session, _ctx) {
|
|
@@ -7387,11 +7914,19 @@ const dash = (options) => {
|
|
|
7387
7914
|
},
|
|
7388
7915
|
hooks: {
|
|
7389
7916
|
before: [{
|
|
7390
|
-
matcher: (ctx) =>
|
|
7917
|
+
matcher: (ctx) => {
|
|
7918
|
+
if (ctx.request?.method !== "GET") return true;
|
|
7919
|
+
const path = new URL(ctx.request.url).pathname;
|
|
7920
|
+
return matchesAnyRoute(path, [routes.SIGN_IN_SOCIAL_CALLBACK, routes.SIGN_IN_OAUTH_CALLBACK]);
|
|
7921
|
+
},
|
|
7391
7922
|
handler: createIdentificationMiddleware(opts)
|
|
7392
7923
|
}],
|
|
7393
7924
|
after: [{
|
|
7394
|
-
matcher: (ctx) =>
|
|
7925
|
+
matcher: (ctx) => {
|
|
7926
|
+
if (ctx.request?.method !== "GET") return true;
|
|
7927
|
+
const path = new URL(ctx.request.url).pathname;
|
|
7928
|
+
return matchesAnyRoute(path, [routes.SIGN_IN_SOCIAL_CALLBACK, routes.SIGN_IN_OAUTH_CALLBACK]);
|
|
7929
|
+
},
|
|
7395
7930
|
handler: createAuthMiddleware(async (_ctx) => {
|
|
7396
7931
|
const ctx = _ctx;
|
|
7397
7932
|
const trigger = getTriggerInfo(ctx, ctx.context.session?.user.id ?? UNKNOWN_USER);
|
|
@@ -7400,6 +7935,17 @@ const dash = (options) => {
|
|
|
7400
7935
|
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_EMAIL, routes.SIGN_IN_EMAIL_OTP]) && ctx.context.returned instanceof Error && body?.email) trackEmailSignInAttempt(ctx, trigger);
|
|
7401
7936
|
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL]) && ctx.context.returned instanceof Error && ctx.body.provider && ctx.body.idToken) trackSocialSignInAttempt(ctx, trigger);
|
|
7402
7937
|
if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL_CALLBACK]) && ctx.context.returned instanceof Error) trackSocialSignInRedirectionAttempt(ctx, trigger);
|
|
7938
|
+
const headerRequestId = ctx.request?.headers.get("X-Request-Id");
|
|
7939
|
+
if (headerRequestId) ctx.setCookie(IDENTIFICATION_COOKIE_NAME, headerRequestId, {
|
|
7940
|
+
maxAge: 600,
|
|
7941
|
+
sameSite: "lax",
|
|
7942
|
+
httpOnly: true,
|
|
7943
|
+
path: "/"
|
|
7944
|
+
});
|
|
7945
|
+
else if (ctx.context.requestId) ctx.setCookie(IDENTIFICATION_COOKIE_NAME, "", {
|
|
7946
|
+
maxAge: 0,
|
|
7947
|
+
path: "/"
|
|
7948
|
+
});
|
|
7403
7949
|
})
|
|
7404
7950
|
}, {
|
|
7405
7951
|
handler: createAuthMiddleware(async (ctx) => {
|
|
@@ -7428,10 +7974,13 @@ const dash = (options) => {
|
|
|
7428
7974
|
endpoints: {
|
|
7429
7975
|
getDashConfig: getConfig(opts),
|
|
7430
7976
|
getDashUsers: getUsers(opts),
|
|
7977
|
+
exportDashUsers: exportUsers(opts),
|
|
7431
7978
|
getOnlineUsersCount: getOnlineUsersCount(opts),
|
|
7432
7979
|
createDashUser: createUser(opts),
|
|
7433
7980
|
deleteDashUser: deleteUser(opts),
|
|
7981
|
+
deleteManyDashUsers: deleteManyUsers(opts),
|
|
7434
7982
|
listDashOrganizations: listOrganizations(opts),
|
|
7983
|
+
exportDashOrganizations: exportOrganizations(opts),
|
|
7435
7984
|
getDashOrganization: getOrganization(opts),
|
|
7436
7985
|
listDashOrganizationMembers: listOrganizationMembers(opts),
|
|
7437
7986
|
listDashOrganizationInvitations: listOrganizationInvitations(opts),
|
|
@@ -7446,6 +7995,7 @@ const dash = (options) => {
|
|
|
7446
7995
|
listDashTeamMembers: listTeamMembers(opts),
|
|
7447
7996
|
createDashOrganization: createOrganization(opts),
|
|
7448
7997
|
deleteDashOrganization: deleteOrganization(opts),
|
|
7998
|
+
deleteManyDashOrganizations: deleteManyOrganizations(opts),
|
|
7449
7999
|
getDashOrganizationOptions: getOrganizationOptions(opts),
|
|
7450
8000
|
deleteDashSessions: deleteSessions(opts),
|
|
7451
8001
|
getDashUser: getUserDetails(opts),
|
|
@@ -7456,6 +8006,7 @@ const dash = (options) => {
|
|
|
7456
8006
|
listAllDashSessions: listAllSessions(opts),
|
|
7457
8007
|
dashRevokeSession: revokeSession(opts),
|
|
7458
8008
|
dashRevokeAllSessions: revokeAllSessions(opts),
|
|
8009
|
+
dashRevokeManySessions: revokeManySessions(opts),
|
|
7459
8010
|
dashImpersonateUser: impersonateUser(opts),
|
|
7460
8011
|
updateDashOrganization: updateOrganization(opts),
|
|
7461
8012
|
createDashTeam: createTeam(opts),
|
|
@@ -7475,8 +8026,10 @@ const dash = (options) => {
|
|
|
7475
8026
|
dashGetUserRetentionData: getUserRetentionData(opts),
|
|
7476
8027
|
dashGetUserMapData: getUserMapData(opts),
|
|
7477
8028
|
dashBanUser: banUser(opts),
|
|
8029
|
+
dashBanManyUsers: banManyUsers(opts),
|
|
7478
8030
|
dashUnbanUser: unbanUser(opts),
|
|
7479
8031
|
dashSendVerificationEmail: sendVerificationEmail(opts),
|
|
8032
|
+
dashSendManyVerificationEmails: sendManyVerificationEmails(opts),
|
|
7480
8033
|
dashSendResetPasswordEmail: sendResetPasswordEmail(opts),
|
|
7481
8034
|
dashEnableTwoFactor: enableTwoFactor(opts),
|
|
7482
8035
|
dashViewTwoFactorTotpUri: viewTwoFactorTotpUri(opts),
|
|
@@ -7506,4 +8059,4 @@ const dash = (options) => {
|
|
|
7506
8059
|
};
|
|
7507
8060
|
|
|
7508
8061
|
//#endregion
|
|
7509
|
-
export { CHALLENGE_TTL, DEFAULT_DIFFICULTY, EMAIL_TEMPLATES, SMS_TEMPLATES, USER_EVENT_TYPES, createEmailSender, createSMSSender, dash, decodePoWChallenge, encodePoWSolution, sendEmail, sendSMS, sentinel, solvePoWChallenge, verifyPoWSolution };
|
|
8062
|
+
export { CHALLENGE_TTL, DEFAULT_DIFFICULTY, EMAIL_TEMPLATES, SMS_TEMPLATES, USER_EVENT_TYPES, createEmailSender, createSMSSender, dash, decodePoWChallenge, encodePoWSolution, sendBulkEmails, sendEmail, sendSMS, sentinel, solvePoWChallenge, verifyPoWSolution };
|