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