@dyrected/core 2.19.0 → 2.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{app-CXF4MdJc.d.cts → app-v23fNf58.d.cts} +171 -28
- package/dist/{app-CXF4MdJc.d.ts → app-v23fNf58.d.ts} +171 -28
- package/dist/index.cjs +511 -21
- package/dist/index.d.cts +73 -5
- package/dist/index.d.ts +73 -5
- package/dist/index.js +491 -21
- package/dist/server.cjs +1167 -136
- package/dist/server.d.cts +79 -29
- package/dist/server.d.ts +79 -29
- package/dist/server.js +1158 -136
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3011,6 +3011,14 @@ async function executeFieldAfterRead(fields, doc, user, db) {
|
|
|
3011
3011
|
|
|
3012
3012
|
// src/trash.ts
|
|
3013
3013
|
var TRASH_COLLECTION = "__trash";
|
|
3014
|
+
var INTERNAL_SYSTEM_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
3015
|
+
"__audit",
|
|
3016
|
+
"__sessions",
|
|
3017
|
+
"__task_locks",
|
|
3018
|
+
"__trash",
|
|
3019
|
+
"__workflow_history",
|
|
3020
|
+
"__lifecycle_events"
|
|
3021
|
+
]);
|
|
3014
3022
|
var TRASH_COLLECTION_CONFIG = {
|
|
3015
3023
|
slug: TRASH_COLLECTION,
|
|
3016
3024
|
labels: { singular: "Trash entry", plural: "Trash entries" },
|
|
@@ -3102,7 +3110,7 @@ function assertValidTrashInConfig(config, source = "config") {
|
|
|
3102
3110
|
if (col.slug === "trash") {
|
|
3103
3111
|
throw new Error(`Collection slug "trash" is reserved by Dyrected for the trash system.`);
|
|
3104
3112
|
}
|
|
3105
|
-
const isSystem = col.slug
|
|
3113
|
+
const isSystem = INTERNAL_SYSTEM_COLLECTIONS.has(col.slug);
|
|
3106
3114
|
if (isSystem && col.trash) {
|
|
3107
3115
|
throw new Error(`System collection "${col.slug}" cannot enable trash.`);
|
|
3108
3116
|
}
|
|
@@ -3481,7 +3489,7 @@ function createTaskRunner(config) {
|
|
|
3481
3489
|
throw error;
|
|
3482
3490
|
}
|
|
3483
3491
|
}
|
|
3484
|
-
async function execute(task,
|
|
3492
|
+
async function execute(task, row2, scheduledFor) {
|
|
3485
3493
|
const started = Date.now();
|
|
3486
3494
|
const lockMs = (task.lockTimeout ?? DEFAULT_LOCK_TIMEOUT_SECONDS) * 1e3;
|
|
3487
3495
|
const token = `${instanceId}:${started}`;
|
|
@@ -3489,7 +3497,7 @@ function createTaskRunner(config) {
|
|
|
3489
3497
|
id: { equals: task.name },
|
|
3490
3498
|
lockedUntil: { less_than: started }
|
|
3491
3499
|
};
|
|
3492
|
-
if (scheduledFor) where.lastScheduledFor = { equals:
|
|
3500
|
+
if (scheduledFor) where.lastScheduledFor = { equals: row2.lastScheduledFor };
|
|
3493
3501
|
const claimed = await db.update({
|
|
3494
3502
|
collection: TASK_LOCKS_COLLECTION,
|
|
3495
3503
|
where,
|
|
@@ -3550,21 +3558,21 @@ function createTaskRunner(config) {
|
|
|
3550
3558
|
async runTask(name) {
|
|
3551
3559
|
const task = tasks.get(name);
|
|
3552
3560
|
if (!task) throw new Error(`Unknown task "${name}".`);
|
|
3553
|
-
const
|
|
3554
|
-
return track(execute(task,
|
|
3561
|
+
const row2 = await ensureLockRow(name, /* @__PURE__ */ new Date());
|
|
3562
|
+
return track(execute(task, row2, void 0));
|
|
3555
3563
|
},
|
|
3556
3564
|
async runDue(now2 = /* @__PURE__ */ new Date()) {
|
|
3557
3565
|
const results = [];
|
|
3558
3566
|
for (const task of tasks.values()) {
|
|
3559
3567
|
if (!task.schedule) continue;
|
|
3560
|
-
const
|
|
3561
|
-
const reference = new Date(String(
|
|
3568
|
+
const row2 = await ensureLockRow(task.name, now2);
|
|
3569
|
+
const reference = new Date(String(row2.lastScheduledFor));
|
|
3562
3570
|
const due = Number.isNaN(reference.getTime()) ? true : nextCronRun(task.schedule, reference).getTime() <= now2.getTime();
|
|
3563
3571
|
if (!due) {
|
|
3564
3572
|
results.push({ name: task.name, status: "skipped", reason: "not-due", durationMs: 0 });
|
|
3565
3573
|
continue;
|
|
3566
3574
|
}
|
|
3567
|
-
results.push(await track(execute(task,
|
|
3575
|
+
results.push(await track(execute(task, row2, floorToMinute(now2))));
|
|
3568
3576
|
}
|
|
3569
3577
|
return results;
|
|
3570
3578
|
},
|
|
@@ -4079,13 +4087,34 @@ function getAdminRoleForCollection(collection) {
|
|
|
4079
4087
|
}
|
|
4080
4088
|
return "admin";
|
|
4081
4089
|
}
|
|
4090
|
+
var STANDARD_ADMIN_ROLES = /* @__PURE__ */ new Set([
|
|
4091
|
+
"admin",
|
|
4092
|
+
"super_admin",
|
|
4093
|
+
"superadmin",
|
|
4094
|
+
"super admin",
|
|
4095
|
+
"super-admin",
|
|
4096
|
+
"owner"
|
|
4097
|
+
]);
|
|
4082
4098
|
function isUserAdmin(user, collection) {
|
|
4083
4099
|
if (!user) return false;
|
|
4084
|
-
const
|
|
4085
|
-
if (
|
|
4100
|
+
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] : [];
|
|
4101
|
+
if (rawRoles.length === 0) return false;
|
|
4102
|
+
const roles = rawRoles.filter((r) => typeof r === "string");
|
|
4086
4103
|
const configuredRole = getAdminRoleForCollection(collection);
|
|
4087
|
-
|
|
4088
|
-
|
|
4104
|
+
for (const r of roles) {
|
|
4105
|
+
if (r === configuredRole || r.toLowerCase() === configuredRole.toLowerCase()) {
|
|
4106
|
+
return true;
|
|
4107
|
+
}
|
|
4108
|
+
const normalized = r.trim().toLowerCase();
|
|
4109
|
+
if (STANDARD_ADMIN_ROLES.has(normalized)) {
|
|
4110
|
+
return true;
|
|
4111
|
+
}
|
|
4112
|
+
const stripped = normalized.replace(/[\s_-]+/g, "");
|
|
4113
|
+
if (stripped === "admin" || stripped === "superadmin" || stripped === "owner") {
|
|
4114
|
+
return true;
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
return false;
|
|
4089
4118
|
}
|
|
4090
4119
|
|
|
4091
4120
|
// src/utils/block-references.ts
|
|
@@ -4308,6 +4337,245 @@ function generateDocumentId(collectionOrSlug) {
|
|
|
4308
4337
|
return generateId({ prefix, type });
|
|
4309
4338
|
}
|
|
4310
4339
|
|
|
4340
|
+
// src/services/email-template.ts
|
|
4341
|
+
var emailTokens = {
|
|
4342
|
+
colors: {
|
|
4343
|
+
canvas: "#f6f7f2",
|
|
4344
|
+
surface: "#ffffff",
|
|
4345
|
+
text: "#171717",
|
|
4346
|
+
muted: "#62665b",
|
|
4347
|
+
subtle: "#8a8f82",
|
|
4348
|
+
border: "#dde0d7",
|
|
4349
|
+
accent: "#b6ff2e",
|
|
4350
|
+
code: "#f1f3ec",
|
|
4351
|
+
codeBorder: "#e2e5dc",
|
|
4352
|
+
dangerSurface: "#fff2f0",
|
|
4353
|
+
dangerBorder: "#ffc9c2",
|
|
4354
|
+
dangerText: "#9f251b"
|
|
4355
|
+
},
|
|
4356
|
+
font: "Arial, 'Helvetica Neue', Helvetica, sans-serif",
|
|
4357
|
+
mono: "'Courier New', Courier, monospace",
|
|
4358
|
+
radius: { card: "12px", control: "6px" },
|
|
4359
|
+
width: "600px"
|
|
4360
|
+
};
|
|
4361
|
+
function escapeHtml(value) {
|
|
4362
|
+
return value.replace(
|
|
4363
|
+
/[&<>'"]/g,
|
|
4364
|
+
(character) => ({
|
|
4365
|
+
"&": "&",
|
|
4366
|
+
"<": "<",
|
|
4367
|
+
">": ">",
|
|
4368
|
+
"'": "'",
|
|
4369
|
+
'"': """
|
|
4370
|
+
})[character]
|
|
4371
|
+
);
|
|
4372
|
+
}
|
|
4373
|
+
function safeHttpUrl(value) {
|
|
4374
|
+
if (value.startsWith("{{") && value.endsWith("}}")) {
|
|
4375
|
+
return value;
|
|
4376
|
+
}
|
|
4377
|
+
try {
|
|
4378
|
+
const url = new URL(value);
|
|
4379
|
+
return url.protocol === "https:" || url.protocol === "http:" ? escapeHtml(url.toString()) : void 0;
|
|
4380
|
+
} catch {
|
|
4381
|
+
return void 0;
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
function heading(content) {
|
|
4385
|
+
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>`;
|
|
4386
|
+
}
|
|
4387
|
+
function paragraph(content, margin = "0 0 16px", size = "15px") {
|
|
4388
|
+
return `<p style="margin:${margin};font-family:${emailTokens.font};font-size:${size};line-height:1.6;color:${emailTokens.colors.muted}">${escapeHtml(content)}</p>`;
|
|
4389
|
+
}
|
|
4390
|
+
function sectionLabel(content) {
|
|
4391
|
+
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>`;
|
|
4392
|
+
}
|
|
4393
|
+
function divider() {
|
|
4394
|
+
return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%"><tr><td style="height:1px;background:${emailTokens.colors.border};font-size:0;line-height:0"> </td></tr></table>`;
|
|
4395
|
+
}
|
|
4396
|
+
function spacer(height = 16) {
|
|
4397
|
+
return table(row(" ", `height:${height}px;font-size:0;line-height:0`));
|
|
4398
|
+
}
|
|
4399
|
+
function row(content, cellStyle = "") {
|
|
4400
|
+
return `<tr><td style="${cellStyle}">${content}</td></tr>`;
|
|
4401
|
+
}
|
|
4402
|
+
function table(content, style = "width:100%") {
|
|
4403
|
+
return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="${style}">${content}</table>`;
|
|
4404
|
+
}
|
|
4405
|
+
function detailBox(content, monospace = false) {
|
|
4406
|
+
const font = monospace ? emailTokens.mono : emailTokens.font;
|
|
4407
|
+
return table(
|
|
4408
|
+
row(
|
|
4409
|
+
escapeHtml(content),
|
|
4410
|
+
`padding:14px 16px;font-family:${font};font-size:13px;line-height:1.5;font-weight:${monospace ? "400" : "700"};color:${emailTokens.colors.text};word-break:break-all`
|
|
4411
|
+
),
|
|
4412
|
+
`width:100%;background:${emailTokens.colors.code};border:1px solid ${emailTokens.colors.codeBorder};border-radius:${emailTokens.radius.control}`
|
|
4413
|
+
);
|
|
4414
|
+
}
|
|
4415
|
+
function eventList(events) {
|
|
4416
|
+
return table(
|
|
4417
|
+
events.map(
|
|
4418
|
+
({ label, value }) => row(
|
|
4419
|
+
`${sectionLabel(label)}${paragraph(value, "0")}`,
|
|
4420
|
+
`padding:12px 0;border-bottom:1px solid ${emailTokens.colors.border}`
|
|
4421
|
+
)
|
|
4422
|
+
).join("")
|
|
4423
|
+
);
|
|
4424
|
+
}
|
|
4425
|
+
function ctaButton(label, href) {
|
|
4426
|
+
const safeHref = safeHttpUrl(href);
|
|
4427
|
+
if (!safeHref) return "";
|
|
4428
|
+
return table(
|
|
4429
|
+
row(
|
|
4430
|
+
`<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>`,
|
|
4431
|
+
"padding:8px 0 24px"
|
|
4432
|
+
),
|
|
4433
|
+
"width:auto"
|
|
4434
|
+
);
|
|
4435
|
+
}
|
|
4436
|
+
function safeLinkDetailBox(href) {
|
|
4437
|
+
const safeHref = safeHttpUrl(href);
|
|
4438
|
+
if (!safeHref) return "";
|
|
4439
|
+
return detailBox(safeHref, true);
|
|
4440
|
+
}
|
|
4441
|
+
function alertBox(content) {
|
|
4442
|
+
return table(
|
|
4443
|
+
row(
|
|
4444
|
+
escapeHtml(content),
|
|
4445
|
+
`padding:13px 16px;font-family:${emailTokens.font};font-size:13px;line-height:1.5;color:${emailTokens.colors.dangerText}`
|
|
4446
|
+
),
|
|
4447
|
+
`width:100%;background:${emailTokens.colors.dangerSurface};border:1px solid ${emailTokens.colors.dangerBorder};border-radius:${emailTokens.radius.control}`
|
|
4448
|
+
);
|
|
4449
|
+
}
|
|
4450
|
+
function resolveBlocks(input) {
|
|
4451
|
+
if (Array.isArray(input)) {
|
|
4452
|
+
const parts = [];
|
|
4453
|
+
for (const item of input) {
|
|
4454
|
+
if (Array.isArray(item)) {
|
|
4455
|
+
for (const sub of item) {
|
|
4456
|
+
if (typeof sub === "string" && sub.length > 0) parts.push(sub);
|
|
4457
|
+
}
|
|
4458
|
+
} else if (typeof item === "string" && item.length > 0) {
|
|
4459
|
+
parts.push(item);
|
|
4460
|
+
}
|
|
4461
|
+
}
|
|
4462
|
+
return parts.join("\n");
|
|
4463
|
+
}
|
|
4464
|
+
return typeof input === "string" ? input : "";
|
|
4465
|
+
}
|
|
4466
|
+
function layout({ preheader, title, content, footer, siteName }) {
|
|
4467
|
+
const resolvedContent = resolveBlocks(content);
|
|
4468
|
+
const resolvedFooter = resolveBlocks(footer);
|
|
4469
|
+
return `<!doctype html>
|
|
4470
|
+
<html lang="en" dir="ltr" xmlns="http://www.w3.org/1999/xhtml">
|
|
4471
|
+
<head>
|
|
4472
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
4473
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4474
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
4475
|
+
<meta name="x-apple-disable-message-reformatting">
|
|
4476
|
+
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
|
|
4477
|
+
<meta name="color-scheme" content="light">
|
|
4478
|
+
<meta name="supported-color-schemes" content="light">
|
|
4479
|
+
<title>${escapeHtml(title)}</title>
|
|
4480
|
+
</head>
|
|
4481
|
+
<body style="margin:0;padding:0;background-color:${emailTokens.colors.canvas};word-spacing:normal;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%">
|
|
4482
|
+
<div style="display:none;max-height:0;max-width:0;overflow:hidden;opacity:0;mso-hide:all;color:transparent;font-size:1px;line-height:1px">
|
|
4483
|
+
${escapeHtml(preheader)}​͏‌   ͏‌
|
|
4484
|
+
</div>
|
|
4485
|
+
<center lang="en" style="width:100%;background-color:${emailTokens.colors.canvas}">
|
|
4486
|
+
${table(
|
|
4487
|
+
row(
|
|
4488
|
+
table(
|
|
4489
|
+
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(
|
|
4490
|
+
`${divider()}${paragraph(resolvedFooter, "20px 0 6px")}${paragraph("Privacy: this message contains account-related information; please avoid forwarding it.", "10px 0px 0px 0px", "12px")}`,
|
|
4491
|
+
"padding:0 32px 28px"
|
|
4492
|
+
),
|
|
4493
|
+
`width:100%;max-width:${emailTokens.width};background:${emailTokens.colors.surface};border:1px solid ${emailTokens.colors.border};border-radius:${emailTokens.radius.card};overflow:hidden`
|
|
4494
|
+
),
|
|
4495
|
+
"padding:32px 12px"
|
|
4496
|
+
),
|
|
4497
|
+
`width:100%;background:${emailTokens.colors.canvas}`
|
|
4498
|
+
)}
|
|
4499
|
+
</center>
|
|
4500
|
+
</body>
|
|
4501
|
+
</html>`;
|
|
4502
|
+
}
|
|
4503
|
+
function getDefaultEmailTemplate(purpose, options) {
|
|
4504
|
+
const siteName = options?.siteName ?? "Dyrected";
|
|
4505
|
+
switch (purpose) {
|
|
4506
|
+
case "invite":
|
|
4507
|
+
return {
|
|
4508
|
+
subject: `You've been invited to ${siteName}`,
|
|
4509
|
+
rawHtml: layout({
|
|
4510
|
+
siteName,
|
|
4511
|
+
preheader: `You've been invited to join ${siteName}.`,
|
|
4512
|
+
title: "You've been invited",
|
|
4513
|
+
content: [
|
|
4514
|
+
paragraph("Use the invitation link below to create your account. The link expires in 7 days."),
|
|
4515
|
+
ctaButton("Accept invitation", "{{url}}"),
|
|
4516
|
+
sectionLabel("Invitation link"),
|
|
4517
|
+
safeLinkDetailBox("{{url}}"),
|
|
4518
|
+
paragraph("If the button does not work, copy and paste the link above into your browser.", "12px 0 0")
|
|
4519
|
+
],
|
|
4520
|
+
footer: "If you weren't expecting this invitation, you can safely ignore this email."
|
|
4521
|
+
})
|
|
4522
|
+
};
|
|
4523
|
+
case "resetPassword":
|
|
4524
|
+
return {
|
|
4525
|
+
subject: "Reset your password",
|
|
4526
|
+
rawHtml: layout({
|
|
4527
|
+
siteName,
|
|
4528
|
+
preheader: `Reset your ${siteName} password.`,
|
|
4529
|
+
title: "Reset your password",
|
|
4530
|
+
content: [
|
|
4531
|
+
paragraph("We received a request to reset your password. The reset link expires in 1 hour."),
|
|
4532
|
+
ctaButton("Reset password", "{{url}}"),
|
|
4533
|
+
sectionLabel("Reset link"),
|
|
4534
|
+
safeLinkDetailBox("{{url}}"),
|
|
4535
|
+
paragraph("If the button does not work, copy and paste the link above into your browser.", "12px 0 0")
|
|
4536
|
+
],
|
|
4537
|
+
footer: "If you didn't request a password reset, you can safely ignore this email."
|
|
4538
|
+
})
|
|
4539
|
+
};
|
|
4540
|
+
case "welcome":
|
|
4541
|
+
return {
|
|
4542
|
+
subject: `Welcome to ${siteName} \u2014 your account is ready`,
|
|
4543
|
+
rawHtml: layout({
|
|
4544
|
+
siteName,
|
|
4545
|
+
preheader: `Your ${siteName} account is ready.`,
|
|
4546
|
+
title: "Welcome \u2014 your account is ready",
|
|
4547
|
+
content: [paragraph("Your account has been created. You can now log in with:"), detailBox("{{email}}")],
|
|
4548
|
+
footer: "If you didn't create this account, you can safely ignore this email."
|
|
4549
|
+
})
|
|
4550
|
+
};
|
|
4551
|
+
case "passwordChanged":
|
|
4552
|
+
return {
|
|
4553
|
+
subject: "Your password has been changed",
|
|
4554
|
+
rawHtml: layout({
|
|
4555
|
+
siteName,
|
|
4556
|
+
preheader: `Your ${siteName} password was changed.`,
|
|
4557
|
+
title: "Password changed",
|
|
4558
|
+
content: [
|
|
4559
|
+
paragraph("The password for your account has been changed."),
|
|
4560
|
+
alertBox("If you did not make this change, please contact support or reset your password immediately.")
|
|
4561
|
+
],
|
|
4562
|
+
footer: "Security notice: this confirms a recent credential change."
|
|
4563
|
+
})
|
|
4564
|
+
};
|
|
4565
|
+
default:
|
|
4566
|
+
return {
|
|
4567
|
+
subject: `Notification from ${siteName}`,
|
|
4568
|
+
rawHtml: layout({
|
|
4569
|
+
siteName,
|
|
4570
|
+
preheader: `Account notification from ${siteName}.`,
|
|
4571
|
+
title: "Account Notification",
|
|
4572
|
+
content: [paragraph(`This is an account notification from ${siteName}.`)],
|
|
4573
|
+
footer: "If you have any questions, please reach out to our team."
|
|
4574
|
+
})
|
|
4575
|
+
};
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
|
|
4311
4579
|
// src/utils/config.ts
|
|
4312
4580
|
var AUDIT_COLLECTION_SLUG = "__audit";
|
|
4313
4581
|
var SYSTEM_FIELDS = [
|
|
@@ -4449,6 +4717,114 @@ var AUTH_SESSIONS_COLLECTION_CONFIG = {
|
|
|
4449
4717
|
},
|
|
4450
4718
|
admin: { hidden: true }
|
|
4451
4719
|
};
|
|
4720
|
+
var EMAIL_TEMPLATES_COLLECTION_SLUG = "__email_templates";
|
|
4721
|
+
var EMAIL_TEMPLATES_COLLECTION_CONFIG = {
|
|
4722
|
+
slug: EMAIL_TEMPLATES_COLLECTION_SLUG,
|
|
4723
|
+
labels: { singular: "Email Template", plural: "Email Templates" },
|
|
4724
|
+
detail: false,
|
|
4725
|
+
fields: [
|
|
4726
|
+
{
|
|
4727
|
+
name: "collectionSlug",
|
|
4728
|
+
type: "select",
|
|
4729
|
+
label: "Applies To",
|
|
4730
|
+
required: true,
|
|
4731
|
+
defaultValue: "*",
|
|
4732
|
+
options: [
|
|
4733
|
+
{ label: "Global (All Users & Collections)", value: "*" }
|
|
4734
|
+
],
|
|
4735
|
+
admin: {
|
|
4736
|
+
description: "Choose which user collection this template applies to, or 'Global' for all users."
|
|
4737
|
+
}
|
|
4738
|
+
},
|
|
4739
|
+
{
|
|
4740
|
+
name: "purpose",
|
|
4741
|
+
type: "select",
|
|
4742
|
+
label: "Purpose",
|
|
4743
|
+
required: true,
|
|
4744
|
+
options: [
|
|
4745
|
+
{ label: "Invite", value: "invite" },
|
|
4746
|
+
{ label: "Password Reset", value: "resetPassword" },
|
|
4747
|
+
{ label: "Welcome", value: "welcome" },
|
|
4748
|
+
{ label: "Password Changed", value: "passwordChanged" }
|
|
4749
|
+
],
|
|
4750
|
+
admin: { width: "50%" }
|
|
4751
|
+
},
|
|
4752
|
+
{
|
|
4753
|
+
name: "format",
|
|
4754
|
+
type: "select",
|
|
4755
|
+
label: "Format",
|
|
4756
|
+
required: true,
|
|
4757
|
+
defaultValue: "html",
|
|
4758
|
+
options: [
|
|
4759
|
+
{ label: "HTML", value: "html" },
|
|
4760
|
+
{ label: "External Template", value: "external_template" }
|
|
4761
|
+
],
|
|
4762
|
+
admin: { width: "50%" }
|
|
4763
|
+
},
|
|
4764
|
+
{
|
|
4765
|
+
name: "subject",
|
|
4766
|
+
type: "text",
|
|
4767
|
+
label: "Subject",
|
|
4768
|
+
admin: {
|
|
4769
|
+
description: "Subject line (optional when using external provider template)"
|
|
4770
|
+
}
|
|
4771
|
+
},
|
|
4772
|
+
{
|
|
4773
|
+
name: "rawHtml",
|
|
4774
|
+
type: "textarea",
|
|
4775
|
+
label: "HTML Content",
|
|
4776
|
+
admin: {
|
|
4777
|
+
description: "Raw HTML email content with {{variable}} tags"
|
|
4778
|
+
}
|
|
4779
|
+
},
|
|
4780
|
+
{
|
|
4781
|
+
name: "externalTemplateId",
|
|
4782
|
+
type: "text",
|
|
4783
|
+
label: "External Template ID",
|
|
4784
|
+
admin: {
|
|
4785
|
+
description: "Template ID or alias from Seamailer, Postmark, SendGrid, etc."
|
|
4786
|
+
}
|
|
4787
|
+
},
|
|
4788
|
+
{
|
|
4789
|
+
name: "active",
|
|
4790
|
+
type: "boolean",
|
|
4791
|
+
label: "Active",
|
|
4792
|
+
defaultValue: true,
|
|
4793
|
+
admin: {
|
|
4794
|
+
description: "When active, this template overrides code config. When disabled, falls back to code."
|
|
4795
|
+
}
|
|
4796
|
+
}
|
|
4797
|
+
],
|
|
4798
|
+
admin: {
|
|
4799
|
+
group: "Settings",
|
|
4800
|
+
icon: "Mail",
|
|
4801
|
+
groupBy: "collectionSlug",
|
|
4802
|
+
defaultColumns: ["purpose", "collectionSlug", "format", "active"]
|
|
4803
|
+
},
|
|
4804
|
+
hooks: {
|
|
4805
|
+
beforeChange: [
|
|
4806
|
+
async ({ data, req }) => {
|
|
4807
|
+
if (data && data.format === "html") {
|
|
4808
|
+
const siteName = req?.config?.admin?.branding?.logoText || "Dyrected";
|
|
4809
|
+
const defaults = getDefaultEmailTemplate(data.purpose || "invite", { siteName });
|
|
4810
|
+
if (!data.subject) {
|
|
4811
|
+
data.subject = defaults.subject;
|
|
4812
|
+
}
|
|
4813
|
+
if (!data.rawHtml) {
|
|
4814
|
+
data.rawHtml = defaults.rawHtml;
|
|
4815
|
+
}
|
|
4816
|
+
}
|
|
4817
|
+
return data;
|
|
4818
|
+
}
|
|
4819
|
+
]
|
|
4820
|
+
},
|
|
4821
|
+
access: {
|
|
4822
|
+
read: ({ user }) => isUserAdmin(user),
|
|
4823
|
+
create: ({ user }) => isUserAdmin(user),
|
|
4824
|
+
update: ({ user }) => isUserAdmin(user),
|
|
4825
|
+
delete: ({ user }) => isUserAdmin(user)
|
|
4826
|
+
}
|
|
4827
|
+
};
|
|
4452
4828
|
function normalizeConfig(config) {
|
|
4453
4829
|
const schemaAwareConfig = normalizeSchemaFragment(config);
|
|
4454
4830
|
assertValidDeclarativeHooksInConfig(schemaAwareConfig, "config");
|
|
@@ -4463,10 +4839,13 @@ function normalizeConfig(config) {
|
|
|
4463
4839
|
const needsTasks = (config.tasks?.length ?? 0) > 0;
|
|
4464
4840
|
const needsAuthSessions = collections.some((col) => !!col.auth);
|
|
4465
4841
|
const needsTrash = schemaAwareConfig.trash?.enabled === true || collections.some((col) => resolveTrashConfig(col, schemaAwareConfig).enabled);
|
|
4466
|
-
const
|
|
4842
|
+
const adminAuthCollection = getAdminAuthCollection({
|
|
4467
4843
|
collections,
|
|
4468
4844
|
adminAuth: schemaAwareConfig.adminAuth
|
|
4469
|
-
})
|
|
4845
|
+
});
|
|
4846
|
+
const adminAuthCollectionSlug = adminAuthCollection?.slug;
|
|
4847
|
+
const adminOnly = ({ user }) => isUserAdmin(user, adminAuthCollection);
|
|
4848
|
+
const adminOtherThanSelf = ({ user, id }) => isUserAdmin(user, adminAuthCollection) && user?.id !== id;
|
|
4470
4849
|
const normalizedCollections = collections.map((col) => {
|
|
4471
4850
|
let fields = col.fields || [];
|
|
4472
4851
|
const existingFieldNames = new Set(fields.map((f) => f.name));
|
|
@@ -4515,7 +4894,7 @@ function normalizeConfig(config) {
|
|
|
4515
4894
|
{ value: "viewer", label: "Viewer" }
|
|
4516
4895
|
],
|
|
4517
4896
|
access: {
|
|
4518
|
-
update:
|
|
4897
|
+
update: adminOnly
|
|
4519
4898
|
}
|
|
4520
4899
|
}
|
|
4521
4900
|
];
|
|
@@ -4533,7 +4912,7 @@ function normalizeConfig(config) {
|
|
|
4533
4912
|
{ value: "pending", label: "Pending" }
|
|
4534
4913
|
],
|
|
4535
4914
|
access: {
|
|
4536
|
-
update:
|
|
4915
|
+
update: adminOtherThanSelf
|
|
4537
4916
|
},
|
|
4538
4917
|
admin: {
|
|
4539
4918
|
condition: `!(data.roles && "admin" in data.roles) && data.role != "admin"`
|
|
@@ -4602,7 +4981,7 @@ function normalizeConfig(config) {
|
|
|
4602
4981
|
access: {
|
|
4603
4982
|
...field.access || {},
|
|
4604
4983
|
// Must be an admin; cannot edit own roles (no self-elevation).
|
|
4605
|
-
update:
|
|
4984
|
+
update: adminOtherThanSelf
|
|
4606
4985
|
}
|
|
4607
4986
|
};
|
|
4608
4987
|
}
|
|
@@ -4611,7 +4990,7 @@ function normalizeConfig(config) {
|
|
|
4611
4990
|
...field,
|
|
4612
4991
|
access: {
|
|
4613
4992
|
...field.access || {},
|
|
4614
|
-
update:
|
|
4993
|
+
update: adminOtherThanSelf
|
|
4615
4994
|
}
|
|
4616
4995
|
};
|
|
4617
4996
|
}
|
|
@@ -4703,6 +5082,52 @@ function normalizeConfig(config) {
|
|
|
4703
5082
|
if (needsTrash && !normalizedCollections.some((col) => col.slug === TRASH_COLLECTION)) {
|
|
4704
5083
|
systemCollections.push(TRASH_COLLECTION_CONFIG);
|
|
4705
5084
|
}
|
|
5085
|
+
const needsEmailTemplates = schemaAwareConfig.email?.adminEditable === true;
|
|
5086
|
+
if (needsEmailTemplates && !normalizedCollections.some((col) => col.slug === EMAIL_TEMPLATES_COLLECTION_SLUG)) {
|
|
5087
|
+
const adminCol = getAdminAuthCollection(schemaAwareConfig);
|
|
5088
|
+
const customAccess = schemaAwareConfig.email?.access;
|
|
5089
|
+
const resolveOpAccess = (op) => {
|
|
5090
|
+
if (typeof customAccess === "function") return customAccess;
|
|
5091
|
+
if (customAccess && typeof customAccess === "object" && customAccess[op]) return customAccess[op];
|
|
5092
|
+
return ({ user }) => isUserAdmin(user, adminCol);
|
|
5093
|
+
};
|
|
5094
|
+
const customPurposes = schemaAwareConfig.email?.purposes ?? [];
|
|
5095
|
+
const purposeOptions = [
|
|
5096
|
+
{ label: "Invite", value: "invite" },
|
|
5097
|
+
{ label: "Password Reset", value: "resetPassword" },
|
|
5098
|
+
{ label: "Welcome", value: "welcome" },
|
|
5099
|
+
{ label: "Password Changed", value: "passwordChanged" },
|
|
5100
|
+
...customPurposes
|
|
5101
|
+
];
|
|
5102
|
+
const targetableCollections = normalizedCollections.filter(
|
|
5103
|
+
(c) => !c.slug.startsWith("__")
|
|
5104
|
+
);
|
|
5105
|
+
const audienceOptions = [
|
|
5106
|
+
{ label: "Global (All Collections)", value: "*" },
|
|
5107
|
+
...targetableCollections.map((c) => ({
|
|
5108
|
+
label: `${c.labels?.singular || c.labels?.plural || c.slug} (${c.slug})`,
|
|
5109
|
+
value: c.slug
|
|
5110
|
+
}))
|
|
5111
|
+
];
|
|
5112
|
+
systemCollections.push({
|
|
5113
|
+
...EMAIL_TEMPLATES_COLLECTION_CONFIG,
|
|
5114
|
+
fields: EMAIL_TEMPLATES_COLLECTION_CONFIG.fields.map((f) => {
|
|
5115
|
+
if (f.name === "collectionSlug") {
|
|
5116
|
+
return { ...f, options: audienceOptions };
|
|
5117
|
+
}
|
|
5118
|
+
if (f.name === "purpose") {
|
|
5119
|
+
return { ...f, options: purposeOptions };
|
|
5120
|
+
}
|
|
5121
|
+
return f;
|
|
5122
|
+
}),
|
|
5123
|
+
access: {
|
|
5124
|
+
read: resolveOpAccess("read"),
|
|
5125
|
+
create: resolveOpAccess("create"),
|
|
5126
|
+
update: resolveOpAccess("update"),
|
|
5127
|
+
delete: resolveOpAccess("delete")
|
|
5128
|
+
}
|
|
5129
|
+
});
|
|
5130
|
+
}
|
|
4706
5131
|
return {
|
|
4707
5132
|
...schemaAwareConfig,
|
|
4708
5133
|
collections: [...normalizedCollections, ...systemCollections],
|
|
@@ -6030,6 +6455,31 @@ function generateOpenApi(config) {
|
|
|
6030
6455
|
spec.paths[`${path}/accept-invite`] = {
|
|
6031
6456
|
post: publicAuthPost("Accept an invitation")
|
|
6032
6457
|
};
|
|
6458
|
+
spec.paths[`${path}/tokens/verify`] = {
|
|
6459
|
+
get: {
|
|
6460
|
+
tags: [collectionTag],
|
|
6461
|
+
summary: `Verify an invite or password reset token`,
|
|
6462
|
+
parameters: [
|
|
6463
|
+
{
|
|
6464
|
+
name: "token",
|
|
6465
|
+
in: "query",
|
|
6466
|
+
required: true,
|
|
6467
|
+
schema: { type: "string" }
|
|
6468
|
+
},
|
|
6469
|
+
{
|
|
6470
|
+
name: "purpose",
|
|
6471
|
+
in: "query",
|
|
6472
|
+
required: false,
|
|
6473
|
+
schema: { type: "string", enum: ["invite", "reset"] }
|
|
6474
|
+
}
|
|
6475
|
+
],
|
|
6476
|
+
responses: {
|
|
6477
|
+
200: {
|
|
6478
|
+
description: "Token verification result"
|
|
6479
|
+
}
|
|
6480
|
+
}
|
|
6481
|
+
}
|
|
6482
|
+
};
|
|
6033
6483
|
spec.paths[`${path}/{id}/change-password`] = {
|
|
6034
6484
|
post: {
|
|
6035
6485
|
tags: [collectionTag],
|
|
@@ -7227,8 +7677,8 @@ function sanitizeDocForAI(options) {
|
|
|
7227
7677
|
// src/utils/index-name.ts
|
|
7228
7678
|
import { createHash } from "crypto";
|
|
7229
7679
|
var MAX_INDEX_NAME_LENGTH = 60;
|
|
7230
|
-
function resolveIndexName(prefix,
|
|
7231
|
-
const full = `${prefix}_${
|
|
7680
|
+
function resolveIndexName(prefix, table2, fields) {
|
|
7681
|
+
const full = `${prefix}_${table2}_${fields.join("_")}`;
|
|
7232
7682
|
if (full.length <= MAX_INDEX_NAME_LENGTH) return full;
|
|
7233
7683
|
const hash = createHash("sha1").update(full).digest("hex").slice(0, 10);
|
|
7234
7684
|
const tail = (fields[fields.length - 1] ?? "").slice(0, 30);
|
|
@@ -7700,7 +8150,7 @@ function compileNavigation(config) {
|
|
|
7700
8150
|
const mentionedTrash = explicitNav.some((i) => i.trash || i.slug === "trash");
|
|
7701
8151
|
if (!mentionedTrash) {
|
|
7702
8152
|
const hasTrash = collections.some(
|
|
7703
|
-
(col) => !col.slug
|
|
8153
|
+
(col) => !INTERNAL_SYSTEM_COLLECTIONS.has(col.slug) && resolveTrashConfig(col, config).enabled
|
|
7704
8154
|
);
|
|
7705
8155
|
if (hasTrash) {
|
|
7706
8156
|
registerItem(void 0, {
|
|
@@ -8030,8 +8480,11 @@ export {
|
|
|
8030
8480
|
DuplicateKeyError,
|
|
8031
8481
|
DyrectedAIError,
|
|
8032
8482
|
DyrectedError,
|
|
8483
|
+
EMAIL_TEMPLATES_COLLECTION_CONFIG,
|
|
8484
|
+
EMAIL_TEMPLATES_COLLECTION_SLUG,
|
|
8033
8485
|
FieldConditionBuilder,
|
|
8034
8486
|
ForbiddenError,
|
|
8487
|
+
INTERNAL_SYSTEM_COLLECTIONS,
|
|
8035
8488
|
LIFECYCLE_EVENTS_COLLECTION,
|
|
8036
8489
|
LIFECYCLE_EVENT_NAMES,
|
|
8037
8490
|
MatchBuilder,
|
|
@@ -8049,6 +8502,7 @@ export {
|
|
|
8049
8502
|
WORKFLOW_HISTORY_COLLECTION,
|
|
8050
8503
|
abs,
|
|
8051
8504
|
addDays,
|
|
8505
|
+
alertBox,
|
|
8052
8506
|
asCollection,
|
|
8053
8507
|
assertValidAdminConditionsInConfig,
|
|
8054
8508
|
assertValidDeclarativeAccessInConfig,
|
|
@@ -8078,6 +8532,7 @@ export {
|
|
|
8078
8532
|
createTaskRunner,
|
|
8079
8533
|
createTrashPurgeTask,
|
|
8080
8534
|
createWorkflowDocument,
|
|
8535
|
+
ctaButton,
|
|
8081
8536
|
defaultValue,
|
|
8082
8537
|
defineAction,
|
|
8083
8538
|
defineArrayField,
|
|
@@ -8124,6 +8579,7 @@ export {
|
|
|
8124
8579
|
defineUrlField,
|
|
8125
8580
|
defineView,
|
|
8126
8581
|
defineWorkspace,
|
|
8582
|
+
detailBox,
|
|
8127
8583
|
diffDays,
|
|
8128
8584
|
dispatchLifecycleEvent,
|
|
8129
8585
|
dispatchPendingLifecycleEvents,
|
|
@@ -8139,10 +8595,14 @@ export {
|
|
|
8139
8595
|
displayTab,
|
|
8140
8596
|
displayTabs,
|
|
8141
8597
|
displayText,
|
|
8598
|
+
divider,
|
|
8599
|
+
emailTokens,
|
|
8142
8600
|
endsWith,
|
|
8601
|
+
escapeHtml,
|
|
8143
8602
|
evaluateDetailComputed,
|
|
8144
8603
|
evaluateJexl,
|
|
8145
8604
|
evaluateJexlSync,
|
|
8605
|
+
eventList,
|
|
8146
8606
|
executeFieldAfterRead,
|
|
8147
8607
|
executeFieldBeforeChange,
|
|
8148
8608
|
expr,
|
|
@@ -8159,8 +8619,10 @@ export {
|
|
|
8159
8619
|
getAdminAuthCollection,
|
|
8160
8620
|
getAdminRoleForCollection,
|
|
8161
8621
|
getAllowedSitesForUser,
|
|
8622
|
+
getDefaultEmailTemplate,
|
|
8162
8623
|
getPublicAdminAuthConfig,
|
|
8163
8624
|
getTrashEntryId,
|
|
8625
|
+
heading,
|
|
8164
8626
|
includes,
|
|
8165
8627
|
initializeWorkflowDocument,
|
|
8166
8628
|
isAICollection,
|
|
@@ -8173,6 +8635,7 @@ export {
|
|
|
8173
8635
|
isUserAdmin,
|
|
8174
8636
|
join,
|
|
8175
8637
|
last,
|
|
8638
|
+
layout,
|
|
8176
8639
|
length,
|
|
8177
8640
|
lower,
|
|
8178
8641
|
maskEmail,
|
|
@@ -8188,6 +8651,7 @@ export {
|
|
|
8188
8651
|
normalizeGroupKey,
|
|
8189
8652
|
now,
|
|
8190
8653
|
numericFieldNames,
|
|
8654
|
+
paragraph,
|
|
8191
8655
|
parseCron,
|
|
8192
8656
|
parseMongoWhere,
|
|
8193
8657
|
parseSort,
|
|
@@ -8204,21 +8668,27 @@ export {
|
|
|
8204
8668
|
resolveAdminAuthCollection,
|
|
8205
8669
|
resolveAllCollectionViews,
|
|
8206
8670
|
resolveAuthorizedSiteId,
|
|
8671
|
+
resolveBlocks,
|
|
8207
8672
|
resolveDocumentTitle,
|
|
8208
8673
|
resolveIndexName,
|
|
8209
8674
|
resolvePrefix,
|
|
8210
8675
|
resolveTrashConfig,
|
|
8211
8676
|
restoreDocument,
|
|
8212
8677
|
round,
|
|
8678
|
+
row,
|
|
8213
8679
|
runCollectionHooks,
|
|
8680
|
+
safeLinkDetailBox,
|
|
8214
8681
|
sanitizeDocForAI,
|
|
8215
8682
|
saveWorkflowDraft,
|
|
8683
|
+
sectionLabel,
|
|
8216
8684
|
simplePublishingWorkflow,
|
|
8217
8685
|
slugify,
|
|
8218
8686
|
softDeleteDocument,
|
|
8687
|
+
spacer,
|
|
8219
8688
|
sqrt,
|
|
8220
8689
|
startsWith,
|
|
8221
8690
|
sum,
|
|
8691
|
+
table,
|
|
8222
8692
|
today,
|
|
8223
8693
|
transitionWorkflow,
|
|
8224
8694
|
trim,
|