@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.js
CHANGED
|
@@ -3061,7 +3061,7 @@ function assertValidTrashInConfig(config, source = "config") {
|
|
|
3061
3061
|
if (col.slug === "trash") {
|
|
3062
3062
|
throw new Error(`Collection slug "trash" is reserved by Dyrected for the trash system.`);
|
|
3063
3063
|
}
|
|
3064
|
-
const isSystem = col.slug
|
|
3064
|
+
const isSystem = INTERNAL_SYSTEM_COLLECTIONS.has(col.slug);
|
|
3065
3065
|
if (isSystem && col.trash) {
|
|
3066
3066
|
throw new Error(`System collection "${col.slug}" cannot enable trash.`);
|
|
3067
3067
|
}
|
|
@@ -3168,12 +3168,20 @@ async function restoreDocument(options) {
|
|
|
3168
3168
|
}
|
|
3169
3169
|
return { restoredDoc: docToRestore };
|
|
3170
3170
|
}
|
|
3171
|
-
var TRASH_COLLECTION, TRASH_COLLECTION_CONFIG;
|
|
3171
|
+
var TRASH_COLLECTION, INTERNAL_SYSTEM_COLLECTIONS, TRASH_COLLECTION_CONFIG;
|
|
3172
3172
|
var init_trash = __esm({
|
|
3173
3173
|
"src/trash.ts"() {
|
|
3174
3174
|
"use strict";
|
|
3175
3175
|
init_hooks();
|
|
3176
3176
|
TRASH_COLLECTION = "__trash";
|
|
3177
|
+
INTERNAL_SYSTEM_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
3178
|
+
"__audit",
|
|
3179
|
+
"__sessions",
|
|
3180
|
+
"__task_locks",
|
|
3181
|
+
"__trash",
|
|
3182
|
+
"__workflow_history",
|
|
3183
|
+
"__lifecycle_events"
|
|
3184
|
+
]);
|
|
3177
3185
|
TRASH_COLLECTION_CONFIG = {
|
|
3178
3186
|
slug: TRASH_COLLECTION,
|
|
3179
3187
|
labels: { singular: "Trash entry", plural: "Trash entries" },
|
|
@@ -4297,12 +4305,17 @@ function sanitizeWhereClause(where, fields) {
|
|
|
4297
4305
|
const result = {};
|
|
4298
4306
|
for (const [key, value] of Object.entries(node)) {
|
|
4299
4307
|
const upperKey = key.toUpperCase();
|
|
4300
|
-
if (upperKey === "AND" || upperKey === "OR") {
|
|
4308
|
+
if (upperKey === "AND" || upperKey === "OR" || upperKey === "NOT") {
|
|
4301
4309
|
if (Array.isArray(value)) {
|
|
4302
4310
|
const processed = value.map((v) => walk(v)).filter((v) => Object.keys(v).length > 0);
|
|
4303
4311
|
if (processed.length > 0) {
|
|
4304
4312
|
result[upperKey] = processed;
|
|
4305
4313
|
}
|
|
4314
|
+
} else if (typeof value === "object" && value !== null) {
|
|
4315
|
+
const processed = walk(value);
|
|
4316
|
+
if (Object.keys(processed).length > 0) {
|
|
4317
|
+
result[upperKey] = processed;
|
|
4318
|
+
}
|
|
4306
4319
|
}
|
|
4307
4320
|
continue;
|
|
4308
4321
|
}
|
|
@@ -6236,13 +6249,27 @@ async function hashPassword(plain) {
|
|
|
6236
6249
|
const derivedKey = await scryptAsync(plain, salt, KEY_LEN);
|
|
6237
6250
|
return `${salt}:${derivedKey.toString("hex")}`;
|
|
6238
6251
|
}
|
|
6252
|
+
function hasUsablePassword(stored) {
|
|
6253
|
+
if (typeof stored !== "string" || !stored) return false;
|
|
6254
|
+
const parts = stored.split(":");
|
|
6255
|
+
return parts.length === 2 && Boolean(parts[0]) && Boolean(parts[1]);
|
|
6256
|
+
}
|
|
6239
6257
|
async function verifyPassword(plain, stored) {
|
|
6240
|
-
|
|
6258
|
+
if (typeof plain !== "string" || !plain || typeof stored !== "string" || !stored) {
|
|
6259
|
+
return false;
|
|
6260
|
+
}
|
|
6261
|
+
const parts = stored.split(":");
|
|
6262
|
+
if (parts.length !== 2) return false;
|
|
6263
|
+
const [salt, storedHash] = parts;
|
|
6241
6264
|
if (!salt || !storedHash) return false;
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6265
|
+
try {
|
|
6266
|
+
const derivedKey = await scryptAsync(plain, salt, KEY_LEN);
|
|
6267
|
+
const storedBuffer = Buffer.from(storedHash, "hex");
|
|
6268
|
+
if (derivedKey.length !== storedBuffer.length) return false;
|
|
6269
|
+
return timingSafeEqual(derivedKey, storedBuffer);
|
|
6270
|
+
} catch {
|
|
6271
|
+
return false;
|
|
6272
|
+
}
|
|
6246
6273
|
}
|
|
6247
6274
|
|
|
6248
6275
|
// src/auth/token.ts
|
|
@@ -6258,6 +6285,8 @@ function getSecret() {
|
|
|
6258
6285
|
}
|
|
6259
6286
|
var DEFAULT_EXPIRY = "7d";
|
|
6260
6287
|
var DEFAULT_SESSION_EXPIRY = DEFAULT_EXPIRY;
|
|
6288
|
+
var DEFAULT_INVITE_EXPIRY = "48h";
|
|
6289
|
+
var DEFAULT_RESET_PASSWORD_EXPIRY = "2h";
|
|
6261
6290
|
function resolveSessionTokenExpiry(collection) {
|
|
6262
6291
|
const auth = collection.auth;
|
|
6263
6292
|
if (auth && typeof auth === "object" && typeof auth.tokenExpiration === "string" && auth.tokenExpiration.length > 0) {
|
|
@@ -6265,6 +6294,20 @@ function resolveSessionTokenExpiry(collection) {
|
|
|
6265
6294
|
}
|
|
6266
6295
|
return DEFAULT_SESSION_EXPIRY;
|
|
6267
6296
|
}
|
|
6297
|
+
function resolveInviteTokenExpiry(collection) {
|
|
6298
|
+
const auth = collection.auth;
|
|
6299
|
+
if (auth && typeof auth === "object" && typeof auth.inviteExpiration === "string" && auth.inviteExpiration.length > 0) {
|
|
6300
|
+
return auth.inviteExpiration;
|
|
6301
|
+
}
|
|
6302
|
+
return DEFAULT_INVITE_EXPIRY;
|
|
6303
|
+
}
|
|
6304
|
+
function resolveResetPasswordTokenExpiry(collection) {
|
|
6305
|
+
const auth = collection.auth;
|
|
6306
|
+
if (auth && typeof auth === "object" && typeof auth.resetPasswordExpiration === "string" && auth.resetPasswordExpiration.length > 0) {
|
|
6307
|
+
return auth.resetPasswordExpiration;
|
|
6308
|
+
}
|
|
6309
|
+
return DEFAULT_RESET_PASSWORD_EXPIRY;
|
|
6310
|
+
}
|
|
6268
6311
|
async function signCollectionToken(payload, expiresIn = DEFAULT_EXPIRY) {
|
|
6269
6312
|
return new SignJWT({ ...payload }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(expiresIn).sign(getSecret());
|
|
6270
6313
|
}
|
|
@@ -6569,13 +6612,34 @@ function getAdminRoleForCollection(collection) {
|
|
|
6569
6612
|
}
|
|
6570
6613
|
return "admin";
|
|
6571
6614
|
}
|
|
6615
|
+
var STANDARD_ADMIN_ROLES = /* @__PURE__ */ new Set([
|
|
6616
|
+
"admin",
|
|
6617
|
+
"super_admin",
|
|
6618
|
+
"superadmin",
|
|
6619
|
+
"super admin",
|
|
6620
|
+
"super-admin",
|
|
6621
|
+
"owner"
|
|
6622
|
+
]);
|
|
6572
6623
|
function isUserAdmin(user, collection) {
|
|
6573
6624
|
if (!user) return false;
|
|
6574
|
-
const
|
|
6575
|
-
if (
|
|
6625
|
+
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] : [];
|
|
6626
|
+
if (rawRoles.length === 0) return false;
|
|
6627
|
+
const roles = rawRoles.filter((r) => typeof r === "string");
|
|
6576
6628
|
const configuredRole = getAdminRoleForCollection(collection);
|
|
6577
|
-
|
|
6578
|
-
|
|
6629
|
+
for (const r of roles) {
|
|
6630
|
+
if (r === configuredRole || r.toLowerCase() === configuredRole.toLowerCase()) {
|
|
6631
|
+
return true;
|
|
6632
|
+
}
|
|
6633
|
+
const normalized = r.trim().toLowerCase();
|
|
6634
|
+
if (STANDARD_ADMIN_ROLES.has(normalized)) {
|
|
6635
|
+
return true;
|
|
6636
|
+
}
|
|
6637
|
+
const stripped = normalized.replace(/[\s_-]+/g, "");
|
|
6638
|
+
if (stripped === "admin" || stripped === "superadmin" || stripped === "owner") {
|
|
6639
|
+
return true;
|
|
6640
|
+
}
|
|
6641
|
+
}
|
|
6642
|
+
return false;
|
|
6579
6643
|
}
|
|
6580
6644
|
|
|
6581
6645
|
// src/utils/collection-search.ts
|
|
@@ -7224,7 +7288,7 @@ function compileNavigation(config) {
|
|
|
7224
7288
|
const mentionedTrash = explicitNav.some((i) => i.trash || i.slug === "trash");
|
|
7225
7289
|
if (!mentionedTrash) {
|
|
7226
7290
|
const hasTrash = collections.some(
|
|
7227
|
-
(col) => !col.slug
|
|
7291
|
+
(col) => !INTERNAL_SYSTEM_COLLECTIONS.has(col.slug) && resolveTrashConfig(col, config).enabled
|
|
7228
7292
|
);
|
|
7229
7293
|
if (hasTrash) {
|
|
7230
7294
|
registerItem(void 0, {
|
|
@@ -11383,9 +11447,16 @@ function resolveAdminUrl(c, config, clientUrl) {
|
|
|
11383
11447
|
function appendQueryParam(url, key, value) {
|
|
11384
11448
|
return `${url}${url.includes("?") ? "&" : "?"}${key}=${encodeURIComponent(value)}`;
|
|
11385
11449
|
}
|
|
11386
|
-
|
|
11387
|
-
|
|
11388
|
-
|
|
11450
|
+
function resolveActionUrl(c, config, customUrl) {
|
|
11451
|
+
if (customUrl && typeof customUrl === "string" && customUrl.trim().length > 0) {
|
|
11452
|
+
try {
|
|
11453
|
+
return new URL(customUrl.trim(), c.req.url).toString();
|
|
11454
|
+
} catch {
|
|
11455
|
+
return customUrl.trim();
|
|
11456
|
+
}
|
|
11457
|
+
}
|
|
11458
|
+
return resolveAdminUrl(c, config);
|
|
11459
|
+
}
|
|
11389
11460
|
|
|
11390
11461
|
// src/auth/lockout.ts
|
|
11391
11462
|
var DEFAULT_MAX_LOGIN_ATTEMPTS = 5;
|
|
@@ -11442,15 +11513,21 @@ var emailTokens = {
|
|
|
11442
11513
|
width: "600px"
|
|
11443
11514
|
};
|
|
11444
11515
|
function escapeHtml(value) {
|
|
11445
|
-
return value.replace(
|
|
11446
|
-
"
|
|
11447
|
-
|
|
11448
|
-
|
|
11449
|
-
|
|
11450
|
-
|
|
11451
|
-
|
|
11516
|
+
return value.replace(
|
|
11517
|
+
/[&<>'"]/g,
|
|
11518
|
+
(character) => ({
|
|
11519
|
+
"&": "&",
|
|
11520
|
+
"<": "<",
|
|
11521
|
+
">": ">",
|
|
11522
|
+
"'": "'",
|
|
11523
|
+
'"': """
|
|
11524
|
+
})[character]
|
|
11525
|
+
);
|
|
11452
11526
|
}
|
|
11453
11527
|
function safeHttpUrl(value) {
|
|
11528
|
+
if (value.startsWith("{{") && value.endsWith("}}")) {
|
|
11529
|
+
return value;
|
|
11530
|
+
}
|
|
11454
11531
|
try {
|
|
11455
11532
|
const url = new URL(value);
|
|
11456
11533
|
return url.protocol === "https:" || url.protocol === "http:" ? escapeHtml(url.toString()) : void 0;
|
|
@@ -11461,8 +11538,8 @@ function safeHttpUrl(value) {
|
|
|
11461
11538
|
function heading(content) {
|
|
11462
11539
|
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>`;
|
|
11463
11540
|
}
|
|
11464
|
-
function paragraph(content, margin = "0 0 16px") {
|
|
11465
|
-
return `<p style="margin:${margin};font-family:${emailTokens.font};font-size
|
|
11541
|
+
function paragraph(content, margin = "0 0 16px", size = "15px") {
|
|
11542
|
+
return `<p style="margin:${margin};font-family:${emailTokens.font};font-size:${size};line-height:1.6;color:${emailTokens.colors.muted}">${escapeHtml(content)}</p>`;
|
|
11466
11543
|
}
|
|
11467
11544
|
function sectionLabel(content) {
|
|
11468
11545
|
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>`;
|
|
@@ -11492,10 +11569,13 @@ function detailBox(content, monospace = false) {
|
|
|
11492
11569
|
function ctaButton(label, href) {
|
|
11493
11570
|
const safeHref = safeHttpUrl(href);
|
|
11494
11571
|
if (!safeHref) return "";
|
|
11495
|
-
return table(
|
|
11496
|
-
|
|
11497
|
-
|
|
11498
|
-
|
|
11572
|
+
return table(
|
|
11573
|
+
row(
|
|
11574
|
+
`<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>`,
|
|
11575
|
+
"padding:8px 0 24px"
|
|
11576
|
+
),
|
|
11577
|
+
"width:auto"
|
|
11578
|
+
);
|
|
11499
11579
|
}
|
|
11500
11580
|
function safeLinkDetailBox(href) {
|
|
11501
11581
|
const safeHref = safeHttpUrl(href);
|
|
@@ -11503,9 +11583,33 @@ function safeLinkDetailBox(href) {
|
|
|
11503
11583
|
return detailBox(safeHref, true);
|
|
11504
11584
|
}
|
|
11505
11585
|
function alertBox(content) {
|
|
11506
|
-
return table(
|
|
11586
|
+
return table(
|
|
11587
|
+
row(
|
|
11588
|
+
escapeHtml(content),
|
|
11589
|
+
`padding:13px 16px;font-family:${emailTokens.font};font-size:13px;line-height:1.5;color:${emailTokens.colors.dangerText}`
|
|
11590
|
+
),
|
|
11591
|
+
`width:100%;background:${emailTokens.colors.dangerSurface};border:1px solid ${emailTokens.colors.dangerBorder};border-radius:${emailTokens.radius.control}`
|
|
11592
|
+
);
|
|
11507
11593
|
}
|
|
11508
|
-
function
|
|
11594
|
+
function resolveBlocks(input) {
|
|
11595
|
+
if (Array.isArray(input)) {
|
|
11596
|
+
const parts = [];
|
|
11597
|
+
for (const item of input) {
|
|
11598
|
+
if (Array.isArray(item)) {
|
|
11599
|
+
for (const sub of item) {
|
|
11600
|
+
if (typeof sub === "string" && sub.length > 0) parts.push(sub);
|
|
11601
|
+
}
|
|
11602
|
+
} else if (typeof item === "string" && item.length > 0) {
|
|
11603
|
+
parts.push(item);
|
|
11604
|
+
}
|
|
11605
|
+
}
|
|
11606
|
+
return parts.join("\n");
|
|
11607
|
+
}
|
|
11608
|
+
return typeof input === "string" ? input : "";
|
|
11609
|
+
}
|
|
11610
|
+
function layout({ preheader, title, content, footer, siteName }) {
|
|
11611
|
+
const resolvedContent = resolveBlocks(content);
|
|
11612
|
+
const resolvedFooter = resolveBlocks(footer);
|
|
11509
11613
|
return `<!doctype html>
|
|
11510
11614
|
<html lang="en" dir="ltr" xmlns="http://www.w3.org/1999/xhtml">
|
|
11511
11615
|
<head>
|
|
@@ -11526,8 +11630,8 @@ function layout({ preheader, title, content, footer }) {
|
|
|
11526
11630
|
${table(
|
|
11527
11631
|
row(
|
|
11528
11632
|
table(
|
|
11529
|
-
row(" ", `height:5px;background:${emailTokens.colors.accent};font-size:0;line-height:0`) + row(`${sectionLabel("Dyrected")}${heading(title)}`, "padding:30px 32px 24px") + row(
|
|
11530
|
-
`${divider()}${paragraph(
|
|
11633
|
+
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(
|
|
11634
|
+
`${divider()}${paragraph(resolvedFooter, "20px 0 6px")}${paragraph("Privacy: this message contains account-related information; please avoid forwarding it.", "10px 0px 0px 0px", "12px")}`,
|
|
11531
11635
|
"padding:0 32px 28px"
|
|
11532
11636
|
),
|
|
11533
11637
|
`width:100%;max-width:${emailTokens.width};background:${emailTokens.colors.surface};border:1px solid ${emailTokens.colors.border};border-radius:${emailTokens.radius.card};overflow:hidden`
|
|
@@ -11540,6 +11644,81 @@ function layout({ preheader, title, content, footer }) {
|
|
|
11540
11644
|
</body>
|
|
11541
11645
|
</html>`;
|
|
11542
11646
|
}
|
|
11647
|
+
function getDefaultEmailTemplate(purpose, options) {
|
|
11648
|
+
const siteName = options?.siteName ?? "Dyrected";
|
|
11649
|
+
switch (purpose) {
|
|
11650
|
+
case "invite":
|
|
11651
|
+
return {
|
|
11652
|
+
subject: `You've been invited to ${siteName}`,
|
|
11653
|
+
rawHtml: layout({
|
|
11654
|
+
siteName,
|
|
11655
|
+
preheader: `You've been invited to join ${siteName}.`,
|
|
11656
|
+
title: "You've been invited",
|
|
11657
|
+
content: [
|
|
11658
|
+
paragraph("Use the invitation link below to create your account. The link expires in 7 days."),
|
|
11659
|
+
ctaButton("Accept invitation", "{{url}}"),
|
|
11660
|
+
sectionLabel("Invitation link"),
|
|
11661
|
+
safeLinkDetailBox("{{url}}"),
|
|
11662
|
+
paragraph("If the button does not work, copy and paste the link above into your browser.", "12px 0 0")
|
|
11663
|
+
],
|
|
11664
|
+
footer: "If you weren't expecting this invitation, you can safely ignore this email."
|
|
11665
|
+
})
|
|
11666
|
+
};
|
|
11667
|
+
case "resetPassword":
|
|
11668
|
+
return {
|
|
11669
|
+
subject: "Reset your password",
|
|
11670
|
+
rawHtml: layout({
|
|
11671
|
+
siteName,
|
|
11672
|
+
preheader: `Reset your ${siteName} password.`,
|
|
11673
|
+
title: "Reset your password",
|
|
11674
|
+
content: [
|
|
11675
|
+
paragraph("We received a request to reset your password. The reset link expires in 1 hour."),
|
|
11676
|
+
ctaButton("Reset password", "{{url}}"),
|
|
11677
|
+
sectionLabel("Reset link"),
|
|
11678
|
+
safeLinkDetailBox("{{url}}"),
|
|
11679
|
+
paragraph("If the button does not work, copy and paste the link above into your browser.", "12px 0 0")
|
|
11680
|
+
],
|
|
11681
|
+
footer: "If you didn't request a password reset, you can safely ignore this email."
|
|
11682
|
+
})
|
|
11683
|
+
};
|
|
11684
|
+
case "welcome":
|
|
11685
|
+
return {
|
|
11686
|
+
subject: `Welcome to ${siteName} \u2014 your account is ready`,
|
|
11687
|
+
rawHtml: layout({
|
|
11688
|
+
siteName,
|
|
11689
|
+
preheader: `Your ${siteName} account is ready.`,
|
|
11690
|
+
title: "Welcome \u2014 your account is ready",
|
|
11691
|
+
content: [paragraph("Your account has been created. You can now log in with:"), detailBox("{{email}}")],
|
|
11692
|
+
footer: "If you didn't create this account, you can safely ignore this email."
|
|
11693
|
+
})
|
|
11694
|
+
};
|
|
11695
|
+
case "passwordChanged":
|
|
11696
|
+
return {
|
|
11697
|
+
subject: "Your password has been changed",
|
|
11698
|
+
rawHtml: layout({
|
|
11699
|
+
siteName,
|
|
11700
|
+
preheader: `Your ${siteName} password was changed.`,
|
|
11701
|
+
title: "Password changed",
|
|
11702
|
+
content: [
|
|
11703
|
+
paragraph("The password for your account has been changed."),
|
|
11704
|
+
alertBox("If you did not make this change, please contact support or reset your password immediately.")
|
|
11705
|
+
],
|
|
11706
|
+
footer: "Security notice: this confirms a recent credential change."
|
|
11707
|
+
})
|
|
11708
|
+
};
|
|
11709
|
+
default:
|
|
11710
|
+
return {
|
|
11711
|
+
subject: `Notification from ${siteName}`,
|
|
11712
|
+
rawHtml: layout({
|
|
11713
|
+
siteName,
|
|
11714
|
+
preheader: `Account notification from ${siteName}.`,
|
|
11715
|
+
title: "Account Notification",
|
|
11716
|
+
content: [paragraph(`This is an account notification from ${siteName}.`)],
|
|
11717
|
+
footer: "If you have any questions, please reach out to our team."
|
|
11718
|
+
})
|
|
11719
|
+
};
|
|
11720
|
+
}
|
|
11721
|
+
}
|
|
11543
11722
|
|
|
11544
11723
|
// src/services/email.service.ts
|
|
11545
11724
|
var _devSend = null;
|
|
@@ -11567,11 +11746,18 @@ async function getDevSend() {
|
|
|
11567
11746
|
password: account.pass
|
|
11568
11747
|
});
|
|
11569
11748
|
_devSend = async ({ to, subject, html }) => {
|
|
11570
|
-
const info = await transport.sendMail({
|
|
11749
|
+
const info = await transport.sendMail({
|
|
11750
|
+
from: '"Dyrected Dev" <dev@dyrected.local>',
|
|
11751
|
+
to,
|
|
11752
|
+
subject,
|
|
11753
|
+
html
|
|
11754
|
+
});
|
|
11755
|
+
const previewUrl = nodemailer.default.getTestMessageUrl(info) || void 0;
|
|
11571
11756
|
logger.info({
|
|
11572
11757
|
msg: "Email preview URL",
|
|
11573
|
-
previewUrl
|
|
11758
|
+
previewUrl
|
|
11574
11759
|
});
|
|
11760
|
+
return { previewUrl };
|
|
11575
11761
|
};
|
|
11576
11762
|
return _devSend;
|
|
11577
11763
|
} catch {
|
|
@@ -11597,69 +11783,286 @@ async function sendEmail(config, payload) {
|
|
|
11597
11783
|
err,
|
|
11598
11784
|
msg: "Email provider send failed",
|
|
11599
11785
|
to: payload.to,
|
|
11600
|
-
subject: payload.subject
|
|
11786
|
+
subject: "subject" in payload ? payload.subject : void 0,
|
|
11787
|
+
template: "template" in payload ? payload.template : void 0
|
|
11601
11788
|
});
|
|
11602
11789
|
throw err;
|
|
11603
11790
|
}
|
|
11604
|
-
return;
|
|
11791
|
+
return { success: true };
|
|
11605
11792
|
}
|
|
11606
11793
|
if (process.env.NODE_ENV !== "production") {
|
|
11607
11794
|
const devSend = await getDevSend();
|
|
11608
|
-
|
|
11795
|
+
if (devSend) {
|
|
11796
|
+
if ("template" in payload && payload.template) {
|
|
11797
|
+
const devHtml = `
|
|
11798
|
+
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px; border: 1px solid #e2e8f0; border-radius: 8px;">
|
|
11799
|
+
<h3 style="margin-top:0; color: #1e293b;">External Template Email (Dev Preview)</h3>
|
|
11800
|
+
<p><strong>To:</strong> ${payload.to}</p>
|
|
11801
|
+
<p><strong>Template ID:</strong> <code>${payload.template}</code></p>
|
|
11802
|
+
${payload.subject ? `<p><strong>Subject:</strong> ${payload.subject}</p>` : ""}
|
|
11803
|
+
<p><strong>Variables:</strong></p>
|
|
11804
|
+
<pre style="background: #f8fafc; padding: 12px; border-radius: 6px; overflow-x: auto;">${JSON.stringify(payload.variables, null, 2)}</pre>
|
|
11805
|
+
</div>
|
|
11806
|
+
`;
|
|
11807
|
+
const res = await devSend({
|
|
11808
|
+
to: payload.to,
|
|
11809
|
+
subject: payload.subject ?? `[Template: ${payload.template}] External Email`,
|
|
11810
|
+
html: devHtml
|
|
11811
|
+
});
|
|
11812
|
+
return { success: true, previewUrl: res?.previewUrl };
|
|
11813
|
+
} else if ("html" in payload) {
|
|
11814
|
+
const res = await devSend({
|
|
11815
|
+
to: payload.to,
|
|
11816
|
+
subject: payload.subject,
|
|
11817
|
+
html: payload.html
|
|
11818
|
+
});
|
|
11819
|
+
return { success: true, previewUrl: res?.previewUrl };
|
|
11820
|
+
}
|
|
11821
|
+
}
|
|
11822
|
+
}
|
|
11823
|
+
return { success: true };
|
|
11824
|
+
}
|
|
11825
|
+
function interpolateVariables(str, args, escapeMode = "html") {
|
|
11826
|
+
if (!str || typeof str !== "string") return str;
|
|
11827
|
+
return str.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, path) => {
|
|
11828
|
+
const parts = path.split(".");
|
|
11829
|
+
let val = args;
|
|
11830
|
+
for (const part of parts) {
|
|
11831
|
+
if (val == null) break;
|
|
11832
|
+
val = val[part];
|
|
11833
|
+
}
|
|
11834
|
+
if (val === void 0 || val === null) return "";
|
|
11835
|
+
const raw = String(val);
|
|
11836
|
+
return escapeMode === "html" ? escapeHtml(raw) : raw;
|
|
11837
|
+
});
|
|
11838
|
+
}
|
|
11839
|
+
function formatDbTemplateDoc(doc, args) {
|
|
11840
|
+
if (doc.format === "external_template" || doc.externalTemplateId) {
|
|
11841
|
+
const rawVars = doc.variables && typeof doc.variables === "object" ? doc.variables : {};
|
|
11842
|
+
const interpolatedVars = { ...args };
|
|
11843
|
+
for (const [key, val] of Object.entries(rawVars)) {
|
|
11844
|
+
if (typeof val === "string") {
|
|
11845
|
+
interpolatedVars[key] = interpolateVariables(val, args);
|
|
11846
|
+
} else {
|
|
11847
|
+
interpolatedVars[key] = val;
|
|
11848
|
+
}
|
|
11849
|
+
}
|
|
11850
|
+
return {
|
|
11851
|
+
template: doc.externalTemplateId ?? "",
|
|
11852
|
+
variables: interpolatedVars,
|
|
11853
|
+
...doc.subject ? { subject: interpolateVariables(doc.subject, args) } : {}
|
|
11854
|
+
};
|
|
11855
|
+
}
|
|
11856
|
+
return {
|
|
11857
|
+
subject: interpolateVariables(doc.subject || "", args),
|
|
11858
|
+
html: interpolateVariables(doc.rawHtml || "", args)
|
|
11859
|
+
};
|
|
11860
|
+
}
|
|
11861
|
+
async function getDbEmailTemplateOverride(db, collectionSlug, purpose, args) {
|
|
11862
|
+
if (!db || typeof db.find !== "function") return null;
|
|
11863
|
+
try {
|
|
11864
|
+
if (collectionSlug) {
|
|
11865
|
+
const specific = await db.find({
|
|
11866
|
+
collection: "__email_templates",
|
|
11867
|
+
where: {
|
|
11868
|
+
collectionSlug,
|
|
11869
|
+
purpose,
|
|
11870
|
+
active: true
|
|
11871
|
+
},
|
|
11872
|
+
limit: 1
|
|
11873
|
+
});
|
|
11874
|
+
const doc2 = specific?.docs?.[0];
|
|
11875
|
+
if (doc2) {
|
|
11876
|
+
return formatDbTemplateDoc(doc2, args);
|
|
11877
|
+
}
|
|
11878
|
+
}
|
|
11879
|
+
const global = await db.find({
|
|
11880
|
+
collection: "__email_templates",
|
|
11881
|
+
where: {
|
|
11882
|
+
collectionSlug: "*",
|
|
11883
|
+
purpose,
|
|
11884
|
+
active: true
|
|
11885
|
+
},
|
|
11886
|
+
limit: 1
|
|
11887
|
+
});
|
|
11888
|
+
const doc = global?.docs?.[0];
|
|
11889
|
+
if (doc) {
|
|
11890
|
+
return formatDbTemplateDoc(doc, args);
|
|
11891
|
+
}
|
|
11892
|
+
} catch {
|
|
11893
|
+
return null;
|
|
11894
|
+
}
|
|
11895
|
+
return null;
|
|
11896
|
+
}
|
|
11897
|
+
function enrichArgs(config, collection, args) {
|
|
11898
|
+
const slug = collection?.slug;
|
|
11899
|
+
const readableSlug = slug?.replace(/^__/, "");
|
|
11900
|
+
const fallbackLabel = readableSlug ? readableSlug.charAt(0).toUpperCase() + readableSlug.slice(1) : "Account";
|
|
11901
|
+
return {
|
|
11902
|
+
...args,
|
|
11903
|
+
collection: slug,
|
|
11904
|
+
collectionLabel: collection?.labels?.singular || collection?.labels?.plural || fallbackLabel,
|
|
11905
|
+
collectionLabels: collection?.labels,
|
|
11906
|
+
siteName: config.admin?.branding?.logoText ?? "Dyrected"
|
|
11907
|
+
};
|
|
11908
|
+
}
|
|
11909
|
+
function toOutboundEmail(to, result, metadata) {
|
|
11910
|
+
if ("template" in result) {
|
|
11911
|
+
return {
|
|
11912
|
+
to,
|
|
11913
|
+
type: "template",
|
|
11914
|
+
template: result.template,
|
|
11915
|
+
variables: result.variables,
|
|
11916
|
+
...result.subject ? { subject: result.subject } : {},
|
|
11917
|
+
...metadata
|
|
11918
|
+
};
|
|
11609
11919
|
}
|
|
11920
|
+
return {
|
|
11921
|
+
to,
|
|
11922
|
+
type: "html",
|
|
11923
|
+
subject: result.subject ?? "Notification",
|
|
11924
|
+
html: result.html,
|
|
11925
|
+
...result.text ? { text: result.text } : {},
|
|
11926
|
+
...metadata
|
|
11927
|
+
};
|
|
11610
11928
|
}
|
|
11611
|
-
function buildWelcomeEmail(config, args) {
|
|
11612
|
-
|
|
11929
|
+
function buildWelcomeEmail(config, args, collection, dbOverride) {
|
|
11930
|
+
if (dbOverride) return dbOverride;
|
|
11931
|
+
const enriched = enrichArgs(config, collection, args);
|
|
11932
|
+
const collectionAuth = collection?.auth;
|
|
11933
|
+
if (collectionAuth && typeof collectionAuth === "object" && collectionAuth.email?.templates?.welcome) {
|
|
11934
|
+
return collectionAuth.email.templates.welcome(enriched);
|
|
11935
|
+
}
|
|
11936
|
+
const custom = config.email?.templates?.welcome?.(enriched);
|
|
11937
|
+
if (custom) return custom;
|
|
11613
11938
|
return {
|
|
11614
|
-
subject:
|
|
11615
|
-
html:
|
|
11616
|
-
|
|
11939
|
+
subject: "Welcome \u2014 your account is ready",
|
|
11940
|
+
html: layout({
|
|
11941
|
+
siteName: enriched.siteName,
|
|
11942
|
+
preheader: `Your ${enriched.siteName} account is ready.`,
|
|
11617
11943
|
title: "Welcome \u2014 your account is ready",
|
|
11618
|
-
content:
|
|
11944
|
+
content: [
|
|
11945
|
+
paragraph("Your account has been created. You can now log in with:"),
|
|
11946
|
+
detailBox(args.email)
|
|
11947
|
+
],
|
|
11619
11948
|
footer: "If you didn't create this account, you can safely ignore this email."
|
|
11620
11949
|
})
|
|
11621
11950
|
};
|
|
11622
11951
|
}
|
|
11623
|
-
function buildInviteEmail(config, args) {
|
|
11624
|
-
|
|
11952
|
+
function buildInviteEmail(config, args, collection, dbOverride) {
|
|
11953
|
+
if (dbOverride) return dbOverride;
|
|
11954
|
+
const enriched = enrichArgs(config, collection, args);
|
|
11955
|
+
const collectionAuth = collection?.auth;
|
|
11956
|
+
if (collectionAuth && typeof collectionAuth === "object" && collectionAuth.email?.templates?.invite) {
|
|
11957
|
+
return collectionAuth.email.templates.invite(enriched);
|
|
11958
|
+
}
|
|
11959
|
+
const custom = config.email?.templates?.invite?.(enriched);
|
|
11960
|
+
if (custom) return custom;
|
|
11625
11961
|
const inviteLink = args.url;
|
|
11626
11962
|
const inviteLinkBlock = inviteLink ? safeLinkDetailBox(inviteLink) : "";
|
|
11627
11963
|
return {
|
|
11628
|
-
subject:
|
|
11629
|
-
html:
|
|
11630
|
-
|
|
11964
|
+
subject: "You've been invited",
|
|
11965
|
+
html: layout({
|
|
11966
|
+
siteName: enriched.siteName,
|
|
11967
|
+
preheader: `You've been invited to join ${enriched.siteName}.`,
|
|
11631
11968
|
title: "You've been invited",
|
|
11632
|
-
content:
|
|
11969
|
+
content: [
|
|
11970
|
+
args.invitedByEmail && paragraph(`You were invited by ${args.invitedByEmail}.`),
|
|
11971
|
+
inviteLink ? [
|
|
11972
|
+
paragraph("Use the invitation link below to create your account. The link expires in 7 days."),
|
|
11973
|
+
ctaButton("Accept invitation", inviteLink),
|
|
11974
|
+
sectionLabel("Invitation link"),
|
|
11975
|
+
inviteLinkBlock,
|
|
11976
|
+
paragraph("If the button does not work, copy and paste the link above into your browser.", "12px 0 0")
|
|
11977
|
+
] : [
|
|
11978
|
+
paragraph("Use the invitation token below to accept your invitation. It expires in 7 days."),
|
|
11979
|
+
sectionLabel("Invitation token"),
|
|
11980
|
+
detailBox(args.token, true)
|
|
11981
|
+
]
|
|
11982
|
+
],
|
|
11633
11983
|
footer: "If you weren't expecting this invitation, you can safely ignore this email."
|
|
11634
11984
|
})
|
|
11635
11985
|
};
|
|
11636
11986
|
}
|
|
11637
|
-
function buildResetPasswordEmail(config, args) {
|
|
11638
|
-
|
|
11987
|
+
function buildResetPasswordEmail(config, args, collection, dbOverride) {
|
|
11988
|
+
if (dbOverride) return dbOverride;
|
|
11989
|
+
const enriched = enrichArgs(config, collection, args);
|
|
11990
|
+
const collectionAuth = collection?.auth;
|
|
11991
|
+
if (collectionAuth && typeof collectionAuth === "object" && collectionAuth.email?.templates?.resetPassword) {
|
|
11992
|
+
return collectionAuth.email.templates.resetPassword(enriched);
|
|
11993
|
+
}
|
|
11994
|
+
const custom = config.email?.templates?.resetPassword?.(enriched);
|
|
11995
|
+
if (custom) return custom;
|
|
11639
11996
|
const resetLink = args.url;
|
|
11640
11997
|
const resetLinkBlock = resetLink ? safeLinkDetailBox(resetLink) : "";
|
|
11641
11998
|
return {
|
|
11642
|
-
subject:
|
|
11643
|
-
html:
|
|
11644
|
-
|
|
11999
|
+
subject: "Reset your password",
|
|
12000
|
+
html: layout({
|
|
12001
|
+
siteName: enriched.siteName,
|
|
12002
|
+
preheader: `Reset your ${enriched.siteName} password.`,
|
|
11645
12003
|
title: "Reset your password",
|
|
11646
|
-
content:
|
|
12004
|
+
content: [
|
|
12005
|
+
resetLink ? [
|
|
12006
|
+
paragraph("We received a request to reset your password. The reset link expires in 1 hour."),
|
|
12007
|
+
ctaButton("Reset password", resetLink),
|
|
12008
|
+
sectionLabel("Reset link"),
|
|
12009
|
+
resetLinkBlock,
|
|
12010
|
+
paragraph("If the button does not work, copy and paste the link above into your browser.", "12px 0 0")
|
|
12011
|
+
] : [
|
|
12012
|
+
paragraph("We received a request to reset your password. The reset token expires in 1 hour."),
|
|
12013
|
+
sectionLabel("Reset token"),
|
|
12014
|
+
detailBox(args.token, true)
|
|
12015
|
+
]
|
|
12016
|
+
],
|
|
11647
12017
|
footer: "If you didn't request a password reset, you can safely ignore this email."
|
|
11648
12018
|
})
|
|
11649
12019
|
};
|
|
11650
12020
|
}
|
|
11651
|
-
function buildPasswordChangedEmail(config, args) {
|
|
11652
|
-
|
|
12021
|
+
function buildPasswordChangedEmail(config, args, collection, dbOverride) {
|
|
12022
|
+
if (dbOverride) return dbOverride;
|
|
12023
|
+
const enriched = enrichArgs(config, collection, args);
|
|
12024
|
+
const collectionAuth = collection?.auth;
|
|
12025
|
+
if (collectionAuth && typeof collectionAuth === "object" && collectionAuth.email?.templates?.passwordChanged) {
|
|
12026
|
+
return collectionAuth.email.templates.passwordChanged(enriched);
|
|
12027
|
+
}
|
|
12028
|
+
const custom = config.email?.templates?.passwordChanged?.(enriched);
|
|
12029
|
+
if (custom) return custom;
|
|
11653
12030
|
return {
|
|
11654
|
-
subject:
|
|
11655
|
-
html:
|
|
11656
|
-
|
|
12031
|
+
subject: "Your password has been changed",
|
|
12032
|
+
html: layout({
|
|
12033
|
+
siteName: enriched.siteName,
|
|
12034
|
+
preheader: `Your ${enriched.siteName} password was changed.`,
|
|
11657
12035
|
title: "Password changed",
|
|
11658
|
-
content:
|
|
12036
|
+
content: [
|
|
12037
|
+
paragraph("The password for this account was just changed:"),
|
|
12038
|
+
detailBox(args.email),
|
|
12039
|
+
spacer(),
|
|
12040
|
+
alertBox("If you did not make this change, please contact support immediately.")
|
|
12041
|
+
],
|
|
11659
12042
|
footer: "This is an automated security notification."
|
|
11660
12043
|
})
|
|
11661
12044
|
};
|
|
11662
12045
|
}
|
|
12046
|
+
async function resolveEmailTemplate(params) {
|
|
12047
|
+
const { config, collection, purpose, args, db } = params;
|
|
12048
|
+
const enriched = enrichArgs(config, collection, args);
|
|
12049
|
+
let dbOverride = null;
|
|
12050
|
+
if (config.email?.adminEditable !== false && db) {
|
|
12051
|
+
dbOverride = await getDbEmailTemplateOverride(db, collection?.slug, purpose, enriched);
|
|
12052
|
+
}
|
|
12053
|
+
switch (purpose) {
|
|
12054
|
+
case "welcome":
|
|
12055
|
+
return buildWelcomeEmail(config, enriched, collection, dbOverride);
|
|
12056
|
+
case "invite":
|
|
12057
|
+
return buildInviteEmail(config, enriched, collection, dbOverride);
|
|
12058
|
+
case "resetPassword":
|
|
12059
|
+
return buildResetPasswordEmail(config, enriched, collection, dbOverride);
|
|
12060
|
+
case "passwordChanged":
|
|
12061
|
+
return buildPasswordChangedEmail(config, enriched, collection, dbOverride);
|
|
12062
|
+
default:
|
|
12063
|
+
throw new Error(`Unknown email template purpose: ${purpose}`);
|
|
12064
|
+
}
|
|
12065
|
+
}
|
|
11663
12066
|
|
|
11664
12067
|
// src/controllers/auth.controller.ts
|
|
11665
12068
|
var AuthController = class {
|
|
@@ -11696,9 +12099,10 @@ var AuthController = class {
|
|
|
11696
12099
|
const data = {
|
|
11697
12100
|
...safeExtraFields,
|
|
11698
12101
|
email,
|
|
11699
|
-
password:
|
|
12102
|
+
password: null,
|
|
12103
|
+
invitedAt: Date.now()
|
|
11700
12104
|
};
|
|
11701
|
-
if (this.hasField("status")) {
|
|
12105
|
+
if (this.hasField("status") && !data.status) {
|
|
11702
12106
|
data.status = "pending";
|
|
11703
12107
|
}
|
|
11704
12108
|
return data;
|
|
@@ -11821,6 +12225,7 @@ var AuthController = class {
|
|
|
11821
12225
|
...rolePayload
|
|
11822
12226
|
}
|
|
11823
12227
|
});
|
|
12228
|
+
const safeUser = this.sanitizeUser(user);
|
|
11824
12229
|
const token = await issueAuthSessionToken({
|
|
11825
12230
|
config,
|
|
11826
12231
|
userId: user.id,
|
|
@@ -11830,15 +12235,28 @@ var AuthController = class {
|
|
|
11830
12235
|
ip: c.get("clientIp"),
|
|
11831
12236
|
authSource: "local"
|
|
11832
12237
|
});
|
|
11833
|
-
|
|
11834
|
-
|
|
12238
|
+
resolveEmailTemplate({
|
|
12239
|
+
config,
|
|
12240
|
+
collection: this.collection,
|
|
12241
|
+
purpose: "welcome",
|
|
12242
|
+
args: { email: body.email, user: safeUser },
|
|
12243
|
+
db
|
|
12244
|
+
}).then(
|
|
12245
|
+
(emailResult) => sendEmail(
|
|
12246
|
+
config,
|
|
12247
|
+
toOutboundEmail(body.email, emailResult, {
|
|
12248
|
+
collection: this.collection.slug,
|
|
12249
|
+
purpose: "welcome",
|
|
12250
|
+
user: safeUser
|
|
12251
|
+
})
|
|
12252
|
+
)
|
|
12253
|
+
).catch(
|
|
11835
12254
|
(err) => getRequestLogger(c, "auth").error({
|
|
11836
12255
|
err,
|
|
11837
12256
|
msg: "Failed to send welcome email",
|
|
11838
12257
|
email: body.email
|
|
11839
12258
|
})
|
|
11840
12259
|
);
|
|
11841
|
-
const safeUser = this.sanitizeUser(user);
|
|
11842
12260
|
return c.json({ token, user: safeUser });
|
|
11843
12261
|
}
|
|
11844
12262
|
// ---------------------------------------------------------------------------
|
|
@@ -11872,6 +12290,16 @@ var AuthController = class {
|
|
|
11872
12290
|
403
|
|
11873
12291
|
);
|
|
11874
12292
|
}
|
|
12293
|
+
if (!hasUsablePassword(user.password)) {
|
|
12294
|
+
return c.json(
|
|
12295
|
+
{
|
|
12296
|
+
error: true,
|
|
12297
|
+
code: "PASSWORD_NOT_SET",
|
|
12298
|
+
message: "This account does not have a password set. Please use the invite link or reset password."
|
|
12299
|
+
},
|
|
12300
|
+
401
|
|
12301
|
+
);
|
|
12302
|
+
}
|
|
11875
12303
|
const lockedUntilMs = getLockedUntilMs(user.lockedUntil);
|
|
11876
12304
|
if (lockedUntilMs !== null && lockedUntilMs > Date.now()) {
|
|
11877
12305
|
const retryAfterSeconds = Math.max(
|
|
@@ -12049,34 +12477,67 @@ var AuthController = class {
|
|
|
12049
12477
|
limit: 1
|
|
12050
12478
|
});
|
|
12051
12479
|
const user = result.docs[0];
|
|
12480
|
+
let resetToken;
|
|
12481
|
+
let url;
|
|
12482
|
+
let emailSent = false;
|
|
12483
|
+
let emailError;
|
|
12052
12484
|
if (user) {
|
|
12053
|
-
const
|
|
12485
|
+
const siteId = c.get("siteId");
|
|
12486
|
+
resetToken = await signCollectionToken(
|
|
12054
12487
|
{
|
|
12055
|
-
sub: user.id,
|
|
12488
|
+
sub: String(user.id),
|
|
12056
12489
|
email: user.email,
|
|
12057
12490
|
collection: this.collection.slug,
|
|
12058
|
-
purpose: "reset"
|
|
12491
|
+
purpose: "reset",
|
|
12492
|
+
...siteId ? { siteId } : {}
|
|
12059
12493
|
},
|
|
12060
|
-
|
|
12494
|
+
resolveResetPasswordTokenExpiry(this.collection)
|
|
12061
12495
|
);
|
|
12062
|
-
const
|
|
12063
|
-
|
|
12064
|
-
|
|
12065
|
-
|
|
12066
|
-
|
|
12067
|
-
|
|
12068
|
-
|
|
12069
|
-
|
|
12070
|
-
|
|
12071
|
-
|
|
12072
|
-
|
|
12073
|
-
|
|
12074
|
-
|
|
12496
|
+
const customResetUrl = body?.resetUrl ?? (typeof this.collection.auth === "object" ? this.collection.auth.urls?.resetPassword : void 0);
|
|
12497
|
+
url = appendQueryParam(
|
|
12498
|
+
resolveActionUrl(c, config, customResetUrl),
|
|
12499
|
+
"token",
|
|
12500
|
+
resetToken
|
|
12501
|
+
);
|
|
12502
|
+
if (body?.sendEmail !== false) {
|
|
12503
|
+
try {
|
|
12504
|
+
const emailResult = await resolveEmailTemplate({
|
|
12505
|
+
config,
|
|
12506
|
+
collection: this.collection,
|
|
12507
|
+
purpose: "resetPassword",
|
|
12508
|
+
args: {
|
|
12509
|
+
token: resetToken,
|
|
12510
|
+
url,
|
|
12511
|
+
user
|
|
12512
|
+
},
|
|
12513
|
+
db
|
|
12514
|
+
});
|
|
12515
|
+
await sendEmail(
|
|
12516
|
+
config,
|
|
12517
|
+
toOutboundEmail(user.email, emailResult, {
|
|
12518
|
+
collection: this.collection.slug,
|
|
12519
|
+
purpose: "resetPassword",
|
|
12520
|
+
user
|
|
12521
|
+
})
|
|
12522
|
+
);
|
|
12523
|
+
emailSent = true;
|
|
12524
|
+
} catch (err) {
|
|
12525
|
+
emailSent = false;
|
|
12526
|
+
emailError = err?.message ?? "Email delivery failed";
|
|
12527
|
+
getRequestLogger(c, "auth").error({
|
|
12528
|
+
err,
|
|
12529
|
+
msg: "Failed to send password reset email",
|
|
12530
|
+
email: user.email
|
|
12531
|
+
});
|
|
12532
|
+
}
|
|
12075
12533
|
}
|
|
12076
12534
|
}
|
|
12077
12535
|
return c.json({
|
|
12078
12536
|
success: true,
|
|
12079
|
-
message: "If an account with that email exists, a reset link has been sent."
|
|
12537
|
+
message: "If an account with that email exists, a reset link has been sent.",
|
|
12538
|
+
...body?.sendEmail === false && resetToken ? { token: resetToken, resetUrl: url } : {},
|
|
12539
|
+
emailSent,
|
|
12540
|
+
...emailError ? { emailError } : {}
|
|
12080
12541
|
});
|
|
12081
12542
|
}
|
|
12082
12543
|
// ---------------------------------------------------------------------------
|
|
@@ -12110,6 +12571,13 @@ var AuthController = class {
|
|
|
12110
12571
|
400
|
|
12111
12572
|
);
|
|
12112
12573
|
}
|
|
12574
|
+
const currentSiteId = c.get("siteId");
|
|
12575
|
+
if (payload.siteId && currentSiteId && payload.siteId !== currentSiteId) {
|
|
12576
|
+
return c.json(
|
|
12577
|
+
{ error: true, code: "SITE_MISMATCH", message: "Token is not valid for this site." },
|
|
12578
|
+
403
|
|
12579
|
+
);
|
|
12580
|
+
}
|
|
12113
12581
|
const hashedPassword = await hashPassword(body.password);
|
|
12114
12582
|
await db.update({
|
|
12115
12583
|
collection: this.collection.slug,
|
|
@@ -12124,10 +12592,24 @@ var AuthController = class {
|
|
|
12124
12592
|
userId: payload.sub,
|
|
12125
12593
|
collection: this.collection.slug
|
|
12126
12594
|
});
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12130
|
-
|
|
12595
|
+
resolveEmailTemplate({
|
|
12596
|
+
config,
|
|
12597
|
+
collection: this.collection,
|
|
12598
|
+
purpose: "passwordChanged",
|
|
12599
|
+
args: {
|
|
12600
|
+
email: payload.email,
|
|
12601
|
+
user: { id: payload.sub, email: payload.email }
|
|
12602
|
+
},
|
|
12603
|
+
db
|
|
12604
|
+
}).then(
|
|
12605
|
+
(emailResult) => sendEmail(
|
|
12606
|
+
config,
|
|
12607
|
+
toOutboundEmail(payload.email, emailResult, {
|
|
12608
|
+
collection: this.collection.slug,
|
|
12609
|
+
purpose: "passwordChanged"
|
|
12610
|
+
})
|
|
12611
|
+
)
|
|
12612
|
+
).catch(
|
|
12131
12613
|
(err) => getRequestLogger(c, "auth").error({
|
|
12132
12614
|
err,
|
|
12133
12615
|
msg: "Failed to send password-changed email",
|
|
@@ -12156,61 +12638,117 @@ var AuthController = class {
|
|
|
12156
12638
|
if (!body?.email) {
|
|
12157
12639
|
return c.json({ error: true, message: "email is required." }, 400);
|
|
12158
12640
|
}
|
|
12159
|
-
const
|
|
12641
|
+
const {
|
|
12642
|
+
email,
|
|
12643
|
+
inviteUrl,
|
|
12644
|
+
sendEmail: shouldSendEmail,
|
|
12645
|
+
data: nestedData,
|
|
12646
|
+
...topLevelFields
|
|
12647
|
+
} = body;
|
|
12648
|
+
const inviteData = {
|
|
12649
|
+
...topLevelFields,
|
|
12650
|
+
...nestedData && typeof nestedData === "object" && !Array.isArray(nestedData) ? nestedData : {}
|
|
12651
|
+
};
|
|
12160
12652
|
const existing = await db.find({
|
|
12161
12653
|
collection: this.collection.slug,
|
|
12162
|
-
where: { email
|
|
12654
|
+
where: { email },
|
|
12163
12655
|
limit: 1
|
|
12164
12656
|
});
|
|
12165
12657
|
const existingUser = existing.docs[0];
|
|
12166
12658
|
const existingIsPending = existingUser?.status === "pending";
|
|
12167
|
-
if (existingUser && !existingIsPending) {
|
|
12659
|
+
if (existingUser && !existingIsPending && hasUsablePassword(existingUser.password)) {
|
|
12168
12660
|
return c.json(
|
|
12169
|
-
{
|
|
12661
|
+
{
|
|
12662
|
+
error: true,
|
|
12663
|
+
code: "USER_ALREADY_EXISTS",
|
|
12664
|
+
message: "An account with that email already exists and has a password set. Use forgot password if needed."
|
|
12665
|
+
},
|
|
12170
12666
|
409
|
|
12171
12667
|
);
|
|
12172
12668
|
}
|
|
12669
|
+
const now2 = Date.now();
|
|
12670
|
+
let targetUser;
|
|
12173
12671
|
if (!existingUser) {
|
|
12174
|
-
await
|
|
12672
|
+
const pendingData = await this.buildPendingInviteData(email, inviteData);
|
|
12673
|
+
pendingData.invitedAt = now2;
|
|
12674
|
+
targetUser = await db.create({
|
|
12175
12675
|
collection: this.collection.slug,
|
|
12176
|
-
data:
|
|
12676
|
+
data: pendingData
|
|
12177
12677
|
});
|
|
12178
|
-
} else
|
|
12179
|
-
|
|
12678
|
+
} else {
|
|
12679
|
+
const updateData = {
|
|
12680
|
+
...inviteData,
|
|
12681
|
+
invitedAt: now2
|
|
12682
|
+
};
|
|
12683
|
+
if (this.hasField("status") && !existingUser.status) {
|
|
12684
|
+
updateData.status = "pending";
|
|
12685
|
+
}
|
|
12686
|
+
targetUser = await db.update({
|
|
12180
12687
|
collection: this.collection.slug,
|
|
12181
12688
|
id: String(existingUser.id),
|
|
12182
|
-
data:
|
|
12689
|
+
data: updateData
|
|
12183
12690
|
});
|
|
12184
12691
|
}
|
|
12692
|
+
const siteId = c.get("siteId");
|
|
12185
12693
|
const inviteToken = await signCollectionToken(
|
|
12186
12694
|
{
|
|
12187
|
-
sub:
|
|
12188
|
-
email
|
|
12695
|
+
sub: email,
|
|
12696
|
+
email,
|
|
12189
12697
|
collection: this.collection.slug,
|
|
12190
|
-
purpose: "invite"
|
|
12698
|
+
purpose: "invite",
|
|
12699
|
+
...siteId ? { siteId } : {}
|
|
12191
12700
|
},
|
|
12192
|
-
|
|
12701
|
+
resolveInviteTokenExpiry(this.collection)
|
|
12193
12702
|
);
|
|
12194
|
-
const
|
|
12195
|
-
|
|
12196
|
-
|
|
12197
|
-
|
|
12198
|
-
|
|
12199
|
-
|
|
12200
|
-
|
|
12201
|
-
|
|
12202
|
-
|
|
12203
|
-
|
|
12204
|
-
|
|
12205
|
-
|
|
12206
|
-
|
|
12207
|
-
|
|
12703
|
+
const customInviteUrl = inviteUrl ?? (typeof this.collection.auth === "object" ? this.collection.auth.urls?.invite : void 0);
|
|
12704
|
+
const url = appendQueryParam(
|
|
12705
|
+
resolveActionUrl(c, config, customInviteUrl),
|
|
12706
|
+
"inviteToken",
|
|
12707
|
+
inviteToken
|
|
12708
|
+
);
|
|
12709
|
+
let emailSent = false;
|
|
12710
|
+
let emailError;
|
|
12711
|
+
if (shouldSendEmail !== false) {
|
|
12712
|
+
try {
|
|
12713
|
+
const emailResult = await resolveEmailTemplate({
|
|
12714
|
+
config,
|
|
12715
|
+
collection: this.collection,
|
|
12716
|
+
purpose: "invite",
|
|
12717
|
+
args: {
|
|
12718
|
+
token: inviteToken,
|
|
12719
|
+
invitedByEmail: requestUser.email,
|
|
12720
|
+
url,
|
|
12721
|
+
data: inviteData,
|
|
12722
|
+
user: targetUser
|
|
12723
|
+
},
|
|
12724
|
+
db
|
|
12725
|
+
});
|
|
12726
|
+
await sendEmail(
|
|
12727
|
+
config,
|
|
12728
|
+
toOutboundEmail(email, emailResult, {
|
|
12729
|
+
collection: this.collection.slug,
|
|
12730
|
+
purpose: "invite",
|
|
12731
|
+
user: targetUser
|
|
12732
|
+
})
|
|
12733
|
+
);
|
|
12734
|
+
emailSent = true;
|
|
12735
|
+
} catch (err) {
|
|
12736
|
+
emailSent = false;
|
|
12737
|
+
emailError = err?.message ?? "Email delivery failed";
|
|
12738
|
+
getRequestLogger(c, "auth").error({
|
|
12739
|
+
err,
|
|
12740
|
+
msg: "Failed to send invite email",
|
|
12741
|
+
email
|
|
12742
|
+
});
|
|
12743
|
+
}
|
|
12208
12744
|
}
|
|
12209
12745
|
return c.json({
|
|
12210
12746
|
success: true,
|
|
12211
|
-
message: `Invite sent to ${
|
|
12747
|
+
message: emailSent ? `Invite sent to ${email}.` : shouldSendEmail === false ? `Invite created for ${email}.` : `Invite created for ${email}, but email delivery failed.`,
|
|
12212
12748
|
token: inviteToken,
|
|
12213
|
-
inviteUrl: url
|
|
12749
|
+
inviteUrl: url,
|
|
12750
|
+
emailSent,
|
|
12751
|
+
...emailError ? { emailError } : {}
|
|
12214
12752
|
});
|
|
12215
12753
|
}
|
|
12216
12754
|
// ---------------------------------------------------------------------------
|
|
@@ -12244,6 +12782,13 @@ var AuthController = class {
|
|
|
12244
12782
|
400
|
|
12245
12783
|
);
|
|
12246
12784
|
}
|
|
12785
|
+
const currentSiteId = c.get("siteId");
|
|
12786
|
+
if (payload.siteId && currentSiteId && payload.siteId !== currentSiteId) {
|
|
12787
|
+
return c.json(
|
|
12788
|
+
{ error: true, code: "SITE_MISMATCH", message: "Token is not valid for this site." },
|
|
12789
|
+
403
|
|
12790
|
+
);
|
|
12791
|
+
}
|
|
12247
12792
|
const inviteeEmail = payload.sub;
|
|
12248
12793
|
const existing = await db.find({
|
|
12249
12794
|
collection: this.collection.slug,
|
|
@@ -12252,12 +12797,26 @@ var AuthController = class {
|
|
|
12252
12797
|
});
|
|
12253
12798
|
const existingUser = existing.docs[0];
|
|
12254
12799
|
const existingIsPending = existingUser?.status === "pending";
|
|
12255
|
-
if (existingUser && !existingIsPending) {
|
|
12800
|
+
if (existingUser && !existingIsPending && hasUsablePassword(existingUser.password)) {
|
|
12256
12801
|
return c.json(
|
|
12257
|
-
{
|
|
12802
|
+
{
|
|
12803
|
+
error: true,
|
|
12804
|
+
code: "INVITE_ALREADY_ACCEPTED",
|
|
12805
|
+
message: "This invitation has already been accepted. Please log in or use forgot password."
|
|
12806
|
+
},
|
|
12258
12807
|
409
|
|
12259
12808
|
);
|
|
12260
12809
|
}
|
|
12810
|
+
if (existingUser && payload.iat && typeof existingUser.invitedAt === "number" && payload.iat * 1e3 < existingUser.invitedAt - 2e3) {
|
|
12811
|
+
return c.json(
|
|
12812
|
+
{
|
|
12813
|
+
error: true,
|
|
12814
|
+
code: "INVITE_SUPERSEDED",
|
|
12815
|
+
message: "This invitation link has been superseded by a newer invitation. Please use the latest link."
|
|
12816
|
+
},
|
|
12817
|
+
400
|
|
12818
|
+
);
|
|
12819
|
+
}
|
|
12261
12820
|
const { token: _t, password: _p, ...extraFields } = body;
|
|
12262
12821
|
const hashedPassword = await hashPassword(body.password);
|
|
12263
12822
|
const user = existingUser ? await db.update({
|
|
@@ -12281,23 +12840,179 @@ var AuthController = class {
|
|
|
12281
12840
|
ip: c.get("clientIp"),
|
|
12282
12841
|
authSource: "local"
|
|
12283
12842
|
});
|
|
12284
|
-
const
|
|
12285
|
-
|
|
12286
|
-
|
|
12287
|
-
|
|
12843
|
+
const safeUser = this.sanitizeUser(user);
|
|
12844
|
+
resolveEmailTemplate({
|
|
12845
|
+
config,
|
|
12846
|
+
collection: this.collection,
|
|
12847
|
+
purpose: "welcome",
|
|
12848
|
+
args: {
|
|
12849
|
+
email: inviteeEmail,
|
|
12850
|
+
user: safeUser
|
|
12851
|
+
},
|
|
12852
|
+
db
|
|
12853
|
+
}).then(
|
|
12854
|
+
(emailResult) => sendEmail(
|
|
12855
|
+
config,
|
|
12856
|
+
toOutboundEmail(inviteeEmail, emailResult, {
|
|
12857
|
+
collection: this.collection.slug,
|
|
12858
|
+
purpose: "welcome",
|
|
12859
|
+
user: safeUser
|
|
12860
|
+
})
|
|
12861
|
+
)
|
|
12862
|
+
).catch(
|
|
12288
12863
|
(err) => getRequestLogger(c, "auth").error({
|
|
12289
12864
|
err,
|
|
12290
12865
|
msg: "Failed to send welcome email",
|
|
12291
12866
|
email: inviteeEmail
|
|
12292
12867
|
})
|
|
12293
12868
|
);
|
|
12294
|
-
const safeUser = this.sanitizeUser(user);
|
|
12295
12869
|
return c.json({ token: sessionToken, user: safeUser }, 201);
|
|
12296
12870
|
}
|
|
12871
|
+
// ---------------------------------------------------------------------------
|
|
12872
|
+
// GET /tokens/verify?token=...&purpose=invite|reset
|
|
12873
|
+
// Public. Validates token signature, expiration, database existence, and state.
|
|
12874
|
+
// ---------------------------------------------------------------------------
|
|
12875
|
+
async verifyToken(c) {
|
|
12876
|
+
const db = c.get("config").db;
|
|
12877
|
+
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
12878
|
+
const token = c.req.query("token");
|
|
12879
|
+
const purpose = c.req.query("purpose");
|
|
12880
|
+
if (!token) {
|
|
12881
|
+
return c.json(
|
|
12882
|
+
{ valid: false, code: "TOKEN_REQUIRED", message: "Token is required." },
|
|
12883
|
+
400
|
|
12884
|
+
);
|
|
12885
|
+
}
|
|
12886
|
+
let payload;
|
|
12887
|
+
try {
|
|
12888
|
+
payload = await verifyCollectionToken(token);
|
|
12889
|
+
} catch {
|
|
12890
|
+
return c.json(
|
|
12891
|
+
{
|
|
12892
|
+
valid: false,
|
|
12893
|
+
code: "TOKEN_EXPIRED_OR_INVALID",
|
|
12894
|
+
message: "Token is invalid or has expired."
|
|
12895
|
+
},
|
|
12896
|
+
200
|
|
12897
|
+
);
|
|
12898
|
+
}
|
|
12899
|
+
if (payload.collection !== this.collection.slug) {
|
|
12900
|
+
return c.json(
|
|
12901
|
+
{
|
|
12902
|
+
valid: false,
|
|
12903
|
+
code: "COLLECTION_MISMATCH",
|
|
12904
|
+
message: "Token does not belong to this collection."
|
|
12905
|
+
},
|
|
12906
|
+
200
|
|
12907
|
+
);
|
|
12908
|
+
}
|
|
12909
|
+
if (purpose && payload.purpose !== purpose) {
|
|
12910
|
+
return c.json(
|
|
12911
|
+
{
|
|
12912
|
+
valid: false,
|
|
12913
|
+
code: "PURPOSE_MISMATCH",
|
|
12914
|
+
message: `Token was issued for '${payload.purpose}', not '${purpose}'.`
|
|
12915
|
+
},
|
|
12916
|
+
200
|
|
12917
|
+
);
|
|
12918
|
+
}
|
|
12919
|
+
const currentSiteId = c.get("siteId");
|
|
12920
|
+
if (payload.siteId && currentSiteId && payload.siteId !== currentSiteId) {
|
|
12921
|
+
return c.json(
|
|
12922
|
+
{
|
|
12923
|
+
valid: false,
|
|
12924
|
+
code: "SITE_MISMATCH",
|
|
12925
|
+
message: "Token is not valid for this site."
|
|
12926
|
+
},
|
|
12927
|
+
200
|
|
12928
|
+
);
|
|
12929
|
+
}
|
|
12930
|
+
if (payload.purpose === "invite") {
|
|
12931
|
+
const result = await db.find({
|
|
12932
|
+
collection: this.collection.slug,
|
|
12933
|
+
where: { email: payload.email ?? payload.sub },
|
|
12934
|
+
limit: 1
|
|
12935
|
+
});
|
|
12936
|
+
const user = result.docs[0];
|
|
12937
|
+
if (!user) {
|
|
12938
|
+
return c.json(
|
|
12939
|
+
{
|
|
12940
|
+
valid: false,
|
|
12941
|
+
code: "USER_NOT_FOUND",
|
|
12942
|
+
message: "Invited account was not found."
|
|
12943
|
+
},
|
|
12944
|
+
200
|
|
12945
|
+
);
|
|
12946
|
+
}
|
|
12947
|
+
if (user._deleted || user.deletedAt) {
|
|
12948
|
+
return c.json(
|
|
12949
|
+
{
|
|
12950
|
+
valid: false,
|
|
12951
|
+
code: "USER_DELETED",
|
|
12952
|
+
message: "Account has been deleted."
|
|
12953
|
+
},
|
|
12954
|
+
200
|
|
12955
|
+
);
|
|
12956
|
+
}
|
|
12957
|
+
if (user.status !== "pending" && hasUsablePassword(user.password)) {
|
|
12958
|
+
return c.json(
|
|
12959
|
+
{
|
|
12960
|
+
valid: false,
|
|
12961
|
+
code: "INVITE_ALREADY_ACCEPTED",
|
|
12962
|
+
message: "This invitation has already been accepted."
|
|
12963
|
+
},
|
|
12964
|
+
200
|
|
12965
|
+
);
|
|
12966
|
+
}
|
|
12967
|
+
if (payload.iat && typeof user.invitedAt === "number" && payload.iat * 1e3 < user.invitedAt - 2e3) {
|
|
12968
|
+
return c.json(
|
|
12969
|
+
{
|
|
12970
|
+
valid: false,
|
|
12971
|
+
code: "INVITE_SUPERSEDED",
|
|
12972
|
+
message: "This invitation link has been superseded by a newer one."
|
|
12973
|
+
},
|
|
12974
|
+
200
|
|
12975
|
+
);
|
|
12976
|
+
}
|
|
12977
|
+
return c.json({
|
|
12978
|
+
valid: true,
|
|
12979
|
+
email: user.email,
|
|
12980
|
+
collection: this.collection.slug
|
|
12981
|
+
});
|
|
12982
|
+
}
|
|
12983
|
+
if (payload.purpose === "reset") {
|
|
12984
|
+
const result = await db.find({
|
|
12985
|
+
collection: this.collection.slug,
|
|
12986
|
+
where: { id: payload.sub },
|
|
12987
|
+
limit: 1
|
|
12988
|
+
});
|
|
12989
|
+
const user = result.docs[0];
|
|
12990
|
+
if (!user || user._deleted || user.deletedAt) {
|
|
12991
|
+
return c.json(
|
|
12992
|
+
{
|
|
12993
|
+
valid: false,
|
|
12994
|
+
code: "USER_NOT_FOUND",
|
|
12995
|
+
message: "Account was not found."
|
|
12996
|
+
},
|
|
12997
|
+
200
|
|
12998
|
+
);
|
|
12999
|
+
}
|
|
13000
|
+
return c.json({
|
|
13001
|
+
valid: true,
|
|
13002
|
+
email: user.email,
|
|
13003
|
+
collection: this.collection.slug
|
|
13004
|
+
});
|
|
13005
|
+
}
|
|
13006
|
+
return c.json({
|
|
13007
|
+
valid: true,
|
|
13008
|
+
email: payload.email,
|
|
13009
|
+
collection: this.collection.slug
|
|
13010
|
+
});
|
|
13011
|
+
}
|
|
12297
13012
|
};
|
|
12298
13013
|
|
|
12299
13014
|
// src/controllers/admin-auth.controller.ts
|
|
12300
|
-
import { randomBytes as
|
|
13015
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
12301
13016
|
import { TextEncoder as TextEncoder2 } from "util";
|
|
12302
13017
|
import {
|
|
12303
13018
|
SignJWT as SignJWT2,
|
|
@@ -12512,7 +13227,7 @@ var AdminAuthController = class {
|
|
|
12512
13227
|
access.roles,
|
|
12513
13228
|
access.data
|
|
12514
13229
|
),
|
|
12515
|
-
password: await hashPassword(
|
|
13230
|
+
password: await hashPassword(randomBytes3(32).toString("hex"))
|
|
12516
13231
|
}
|
|
12517
13232
|
});
|
|
12518
13233
|
} else if (user) {
|
|
@@ -12599,7 +13314,7 @@ var AdminAuthController = class {
|
|
|
12599
13314
|
}
|
|
12600
13315
|
async buildOidcStartUrl(provider, returnTo, siteId, origin) {
|
|
12601
13316
|
const discovery = await this.getOidcDiscovery(provider);
|
|
12602
|
-
const nonce =
|
|
13317
|
+
const nonce = randomBytes3(16).toString("hex");
|
|
12603
13318
|
const state = await this.signState({
|
|
12604
13319
|
providerId: provider.id,
|
|
12605
13320
|
returnTo,
|
|
@@ -15422,6 +16137,31 @@ function generateOpenApi(config) {
|
|
|
15422
16137
|
spec.paths[`${path}/accept-invite`] = {
|
|
15423
16138
|
post: publicAuthPost("Accept an invitation")
|
|
15424
16139
|
};
|
|
16140
|
+
spec.paths[`${path}/tokens/verify`] = {
|
|
16141
|
+
get: {
|
|
16142
|
+
tags: [collectionTag],
|
|
16143
|
+
summary: `Verify an invite or password reset token`,
|
|
16144
|
+
parameters: [
|
|
16145
|
+
{
|
|
16146
|
+
name: "token",
|
|
16147
|
+
in: "query",
|
|
16148
|
+
required: true,
|
|
16149
|
+
schema: { type: "string" }
|
|
16150
|
+
},
|
|
16151
|
+
{
|
|
16152
|
+
name: "purpose",
|
|
16153
|
+
in: "query",
|
|
16154
|
+
required: false,
|
|
16155
|
+
schema: { type: "string", enum: ["invite", "reset"] }
|
|
16156
|
+
}
|
|
16157
|
+
],
|
|
16158
|
+
responses: {
|
|
16159
|
+
200: {
|
|
16160
|
+
description: "Token verification result"
|
|
16161
|
+
}
|
|
16162
|
+
}
|
|
16163
|
+
}
|
|
16164
|
+
};
|
|
15425
16165
|
spec.paths[`${path}/{id}/change-password`] = {
|
|
15426
16166
|
post: {
|
|
15427
16167
|
tags: [collectionTag],
|
|
@@ -16032,7 +16772,7 @@ function registerRoutes(app, config) {
|
|
|
16032
16772
|
return resolveBooleanAccess(requestConfig, access, accessArgs);
|
|
16033
16773
|
};
|
|
16034
16774
|
const filteredCollections = await Promise.all(
|
|
16035
|
-
collections.filter((col) => !col.slug
|
|
16775
|
+
collections.filter((col) => !INTERNAL_SYSTEM_COLLECTIONS.has(col.slug) && (!siteId || col.shared || !col.siteId || col.siteId === siteId)).map(async (col) => ({
|
|
16036
16776
|
slug: col.slug,
|
|
16037
16777
|
labels: col.labels,
|
|
16038
16778
|
shared: !!col.shared,
|
|
@@ -16174,7 +16914,7 @@ function registerRoutes(app, config) {
|
|
|
16174
16914
|
model: effectiveAi?.model
|
|
16175
16915
|
},
|
|
16176
16916
|
trash: {
|
|
16177
|
-
enabled: collections.some((col) => !col.slug
|
|
16917
|
+
enabled: collections.some((col) => !INTERNAL_SYSTEM_COLLECTIONS.has(col.slug) && resolveTrashConfig(col, requestConfig).enabled),
|
|
16178
16918
|
retentionDays: requestConfig.trash?.retentionDays !== void 0 ? requestConfig.trash.retentionDays : 30,
|
|
16179
16919
|
allowPermanentDelete: requestConfig.trash?.allowPermanentDelete !== false
|
|
16180
16920
|
},
|
|
@@ -16185,7 +16925,7 @@ function registerRoutes(app, config) {
|
|
|
16185
16925
|
uploadCollectionConfigured: requestConfig.collections.some((collection) => !!collection.upload),
|
|
16186
16926
|
trashPurgeOverdue: await (async () => {
|
|
16187
16927
|
const hasRetention = collections.some(
|
|
16188
|
-
(col) => !col.slug
|
|
16928
|
+
(col) => !INTERNAL_SYSTEM_COLLECTIONS.has(col.slug) && resolveTrashConfig(col, requestConfig).retentionDays !== null
|
|
16189
16929
|
);
|
|
16190
16930
|
if (!hasRetention || !requestConfig.db) return false;
|
|
16191
16931
|
try {
|
|
@@ -16823,6 +17563,7 @@ function registerRoutes(app, config) {
|
|
|
16823
17563
|
app.post(`${path}/reset-password`, (c) => authController.resetPassword(c));
|
|
16824
17564
|
app.post(`${path}/invite`, requireAuth(config), (c) => authController.invite(c));
|
|
16825
17565
|
app.post(`${path}/accept-invite`, (c) => authController.acceptInvite(c));
|
|
17566
|
+
app.get(`${path}/tokens/verify`, (c) => authController.verifyToken(c));
|
|
16826
17567
|
}
|
|
16827
17568
|
const auditController = new AuditController();
|
|
16828
17569
|
for (const collection of config.collections) {
|
|
@@ -16867,6 +17608,126 @@ function registerRoutes(app, config) {
|
|
|
16867
17608
|
app.get(path, (c) => controller.get(c));
|
|
16868
17609
|
app.patch(path, (c) => controller.update(c));
|
|
16869
17610
|
}
|
|
17611
|
+
const testEmailRateLimitMap = /* @__PURE__ */ new Map();
|
|
17612
|
+
const TEST_EMAIL_MAX = 5;
|
|
17613
|
+
const TEST_EMAIL_WINDOW_MS = 6e4;
|
|
17614
|
+
const handleTestEmailDispatch = async (c) => {
|
|
17615
|
+
const user = c.get("user");
|
|
17616
|
+
const adminCol = config.collections.find((col) => col.slug === "__admins") || config.collections.find((col) => col.auth);
|
|
17617
|
+
if (!isUserAdmin(user, adminCol)) {
|
|
17618
|
+
return c.json({ error: true, message: "Forbidden: Admin access required to dispatch test emails" }, 403);
|
|
17619
|
+
}
|
|
17620
|
+
const userId = String(user?.id ?? user?.sub ?? "unknown");
|
|
17621
|
+
const now2 = Date.now();
|
|
17622
|
+
const window2 = (testEmailRateLimitMap.get(userId) ?? []).filter(
|
|
17623
|
+
(ts) => now2 - ts < TEST_EMAIL_WINDOW_MS
|
|
17624
|
+
);
|
|
17625
|
+
if (window2.length >= TEST_EMAIL_MAX) {
|
|
17626
|
+
return c.json(
|
|
17627
|
+
{ error: true, message: `Rate limit exceeded: maximum ${TEST_EMAIL_MAX} test emails per minute.` },
|
|
17628
|
+
429
|
|
17629
|
+
);
|
|
17630
|
+
}
|
|
17631
|
+
window2.push(now2);
|
|
17632
|
+
testEmailRateLimitMap.set(userId, window2);
|
|
17633
|
+
const body = await c.req.json().catch(() => ({}));
|
|
17634
|
+
const {
|
|
17635
|
+
to,
|
|
17636
|
+
subject,
|
|
17637
|
+
html,
|
|
17638
|
+
format = "html",
|
|
17639
|
+
externalTemplateId,
|
|
17640
|
+
purpose = "invite",
|
|
17641
|
+
collectionSlug = "users"
|
|
17642
|
+
} = body;
|
|
17643
|
+
if (!to || typeof to !== "string" || !to.includes("@")) {
|
|
17644
|
+
return c.json({ error: true, message: "A valid recipient email address is required" }, 400);
|
|
17645
|
+
}
|
|
17646
|
+
const siteName = config.admin?.branding?.logoText || "Dyrected";
|
|
17647
|
+
let appUrl = config.admin?.siteUrl;
|
|
17648
|
+
if (!appUrl) {
|
|
17649
|
+
try {
|
|
17650
|
+
appUrl = new URL(c.req.url).origin;
|
|
17651
|
+
} catch {
|
|
17652
|
+
appUrl = "http://localhost:3000";
|
|
17653
|
+
}
|
|
17654
|
+
}
|
|
17655
|
+
const targetCol = config.collections.find((x) => x.slug === collectionSlug);
|
|
17656
|
+
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));
|
|
17657
|
+
const mockData = {
|
|
17658
|
+
"{{url}}": `${appUrl}/auth/setup-password?token=mock_test_token_${Date.now()}`,
|
|
17659
|
+
"{{token}}": `mock_test_token_${Date.now()}`,
|
|
17660
|
+
"{{email}}": to,
|
|
17661
|
+
"{{collectionLabel}}": resolvedCollectionLabel,
|
|
17662
|
+
"{{siteName}}": siteName,
|
|
17663
|
+
"{{user.name}}": typeof user?.name === "string" ? user.name : "Test Recipient",
|
|
17664
|
+
"{{user.first_name}}": typeof user?.first_name === "string" ? user.first_name : "Test",
|
|
17665
|
+
"{{user.email}}": to
|
|
17666
|
+
};
|
|
17667
|
+
try {
|
|
17668
|
+
let result;
|
|
17669
|
+
if (format === "external_template") {
|
|
17670
|
+
if (!externalTemplateId) {
|
|
17671
|
+
return c.json({ error: true, message: "External template ID is required when format is external_template" }, 400);
|
|
17672
|
+
}
|
|
17673
|
+
const variables = {
|
|
17674
|
+
email: to,
|
|
17675
|
+
url: `${appUrl}/auth/setup-password?token=mock_test_token_${Date.now()}`,
|
|
17676
|
+
token: `mock_test_token_${Date.now()}`,
|
|
17677
|
+
collection: collectionSlug,
|
|
17678
|
+
collectionLabel: resolvedCollectionLabel,
|
|
17679
|
+
siteName,
|
|
17680
|
+
user: {
|
|
17681
|
+
name: typeof user?.name === "string" ? user.name : "Test Recipient",
|
|
17682
|
+
first_name: typeof user?.first_name === "string" ? user.first_name : "Test",
|
|
17683
|
+
email: to
|
|
17684
|
+
}
|
|
17685
|
+
};
|
|
17686
|
+
let testSubject = subject || `[TEST] ${purpose} email`;
|
|
17687
|
+
for (const [tag, sample] of Object.entries(mockData)) {
|
|
17688
|
+
testSubject = testSubject.split(tag).join(sample);
|
|
17689
|
+
}
|
|
17690
|
+
result = await sendEmail(config, {
|
|
17691
|
+
to,
|
|
17692
|
+
type: "template",
|
|
17693
|
+
template: externalTemplateId,
|
|
17694
|
+
variables,
|
|
17695
|
+
subject: testSubject,
|
|
17696
|
+
purpose,
|
|
17697
|
+
collection: collectionSlug
|
|
17698
|
+
});
|
|
17699
|
+
} else {
|
|
17700
|
+
let finalSubject = subject || `[TEST] ${purpose} email`;
|
|
17701
|
+
let finalHtml = html || "";
|
|
17702
|
+
for (const [tag, sample] of Object.entries(mockData)) {
|
|
17703
|
+
finalSubject = finalSubject.split(tag).join(sample);
|
|
17704
|
+
finalHtml = finalHtml.split(tag).join(sample);
|
|
17705
|
+
}
|
|
17706
|
+
result = await sendEmail(config, {
|
|
17707
|
+
to,
|
|
17708
|
+
type: "html",
|
|
17709
|
+
subject: finalSubject,
|
|
17710
|
+
html: finalHtml,
|
|
17711
|
+
purpose,
|
|
17712
|
+
collection: collectionSlug
|
|
17713
|
+
});
|
|
17714
|
+
}
|
|
17715
|
+
return c.json({
|
|
17716
|
+
success: true,
|
|
17717
|
+
message: `Test email sent to ${to}`,
|
|
17718
|
+
previewUrl: result?.previewUrl
|
|
17719
|
+
});
|
|
17720
|
+
} catch (err) {
|
|
17721
|
+
getConfigLogger(config, "email").error({
|
|
17722
|
+
err,
|
|
17723
|
+
msg: "Failed to dispatch test email",
|
|
17724
|
+
to
|
|
17725
|
+
});
|
|
17726
|
+
return c.json({ error: true, message: err.message || "Failed to send test email" }, 500);
|
|
17727
|
+
}
|
|
17728
|
+
};
|
|
17729
|
+
app.post("/api/__email_templates/test", requireAuth(config), handleTestEmailDispatch);
|
|
17730
|
+
app.post("/api/email-templates/test", requireAuth(config), handleTestEmailDispatch);
|
|
16870
17731
|
if (!process.env.DYRECTED_JWT_SECRET) {
|
|
16871
17732
|
getConfigLogger(config, "router").warn({
|
|
16872
17733
|
msg: "DYRECTED_JWT_SECRET is not set; token-mode live preview is signing with an insecure default"
|
|
@@ -17321,6 +18182,114 @@ var AUTH_SESSIONS_COLLECTION_CONFIG = {
|
|
|
17321
18182
|
},
|
|
17322
18183
|
admin: { hidden: true }
|
|
17323
18184
|
};
|
|
18185
|
+
var EMAIL_TEMPLATES_COLLECTION_SLUG = "__email_templates";
|
|
18186
|
+
var EMAIL_TEMPLATES_COLLECTION_CONFIG = {
|
|
18187
|
+
slug: EMAIL_TEMPLATES_COLLECTION_SLUG,
|
|
18188
|
+
labels: { singular: "Email Template", plural: "Email Templates" },
|
|
18189
|
+
detail: false,
|
|
18190
|
+
fields: [
|
|
18191
|
+
{
|
|
18192
|
+
name: "collectionSlug",
|
|
18193
|
+
type: "select",
|
|
18194
|
+
label: "Applies To",
|
|
18195
|
+
required: true,
|
|
18196
|
+
defaultValue: "*",
|
|
18197
|
+
options: [
|
|
18198
|
+
{ label: "Global (All Users & Collections)", value: "*" }
|
|
18199
|
+
],
|
|
18200
|
+
admin: {
|
|
18201
|
+
description: "Choose which user collection this template applies to, or 'Global' for all users."
|
|
18202
|
+
}
|
|
18203
|
+
},
|
|
18204
|
+
{
|
|
18205
|
+
name: "purpose",
|
|
18206
|
+
type: "select",
|
|
18207
|
+
label: "Purpose",
|
|
18208
|
+
required: true,
|
|
18209
|
+
options: [
|
|
18210
|
+
{ label: "Invite", value: "invite" },
|
|
18211
|
+
{ label: "Password Reset", value: "resetPassword" },
|
|
18212
|
+
{ label: "Welcome", value: "welcome" },
|
|
18213
|
+
{ label: "Password Changed", value: "passwordChanged" }
|
|
18214
|
+
],
|
|
18215
|
+
admin: { width: "50%" }
|
|
18216
|
+
},
|
|
18217
|
+
{
|
|
18218
|
+
name: "format",
|
|
18219
|
+
type: "select",
|
|
18220
|
+
label: "Format",
|
|
18221
|
+
required: true,
|
|
18222
|
+
defaultValue: "html",
|
|
18223
|
+
options: [
|
|
18224
|
+
{ label: "HTML", value: "html" },
|
|
18225
|
+
{ label: "External Template", value: "external_template" }
|
|
18226
|
+
],
|
|
18227
|
+
admin: { width: "50%" }
|
|
18228
|
+
},
|
|
18229
|
+
{
|
|
18230
|
+
name: "subject",
|
|
18231
|
+
type: "text",
|
|
18232
|
+
label: "Subject",
|
|
18233
|
+
admin: {
|
|
18234
|
+
description: "Subject line (optional when using external provider template)"
|
|
18235
|
+
}
|
|
18236
|
+
},
|
|
18237
|
+
{
|
|
18238
|
+
name: "rawHtml",
|
|
18239
|
+
type: "textarea",
|
|
18240
|
+
label: "HTML Content",
|
|
18241
|
+
admin: {
|
|
18242
|
+
description: "Raw HTML email content with {{variable}} tags"
|
|
18243
|
+
}
|
|
18244
|
+
},
|
|
18245
|
+
{
|
|
18246
|
+
name: "externalTemplateId",
|
|
18247
|
+
type: "text",
|
|
18248
|
+
label: "External Template ID",
|
|
18249
|
+
admin: {
|
|
18250
|
+
description: "Template ID or alias from Seamailer, Postmark, SendGrid, etc."
|
|
18251
|
+
}
|
|
18252
|
+
},
|
|
18253
|
+
{
|
|
18254
|
+
name: "active",
|
|
18255
|
+
type: "boolean",
|
|
18256
|
+
label: "Active",
|
|
18257
|
+
defaultValue: true,
|
|
18258
|
+
admin: {
|
|
18259
|
+
description: "When active, this template overrides code config. When disabled, falls back to code."
|
|
18260
|
+
}
|
|
18261
|
+
}
|
|
18262
|
+
],
|
|
18263
|
+
admin: {
|
|
18264
|
+
group: "Settings",
|
|
18265
|
+
icon: "Mail",
|
|
18266
|
+
groupBy: "collectionSlug",
|
|
18267
|
+
defaultColumns: ["purpose", "collectionSlug", "format", "active"]
|
|
18268
|
+
},
|
|
18269
|
+
hooks: {
|
|
18270
|
+
beforeChange: [
|
|
18271
|
+
async ({ data, req }) => {
|
|
18272
|
+
if (data && data.format === "html") {
|
|
18273
|
+
const siteName = req?.config?.admin?.branding?.logoText || "Dyrected";
|
|
18274
|
+
const defaults = getDefaultEmailTemplate(data.purpose || "invite", { siteName });
|
|
18275
|
+
if (!data.subject) {
|
|
18276
|
+
data.subject = defaults.subject;
|
|
18277
|
+
}
|
|
18278
|
+
if (!data.rawHtml) {
|
|
18279
|
+
data.rawHtml = defaults.rawHtml;
|
|
18280
|
+
}
|
|
18281
|
+
}
|
|
18282
|
+
return data;
|
|
18283
|
+
}
|
|
18284
|
+
]
|
|
18285
|
+
},
|
|
18286
|
+
access: {
|
|
18287
|
+
read: ({ user }) => isUserAdmin(user),
|
|
18288
|
+
create: ({ user }) => isUserAdmin(user),
|
|
18289
|
+
update: ({ user }) => isUserAdmin(user),
|
|
18290
|
+
delete: ({ user }) => isUserAdmin(user)
|
|
18291
|
+
}
|
|
18292
|
+
};
|
|
17324
18293
|
function normalizeConfig(config) {
|
|
17325
18294
|
const schemaAwareConfig = normalizeSchemaFragment(config);
|
|
17326
18295
|
assertValidDeclarativeHooksInConfig(schemaAwareConfig, "config");
|
|
@@ -17335,10 +18304,13 @@ function normalizeConfig(config) {
|
|
|
17335
18304
|
const needsTasks = (config.tasks?.length ?? 0) > 0;
|
|
17336
18305
|
const needsAuthSessions = collections.some((col) => !!col.auth);
|
|
17337
18306
|
const needsTrash = schemaAwareConfig.trash?.enabled === true || collections.some((col) => resolveTrashConfig(col, schemaAwareConfig).enabled);
|
|
17338
|
-
const
|
|
18307
|
+
const adminAuthCollection = getAdminAuthCollection({
|
|
17339
18308
|
collections,
|
|
17340
18309
|
adminAuth: schemaAwareConfig.adminAuth
|
|
17341
|
-
})
|
|
18310
|
+
});
|
|
18311
|
+
const adminAuthCollectionSlug = adminAuthCollection?.slug;
|
|
18312
|
+
const adminOnly = ({ user }) => isUserAdmin(user, adminAuthCollection);
|
|
18313
|
+
const adminOtherThanSelf = ({ user, id }) => isUserAdmin(user, adminAuthCollection) && user?.id !== id;
|
|
17342
18314
|
const normalizedCollections = collections.map((col) => {
|
|
17343
18315
|
let fields = col.fields || [];
|
|
17344
18316
|
const existingFieldNames = new Set(fields.map((f) => f.name));
|
|
@@ -17387,7 +18359,7 @@ function normalizeConfig(config) {
|
|
|
17387
18359
|
{ value: "viewer", label: "Viewer" }
|
|
17388
18360
|
],
|
|
17389
18361
|
access: {
|
|
17390
|
-
update:
|
|
18362
|
+
update: adminOnly
|
|
17391
18363
|
}
|
|
17392
18364
|
}
|
|
17393
18365
|
];
|
|
@@ -17405,7 +18377,7 @@ function normalizeConfig(config) {
|
|
|
17405
18377
|
{ value: "pending", label: "Pending" }
|
|
17406
18378
|
],
|
|
17407
18379
|
access: {
|
|
17408
|
-
update:
|
|
18380
|
+
update: adminOtherThanSelf
|
|
17409
18381
|
},
|
|
17410
18382
|
admin: {
|
|
17411
18383
|
condition: `!(data.roles && "admin" in data.roles) && data.role != "admin"`
|
|
@@ -17474,7 +18446,7 @@ function normalizeConfig(config) {
|
|
|
17474
18446
|
access: {
|
|
17475
18447
|
...field.access || {},
|
|
17476
18448
|
// Must be an admin; cannot edit own roles (no self-elevation).
|
|
17477
|
-
update:
|
|
18449
|
+
update: adminOtherThanSelf
|
|
17478
18450
|
}
|
|
17479
18451
|
};
|
|
17480
18452
|
}
|
|
@@ -17483,7 +18455,7 @@ function normalizeConfig(config) {
|
|
|
17483
18455
|
...field,
|
|
17484
18456
|
access: {
|
|
17485
18457
|
...field.access || {},
|
|
17486
|
-
update:
|
|
18458
|
+
update: adminOtherThanSelf
|
|
17487
18459
|
}
|
|
17488
18460
|
};
|
|
17489
18461
|
}
|
|
@@ -17575,6 +18547,52 @@ function normalizeConfig(config) {
|
|
|
17575
18547
|
if (needsTrash && !normalizedCollections.some((col) => col.slug === TRASH_COLLECTION)) {
|
|
17576
18548
|
systemCollections.push(TRASH_COLLECTION_CONFIG);
|
|
17577
18549
|
}
|
|
18550
|
+
const needsEmailTemplates = schemaAwareConfig.email?.adminEditable === true;
|
|
18551
|
+
if (needsEmailTemplates && !normalizedCollections.some((col) => col.slug === EMAIL_TEMPLATES_COLLECTION_SLUG)) {
|
|
18552
|
+
const adminCol = getAdminAuthCollection(schemaAwareConfig);
|
|
18553
|
+
const customAccess = schemaAwareConfig.email?.access;
|
|
18554
|
+
const resolveOpAccess = (op) => {
|
|
18555
|
+
if (typeof customAccess === "function") return customAccess;
|
|
18556
|
+
if (customAccess && typeof customAccess === "object" && customAccess[op]) return customAccess[op];
|
|
18557
|
+
return ({ user }) => isUserAdmin(user, adminCol);
|
|
18558
|
+
};
|
|
18559
|
+
const customPurposes = schemaAwareConfig.email?.purposes ?? [];
|
|
18560
|
+
const purposeOptions = [
|
|
18561
|
+
{ label: "Invite", value: "invite" },
|
|
18562
|
+
{ label: "Password Reset", value: "resetPassword" },
|
|
18563
|
+
{ label: "Welcome", value: "welcome" },
|
|
18564
|
+
{ label: "Password Changed", value: "passwordChanged" },
|
|
18565
|
+
...customPurposes
|
|
18566
|
+
];
|
|
18567
|
+
const targetableCollections = normalizedCollections.filter(
|
|
18568
|
+
(c) => !c.slug.startsWith("__")
|
|
18569
|
+
);
|
|
18570
|
+
const audienceOptions = [
|
|
18571
|
+
{ label: "Global (All Collections)", value: "*" },
|
|
18572
|
+
...targetableCollections.map((c) => ({
|
|
18573
|
+
label: `${c.labels?.singular || c.labels?.plural || c.slug} (${c.slug})`,
|
|
18574
|
+
value: c.slug
|
|
18575
|
+
}))
|
|
18576
|
+
];
|
|
18577
|
+
systemCollections.push({
|
|
18578
|
+
...EMAIL_TEMPLATES_COLLECTION_CONFIG,
|
|
18579
|
+
fields: EMAIL_TEMPLATES_COLLECTION_CONFIG.fields.map((f) => {
|
|
18580
|
+
if (f.name === "collectionSlug") {
|
|
18581
|
+
return { ...f, options: audienceOptions };
|
|
18582
|
+
}
|
|
18583
|
+
if (f.name === "purpose") {
|
|
18584
|
+
return { ...f, options: purposeOptions };
|
|
18585
|
+
}
|
|
18586
|
+
return f;
|
|
18587
|
+
}),
|
|
18588
|
+
access: {
|
|
18589
|
+
read: resolveOpAccess("read"),
|
|
18590
|
+
create: resolveOpAccess("create"),
|
|
18591
|
+
update: resolveOpAccess("update"),
|
|
18592
|
+
delete: resolveOpAccess("delete")
|
|
18593
|
+
}
|
|
18594
|
+
});
|
|
18595
|
+
}
|
|
17578
18596
|
return {
|
|
17579
18597
|
...schemaAwareConfig,
|
|
17580
18598
|
collections: [...normalizedCollections, ...systemCollections],
|
|
@@ -17888,8 +18906,10 @@ export {
|
|
|
17888
18906
|
AUTH_SESSIONS_COLLECTION,
|
|
17889
18907
|
AuthController,
|
|
17890
18908
|
CollectionController,
|
|
18909
|
+
DEFAULT_INVITE_EXPIRY,
|
|
17891
18910
|
DEFAULT_LOCK_TIME_MS,
|
|
17892
18911
|
DEFAULT_MAX_LOGIN_ATTEMPTS,
|
|
18912
|
+
DEFAULT_RESET_PASSWORD_EXPIRY,
|
|
17893
18913
|
DEFAULT_SESSION_EXPIRY,
|
|
17894
18914
|
EmbeddingService,
|
|
17895
18915
|
GlobalController,
|
|
@@ -17921,13 +18941,16 @@ export {
|
|
|
17921
18941
|
getAIModel,
|
|
17922
18942
|
getAuthSession,
|
|
17923
18943
|
getConfigLogger,
|
|
18944
|
+
getDbEmailTemplateOverride,
|
|
17924
18945
|
getEmbeddingModel,
|
|
17925
18946
|
getLockedUntilMs,
|
|
17926
18947
|
getObservabilityRuntime,
|
|
17927
18948
|
getRequestLogger,
|
|
17928
18949
|
hasEmbeddingApiKey,
|
|
18950
|
+
hasUsablePassword,
|
|
17929
18951
|
hashContent,
|
|
17930
18952
|
hashPassword,
|
|
18953
|
+
interpolateVariables,
|
|
17931
18954
|
isAuthSessionActive,
|
|
17932
18955
|
issueAuthSessionToken,
|
|
17933
18956
|
normalizeFieldValue,
|
|
@@ -17936,7 +18959,10 @@ export {
|
|
|
17936
18959
|
registerRoutes,
|
|
17937
18960
|
renderPrometheusMetrics,
|
|
17938
18961
|
resolveAuthLockoutConfig,
|
|
18962
|
+
resolveEmailTemplate,
|
|
18963
|
+
resolveInviteTokenExpiry,
|
|
17939
18964
|
resolveObservabilityConfig,
|
|
18965
|
+
resolveResetPasswordTokenExpiry,
|
|
17940
18966
|
resolveSessionTokenExpiry,
|
|
17941
18967
|
revokeAllAuthSessions,
|
|
17942
18968
|
revokeAuthSession,
|
|
@@ -17946,6 +18972,7 @@ export {
|
|
|
17946
18972
|
serializeViewForApi,
|
|
17947
18973
|
shouldSampleRequest,
|
|
17948
18974
|
signCollectionToken,
|
|
18975
|
+
toOutboundEmail,
|
|
17949
18976
|
touchAuthSession,
|
|
17950
18977
|
validatePayloadAgainstFields,
|
|
17951
18978
|
verifyCollectionToken,
|