@dyrected/core 2.19.0 → 2.20.0
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/{app-CXF4MdJc.d.cts → app-v23fNf58.d.cts} +171 -28
- package/dist/{app-CXF4MdJc.d.ts → app-v23fNf58.d.ts} +171 -28
- package/dist/index.cjs +511 -21
- package/dist/index.d.cts +73 -5
- package/dist/index.d.ts +73 -5
- package/dist/index.js +491 -21
- package/dist/server.cjs +1167 -136
- package/dist/server.d.cts +79 -29
- package/dist/server.d.ts +79 -29
- package/dist/server.js +1158 -136
- package/package.json +1 -1
package/dist/server.cjs
CHANGED
|
@@ -3063,7 +3063,7 @@ function assertValidTrashInConfig(config, source = "config") {
|
|
|
3063
3063
|
if (col.slug === "trash") {
|
|
3064
3064
|
throw new Error(`Collection slug "trash" is reserved by Dyrected for the trash system.`);
|
|
3065
3065
|
}
|
|
3066
|
-
const isSystem = col.slug
|
|
3066
|
+
const isSystem = INTERNAL_SYSTEM_COLLECTIONS.has(col.slug);
|
|
3067
3067
|
if (isSystem && col.trash) {
|
|
3068
3068
|
throw new Error(`System collection "${col.slug}" cannot enable trash.`);
|
|
3069
3069
|
}
|
|
@@ -3170,12 +3170,20 @@ async function restoreDocument(options) {
|
|
|
3170
3170
|
}
|
|
3171
3171
|
return { restoredDoc: docToRestore };
|
|
3172
3172
|
}
|
|
3173
|
-
var TRASH_COLLECTION, TRASH_COLLECTION_CONFIG;
|
|
3173
|
+
var TRASH_COLLECTION, INTERNAL_SYSTEM_COLLECTIONS, TRASH_COLLECTION_CONFIG;
|
|
3174
3174
|
var init_trash = __esm({
|
|
3175
3175
|
"src/trash.ts"() {
|
|
3176
3176
|
"use strict";
|
|
3177
3177
|
init_hooks();
|
|
3178
3178
|
TRASH_COLLECTION = "__trash";
|
|
3179
|
+
INTERNAL_SYSTEM_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
3180
|
+
"__audit",
|
|
3181
|
+
"__sessions",
|
|
3182
|
+
"__task_locks",
|
|
3183
|
+
"__trash",
|
|
3184
|
+
"__workflow_history",
|
|
3185
|
+
"__lifecycle_events"
|
|
3186
|
+
]);
|
|
3179
3187
|
TRASH_COLLECTION_CONFIG = {
|
|
3180
3188
|
slug: TRASH_COLLECTION,
|
|
3181
3189
|
labels: { singular: "Trash entry", plural: "Trash entries" },
|
|
@@ -5415,8 +5423,10 @@ __export(server_exports, {
|
|
|
5415
5423
|
AUTH_SESSIONS_COLLECTION: () => AUTH_SESSIONS_COLLECTION,
|
|
5416
5424
|
AuthController: () => AuthController,
|
|
5417
5425
|
CollectionController: () => CollectionController,
|
|
5426
|
+
DEFAULT_INVITE_EXPIRY: () => DEFAULT_INVITE_EXPIRY,
|
|
5418
5427
|
DEFAULT_LOCK_TIME_MS: () => DEFAULT_LOCK_TIME_MS,
|
|
5419
5428
|
DEFAULT_MAX_LOGIN_ATTEMPTS: () => DEFAULT_MAX_LOGIN_ATTEMPTS,
|
|
5429
|
+
DEFAULT_RESET_PASSWORD_EXPIRY: () => DEFAULT_RESET_PASSWORD_EXPIRY,
|
|
5420
5430
|
DEFAULT_SESSION_EXPIRY: () => DEFAULT_SESSION_EXPIRY,
|
|
5421
5431
|
EmbeddingService: () => EmbeddingService,
|
|
5422
5432
|
GlobalController: () => GlobalController,
|
|
@@ -5448,13 +5458,16 @@ __export(server_exports, {
|
|
|
5448
5458
|
getAIModel: () => getAIModel,
|
|
5449
5459
|
getAuthSession: () => getAuthSession,
|
|
5450
5460
|
getConfigLogger: () => getConfigLogger,
|
|
5461
|
+
getDbEmailTemplateOverride: () => getDbEmailTemplateOverride,
|
|
5451
5462
|
getEmbeddingModel: () => getEmbeddingModel,
|
|
5452
5463
|
getLockedUntilMs: () => getLockedUntilMs,
|
|
5453
5464
|
getObservabilityRuntime: () => getObservabilityRuntime,
|
|
5454
5465
|
getRequestLogger: () => getRequestLogger,
|
|
5455
5466
|
hasEmbeddingApiKey: () => hasEmbeddingApiKey,
|
|
5467
|
+
hasUsablePassword: () => hasUsablePassword,
|
|
5456
5468
|
hashContent: () => hashContent,
|
|
5457
5469
|
hashPassword: () => hashPassword,
|
|
5470
|
+
interpolateVariables: () => interpolateVariables,
|
|
5458
5471
|
isAuthSessionActive: () => isAuthSessionActive,
|
|
5459
5472
|
issueAuthSessionToken: () => issueAuthSessionToken,
|
|
5460
5473
|
normalizeFieldValue: () => normalizeFieldValue,
|
|
@@ -5463,7 +5476,10 @@ __export(server_exports, {
|
|
|
5463
5476
|
registerRoutes: () => registerRoutes,
|
|
5464
5477
|
renderPrometheusMetrics: () => renderPrometheusMetrics,
|
|
5465
5478
|
resolveAuthLockoutConfig: () => resolveAuthLockoutConfig,
|
|
5479
|
+
resolveEmailTemplate: () => resolveEmailTemplate,
|
|
5480
|
+
resolveInviteTokenExpiry: () => resolveInviteTokenExpiry,
|
|
5466
5481
|
resolveObservabilityConfig: () => resolveObservabilityConfig,
|
|
5482
|
+
resolveResetPasswordTokenExpiry: () => resolveResetPasswordTokenExpiry,
|
|
5467
5483
|
resolveSessionTokenExpiry: () => resolveSessionTokenExpiry,
|
|
5468
5484
|
revokeAllAuthSessions: () => revokeAllAuthSessions,
|
|
5469
5485
|
revokeAuthSession: () => revokeAuthSession,
|
|
@@ -5473,6 +5489,7 @@ __export(server_exports, {
|
|
|
5473
5489
|
serializeViewForApi: () => serializeViewForApi,
|
|
5474
5490
|
shouldSampleRequest: () => shouldSampleRequest,
|
|
5475
5491
|
signCollectionToken: () => signCollectionToken,
|
|
5492
|
+
toOutboundEmail: () => toOutboundEmail,
|
|
5476
5493
|
touchAuthSession: () => touchAuthSession,
|
|
5477
5494
|
validatePayloadAgainstFields: () => validatePayloadAgainstFields,
|
|
5478
5495
|
verifyCollectionToken: () => verifyCollectionToken,
|
|
@@ -6297,13 +6314,27 @@ async function hashPassword(plain) {
|
|
|
6297
6314
|
const derivedKey = await scryptAsync(plain, salt, KEY_LEN);
|
|
6298
6315
|
return `${salt}:${derivedKey.toString("hex")}`;
|
|
6299
6316
|
}
|
|
6317
|
+
function hasUsablePassword(stored) {
|
|
6318
|
+
if (typeof stored !== "string" || !stored) return false;
|
|
6319
|
+
const parts = stored.split(":");
|
|
6320
|
+
return parts.length === 2 && Boolean(parts[0]) && Boolean(parts[1]);
|
|
6321
|
+
}
|
|
6300
6322
|
async function verifyPassword(plain, stored) {
|
|
6301
|
-
|
|
6323
|
+
if (typeof plain !== "string" || !plain || typeof stored !== "string" || !stored) {
|
|
6324
|
+
return false;
|
|
6325
|
+
}
|
|
6326
|
+
const parts = stored.split(":");
|
|
6327
|
+
if (parts.length !== 2) return false;
|
|
6328
|
+
const [salt, storedHash] = parts;
|
|
6302
6329
|
if (!salt || !storedHash) return false;
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6330
|
+
try {
|
|
6331
|
+
const derivedKey = await scryptAsync(plain, salt, KEY_LEN);
|
|
6332
|
+
const storedBuffer = Buffer.from(storedHash, "hex");
|
|
6333
|
+
if (derivedKey.length !== storedBuffer.length) return false;
|
|
6334
|
+
return (0, import_node_crypto.timingSafeEqual)(derivedKey, storedBuffer);
|
|
6335
|
+
} catch {
|
|
6336
|
+
return false;
|
|
6337
|
+
}
|
|
6307
6338
|
}
|
|
6308
6339
|
|
|
6309
6340
|
// src/auth/token.ts
|
|
@@ -6319,6 +6350,8 @@ function getSecret() {
|
|
|
6319
6350
|
}
|
|
6320
6351
|
var DEFAULT_EXPIRY = "7d";
|
|
6321
6352
|
var DEFAULT_SESSION_EXPIRY = DEFAULT_EXPIRY;
|
|
6353
|
+
var DEFAULT_INVITE_EXPIRY = "48h";
|
|
6354
|
+
var DEFAULT_RESET_PASSWORD_EXPIRY = "2h";
|
|
6322
6355
|
function resolveSessionTokenExpiry(collection) {
|
|
6323
6356
|
const auth = collection.auth;
|
|
6324
6357
|
if (auth && typeof auth === "object" && typeof auth.tokenExpiration === "string" && auth.tokenExpiration.length > 0) {
|
|
@@ -6326,6 +6359,20 @@ function resolveSessionTokenExpiry(collection) {
|
|
|
6326
6359
|
}
|
|
6327
6360
|
return DEFAULT_SESSION_EXPIRY;
|
|
6328
6361
|
}
|
|
6362
|
+
function resolveInviteTokenExpiry(collection) {
|
|
6363
|
+
const auth = collection.auth;
|
|
6364
|
+
if (auth && typeof auth === "object" && typeof auth.inviteExpiration === "string" && auth.inviteExpiration.length > 0) {
|
|
6365
|
+
return auth.inviteExpiration;
|
|
6366
|
+
}
|
|
6367
|
+
return DEFAULT_INVITE_EXPIRY;
|
|
6368
|
+
}
|
|
6369
|
+
function resolveResetPasswordTokenExpiry(collection) {
|
|
6370
|
+
const auth = collection.auth;
|
|
6371
|
+
if (auth && typeof auth === "object" && typeof auth.resetPasswordExpiration === "string" && auth.resetPasswordExpiration.length > 0) {
|
|
6372
|
+
return auth.resetPasswordExpiration;
|
|
6373
|
+
}
|
|
6374
|
+
return DEFAULT_RESET_PASSWORD_EXPIRY;
|
|
6375
|
+
}
|
|
6329
6376
|
async function signCollectionToken(payload, expiresIn = DEFAULT_EXPIRY) {
|
|
6330
6377
|
return new import_jose.SignJWT({ ...payload }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(expiresIn).sign(getSecret());
|
|
6331
6378
|
}
|
|
@@ -6630,13 +6677,34 @@ function getAdminRoleForCollection(collection) {
|
|
|
6630
6677
|
}
|
|
6631
6678
|
return "admin";
|
|
6632
6679
|
}
|
|
6680
|
+
var STANDARD_ADMIN_ROLES = /* @__PURE__ */ new Set([
|
|
6681
|
+
"admin",
|
|
6682
|
+
"super_admin",
|
|
6683
|
+
"superadmin",
|
|
6684
|
+
"super admin",
|
|
6685
|
+
"super-admin",
|
|
6686
|
+
"owner"
|
|
6687
|
+
]);
|
|
6633
6688
|
function isUserAdmin(user, collection) {
|
|
6634
6689
|
if (!user) return false;
|
|
6635
|
-
const
|
|
6636
|
-
if (
|
|
6690
|
+
const rawRoles = Array.isArray(user.roles) ? user.roles : typeof user.roles === "string" ? [user.roles] : Array.isArray(user.role) ? user.role : typeof user.role === "string" ? [user.role] : [];
|
|
6691
|
+
if (rawRoles.length === 0) return false;
|
|
6692
|
+
const roles = rawRoles.filter((r) => typeof r === "string");
|
|
6637
6693
|
const configuredRole = getAdminRoleForCollection(collection);
|
|
6638
|
-
|
|
6639
|
-
|
|
6694
|
+
for (const r of roles) {
|
|
6695
|
+
if (r === configuredRole || r.toLowerCase() === configuredRole.toLowerCase()) {
|
|
6696
|
+
return true;
|
|
6697
|
+
}
|
|
6698
|
+
const normalized = r.trim().toLowerCase();
|
|
6699
|
+
if (STANDARD_ADMIN_ROLES.has(normalized)) {
|
|
6700
|
+
return true;
|
|
6701
|
+
}
|
|
6702
|
+
const stripped = normalized.replace(/[\s_-]+/g, "");
|
|
6703
|
+
if (stripped === "admin" || stripped === "superadmin" || stripped === "owner") {
|
|
6704
|
+
return true;
|
|
6705
|
+
}
|
|
6706
|
+
}
|
|
6707
|
+
return false;
|
|
6640
6708
|
}
|
|
6641
6709
|
|
|
6642
6710
|
// src/utils/collection-search.ts
|
|
@@ -7285,7 +7353,7 @@ function compileNavigation(config) {
|
|
|
7285
7353
|
const mentionedTrash = explicitNav.some((i) => i.trash || i.slug === "trash");
|
|
7286
7354
|
if (!mentionedTrash) {
|
|
7287
7355
|
const hasTrash = collections.some(
|
|
7288
|
-
(col) => !col.slug
|
|
7356
|
+
(col) => !INTERNAL_SYSTEM_COLLECTIONS.has(col.slug) && resolveTrashConfig(col, config).enabled
|
|
7289
7357
|
);
|
|
7290
7358
|
if (hasTrash) {
|
|
7291
7359
|
registerItem(void 0, {
|
|
@@ -11444,9 +11512,16 @@ function resolveAdminUrl(c, config, clientUrl) {
|
|
|
11444
11512
|
function appendQueryParam(url, key, value) {
|
|
11445
11513
|
return `${url}${url.includes("?") ? "&" : "?"}${key}=${encodeURIComponent(value)}`;
|
|
11446
11514
|
}
|
|
11447
|
-
|
|
11448
|
-
|
|
11449
|
-
|
|
11515
|
+
function resolveActionUrl(c, config, customUrl) {
|
|
11516
|
+
if (customUrl && typeof customUrl === "string" && customUrl.trim().length > 0) {
|
|
11517
|
+
try {
|
|
11518
|
+
return new URL(customUrl.trim(), c.req.url).toString();
|
|
11519
|
+
} catch {
|
|
11520
|
+
return customUrl.trim();
|
|
11521
|
+
}
|
|
11522
|
+
}
|
|
11523
|
+
return resolveAdminUrl(c, config);
|
|
11524
|
+
}
|
|
11450
11525
|
|
|
11451
11526
|
// src/auth/lockout.ts
|
|
11452
11527
|
var DEFAULT_MAX_LOGIN_ATTEMPTS = 5;
|
|
@@ -11503,15 +11578,21 @@ var emailTokens = {
|
|
|
11503
11578
|
width: "600px"
|
|
11504
11579
|
};
|
|
11505
11580
|
function escapeHtml(value) {
|
|
11506
|
-
return value.replace(
|
|
11507
|
-
"
|
|
11508
|
-
|
|
11509
|
-
|
|
11510
|
-
|
|
11511
|
-
|
|
11512
|
-
|
|
11581
|
+
return value.replace(
|
|
11582
|
+
/[&<>'"]/g,
|
|
11583
|
+
(character) => ({
|
|
11584
|
+
"&": "&",
|
|
11585
|
+
"<": "<",
|
|
11586
|
+
">": ">",
|
|
11587
|
+
"'": "'",
|
|
11588
|
+
'"': """
|
|
11589
|
+
})[character]
|
|
11590
|
+
);
|
|
11513
11591
|
}
|
|
11514
11592
|
function safeHttpUrl(value) {
|
|
11593
|
+
if (value.startsWith("{{") && value.endsWith("}}")) {
|
|
11594
|
+
return value;
|
|
11595
|
+
}
|
|
11515
11596
|
try {
|
|
11516
11597
|
const url = new URL(value);
|
|
11517
11598
|
return url.protocol === "https:" || url.protocol === "http:" ? escapeHtml(url.toString()) : void 0;
|
|
@@ -11522,8 +11603,8 @@ function safeHttpUrl(value) {
|
|
|
11522
11603
|
function heading(content) {
|
|
11523
11604
|
return `<h1 style="margin:0;font-family:${emailTokens.font};font-size:24px;line-height:1.25;font-weight:700;color:${emailTokens.colors.text}">${escapeHtml(content)}</h1>`;
|
|
11524
11605
|
}
|
|
11525
|
-
function paragraph(content, margin = "0 0 16px") {
|
|
11526
|
-
return `<p style="margin:${margin};font-family:${emailTokens.font};font-size
|
|
11606
|
+
function paragraph(content, margin = "0 0 16px", size = "15px") {
|
|
11607
|
+
return `<p style="margin:${margin};font-family:${emailTokens.font};font-size:${size};line-height:1.6;color:${emailTokens.colors.muted}">${escapeHtml(content)}</p>`;
|
|
11527
11608
|
}
|
|
11528
11609
|
function sectionLabel(content) {
|
|
11529
11610
|
return `<p style="margin:0 0 8px;font-family:${emailTokens.font};font-size:11px;line-height:1.4;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:${emailTokens.colors.subtle}">${escapeHtml(content)}</p>`;
|
|
@@ -11553,10 +11634,13 @@ function detailBox(content, monospace = false) {
|
|
|
11553
11634
|
function ctaButton(label, href) {
|
|
11554
11635
|
const safeHref = safeHttpUrl(href);
|
|
11555
11636
|
if (!safeHref) return "";
|
|
11556
|
-
return table(
|
|
11557
|
-
|
|
11558
|
-
|
|
11559
|
-
|
|
11637
|
+
return table(
|
|
11638
|
+
row(
|
|
11639
|
+
`<a href="${safeHref}" style="display:inline-block;padding:13px 22px;border-radius:${emailTokens.radius.control};background:${emailTokens.colors.accent};font-family:${emailTokens.font};font-size:14px;line-height:1.2;font-weight:700;color:${emailTokens.colors.text};text-decoration:none">${escapeHtml(label)}</a>`,
|
|
11640
|
+
"padding:8px 0 24px"
|
|
11641
|
+
),
|
|
11642
|
+
"width:auto"
|
|
11643
|
+
);
|
|
11560
11644
|
}
|
|
11561
11645
|
function safeLinkDetailBox(href) {
|
|
11562
11646
|
const safeHref = safeHttpUrl(href);
|
|
@@ -11564,9 +11648,33 @@ function safeLinkDetailBox(href) {
|
|
|
11564
11648
|
return detailBox(safeHref, true);
|
|
11565
11649
|
}
|
|
11566
11650
|
function alertBox(content) {
|
|
11567
|
-
return table(
|
|
11651
|
+
return table(
|
|
11652
|
+
row(
|
|
11653
|
+
escapeHtml(content),
|
|
11654
|
+
`padding:13px 16px;font-family:${emailTokens.font};font-size:13px;line-height:1.5;color:${emailTokens.colors.dangerText}`
|
|
11655
|
+
),
|
|
11656
|
+
`width:100%;background:${emailTokens.colors.dangerSurface};border:1px solid ${emailTokens.colors.dangerBorder};border-radius:${emailTokens.radius.control}`
|
|
11657
|
+
);
|
|
11568
11658
|
}
|
|
11569
|
-
function
|
|
11659
|
+
function resolveBlocks(input) {
|
|
11660
|
+
if (Array.isArray(input)) {
|
|
11661
|
+
const parts = [];
|
|
11662
|
+
for (const item of input) {
|
|
11663
|
+
if (Array.isArray(item)) {
|
|
11664
|
+
for (const sub of item) {
|
|
11665
|
+
if (typeof sub === "string" && sub.length > 0) parts.push(sub);
|
|
11666
|
+
}
|
|
11667
|
+
} else if (typeof item === "string" && item.length > 0) {
|
|
11668
|
+
parts.push(item);
|
|
11669
|
+
}
|
|
11670
|
+
}
|
|
11671
|
+
return parts.join("\n");
|
|
11672
|
+
}
|
|
11673
|
+
return typeof input === "string" ? input : "";
|
|
11674
|
+
}
|
|
11675
|
+
function layout({ preheader, title, content, footer, siteName }) {
|
|
11676
|
+
const resolvedContent = resolveBlocks(content);
|
|
11677
|
+
const resolvedFooter = resolveBlocks(footer);
|
|
11570
11678
|
return `<!doctype html>
|
|
11571
11679
|
<html lang="en" dir="ltr" xmlns="http://www.w3.org/1999/xhtml">
|
|
11572
11680
|
<head>
|
|
@@ -11587,8 +11695,8 @@ function layout({ preheader, title, content, footer }) {
|
|
|
11587
11695
|
${table(
|
|
11588
11696
|
row(
|
|
11589
11697
|
table(
|
|
11590
|
-
row(" ", `height:5px;background:${emailTokens.colors.accent};font-size:0;line-height:0`) + row(`${sectionLabel("Dyrected")}${heading(title)}`, "padding:30px 32px 24px") + row(
|
|
11591
|
-
`${divider()}${paragraph(
|
|
11698
|
+
row(" ", `height:5px;background:${emailTokens.colors.accent};font-size:0;line-height:0`) + row(`${sectionLabel(siteName ?? "Dyrected")}${heading(title)}`, "padding:30px 32px 24px") + row(resolvedContent, "padding:0 32px 32px") + row(
|
|
11699
|
+
`${divider()}${paragraph(resolvedFooter, "20px 0 6px")}${paragraph("Privacy: this message contains account-related information; please avoid forwarding it.", "10px 0px 0px 0px", "12px")}`,
|
|
11592
11700
|
"padding:0 32px 28px"
|
|
11593
11701
|
),
|
|
11594
11702
|
`width:100%;max-width:${emailTokens.width};background:${emailTokens.colors.surface};border:1px solid ${emailTokens.colors.border};border-radius:${emailTokens.radius.card};overflow:hidden`
|
|
@@ -11601,6 +11709,81 @@ function layout({ preheader, title, content, footer }) {
|
|
|
11601
11709
|
</body>
|
|
11602
11710
|
</html>`;
|
|
11603
11711
|
}
|
|
11712
|
+
function getDefaultEmailTemplate(purpose, options) {
|
|
11713
|
+
const siteName = options?.siteName ?? "Dyrected";
|
|
11714
|
+
switch (purpose) {
|
|
11715
|
+
case "invite":
|
|
11716
|
+
return {
|
|
11717
|
+
subject: `You've been invited to ${siteName}`,
|
|
11718
|
+
rawHtml: layout({
|
|
11719
|
+
siteName,
|
|
11720
|
+
preheader: `You've been invited to join ${siteName}.`,
|
|
11721
|
+
title: "You've been invited",
|
|
11722
|
+
content: [
|
|
11723
|
+
paragraph("Use the invitation link below to create your account. The link expires in 7 days."),
|
|
11724
|
+
ctaButton("Accept invitation", "{{url}}"),
|
|
11725
|
+
sectionLabel("Invitation link"),
|
|
11726
|
+
safeLinkDetailBox("{{url}}"),
|
|
11727
|
+
paragraph("If the button does not work, copy and paste the link above into your browser.", "12px 0 0")
|
|
11728
|
+
],
|
|
11729
|
+
footer: "If you weren't expecting this invitation, you can safely ignore this email."
|
|
11730
|
+
})
|
|
11731
|
+
};
|
|
11732
|
+
case "resetPassword":
|
|
11733
|
+
return {
|
|
11734
|
+
subject: "Reset your password",
|
|
11735
|
+
rawHtml: layout({
|
|
11736
|
+
siteName,
|
|
11737
|
+
preheader: `Reset your ${siteName} password.`,
|
|
11738
|
+
title: "Reset your password",
|
|
11739
|
+
content: [
|
|
11740
|
+
paragraph("We received a request to reset your password. The reset link expires in 1 hour."),
|
|
11741
|
+
ctaButton("Reset password", "{{url}}"),
|
|
11742
|
+
sectionLabel("Reset link"),
|
|
11743
|
+
safeLinkDetailBox("{{url}}"),
|
|
11744
|
+
paragraph("If the button does not work, copy and paste the link above into your browser.", "12px 0 0")
|
|
11745
|
+
],
|
|
11746
|
+
footer: "If you didn't request a password reset, you can safely ignore this email."
|
|
11747
|
+
})
|
|
11748
|
+
};
|
|
11749
|
+
case "welcome":
|
|
11750
|
+
return {
|
|
11751
|
+
subject: `Welcome to ${siteName} \u2014 your account is ready`,
|
|
11752
|
+
rawHtml: layout({
|
|
11753
|
+
siteName,
|
|
11754
|
+
preheader: `Your ${siteName} account is ready.`,
|
|
11755
|
+
title: "Welcome \u2014 your account is ready",
|
|
11756
|
+
content: [paragraph("Your account has been created. You can now log in with:"), detailBox("{{email}}")],
|
|
11757
|
+
footer: "If you didn't create this account, you can safely ignore this email."
|
|
11758
|
+
})
|
|
11759
|
+
};
|
|
11760
|
+
case "passwordChanged":
|
|
11761
|
+
return {
|
|
11762
|
+
subject: "Your password has been changed",
|
|
11763
|
+
rawHtml: layout({
|
|
11764
|
+
siteName,
|
|
11765
|
+
preheader: `Your ${siteName} password was changed.`,
|
|
11766
|
+
title: "Password changed",
|
|
11767
|
+
content: [
|
|
11768
|
+
paragraph("The password for your account has been changed."),
|
|
11769
|
+
alertBox("If you did not make this change, please contact support or reset your password immediately.")
|
|
11770
|
+
],
|
|
11771
|
+
footer: "Security notice: this confirms a recent credential change."
|
|
11772
|
+
})
|
|
11773
|
+
};
|
|
11774
|
+
default:
|
|
11775
|
+
return {
|
|
11776
|
+
subject: `Notification from ${siteName}`,
|
|
11777
|
+
rawHtml: layout({
|
|
11778
|
+
siteName,
|
|
11779
|
+
preheader: `Account notification from ${siteName}.`,
|
|
11780
|
+
title: "Account Notification",
|
|
11781
|
+
content: [paragraph(`This is an account notification from ${siteName}.`)],
|
|
11782
|
+
footer: "If you have any questions, please reach out to our team."
|
|
11783
|
+
})
|
|
11784
|
+
};
|
|
11785
|
+
}
|
|
11786
|
+
}
|
|
11604
11787
|
|
|
11605
11788
|
// src/services/email.service.ts
|
|
11606
11789
|
var _devSend = null;
|
|
@@ -11628,11 +11811,18 @@ async function getDevSend() {
|
|
|
11628
11811
|
password: account.pass
|
|
11629
11812
|
});
|
|
11630
11813
|
_devSend = async ({ to, subject, html }) => {
|
|
11631
|
-
const info = await transport.sendMail({
|
|
11814
|
+
const info = await transport.sendMail({
|
|
11815
|
+
from: '"Dyrected Dev" <dev@dyrected.local>',
|
|
11816
|
+
to,
|
|
11817
|
+
subject,
|
|
11818
|
+
html
|
|
11819
|
+
});
|
|
11820
|
+
const previewUrl = nodemailer.default.getTestMessageUrl(info) || void 0;
|
|
11632
11821
|
logger.info({
|
|
11633
11822
|
msg: "Email preview URL",
|
|
11634
|
-
previewUrl
|
|
11823
|
+
previewUrl
|
|
11635
11824
|
});
|
|
11825
|
+
return { previewUrl };
|
|
11636
11826
|
};
|
|
11637
11827
|
return _devSend;
|
|
11638
11828
|
} catch {
|
|
@@ -11658,69 +11848,286 @@ async function sendEmail(config, payload) {
|
|
|
11658
11848
|
err,
|
|
11659
11849
|
msg: "Email provider send failed",
|
|
11660
11850
|
to: payload.to,
|
|
11661
|
-
subject: payload.subject
|
|
11851
|
+
subject: "subject" in payload ? payload.subject : void 0,
|
|
11852
|
+
template: "template" in payload ? payload.template : void 0
|
|
11662
11853
|
});
|
|
11663
11854
|
throw err;
|
|
11664
11855
|
}
|
|
11665
|
-
return;
|
|
11856
|
+
return { success: true };
|
|
11666
11857
|
}
|
|
11667
11858
|
if (process.env.NODE_ENV !== "production") {
|
|
11668
11859
|
const devSend = await getDevSend();
|
|
11669
|
-
|
|
11860
|
+
if (devSend) {
|
|
11861
|
+
if ("template" in payload && payload.template) {
|
|
11862
|
+
const devHtml = `
|
|
11863
|
+
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px; border: 1px solid #e2e8f0; border-radius: 8px;">
|
|
11864
|
+
<h3 style="margin-top:0; color: #1e293b;">External Template Email (Dev Preview)</h3>
|
|
11865
|
+
<p><strong>To:</strong> ${payload.to}</p>
|
|
11866
|
+
<p><strong>Template ID:</strong> <code>${payload.template}</code></p>
|
|
11867
|
+
${payload.subject ? `<p><strong>Subject:</strong> ${payload.subject}</p>` : ""}
|
|
11868
|
+
<p><strong>Variables:</strong></p>
|
|
11869
|
+
<pre style="background: #f8fafc; padding: 12px; border-radius: 6px; overflow-x: auto;">${JSON.stringify(payload.variables, null, 2)}</pre>
|
|
11870
|
+
</div>
|
|
11871
|
+
`;
|
|
11872
|
+
const res = await devSend({
|
|
11873
|
+
to: payload.to,
|
|
11874
|
+
subject: payload.subject ?? `[Template: ${payload.template}] External Email`,
|
|
11875
|
+
html: devHtml
|
|
11876
|
+
});
|
|
11877
|
+
return { success: true, previewUrl: res?.previewUrl };
|
|
11878
|
+
} else if ("html" in payload) {
|
|
11879
|
+
const res = await devSend({
|
|
11880
|
+
to: payload.to,
|
|
11881
|
+
subject: payload.subject,
|
|
11882
|
+
html: payload.html
|
|
11883
|
+
});
|
|
11884
|
+
return { success: true, previewUrl: res?.previewUrl };
|
|
11885
|
+
}
|
|
11886
|
+
}
|
|
11887
|
+
}
|
|
11888
|
+
return { success: true };
|
|
11889
|
+
}
|
|
11890
|
+
function interpolateVariables(str, args, escapeMode = "html") {
|
|
11891
|
+
if (!str || typeof str !== "string") return str;
|
|
11892
|
+
return str.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, path) => {
|
|
11893
|
+
const parts = path.split(".");
|
|
11894
|
+
let val = args;
|
|
11895
|
+
for (const part of parts) {
|
|
11896
|
+
if (val == null) break;
|
|
11897
|
+
val = val[part];
|
|
11898
|
+
}
|
|
11899
|
+
if (val === void 0 || val === null) return "";
|
|
11900
|
+
const raw = String(val);
|
|
11901
|
+
return escapeMode === "html" ? escapeHtml(raw) : raw;
|
|
11902
|
+
});
|
|
11903
|
+
}
|
|
11904
|
+
function formatDbTemplateDoc(doc, args) {
|
|
11905
|
+
if (doc.format === "external_template" || doc.externalTemplateId) {
|
|
11906
|
+
const rawVars = doc.variables && typeof doc.variables === "object" ? doc.variables : {};
|
|
11907
|
+
const interpolatedVars = { ...args };
|
|
11908
|
+
for (const [key, val] of Object.entries(rawVars)) {
|
|
11909
|
+
if (typeof val === "string") {
|
|
11910
|
+
interpolatedVars[key] = interpolateVariables(val, args);
|
|
11911
|
+
} else {
|
|
11912
|
+
interpolatedVars[key] = val;
|
|
11913
|
+
}
|
|
11914
|
+
}
|
|
11915
|
+
return {
|
|
11916
|
+
template: doc.externalTemplateId ?? "",
|
|
11917
|
+
variables: interpolatedVars,
|
|
11918
|
+
...doc.subject ? { subject: interpolateVariables(doc.subject, args) } : {}
|
|
11919
|
+
};
|
|
11920
|
+
}
|
|
11921
|
+
return {
|
|
11922
|
+
subject: interpolateVariables(doc.subject || "", args),
|
|
11923
|
+
html: interpolateVariables(doc.rawHtml || "", args)
|
|
11924
|
+
};
|
|
11925
|
+
}
|
|
11926
|
+
async function getDbEmailTemplateOverride(db, collectionSlug, purpose, args) {
|
|
11927
|
+
if (!db || typeof db.find !== "function") return null;
|
|
11928
|
+
try {
|
|
11929
|
+
if (collectionSlug) {
|
|
11930
|
+
const specific = await db.find({
|
|
11931
|
+
collection: "__email_templates",
|
|
11932
|
+
where: {
|
|
11933
|
+
collectionSlug,
|
|
11934
|
+
purpose,
|
|
11935
|
+
active: true
|
|
11936
|
+
},
|
|
11937
|
+
limit: 1
|
|
11938
|
+
});
|
|
11939
|
+
const doc2 = specific?.docs?.[0];
|
|
11940
|
+
if (doc2) {
|
|
11941
|
+
return formatDbTemplateDoc(doc2, args);
|
|
11942
|
+
}
|
|
11943
|
+
}
|
|
11944
|
+
const global = await db.find({
|
|
11945
|
+
collection: "__email_templates",
|
|
11946
|
+
where: {
|
|
11947
|
+
collectionSlug: "*",
|
|
11948
|
+
purpose,
|
|
11949
|
+
active: true
|
|
11950
|
+
},
|
|
11951
|
+
limit: 1
|
|
11952
|
+
});
|
|
11953
|
+
const doc = global?.docs?.[0];
|
|
11954
|
+
if (doc) {
|
|
11955
|
+
return formatDbTemplateDoc(doc, args);
|
|
11956
|
+
}
|
|
11957
|
+
} catch {
|
|
11958
|
+
return null;
|
|
11670
11959
|
}
|
|
11960
|
+
return null;
|
|
11671
11961
|
}
|
|
11672
|
-
function
|
|
11673
|
-
const
|
|
11962
|
+
function enrichArgs(config, collection, args) {
|
|
11963
|
+
const slug = collection?.slug;
|
|
11964
|
+
const readableSlug = slug?.replace(/^__/, "");
|
|
11965
|
+
const fallbackLabel = readableSlug ? readableSlug.charAt(0).toUpperCase() + readableSlug.slice(1) : "Account";
|
|
11674
11966
|
return {
|
|
11675
|
-
|
|
11676
|
-
|
|
11677
|
-
|
|
11967
|
+
...args,
|
|
11968
|
+
collection: slug,
|
|
11969
|
+
collectionLabel: collection?.labels?.singular || collection?.labels?.plural || fallbackLabel,
|
|
11970
|
+
collectionLabels: collection?.labels,
|
|
11971
|
+
siteName: config.admin?.branding?.logoText ?? "Dyrected"
|
|
11972
|
+
};
|
|
11973
|
+
}
|
|
11974
|
+
function toOutboundEmail(to, result, metadata) {
|
|
11975
|
+
if ("template" in result) {
|
|
11976
|
+
return {
|
|
11977
|
+
to,
|
|
11978
|
+
type: "template",
|
|
11979
|
+
template: result.template,
|
|
11980
|
+
variables: result.variables,
|
|
11981
|
+
...result.subject ? { subject: result.subject } : {},
|
|
11982
|
+
...metadata
|
|
11983
|
+
};
|
|
11984
|
+
}
|
|
11985
|
+
return {
|
|
11986
|
+
to,
|
|
11987
|
+
type: "html",
|
|
11988
|
+
subject: result.subject ?? "Notification",
|
|
11989
|
+
html: result.html,
|
|
11990
|
+
...result.text ? { text: result.text } : {},
|
|
11991
|
+
...metadata
|
|
11992
|
+
};
|
|
11993
|
+
}
|
|
11994
|
+
function buildWelcomeEmail(config, args, collection, dbOverride) {
|
|
11995
|
+
if (dbOverride) return dbOverride;
|
|
11996
|
+
const enriched = enrichArgs(config, collection, args);
|
|
11997
|
+
const collectionAuth = collection?.auth;
|
|
11998
|
+
if (collectionAuth && typeof collectionAuth === "object" && collectionAuth.email?.templates?.welcome) {
|
|
11999
|
+
return collectionAuth.email.templates.welcome(enriched);
|
|
12000
|
+
}
|
|
12001
|
+
const custom = config.email?.templates?.welcome?.(enriched);
|
|
12002
|
+
if (custom) return custom;
|
|
12003
|
+
return {
|
|
12004
|
+
subject: "Welcome \u2014 your account is ready",
|
|
12005
|
+
html: layout({
|
|
12006
|
+
siteName: enriched.siteName,
|
|
12007
|
+
preheader: `Your ${enriched.siteName} account is ready.`,
|
|
11678
12008
|
title: "Welcome \u2014 your account is ready",
|
|
11679
|
-
content:
|
|
12009
|
+
content: [
|
|
12010
|
+
paragraph("Your account has been created. You can now log in with:"),
|
|
12011
|
+
detailBox(args.email)
|
|
12012
|
+
],
|
|
11680
12013
|
footer: "If you didn't create this account, you can safely ignore this email."
|
|
11681
12014
|
})
|
|
11682
12015
|
};
|
|
11683
12016
|
}
|
|
11684
|
-
function buildInviteEmail(config, args) {
|
|
11685
|
-
|
|
12017
|
+
function buildInviteEmail(config, args, collection, dbOverride) {
|
|
12018
|
+
if (dbOverride) return dbOverride;
|
|
12019
|
+
const enriched = enrichArgs(config, collection, args);
|
|
12020
|
+
const collectionAuth = collection?.auth;
|
|
12021
|
+
if (collectionAuth && typeof collectionAuth === "object" && collectionAuth.email?.templates?.invite) {
|
|
12022
|
+
return collectionAuth.email.templates.invite(enriched);
|
|
12023
|
+
}
|
|
12024
|
+
const custom = config.email?.templates?.invite?.(enriched);
|
|
12025
|
+
if (custom) return custom;
|
|
11686
12026
|
const inviteLink = args.url;
|
|
11687
12027
|
const inviteLinkBlock = inviteLink ? safeLinkDetailBox(inviteLink) : "";
|
|
11688
12028
|
return {
|
|
11689
|
-
subject:
|
|
11690
|
-
html:
|
|
11691
|
-
|
|
12029
|
+
subject: "You've been invited",
|
|
12030
|
+
html: layout({
|
|
12031
|
+
siteName: enriched.siteName,
|
|
12032
|
+
preheader: `You've been invited to join ${enriched.siteName}.`,
|
|
11692
12033
|
title: "You've been invited",
|
|
11693
|
-
content:
|
|
12034
|
+
content: [
|
|
12035
|
+
args.invitedByEmail && paragraph(`You were invited by ${args.invitedByEmail}.`),
|
|
12036
|
+
inviteLink ? [
|
|
12037
|
+
paragraph("Use the invitation link below to create your account. The link expires in 7 days."),
|
|
12038
|
+
ctaButton("Accept invitation", inviteLink),
|
|
12039
|
+
sectionLabel("Invitation link"),
|
|
12040
|
+
inviteLinkBlock,
|
|
12041
|
+
paragraph("If the button does not work, copy and paste the link above into your browser.", "12px 0 0")
|
|
12042
|
+
] : [
|
|
12043
|
+
paragraph("Use the invitation token below to accept your invitation. It expires in 7 days."),
|
|
12044
|
+
sectionLabel("Invitation token"),
|
|
12045
|
+
detailBox(args.token, true)
|
|
12046
|
+
]
|
|
12047
|
+
],
|
|
11694
12048
|
footer: "If you weren't expecting this invitation, you can safely ignore this email."
|
|
11695
12049
|
})
|
|
11696
12050
|
};
|
|
11697
12051
|
}
|
|
11698
|
-
function buildResetPasswordEmail(config, args) {
|
|
11699
|
-
|
|
12052
|
+
function buildResetPasswordEmail(config, args, collection, dbOverride) {
|
|
12053
|
+
if (dbOverride) return dbOverride;
|
|
12054
|
+
const enriched = enrichArgs(config, collection, args);
|
|
12055
|
+
const collectionAuth = collection?.auth;
|
|
12056
|
+
if (collectionAuth && typeof collectionAuth === "object" && collectionAuth.email?.templates?.resetPassword) {
|
|
12057
|
+
return collectionAuth.email.templates.resetPassword(enriched);
|
|
12058
|
+
}
|
|
12059
|
+
const custom = config.email?.templates?.resetPassword?.(enriched);
|
|
12060
|
+
if (custom) return custom;
|
|
11700
12061
|
const resetLink = args.url;
|
|
11701
12062
|
const resetLinkBlock = resetLink ? safeLinkDetailBox(resetLink) : "";
|
|
11702
12063
|
return {
|
|
11703
|
-
subject:
|
|
11704
|
-
html:
|
|
11705
|
-
|
|
12064
|
+
subject: "Reset your password",
|
|
12065
|
+
html: layout({
|
|
12066
|
+
siteName: enriched.siteName,
|
|
12067
|
+
preheader: `Reset your ${enriched.siteName} password.`,
|
|
11706
12068
|
title: "Reset your password",
|
|
11707
|
-
content:
|
|
12069
|
+
content: [
|
|
12070
|
+
resetLink ? [
|
|
12071
|
+
paragraph("We received a request to reset your password. The reset link expires in 1 hour."),
|
|
12072
|
+
ctaButton("Reset password", resetLink),
|
|
12073
|
+
sectionLabel("Reset link"),
|
|
12074
|
+
resetLinkBlock,
|
|
12075
|
+
paragraph("If the button does not work, copy and paste the link above into your browser.", "12px 0 0")
|
|
12076
|
+
] : [
|
|
12077
|
+
paragraph("We received a request to reset your password. The reset token expires in 1 hour."),
|
|
12078
|
+
sectionLabel("Reset token"),
|
|
12079
|
+
detailBox(args.token, true)
|
|
12080
|
+
]
|
|
12081
|
+
],
|
|
11708
12082
|
footer: "If you didn't request a password reset, you can safely ignore this email."
|
|
11709
12083
|
})
|
|
11710
12084
|
};
|
|
11711
12085
|
}
|
|
11712
|
-
function buildPasswordChangedEmail(config, args) {
|
|
11713
|
-
|
|
12086
|
+
function buildPasswordChangedEmail(config, args, collection, dbOverride) {
|
|
12087
|
+
if (dbOverride) return dbOverride;
|
|
12088
|
+
const enriched = enrichArgs(config, collection, args);
|
|
12089
|
+
const collectionAuth = collection?.auth;
|
|
12090
|
+
if (collectionAuth && typeof collectionAuth === "object" && collectionAuth.email?.templates?.passwordChanged) {
|
|
12091
|
+
return collectionAuth.email.templates.passwordChanged(enriched);
|
|
12092
|
+
}
|
|
12093
|
+
const custom = config.email?.templates?.passwordChanged?.(enriched);
|
|
12094
|
+
if (custom) return custom;
|
|
11714
12095
|
return {
|
|
11715
|
-
subject:
|
|
11716
|
-
html:
|
|
11717
|
-
|
|
12096
|
+
subject: "Your password has been changed",
|
|
12097
|
+
html: layout({
|
|
12098
|
+
siteName: enriched.siteName,
|
|
12099
|
+
preheader: `Your ${enriched.siteName} password was changed.`,
|
|
11718
12100
|
title: "Password changed",
|
|
11719
|
-
content:
|
|
12101
|
+
content: [
|
|
12102
|
+
paragraph("The password for this account was just changed:"),
|
|
12103
|
+
detailBox(args.email),
|
|
12104
|
+
spacer(),
|
|
12105
|
+
alertBox("If you did not make this change, please contact support immediately.")
|
|
12106
|
+
],
|
|
11720
12107
|
footer: "This is an automated security notification."
|
|
11721
12108
|
})
|
|
11722
12109
|
};
|
|
11723
12110
|
}
|
|
12111
|
+
async function resolveEmailTemplate(params) {
|
|
12112
|
+
const { config, collection, purpose, args, db } = params;
|
|
12113
|
+
const enriched = enrichArgs(config, collection, args);
|
|
12114
|
+
let dbOverride = null;
|
|
12115
|
+
if (config.email?.adminEditable !== false && db) {
|
|
12116
|
+
dbOverride = await getDbEmailTemplateOverride(db, collection?.slug, purpose, enriched);
|
|
12117
|
+
}
|
|
12118
|
+
switch (purpose) {
|
|
12119
|
+
case "welcome":
|
|
12120
|
+
return buildWelcomeEmail(config, enriched, collection, dbOverride);
|
|
12121
|
+
case "invite":
|
|
12122
|
+
return buildInviteEmail(config, enriched, collection, dbOverride);
|
|
12123
|
+
case "resetPassword":
|
|
12124
|
+
return buildResetPasswordEmail(config, enriched, collection, dbOverride);
|
|
12125
|
+
case "passwordChanged":
|
|
12126
|
+
return buildPasswordChangedEmail(config, enriched, collection, dbOverride);
|
|
12127
|
+
default:
|
|
12128
|
+
throw new Error(`Unknown email template purpose: ${purpose}`);
|
|
12129
|
+
}
|
|
12130
|
+
}
|
|
11724
12131
|
|
|
11725
12132
|
// src/controllers/auth.controller.ts
|
|
11726
12133
|
var AuthController = class {
|
|
@@ -11757,9 +12164,10 @@ var AuthController = class {
|
|
|
11757
12164
|
const data = {
|
|
11758
12165
|
...safeExtraFields,
|
|
11759
12166
|
email,
|
|
11760
|
-
password:
|
|
12167
|
+
password: null,
|
|
12168
|
+
invitedAt: Date.now()
|
|
11761
12169
|
};
|
|
11762
|
-
if (this.hasField("status")) {
|
|
12170
|
+
if (this.hasField("status") && !data.status) {
|
|
11763
12171
|
data.status = "pending";
|
|
11764
12172
|
}
|
|
11765
12173
|
return data;
|
|
@@ -11882,6 +12290,7 @@ var AuthController = class {
|
|
|
11882
12290
|
...rolePayload
|
|
11883
12291
|
}
|
|
11884
12292
|
});
|
|
12293
|
+
const safeUser = this.sanitizeUser(user);
|
|
11885
12294
|
const token = await issueAuthSessionToken({
|
|
11886
12295
|
config,
|
|
11887
12296
|
userId: user.id,
|
|
@@ -11891,15 +12300,28 @@ var AuthController = class {
|
|
|
11891
12300
|
ip: c.get("clientIp"),
|
|
11892
12301
|
authSource: "local"
|
|
11893
12302
|
});
|
|
11894
|
-
|
|
11895
|
-
|
|
12303
|
+
resolveEmailTemplate({
|
|
12304
|
+
config,
|
|
12305
|
+
collection: this.collection,
|
|
12306
|
+
purpose: "welcome",
|
|
12307
|
+
args: { email: body.email, user: safeUser },
|
|
12308
|
+
db
|
|
12309
|
+
}).then(
|
|
12310
|
+
(emailResult) => sendEmail(
|
|
12311
|
+
config,
|
|
12312
|
+
toOutboundEmail(body.email, emailResult, {
|
|
12313
|
+
collection: this.collection.slug,
|
|
12314
|
+
purpose: "welcome",
|
|
12315
|
+
user: safeUser
|
|
12316
|
+
})
|
|
12317
|
+
)
|
|
12318
|
+
).catch(
|
|
11896
12319
|
(err) => getRequestLogger(c, "auth").error({
|
|
11897
12320
|
err,
|
|
11898
12321
|
msg: "Failed to send welcome email",
|
|
11899
12322
|
email: body.email
|
|
11900
12323
|
})
|
|
11901
12324
|
);
|
|
11902
|
-
const safeUser = this.sanitizeUser(user);
|
|
11903
12325
|
return c.json({ token, user: safeUser });
|
|
11904
12326
|
}
|
|
11905
12327
|
// ---------------------------------------------------------------------------
|
|
@@ -11933,6 +12355,16 @@ var AuthController = class {
|
|
|
11933
12355
|
403
|
|
11934
12356
|
);
|
|
11935
12357
|
}
|
|
12358
|
+
if (!hasUsablePassword(user.password)) {
|
|
12359
|
+
return c.json(
|
|
12360
|
+
{
|
|
12361
|
+
error: true,
|
|
12362
|
+
code: "PASSWORD_NOT_SET",
|
|
12363
|
+
message: "This account does not have a password set. Please use the invite link or reset password."
|
|
12364
|
+
},
|
|
12365
|
+
401
|
|
12366
|
+
);
|
|
12367
|
+
}
|
|
11936
12368
|
const lockedUntilMs = getLockedUntilMs(user.lockedUntil);
|
|
11937
12369
|
if (lockedUntilMs !== null && lockedUntilMs > Date.now()) {
|
|
11938
12370
|
const retryAfterSeconds = Math.max(
|
|
@@ -12110,34 +12542,67 @@ var AuthController = class {
|
|
|
12110
12542
|
limit: 1
|
|
12111
12543
|
});
|
|
12112
12544
|
const user = result.docs[0];
|
|
12545
|
+
let resetToken;
|
|
12546
|
+
let url;
|
|
12547
|
+
let emailSent = false;
|
|
12548
|
+
let emailError;
|
|
12113
12549
|
if (user) {
|
|
12114
|
-
const
|
|
12550
|
+
const siteId = c.get("siteId");
|
|
12551
|
+
resetToken = await signCollectionToken(
|
|
12115
12552
|
{
|
|
12116
|
-
sub: user.id,
|
|
12553
|
+
sub: String(user.id),
|
|
12117
12554
|
email: user.email,
|
|
12118
12555
|
collection: this.collection.slug,
|
|
12119
|
-
purpose: "reset"
|
|
12556
|
+
purpose: "reset",
|
|
12557
|
+
...siteId ? { siteId } : {}
|
|
12120
12558
|
},
|
|
12121
|
-
|
|
12559
|
+
resolveResetPasswordTokenExpiry(this.collection)
|
|
12122
12560
|
);
|
|
12123
|
-
const
|
|
12124
|
-
|
|
12125
|
-
|
|
12126
|
-
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12130
|
-
|
|
12131
|
-
|
|
12132
|
-
|
|
12133
|
-
|
|
12134
|
-
|
|
12135
|
-
|
|
12561
|
+
const customResetUrl = body?.resetUrl ?? (typeof this.collection.auth === "object" ? this.collection.auth.urls?.resetPassword : void 0);
|
|
12562
|
+
url = appendQueryParam(
|
|
12563
|
+
resolveActionUrl(c, config, customResetUrl),
|
|
12564
|
+
"token",
|
|
12565
|
+
resetToken
|
|
12566
|
+
);
|
|
12567
|
+
if (body?.sendEmail !== false) {
|
|
12568
|
+
try {
|
|
12569
|
+
const emailResult = await resolveEmailTemplate({
|
|
12570
|
+
config,
|
|
12571
|
+
collection: this.collection,
|
|
12572
|
+
purpose: "resetPassword",
|
|
12573
|
+
args: {
|
|
12574
|
+
token: resetToken,
|
|
12575
|
+
url,
|
|
12576
|
+
user
|
|
12577
|
+
},
|
|
12578
|
+
db
|
|
12579
|
+
});
|
|
12580
|
+
await sendEmail(
|
|
12581
|
+
config,
|
|
12582
|
+
toOutboundEmail(user.email, emailResult, {
|
|
12583
|
+
collection: this.collection.slug,
|
|
12584
|
+
purpose: "resetPassword",
|
|
12585
|
+
user
|
|
12586
|
+
})
|
|
12587
|
+
);
|
|
12588
|
+
emailSent = true;
|
|
12589
|
+
} catch (err) {
|
|
12590
|
+
emailSent = false;
|
|
12591
|
+
emailError = err?.message ?? "Email delivery failed";
|
|
12592
|
+
getRequestLogger(c, "auth").error({
|
|
12593
|
+
err,
|
|
12594
|
+
msg: "Failed to send password reset email",
|
|
12595
|
+
email: user.email
|
|
12596
|
+
});
|
|
12597
|
+
}
|
|
12136
12598
|
}
|
|
12137
12599
|
}
|
|
12138
12600
|
return c.json({
|
|
12139
12601
|
success: true,
|
|
12140
|
-
message: "If an account with that email exists, a reset link has been sent."
|
|
12602
|
+
message: "If an account with that email exists, a reset link has been sent.",
|
|
12603
|
+
...body?.sendEmail === false && resetToken ? { token: resetToken, resetUrl: url } : {},
|
|
12604
|
+
emailSent,
|
|
12605
|
+
...emailError ? { emailError } : {}
|
|
12141
12606
|
});
|
|
12142
12607
|
}
|
|
12143
12608
|
// ---------------------------------------------------------------------------
|
|
@@ -12171,6 +12636,13 @@ var AuthController = class {
|
|
|
12171
12636
|
400
|
|
12172
12637
|
);
|
|
12173
12638
|
}
|
|
12639
|
+
const currentSiteId = c.get("siteId");
|
|
12640
|
+
if (payload.siteId && currentSiteId && payload.siteId !== currentSiteId) {
|
|
12641
|
+
return c.json(
|
|
12642
|
+
{ error: true, code: "SITE_MISMATCH", message: "Token is not valid for this site." },
|
|
12643
|
+
403
|
|
12644
|
+
);
|
|
12645
|
+
}
|
|
12174
12646
|
const hashedPassword = await hashPassword(body.password);
|
|
12175
12647
|
await db.update({
|
|
12176
12648
|
collection: this.collection.slug,
|
|
@@ -12185,10 +12657,24 @@ var AuthController = class {
|
|
|
12185
12657
|
userId: payload.sub,
|
|
12186
12658
|
collection: this.collection.slug
|
|
12187
12659
|
});
|
|
12188
|
-
|
|
12189
|
-
|
|
12190
|
-
|
|
12191
|
-
|
|
12660
|
+
resolveEmailTemplate({
|
|
12661
|
+
config,
|
|
12662
|
+
collection: this.collection,
|
|
12663
|
+
purpose: "passwordChanged",
|
|
12664
|
+
args: {
|
|
12665
|
+
email: payload.email,
|
|
12666
|
+
user: { id: payload.sub, email: payload.email }
|
|
12667
|
+
},
|
|
12668
|
+
db
|
|
12669
|
+
}).then(
|
|
12670
|
+
(emailResult) => sendEmail(
|
|
12671
|
+
config,
|
|
12672
|
+
toOutboundEmail(payload.email, emailResult, {
|
|
12673
|
+
collection: this.collection.slug,
|
|
12674
|
+
purpose: "passwordChanged"
|
|
12675
|
+
})
|
|
12676
|
+
)
|
|
12677
|
+
).catch(
|
|
12192
12678
|
(err) => getRequestLogger(c, "auth").error({
|
|
12193
12679
|
err,
|
|
12194
12680
|
msg: "Failed to send password-changed email",
|
|
@@ -12217,61 +12703,117 @@ var AuthController = class {
|
|
|
12217
12703
|
if (!body?.email) {
|
|
12218
12704
|
return c.json({ error: true, message: "email is required." }, 400);
|
|
12219
12705
|
}
|
|
12220
|
-
const
|
|
12706
|
+
const {
|
|
12707
|
+
email,
|
|
12708
|
+
inviteUrl,
|
|
12709
|
+
sendEmail: shouldSendEmail,
|
|
12710
|
+
data: nestedData,
|
|
12711
|
+
...topLevelFields
|
|
12712
|
+
} = body;
|
|
12713
|
+
const inviteData = {
|
|
12714
|
+
...topLevelFields,
|
|
12715
|
+
...nestedData && typeof nestedData === "object" && !Array.isArray(nestedData) ? nestedData : {}
|
|
12716
|
+
};
|
|
12221
12717
|
const existing = await db.find({
|
|
12222
12718
|
collection: this.collection.slug,
|
|
12223
|
-
where: { email
|
|
12719
|
+
where: { email },
|
|
12224
12720
|
limit: 1
|
|
12225
12721
|
});
|
|
12226
12722
|
const existingUser = existing.docs[0];
|
|
12227
12723
|
const existingIsPending = existingUser?.status === "pending";
|
|
12228
|
-
if (existingUser && !existingIsPending) {
|
|
12724
|
+
if (existingUser && !existingIsPending && hasUsablePassword(existingUser.password)) {
|
|
12229
12725
|
return c.json(
|
|
12230
|
-
{
|
|
12726
|
+
{
|
|
12727
|
+
error: true,
|
|
12728
|
+
code: "USER_ALREADY_EXISTS",
|
|
12729
|
+
message: "An account with that email already exists and has a password set. Use forgot password if needed."
|
|
12730
|
+
},
|
|
12231
12731
|
409
|
|
12232
12732
|
);
|
|
12233
12733
|
}
|
|
12734
|
+
const now2 = Date.now();
|
|
12735
|
+
let targetUser;
|
|
12234
12736
|
if (!existingUser) {
|
|
12235
|
-
await
|
|
12737
|
+
const pendingData = await this.buildPendingInviteData(email, inviteData);
|
|
12738
|
+
pendingData.invitedAt = now2;
|
|
12739
|
+
targetUser = await db.create({
|
|
12236
12740
|
collection: this.collection.slug,
|
|
12237
|
-
data:
|
|
12741
|
+
data: pendingData
|
|
12238
12742
|
});
|
|
12239
|
-
} else
|
|
12240
|
-
|
|
12743
|
+
} else {
|
|
12744
|
+
const updateData = {
|
|
12745
|
+
...inviteData,
|
|
12746
|
+
invitedAt: now2
|
|
12747
|
+
};
|
|
12748
|
+
if (this.hasField("status") && !existingUser.status) {
|
|
12749
|
+
updateData.status = "pending";
|
|
12750
|
+
}
|
|
12751
|
+
targetUser = await db.update({
|
|
12241
12752
|
collection: this.collection.slug,
|
|
12242
12753
|
id: String(existingUser.id),
|
|
12243
|
-
data:
|
|
12754
|
+
data: updateData
|
|
12244
12755
|
});
|
|
12245
12756
|
}
|
|
12757
|
+
const siteId = c.get("siteId");
|
|
12246
12758
|
const inviteToken = await signCollectionToken(
|
|
12247
12759
|
{
|
|
12248
|
-
sub:
|
|
12249
|
-
email
|
|
12760
|
+
sub: email,
|
|
12761
|
+
email,
|
|
12250
12762
|
collection: this.collection.slug,
|
|
12251
|
-
purpose: "invite"
|
|
12763
|
+
purpose: "invite",
|
|
12764
|
+
...siteId ? { siteId } : {}
|
|
12252
12765
|
},
|
|
12253
|
-
|
|
12766
|
+
resolveInviteTokenExpiry(this.collection)
|
|
12254
12767
|
);
|
|
12255
|
-
const
|
|
12256
|
-
|
|
12257
|
-
|
|
12258
|
-
|
|
12259
|
-
|
|
12260
|
-
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
|
|
12768
|
+
const customInviteUrl = inviteUrl ?? (typeof this.collection.auth === "object" ? this.collection.auth.urls?.invite : void 0);
|
|
12769
|
+
const url = appendQueryParam(
|
|
12770
|
+
resolveActionUrl(c, config, customInviteUrl),
|
|
12771
|
+
"inviteToken",
|
|
12772
|
+
inviteToken
|
|
12773
|
+
);
|
|
12774
|
+
let emailSent = false;
|
|
12775
|
+
let emailError;
|
|
12776
|
+
if (shouldSendEmail !== false) {
|
|
12777
|
+
try {
|
|
12778
|
+
const emailResult = await resolveEmailTemplate({
|
|
12779
|
+
config,
|
|
12780
|
+
collection: this.collection,
|
|
12781
|
+
purpose: "invite",
|
|
12782
|
+
args: {
|
|
12783
|
+
token: inviteToken,
|
|
12784
|
+
invitedByEmail: requestUser.email,
|
|
12785
|
+
url,
|
|
12786
|
+
data: inviteData,
|
|
12787
|
+
user: targetUser
|
|
12788
|
+
},
|
|
12789
|
+
db
|
|
12790
|
+
});
|
|
12791
|
+
await sendEmail(
|
|
12792
|
+
config,
|
|
12793
|
+
toOutboundEmail(email, emailResult, {
|
|
12794
|
+
collection: this.collection.slug,
|
|
12795
|
+
purpose: "invite",
|
|
12796
|
+
user: targetUser
|
|
12797
|
+
})
|
|
12798
|
+
);
|
|
12799
|
+
emailSent = true;
|
|
12800
|
+
} catch (err) {
|
|
12801
|
+
emailSent = false;
|
|
12802
|
+
emailError = err?.message ?? "Email delivery failed";
|
|
12803
|
+
getRequestLogger(c, "auth").error({
|
|
12804
|
+
err,
|
|
12805
|
+
msg: "Failed to send invite email",
|
|
12806
|
+
email
|
|
12807
|
+
});
|
|
12808
|
+
}
|
|
12269
12809
|
}
|
|
12270
12810
|
return c.json({
|
|
12271
12811
|
success: true,
|
|
12272
|
-
message: `Invite sent to ${
|
|
12812
|
+
message: emailSent ? `Invite sent to ${email}.` : shouldSendEmail === false ? `Invite created for ${email}.` : `Invite created for ${email}, but email delivery failed.`,
|
|
12273
12813
|
token: inviteToken,
|
|
12274
|
-
inviteUrl: url
|
|
12814
|
+
inviteUrl: url,
|
|
12815
|
+
emailSent,
|
|
12816
|
+
...emailError ? { emailError } : {}
|
|
12275
12817
|
});
|
|
12276
12818
|
}
|
|
12277
12819
|
// ---------------------------------------------------------------------------
|
|
@@ -12305,6 +12847,13 @@ var AuthController = class {
|
|
|
12305
12847
|
400
|
|
12306
12848
|
);
|
|
12307
12849
|
}
|
|
12850
|
+
const currentSiteId = c.get("siteId");
|
|
12851
|
+
if (payload.siteId && currentSiteId && payload.siteId !== currentSiteId) {
|
|
12852
|
+
return c.json(
|
|
12853
|
+
{ error: true, code: "SITE_MISMATCH", message: "Token is not valid for this site." },
|
|
12854
|
+
403
|
|
12855
|
+
);
|
|
12856
|
+
}
|
|
12308
12857
|
const inviteeEmail = payload.sub;
|
|
12309
12858
|
const existing = await db.find({
|
|
12310
12859
|
collection: this.collection.slug,
|
|
@@ -12313,12 +12862,26 @@ var AuthController = class {
|
|
|
12313
12862
|
});
|
|
12314
12863
|
const existingUser = existing.docs[0];
|
|
12315
12864
|
const existingIsPending = existingUser?.status === "pending";
|
|
12316
|
-
if (existingUser && !existingIsPending) {
|
|
12865
|
+
if (existingUser && !existingIsPending && hasUsablePassword(existingUser.password)) {
|
|
12317
12866
|
return c.json(
|
|
12318
|
-
{
|
|
12867
|
+
{
|
|
12868
|
+
error: true,
|
|
12869
|
+
code: "INVITE_ALREADY_ACCEPTED",
|
|
12870
|
+
message: "This invitation has already been accepted. Please log in or use forgot password."
|
|
12871
|
+
},
|
|
12319
12872
|
409
|
|
12320
12873
|
);
|
|
12321
12874
|
}
|
|
12875
|
+
if (existingUser && payload.iat && typeof existingUser.invitedAt === "number" && payload.iat * 1e3 < existingUser.invitedAt - 2e3) {
|
|
12876
|
+
return c.json(
|
|
12877
|
+
{
|
|
12878
|
+
error: true,
|
|
12879
|
+
code: "INVITE_SUPERSEDED",
|
|
12880
|
+
message: "This invitation link has been superseded by a newer invitation. Please use the latest link."
|
|
12881
|
+
},
|
|
12882
|
+
400
|
|
12883
|
+
);
|
|
12884
|
+
}
|
|
12322
12885
|
const { token: _t, password: _p, ...extraFields } = body;
|
|
12323
12886
|
const hashedPassword = await hashPassword(body.password);
|
|
12324
12887
|
const user = existingUser ? await db.update({
|
|
@@ -12342,23 +12905,179 @@ var AuthController = class {
|
|
|
12342
12905
|
ip: c.get("clientIp"),
|
|
12343
12906
|
authSource: "local"
|
|
12344
12907
|
});
|
|
12345
|
-
const
|
|
12346
|
-
|
|
12347
|
-
|
|
12348
|
-
|
|
12908
|
+
const safeUser = this.sanitizeUser(user);
|
|
12909
|
+
resolveEmailTemplate({
|
|
12910
|
+
config,
|
|
12911
|
+
collection: this.collection,
|
|
12912
|
+
purpose: "welcome",
|
|
12913
|
+
args: {
|
|
12914
|
+
email: inviteeEmail,
|
|
12915
|
+
user: safeUser
|
|
12916
|
+
},
|
|
12917
|
+
db
|
|
12918
|
+
}).then(
|
|
12919
|
+
(emailResult) => sendEmail(
|
|
12920
|
+
config,
|
|
12921
|
+
toOutboundEmail(inviteeEmail, emailResult, {
|
|
12922
|
+
collection: this.collection.slug,
|
|
12923
|
+
purpose: "welcome",
|
|
12924
|
+
user: safeUser
|
|
12925
|
+
})
|
|
12926
|
+
)
|
|
12927
|
+
).catch(
|
|
12349
12928
|
(err) => getRequestLogger(c, "auth").error({
|
|
12350
12929
|
err,
|
|
12351
12930
|
msg: "Failed to send welcome email",
|
|
12352
12931
|
email: inviteeEmail
|
|
12353
12932
|
})
|
|
12354
12933
|
);
|
|
12355
|
-
const safeUser = this.sanitizeUser(user);
|
|
12356
12934
|
return c.json({ token: sessionToken, user: safeUser }, 201);
|
|
12357
12935
|
}
|
|
12936
|
+
// ---------------------------------------------------------------------------
|
|
12937
|
+
// GET /tokens/verify?token=...&purpose=invite|reset
|
|
12938
|
+
// Public. Validates token signature, expiration, database existence, and state.
|
|
12939
|
+
// ---------------------------------------------------------------------------
|
|
12940
|
+
async verifyToken(c) {
|
|
12941
|
+
const db = c.get("config").db;
|
|
12942
|
+
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
12943
|
+
const token = c.req.query("token");
|
|
12944
|
+
const purpose = c.req.query("purpose");
|
|
12945
|
+
if (!token) {
|
|
12946
|
+
return c.json(
|
|
12947
|
+
{ valid: false, code: "TOKEN_REQUIRED", message: "Token is required." },
|
|
12948
|
+
400
|
|
12949
|
+
);
|
|
12950
|
+
}
|
|
12951
|
+
let payload;
|
|
12952
|
+
try {
|
|
12953
|
+
payload = await verifyCollectionToken(token);
|
|
12954
|
+
} catch {
|
|
12955
|
+
return c.json(
|
|
12956
|
+
{
|
|
12957
|
+
valid: false,
|
|
12958
|
+
code: "TOKEN_EXPIRED_OR_INVALID",
|
|
12959
|
+
message: "Token is invalid or has expired."
|
|
12960
|
+
},
|
|
12961
|
+
200
|
|
12962
|
+
);
|
|
12963
|
+
}
|
|
12964
|
+
if (payload.collection !== this.collection.slug) {
|
|
12965
|
+
return c.json(
|
|
12966
|
+
{
|
|
12967
|
+
valid: false,
|
|
12968
|
+
code: "COLLECTION_MISMATCH",
|
|
12969
|
+
message: "Token does not belong to this collection."
|
|
12970
|
+
},
|
|
12971
|
+
200
|
|
12972
|
+
);
|
|
12973
|
+
}
|
|
12974
|
+
if (purpose && payload.purpose !== purpose) {
|
|
12975
|
+
return c.json(
|
|
12976
|
+
{
|
|
12977
|
+
valid: false,
|
|
12978
|
+
code: "PURPOSE_MISMATCH",
|
|
12979
|
+
message: `Token was issued for '${payload.purpose}', not '${purpose}'.`
|
|
12980
|
+
},
|
|
12981
|
+
200
|
|
12982
|
+
);
|
|
12983
|
+
}
|
|
12984
|
+
const currentSiteId = c.get("siteId");
|
|
12985
|
+
if (payload.siteId && currentSiteId && payload.siteId !== currentSiteId) {
|
|
12986
|
+
return c.json(
|
|
12987
|
+
{
|
|
12988
|
+
valid: false,
|
|
12989
|
+
code: "SITE_MISMATCH",
|
|
12990
|
+
message: "Token is not valid for this site."
|
|
12991
|
+
},
|
|
12992
|
+
200
|
|
12993
|
+
);
|
|
12994
|
+
}
|
|
12995
|
+
if (payload.purpose === "invite") {
|
|
12996
|
+
const result = await db.find({
|
|
12997
|
+
collection: this.collection.slug,
|
|
12998
|
+
where: { email: payload.email ?? payload.sub },
|
|
12999
|
+
limit: 1
|
|
13000
|
+
});
|
|
13001
|
+
const user = result.docs[0];
|
|
13002
|
+
if (!user) {
|
|
13003
|
+
return c.json(
|
|
13004
|
+
{
|
|
13005
|
+
valid: false,
|
|
13006
|
+
code: "USER_NOT_FOUND",
|
|
13007
|
+
message: "Invited account was not found."
|
|
13008
|
+
},
|
|
13009
|
+
200
|
|
13010
|
+
);
|
|
13011
|
+
}
|
|
13012
|
+
if (user._deleted || user.deletedAt) {
|
|
13013
|
+
return c.json(
|
|
13014
|
+
{
|
|
13015
|
+
valid: false,
|
|
13016
|
+
code: "USER_DELETED",
|
|
13017
|
+
message: "Account has been deleted."
|
|
13018
|
+
},
|
|
13019
|
+
200
|
|
13020
|
+
);
|
|
13021
|
+
}
|
|
13022
|
+
if (user.status !== "pending" && hasUsablePassword(user.password)) {
|
|
13023
|
+
return c.json(
|
|
13024
|
+
{
|
|
13025
|
+
valid: false,
|
|
13026
|
+
code: "INVITE_ALREADY_ACCEPTED",
|
|
13027
|
+
message: "This invitation has already been accepted."
|
|
13028
|
+
},
|
|
13029
|
+
200
|
|
13030
|
+
);
|
|
13031
|
+
}
|
|
13032
|
+
if (payload.iat && typeof user.invitedAt === "number" && payload.iat * 1e3 < user.invitedAt - 2e3) {
|
|
13033
|
+
return c.json(
|
|
13034
|
+
{
|
|
13035
|
+
valid: false,
|
|
13036
|
+
code: "INVITE_SUPERSEDED",
|
|
13037
|
+
message: "This invitation link has been superseded by a newer one."
|
|
13038
|
+
},
|
|
13039
|
+
200
|
|
13040
|
+
);
|
|
13041
|
+
}
|
|
13042
|
+
return c.json({
|
|
13043
|
+
valid: true,
|
|
13044
|
+
email: user.email,
|
|
13045
|
+
collection: this.collection.slug
|
|
13046
|
+
});
|
|
13047
|
+
}
|
|
13048
|
+
if (payload.purpose === "reset") {
|
|
13049
|
+
const result = await db.find({
|
|
13050
|
+
collection: this.collection.slug,
|
|
13051
|
+
where: { id: payload.sub },
|
|
13052
|
+
limit: 1
|
|
13053
|
+
});
|
|
13054
|
+
const user = result.docs[0];
|
|
13055
|
+
if (!user || user._deleted || user.deletedAt) {
|
|
13056
|
+
return c.json(
|
|
13057
|
+
{
|
|
13058
|
+
valid: false,
|
|
13059
|
+
code: "USER_NOT_FOUND",
|
|
13060
|
+
message: "Account was not found."
|
|
13061
|
+
},
|
|
13062
|
+
200
|
|
13063
|
+
);
|
|
13064
|
+
}
|
|
13065
|
+
return c.json({
|
|
13066
|
+
valid: true,
|
|
13067
|
+
email: user.email,
|
|
13068
|
+
collection: this.collection.slug
|
|
13069
|
+
});
|
|
13070
|
+
}
|
|
13071
|
+
return c.json({
|
|
13072
|
+
valid: true,
|
|
13073
|
+
email: payload.email,
|
|
13074
|
+
collection: this.collection.slug
|
|
13075
|
+
});
|
|
13076
|
+
}
|
|
12358
13077
|
};
|
|
12359
13078
|
|
|
12360
13079
|
// src/controllers/admin-auth.controller.ts
|
|
12361
|
-
var
|
|
13080
|
+
var import_node_crypto3 = require("crypto");
|
|
12362
13081
|
var import_node_util2 = require("util");
|
|
12363
13082
|
var import_jose2 = require("jose");
|
|
12364
13083
|
var AdminAuthController = class {
|
|
@@ -12568,7 +13287,7 @@ var AdminAuthController = class {
|
|
|
12568
13287
|
access.roles,
|
|
12569
13288
|
access.data
|
|
12570
13289
|
),
|
|
12571
|
-
password: await hashPassword((0,
|
|
13290
|
+
password: await hashPassword((0, import_node_crypto3.randomBytes)(32).toString("hex"))
|
|
12572
13291
|
}
|
|
12573
13292
|
});
|
|
12574
13293
|
} else if (user) {
|
|
@@ -12655,7 +13374,7 @@ var AdminAuthController = class {
|
|
|
12655
13374
|
}
|
|
12656
13375
|
async buildOidcStartUrl(provider, returnTo, siteId, origin) {
|
|
12657
13376
|
const discovery = await this.getOidcDiscovery(provider);
|
|
12658
|
-
const nonce = (0,
|
|
13377
|
+
const nonce = (0, import_node_crypto3.randomBytes)(16).toString("hex");
|
|
12659
13378
|
const state = await this.signState({
|
|
12660
13379
|
providerId: provider.id,
|
|
12661
13380
|
returnTo,
|
|
@@ -15478,6 +16197,31 @@ function generateOpenApi(config) {
|
|
|
15478
16197
|
spec.paths[`${path}/accept-invite`] = {
|
|
15479
16198
|
post: publicAuthPost("Accept an invitation")
|
|
15480
16199
|
};
|
|
16200
|
+
spec.paths[`${path}/tokens/verify`] = {
|
|
16201
|
+
get: {
|
|
16202
|
+
tags: [collectionTag],
|
|
16203
|
+
summary: `Verify an invite or password reset token`,
|
|
16204
|
+
parameters: [
|
|
16205
|
+
{
|
|
16206
|
+
name: "token",
|
|
16207
|
+
in: "query",
|
|
16208
|
+
required: true,
|
|
16209
|
+
schema: { type: "string" }
|
|
16210
|
+
},
|
|
16211
|
+
{
|
|
16212
|
+
name: "purpose",
|
|
16213
|
+
in: "query",
|
|
16214
|
+
required: false,
|
|
16215
|
+
schema: { type: "string", enum: ["invite", "reset"] }
|
|
16216
|
+
}
|
|
16217
|
+
],
|
|
16218
|
+
responses: {
|
|
16219
|
+
200: {
|
|
16220
|
+
description: "Token verification result"
|
|
16221
|
+
}
|
|
16222
|
+
}
|
|
16223
|
+
}
|
|
16224
|
+
};
|
|
15481
16225
|
spec.paths[`${path}/{id}/change-password`] = {
|
|
15482
16226
|
post: {
|
|
15483
16227
|
tags: [collectionTag],
|
|
@@ -16088,7 +16832,7 @@ function registerRoutes(app, config) {
|
|
|
16088
16832
|
return resolveBooleanAccess(requestConfig, access, accessArgs);
|
|
16089
16833
|
};
|
|
16090
16834
|
const filteredCollections = await Promise.all(
|
|
16091
|
-
collections.filter((col) => !col.slug
|
|
16835
|
+
collections.filter((col) => !INTERNAL_SYSTEM_COLLECTIONS.has(col.slug) && (!siteId || col.shared || !col.siteId || col.siteId === siteId)).map(async (col) => ({
|
|
16092
16836
|
slug: col.slug,
|
|
16093
16837
|
labels: col.labels,
|
|
16094
16838
|
shared: !!col.shared,
|
|
@@ -16230,7 +16974,7 @@ function registerRoutes(app, config) {
|
|
|
16230
16974
|
model: effectiveAi?.model
|
|
16231
16975
|
},
|
|
16232
16976
|
trash: {
|
|
16233
|
-
enabled: collections.some((col) => !col.slug
|
|
16977
|
+
enabled: collections.some((col) => !INTERNAL_SYSTEM_COLLECTIONS.has(col.slug) && resolveTrashConfig(col, requestConfig).enabled),
|
|
16234
16978
|
retentionDays: requestConfig.trash?.retentionDays !== void 0 ? requestConfig.trash.retentionDays : 30,
|
|
16235
16979
|
allowPermanentDelete: requestConfig.trash?.allowPermanentDelete !== false
|
|
16236
16980
|
},
|
|
@@ -16241,7 +16985,7 @@ function registerRoutes(app, config) {
|
|
|
16241
16985
|
uploadCollectionConfigured: requestConfig.collections.some((collection) => !!collection.upload),
|
|
16242
16986
|
trashPurgeOverdue: await (async () => {
|
|
16243
16987
|
const hasRetention = collections.some(
|
|
16244
|
-
(col) => !col.slug
|
|
16988
|
+
(col) => !INTERNAL_SYSTEM_COLLECTIONS.has(col.slug) && resolveTrashConfig(col, requestConfig).retentionDays !== null
|
|
16245
16989
|
);
|
|
16246
16990
|
if (!hasRetention || !requestConfig.db) return false;
|
|
16247
16991
|
try {
|
|
@@ -16879,6 +17623,7 @@ function registerRoutes(app, config) {
|
|
|
16879
17623
|
app.post(`${path}/reset-password`, (c) => authController.resetPassword(c));
|
|
16880
17624
|
app.post(`${path}/invite`, requireAuth(config), (c) => authController.invite(c));
|
|
16881
17625
|
app.post(`${path}/accept-invite`, (c) => authController.acceptInvite(c));
|
|
17626
|
+
app.get(`${path}/tokens/verify`, (c) => authController.verifyToken(c));
|
|
16882
17627
|
}
|
|
16883
17628
|
const auditController = new AuditController();
|
|
16884
17629
|
for (const collection of config.collections) {
|
|
@@ -16923,6 +17668,126 @@ function registerRoutes(app, config) {
|
|
|
16923
17668
|
app.get(path, (c) => controller.get(c));
|
|
16924
17669
|
app.patch(path, (c) => controller.update(c));
|
|
16925
17670
|
}
|
|
17671
|
+
const testEmailRateLimitMap = /* @__PURE__ */ new Map();
|
|
17672
|
+
const TEST_EMAIL_MAX = 5;
|
|
17673
|
+
const TEST_EMAIL_WINDOW_MS = 6e4;
|
|
17674
|
+
const handleTestEmailDispatch = async (c) => {
|
|
17675
|
+
const user = c.get("user");
|
|
17676
|
+
const adminCol = config.collections.find((col) => col.slug === "__admins") || config.collections.find((col) => col.auth);
|
|
17677
|
+
if (!isUserAdmin(user, adminCol)) {
|
|
17678
|
+
return c.json({ error: true, message: "Forbidden: Admin access required to dispatch test emails" }, 403);
|
|
17679
|
+
}
|
|
17680
|
+
const userId = String(user?.id ?? user?.sub ?? "unknown");
|
|
17681
|
+
const now2 = Date.now();
|
|
17682
|
+
const window2 = (testEmailRateLimitMap.get(userId) ?? []).filter(
|
|
17683
|
+
(ts) => now2 - ts < TEST_EMAIL_WINDOW_MS
|
|
17684
|
+
);
|
|
17685
|
+
if (window2.length >= TEST_EMAIL_MAX) {
|
|
17686
|
+
return c.json(
|
|
17687
|
+
{ error: true, message: `Rate limit exceeded: maximum ${TEST_EMAIL_MAX} test emails per minute.` },
|
|
17688
|
+
429
|
|
17689
|
+
);
|
|
17690
|
+
}
|
|
17691
|
+
window2.push(now2);
|
|
17692
|
+
testEmailRateLimitMap.set(userId, window2);
|
|
17693
|
+
const body = await c.req.json().catch(() => ({}));
|
|
17694
|
+
const {
|
|
17695
|
+
to,
|
|
17696
|
+
subject,
|
|
17697
|
+
html,
|
|
17698
|
+
format = "html",
|
|
17699
|
+
externalTemplateId,
|
|
17700
|
+
purpose = "invite",
|
|
17701
|
+
collectionSlug = "users"
|
|
17702
|
+
} = body;
|
|
17703
|
+
if (!to || typeof to !== "string" || !to.includes("@")) {
|
|
17704
|
+
return c.json({ error: true, message: "A valid recipient email address is required" }, 400);
|
|
17705
|
+
}
|
|
17706
|
+
const siteName = config.admin?.branding?.logoText || "Dyrected";
|
|
17707
|
+
let appUrl = config.admin?.siteUrl;
|
|
17708
|
+
if (!appUrl) {
|
|
17709
|
+
try {
|
|
17710
|
+
appUrl = new URL(c.req.url).origin;
|
|
17711
|
+
} catch {
|
|
17712
|
+
appUrl = "http://localhost:3000";
|
|
17713
|
+
}
|
|
17714
|
+
}
|
|
17715
|
+
const targetCol = config.collections.find((x) => x.slug === collectionSlug);
|
|
17716
|
+
const resolvedCollectionLabel = collectionSlug === "*" ? "Account" : targetCol?.labels?.singular || targetCol?.labels?.plural || (collectionSlug.startsWith("__") ? collectionSlug.slice(2).charAt(0).toUpperCase() + collectionSlug.slice(3) : collectionSlug.charAt(0).toUpperCase() + collectionSlug.slice(1));
|
|
17717
|
+
const mockData = {
|
|
17718
|
+
"{{url}}": `${appUrl}/auth/setup-password?token=mock_test_token_${Date.now()}`,
|
|
17719
|
+
"{{token}}": `mock_test_token_${Date.now()}`,
|
|
17720
|
+
"{{email}}": to,
|
|
17721
|
+
"{{collectionLabel}}": resolvedCollectionLabel,
|
|
17722
|
+
"{{siteName}}": siteName,
|
|
17723
|
+
"{{user.name}}": typeof user?.name === "string" ? user.name : "Test Recipient",
|
|
17724
|
+
"{{user.first_name}}": typeof user?.first_name === "string" ? user.first_name : "Test",
|
|
17725
|
+
"{{user.email}}": to
|
|
17726
|
+
};
|
|
17727
|
+
try {
|
|
17728
|
+
let result;
|
|
17729
|
+
if (format === "external_template") {
|
|
17730
|
+
if (!externalTemplateId) {
|
|
17731
|
+
return c.json({ error: true, message: "External template ID is required when format is external_template" }, 400);
|
|
17732
|
+
}
|
|
17733
|
+
const variables = {
|
|
17734
|
+
email: to,
|
|
17735
|
+
url: `${appUrl}/auth/setup-password?token=mock_test_token_${Date.now()}`,
|
|
17736
|
+
token: `mock_test_token_${Date.now()}`,
|
|
17737
|
+
collection: collectionSlug,
|
|
17738
|
+
collectionLabel: resolvedCollectionLabel,
|
|
17739
|
+
siteName,
|
|
17740
|
+
user: {
|
|
17741
|
+
name: typeof user?.name === "string" ? user.name : "Test Recipient",
|
|
17742
|
+
first_name: typeof user?.first_name === "string" ? user.first_name : "Test",
|
|
17743
|
+
email: to
|
|
17744
|
+
}
|
|
17745
|
+
};
|
|
17746
|
+
let testSubject = subject || `[TEST] ${purpose} email`;
|
|
17747
|
+
for (const [tag, sample] of Object.entries(mockData)) {
|
|
17748
|
+
testSubject = testSubject.split(tag).join(sample);
|
|
17749
|
+
}
|
|
17750
|
+
result = await sendEmail(config, {
|
|
17751
|
+
to,
|
|
17752
|
+
type: "template",
|
|
17753
|
+
template: externalTemplateId,
|
|
17754
|
+
variables,
|
|
17755
|
+
subject: testSubject,
|
|
17756
|
+
purpose,
|
|
17757
|
+
collection: collectionSlug
|
|
17758
|
+
});
|
|
17759
|
+
} else {
|
|
17760
|
+
let finalSubject = subject || `[TEST] ${purpose} email`;
|
|
17761
|
+
let finalHtml = html || "";
|
|
17762
|
+
for (const [tag, sample] of Object.entries(mockData)) {
|
|
17763
|
+
finalSubject = finalSubject.split(tag).join(sample);
|
|
17764
|
+
finalHtml = finalHtml.split(tag).join(sample);
|
|
17765
|
+
}
|
|
17766
|
+
result = await sendEmail(config, {
|
|
17767
|
+
to,
|
|
17768
|
+
type: "html",
|
|
17769
|
+
subject: finalSubject,
|
|
17770
|
+
html: finalHtml,
|
|
17771
|
+
purpose,
|
|
17772
|
+
collection: collectionSlug
|
|
17773
|
+
});
|
|
17774
|
+
}
|
|
17775
|
+
return c.json({
|
|
17776
|
+
success: true,
|
|
17777
|
+
message: `Test email sent to ${to}`,
|
|
17778
|
+
previewUrl: result?.previewUrl
|
|
17779
|
+
});
|
|
17780
|
+
} catch (err) {
|
|
17781
|
+
getConfigLogger(config, "email").error({
|
|
17782
|
+
err,
|
|
17783
|
+
msg: "Failed to dispatch test email",
|
|
17784
|
+
to
|
|
17785
|
+
});
|
|
17786
|
+
return c.json({ error: true, message: err.message || "Failed to send test email" }, 500);
|
|
17787
|
+
}
|
|
17788
|
+
};
|
|
17789
|
+
app.post("/api/__email_templates/test", requireAuth(config), handleTestEmailDispatch);
|
|
17790
|
+
app.post("/api/email-templates/test", requireAuth(config), handleTestEmailDispatch);
|
|
16926
17791
|
if (!process.env.DYRECTED_JWT_SECRET) {
|
|
16927
17792
|
getConfigLogger(config, "router").warn({
|
|
16928
17793
|
msg: "DYRECTED_JWT_SECRET is not set; token-mode live preview is signing with an insecure default"
|
|
@@ -17377,6 +18242,114 @@ var AUTH_SESSIONS_COLLECTION_CONFIG = {
|
|
|
17377
18242
|
},
|
|
17378
18243
|
admin: { hidden: true }
|
|
17379
18244
|
};
|
|
18245
|
+
var EMAIL_TEMPLATES_COLLECTION_SLUG = "__email_templates";
|
|
18246
|
+
var EMAIL_TEMPLATES_COLLECTION_CONFIG = {
|
|
18247
|
+
slug: EMAIL_TEMPLATES_COLLECTION_SLUG,
|
|
18248
|
+
labels: { singular: "Email Template", plural: "Email Templates" },
|
|
18249
|
+
detail: false,
|
|
18250
|
+
fields: [
|
|
18251
|
+
{
|
|
18252
|
+
name: "collectionSlug",
|
|
18253
|
+
type: "select",
|
|
18254
|
+
label: "Applies To",
|
|
18255
|
+
required: true,
|
|
18256
|
+
defaultValue: "*",
|
|
18257
|
+
options: [
|
|
18258
|
+
{ label: "Global (All Users & Collections)", value: "*" }
|
|
18259
|
+
],
|
|
18260
|
+
admin: {
|
|
18261
|
+
description: "Choose which user collection this template applies to, or 'Global' for all users."
|
|
18262
|
+
}
|
|
18263
|
+
},
|
|
18264
|
+
{
|
|
18265
|
+
name: "purpose",
|
|
18266
|
+
type: "select",
|
|
18267
|
+
label: "Purpose",
|
|
18268
|
+
required: true,
|
|
18269
|
+
options: [
|
|
18270
|
+
{ label: "Invite", value: "invite" },
|
|
18271
|
+
{ label: "Password Reset", value: "resetPassword" },
|
|
18272
|
+
{ label: "Welcome", value: "welcome" },
|
|
18273
|
+
{ label: "Password Changed", value: "passwordChanged" }
|
|
18274
|
+
],
|
|
18275
|
+
admin: { width: "50%" }
|
|
18276
|
+
},
|
|
18277
|
+
{
|
|
18278
|
+
name: "format",
|
|
18279
|
+
type: "select",
|
|
18280
|
+
label: "Format",
|
|
18281
|
+
required: true,
|
|
18282
|
+
defaultValue: "html",
|
|
18283
|
+
options: [
|
|
18284
|
+
{ label: "HTML", value: "html" },
|
|
18285
|
+
{ label: "External Template", value: "external_template" }
|
|
18286
|
+
],
|
|
18287
|
+
admin: { width: "50%" }
|
|
18288
|
+
},
|
|
18289
|
+
{
|
|
18290
|
+
name: "subject",
|
|
18291
|
+
type: "text",
|
|
18292
|
+
label: "Subject",
|
|
18293
|
+
admin: {
|
|
18294
|
+
description: "Subject line (optional when using external provider template)"
|
|
18295
|
+
}
|
|
18296
|
+
},
|
|
18297
|
+
{
|
|
18298
|
+
name: "rawHtml",
|
|
18299
|
+
type: "textarea",
|
|
18300
|
+
label: "HTML Content",
|
|
18301
|
+
admin: {
|
|
18302
|
+
description: "Raw HTML email content with {{variable}} tags"
|
|
18303
|
+
}
|
|
18304
|
+
},
|
|
18305
|
+
{
|
|
18306
|
+
name: "externalTemplateId",
|
|
18307
|
+
type: "text",
|
|
18308
|
+
label: "External Template ID",
|
|
18309
|
+
admin: {
|
|
18310
|
+
description: "Template ID or alias from Seamailer, Postmark, SendGrid, etc."
|
|
18311
|
+
}
|
|
18312
|
+
},
|
|
18313
|
+
{
|
|
18314
|
+
name: "active",
|
|
18315
|
+
type: "boolean",
|
|
18316
|
+
label: "Active",
|
|
18317
|
+
defaultValue: true,
|
|
18318
|
+
admin: {
|
|
18319
|
+
description: "When active, this template overrides code config. When disabled, falls back to code."
|
|
18320
|
+
}
|
|
18321
|
+
}
|
|
18322
|
+
],
|
|
18323
|
+
admin: {
|
|
18324
|
+
group: "Settings",
|
|
18325
|
+
icon: "Mail",
|
|
18326
|
+
groupBy: "collectionSlug",
|
|
18327
|
+
defaultColumns: ["purpose", "collectionSlug", "format", "active"]
|
|
18328
|
+
},
|
|
18329
|
+
hooks: {
|
|
18330
|
+
beforeChange: [
|
|
18331
|
+
async ({ data, req }) => {
|
|
18332
|
+
if (data && data.format === "html") {
|
|
18333
|
+
const siteName = req?.config?.admin?.branding?.logoText || "Dyrected";
|
|
18334
|
+
const defaults = getDefaultEmailTemplate(data.purpose || "invite", { siteName });
|
|
18335
|
+
if (!data.subject) {
|
|
18336
|
+
data.subject = defaults.subject;
|
|
18337
|
+
}
|
|
18338
|
+
if (!data.rawHtml) {
|
|
18339
|
+
data.rawHtml = defaults.rawHtml;
|
|
18340
|
+
}
|
|
18341
|
+
}
|
|
18342
|
+
return data;
|
|
18343
|
+
}
|
|
18344
|
+
]
|
|
18345
|
+
},
|
|
18346
|
+
access: {
|
|
18347
|
+
read: ({ user }) => isUserAdmin(user),
|
|
18348
|
+
create: ({ user }) => isUserAdmin(user),
|
|
18349
|
+
update: ({ user }) => isUserAdmin(user),
|
|
18350
|
+
delete: ({ user }) => isUserAdmin(user)
|
|
18351
|
+
}
|
|
18352
|
+
};
|
|
17380
18353
|
function normalizeConfig(config) {
|
|
17381
18354
|
const schemaAwareConfig = normalizeSchemaFragment(config);
|
|
17382
18355
|
assertValidDeclarativeHooksInConfig(schemaAwareConfig, "config");
|
|
@@ -17391,10 +18364,13 @@ function normalizeConfig(config) {
|
|
|
17391
18364
|
const needsTasks = (config.tasks?.length ?? 0) > 0;
|
|
17392
18365
|
const needsAuthSessions = collections.some((col) => !!col.auth);
|
|
17393
18366
|
const needsTrash = schemaAwareConfig.trash?.enabled === true || collections.some((col) => resolveTrashConfig(col, schemaAwareConfig).enabled);
|
|
17394
|
-
const
|
|
18367
|
+
const adminAuthCollection = getAdminAuthCollection({
|
|
17395
18368
|
collections,
|
|
17396
18369
|
adminAuth: schemaAwareConfig.adminAuth
|
|
17397
|
-
})
|
|
18370
|
+
});
|
|
18371
|
+
const adminAuthCollectionSlug = adminAuthCollection?.slug;
|
|
18372
|
+
const adminOnly = ({ user }) => isUserAdmin(user, adminAuthCollection);
|
|
18373
|
+
const adminOtherThanSelf = ({ user, id }) => isUserAdmin(user, adminAuthCollection) && user?.id !== id;
|
|
17398
18374
|
const normalizedCollections = collections.map((col) => {
|
|
17399
18375
|
let fields = col.fields || [];
|
|
17400
18376
|
const existingFieldNames = new Set(fields.map((f) => f.name));
|
|
@@ -17443,7 +18419,7 @@ function normalizeConfig(config) {
|
|
|
17443
18419
|
{ value: "viewer", label: "Viewer" }
|
|
17444
18420
|
],
|
|
17445
18421
|
access: {
|
|
17446
|
-
update:
|
|
18422
|
+
update: adminOnly
|
|
17447
18423
|
}
|
|
17448
18424
|
}
|
|
17449
18425
|
];
|
|
@@ -17461,7 +18437,7 @@ function normalizeConfig(config) {
|
|
|
17461
18437
|
{ value: "pending", label: "Pending" }
|
|
17462
18438
|
],
|
|
17463
18439
|
access: {
|
|
17464
|
-
update:
|
|
18440
|
+
update: adminOtherThanSelf
|
|
17465
18441
|
},
|
|
17466
18442
|
admin: {
|
|
17467
18443
|
condition: `!(data.roles && "admin" in data.roles) && data.role != "admin"`
|
|
@@ -17530,7 +18506,7 @@ function normalizeConfig(config) {
|
|
|
17530
18506
|
access: {
|
|
17531
18507
|
...field.access || {},
|
|
17532
18508
|
// Must be an admin; cannot edit own roles (no self-elevation).
|
|
17533
|
-
update:
|
|
18509
|
+
update: adminOtherThanSelf
|
|
17534
18510
|
}
|
|
17535
18511
|
};
|
|
17536
18512
|
}
|
|
@@ -17539,7 +18515,7 @@ function normalizeConfig(config) {
|
|
|
17539
18515
|
...field,
|
|
17540
18516
|
access: {
|
|
17541
18517
|
...field.access || {},
|
|
17542
|
-
update:
|
|
18518
|
+
update: adminOtherThanSelf
|
|
17543
18519
|
}
|
|
17544
18520
|
};
|
|
17545
18521
|
}
|
|
@@ -17631,6 +18607,52 @@ function normalizeConfig(config) {
|
|
|
17631
18607
|
if (needsTrash && !normalizedCollections.some((col) => col.slug === TRASH_COLLECTION)) {
|
|
17632
18608
|
systemCollections.push(TRASH_COLLECTION_CONFIG);
|
|
17633
18609
|
}
|
|
18610
|
+
const needsEmailTemplates = schemaAwareConfig.email?.adminEditable === true;
|
|
18611
|
+
if (needsEmailTemplates && !normalizedCollections.some((col) => col.slug === EMAIL_TEMPLATES_COLLECTION_SLUG)) {
|
|
18612
|
+
const adminCol = getAdminAuthCollection(schemaAwareConfig);
|
|
18613
|
+
const customAccess = schemaAwareConfig.email?.access;
|
|
18614
|
+
const resolveOpAccess = (op) => {
|
|
18615
|
+
if (typeof customAccess === "function") return customAccess;
|
|
18616
|
+
if (customAccess && typeof customAccess === "object" && customAccess[op]) return customAccess[op];
|
|
18617
|
+
return ({ user }) => isUserAdmin(user, adminCol);
|
|
18618
|
+
};
|
|
18619
|
+
const customPurposes = schemaAwareConfig.email?.purposes ?? [];
|
|
18620
|
+
const purposeOptions = [
|
|
18621
|
+
{ label: "Invite", value: "invite" },
|
|
18622
|
+
{ label: "Password Reset", value: "resetPassword" },
|
|
18623
|
+
{ label: "Welcome", value: "welcome" },
|
|
18624
|
+
{ label: "Password Changed", value: "passwordChanged" },
|
|
18625
|
+
...customPurposes
|
|
18626
|
+
];
|
|
18627
|
+
const targetableCollections = normalizedCollections.filter(
|
|
18628
|
+
(c) => !c.slug.startsWith("__")
|
|
18629
|
+
);
|
|
18630
|
+
const audienceOptions = [
|
|
18631
|
+
{ label: "Global (All Collections)", value: "*" },
|
|
18632
|
+
...targetableCollections.map((c) => ({
|
|
18633
|
+
label: `${c.labels?.singular || c.labels?.plural || c.slug} (${c.slug})`,
|
|
18634
|
+
value: c.slug
|
|
18635
|
+
}))
|
|
18636
|
+
];
|
|
18637
|
+
systemCollections.push({
|
|
18638
|
+
...EMAIL_TEMPLATES_COLLECTION_CONFIG,
|
|
18639
|
+
fields: EMAIL_TEMPLATES_COLLECTION_CONFIG.fields.map((f) => {
|
|
18640
|
+
if (f.name === "collectionSlug") {
|
|
18641
|
+
return { ...f, options: audienceOptions };
|
|
18642
|
+
}
|
|
18643
|
+
if (f.name === "purpose") {
|
|
18644
|
+
return { ...f, options: purposeOptions };
|
|
18645
|
+
}
|
|
18646
|
+
return f;
|
|
18647
|
+
}),
|
|
18648
|
+
access: {
|
|
18649
|
+
read: resolveOpAccess("read"),
|
|
18650
|
+
create: resolveOpAccess("create"),
|
|
18651
|
+
update: resolveOpAccess("update"),
|
|
18652
|
+
delete: resolveOpAccess("delete")
|
|
18653
|
+
}
|
|
18654
|
+
});
|
|
18655
|
+
}
|
|
17634
18656
|
return {
|
|
17635
18657
|
...schemaAwareConfig,
|
|
17636
18658
|
collections: [...normalizedCollections, ...systemCollections],
|
|
@@ -17945,8 +18967,10 @@ init_embedding_service();
|
|
|
17945
18967
|
AUTH_SESSIONS_COLLECTION,
|
|
17946
18968
|
AuthController,
|
|
17947
18969
|
CollectionController,
|
|
18970
|
+
DEFAULT_INVITE_EXPIRY,
|
|
17948
18971
|
DEFAULT_LOCK_TIME_MS,
|
|
17949
18972
|
DEFAULT_MAX_LOGIN_ATTEMPTS,
|
|
18973
|
+
DEFAULT_RESET_PASSWORD_EXPIRY,
|
|
17950
18974
|
DEFAULT_SESSION_EXPIRY,
|
|
17951
18975
|
EmbeddingService,
|
|
17952
18976
|
GlobalController,
|
|
@@ -17978,13 +19002,16 @@ init_embedding_service();
|
|
|
17978
19002
|
getAIModel,
|
|
17979
19003
|
getAuthSession,
|
|
17980
19004
|
getConfigLogger,
|
|
19005
|
+
getDbEmailTemplateOverride,
|
|
17981
19006
|
getEmbeddingModel,
|
|
17982
19007
|
getLockedUntilMs,
|
|
17983
19008
|
getObservabilityRuntime,
|
|
17984
19009
|
getRequestLogger,
|
|
17985
19010
|
hasEmbeddingApiKey,
|
|
19011
|
+
hasUsablePassword,
|
|
17986
19012
|
hashContent,
|
|
17987
19013
|
hashPassword,
|
|
19014
|
+
interpolateVariables,
|
|
17988
19015
|
isAuthSessionActive,
|
|
17989
19016
|
issueAuthSessionToken,
|
|
17990
19017
|
normalizeFieldValue,
|
|
@@ -17993,7 +19020,10 @@ init_embedding_service();
|
|
|
17993
19020
|
registerRoutes,
|
|
17994
19021
|
renderPrometheusMetrics,
|
|
17995
19022
|
resolveAuthLockoutConfig,
|
|
19023
|
+
resolveEmailTemplate,
|
|
19024
|
+
resolveInviteTokenExpiry,
|
|
17996
19025
|
resolveObservabilityConfig,
|
|
19026
|
+
resolveResetPasswordTokenExpiry,
|
|
17997
19027
|
resolveSessionTokenExpiry,
|
|
17998
19028
|
revokeAllAuthSessions,
|
|
17999
19029
|
revokeAuthSession,
|
|
@@ -18003,6 +19033,7 @@ init_embedding_service();
|
|
|
18003
19033
|
serializeViewForApi,
|
|
18004
19034
|
shouldSampleRequest,
|
|
18005
19035
|
signCollectionToken,
|
|
19036
|
+
toOutboundEmail,
|
|
18006
19037
|
touchAuthSession,
|
|
18007
19038
|
validatePayloadAgainstFields,
|
|
18008
19039
|
verifyCollectionToken,
|