@dyrected/core 2.5.29 → 2.5.32
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/chunk-6ZXEGKT6.js +619 -0
- package/dist/chunk-CQDVPMEU.js +613 -0
- package/dist/chunk-Y4AYI52W.js +631 -0
- package/dist/chunk-YDOBB7MY.js +1457 -0
- package/dist/index-CdQwnbfh.d.cts +1889 -0
- package/dist/index-CdQwnbfh.d.ts +1889 -0
- package/dist/index-DTrpTru7.d.cts +1881 -0
- package/dist/index-DTrpTru7.d.ts +1881 -0
- package/dist/index-DzUlE5Hc.d.cts +1751 -0
- package/dist/index-DzUlE5Hc.d.ts +1751 -0
- package/dist/index-M_m6dXZV.d.cts +1889 -0
- package/dist/index-M_m6dXZV.d.ts +1889 -0
- package/dist/index-qhLus8ZO.d.cts +1881 -0
- package/dist/index-qhLus8ZO.d.ts +1881 -0
- package/dist/index.cjs +1159 -812
- package/dist/index.d.cts +66 -18
- package/dist/index.d.ts +66 -18
- package/dist/index.js +42 -825
- package/dist/server.cjs +1079 -217
- package/dist/server.d.cts +33 -1
- package/dist/server.d.ts +33 -1
- package/dist/server.js +267 -494
- package/dist/where-sanitizer-WJ2W2QCE.js +54 -0
- package/package.json +2 -1
package/dist/server.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
+
WORKFLOW_HISTORY_COLLECTION,
|
|
3
|
+
canViewWorkflowDraft,
|
|
4
|
+
createWorkflowDocument,
|
|
2
5
|
executeFieldAfterRead,
|
|
3
6
|
executeFieldBeforeChange,
|
|
7
|
+
generateOpenApi,
|
|
8
|
+
initializeWorkflowDocument,
|
|
9
|
+
materializeWorkflowDocument,
|
|
4
10
|
normalizeConfig,
|
|
5
|
-
runCollectionHooks
|
|
6
|
-
|
|
11
|
+
runCollectionHooks,
|
|
12
|
+
saveWorkflowDraft,
|
|
13
|
+
transitionWorkflow
|
|
14
|
+
} from "./chunk-YDOBB7MY.js";
|
|
7
15
|
|
|
8
16
|
// src/app.ts
|
|
9
17
|
import { Hono } from "hono";
|
|
@@ -276,6 +284,20 @@ function createReadonlyDb(db) {
|
|
|
276
284
|
});
|
|
277
285
|
}
|
|
278
286
|
|
|
287
|
+
// src/auth/jexl.ts
|
|
288
|
+
import jexl from "jexl";
|
|
289
|
+
async function evaluateAccess(expression, context) {
|
|
290
|
+
if (expression === void 0 || expression === null) return false;
|
|
291
|
+
if (typeof expression === "boolean") return expression;
|
|
292
|
+
try {
|
|
293
|
+
const result = await jexl.eval(expression, context);
|
|
294
|
+
return !!result;
|
|
295
|
+
} catch (err) {
|
|
296
|
+
console.error("[dyrected/core] Jexl evaluation failed:", err);
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
279
301
|
// src/controllers/collection.controller.ts
|
|
280
302
|
var CollectionController = class {
|
|
281
303
|
collection;
|
|
@@ -304,7 +326,7 @@ var CollectionController = class {
|
|
|
304
326
|
if (this.collection.admin?.filterable === false) {
|
|
305
327
|
where = void 0;
|
|
306
328
|
} else {
|
|
307
|
-
const { sanitizeWhereClause } = await import("./where-sanitizer-
|
|
329
|
+
const { sanitizeWhereClause } = await import("./where-sanitizer-WJ2W2QCE.js");
|
|
308
330
|
where = sanitizeWhereClause(where, this.collection.fields);
|
|
309
331
|
if (Object.keys(where).length === 0) {
|
|
310
332
|
where = void 0;
|
|
@@ -320,6 +342,9 @@ var CollectionController = class {
|
|
|
320
342
|
if (beforeReadResult !== void 0) {
|
|
321
343
|
where = beforeReadResult;
|
|
322
344
|
}
|
|
345
|
+
if (this.collection.workflow && !canViewWorkflowDraft(this.collection.workflow, user)) {
|
|
346
|
+
where = where ? { AND: [where, { __published: { exists: true } }] } : { __published: { exists: true } };
|
|
347
|
+
}
|
|
323
348
|
let result = await db.find({
|
|
324
349
|
collection: this.collection.slug,
|
|
325
350
|
limit,
|
|
@@ -340,7 +365,7 @@ var CollectionController = class {
|
|
|
340
365
|
where
|
|
341
366
|
});
|
|
342
367
|
}
|
|
343
|
-
result.docs = result.docs.map((doc) => DefaultsService.apply(this.collection.fields, doc));
|
|
368
|
+
result.docs = result.docs.map((doc) => this.collection.workflow ? materializeWorkflowDocument(doc, this.collection.workflow, user) : doc).filter((doc) => doc !== null).map((doc) => DefaultsService.apply(this.collection.fields, doc));
|
|
344
369
|
const processedDocs = [];
|
|
345
370
|
for (const doc of result.docs) {
|
|
346
371
|
const docWithCollectionHooks = await runCollectionHooks(this.collection.hooks?.afterRead, {
|
|
@@ -368,8 +393,12 @@ var CollectionController = class {
|
|
|
368
393
|
const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 10;
|
|
369
394
|
const user = c.get("user");
|
|
370
395
|
if (!id) return c.json({ message: "Missing ID" }, 400);
|
|
371
|
-
|
|
396
|
+
let doc = await db.findOne({ collection: this.collection.slug, id });
|
|
372
397
|
if (!doc) return c.json({ message: "Not Found" }, 404);
|
|
398
|
+
if (this.collection.workflow) {
|
|
399
|
+
doc = materializeWorkflowDocument(doc, this.collection.workflow, user);
|
|
400
|
+
if (!doc) return c.json({ message: "Not Found" }, 404);
|
|
401
|
+
}
|
|
373
402
|
const docWithDefaults = DefaultsService.apply(this.collection.fields, doc);
|
|
374
403
|
const docWithCollectionHooks = await runCollectionHooks(this.collection.hooks?.afterRead, {
|
|
375
404
|
doc: docWithDefaults,
|
|
@@ -409,6 +438,9 @@ var CollectionController = class {
|
|
|
409
438
|
createdBy: user?.sub ?? null,
|
|
410
439
|
updatedBy: user?.sub ?? null
|
|
411
440
|
};
|
|
441
|
+
if (this.collection.workflow) {
|
|
442
|
+
data = initializeWorkflowDocument(data, this.collection.workflow);
|
|
443
|
+
}
|
|
412
444
|
if (this.collection.auth && data.password) {
|
|
413
445
|
data.password = await hashPassword(data.password);
|
|
414
446
|
}
|
|
@@ -420,7 +452,7 @@ var CollectionController = class {
|
|
|
420
452
|
operation: "create",
|
|
421
453
|
db: readonlyDb
|
|
422
454
|
});
|
|
423
|
-
const doc = await db.create({ collection: this.collection.slug, data });
|
|
455
|
+
const doc = this.collection.workflow ? (await createWorkflowDocument({ config, collection: this.collection, data, user })).doc : await db.create({ collection: this.collection.slug, data });
|
|
424
456
|
if (this.collection.audit && db) {
|
|
425
457
|
AuditService.log(db, {
|
|
426
458
|
operation: "create",
|
|
@@ -438,8 +470,9 @@ var CollectionController = class {
|
|
|
438
470
|
operation: "create",
|
|
439
471
|
db
|
|
440
472
|
}, { isolated: true });
|
|
473
|
+
const responseDoc = this.collection.workflow ? materializeWorkflowDocument(doc, this.collection.workflow, user) : doc;
|
|
441
474
|
const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
|
|
442
|
-
doc,
|
|
475
|
+
doc: responseDoc,
|
|
443
476
|
req: c.req,
|
|
444
477
|
user,
|
|
445
478
|
db: readonlyDb
|
|
@@ -502,8 +535,9 @@ var CollectionController = class {
|
|
|
502
535
|
operation: "create",
|
|
503
536
|
db
|
|
504
537
|
}, { isolated: true });
|
|
538
|
+
const responseDoc = this.collection.workflow ? materializeWorkflowDocument(doc, this.collection.workflow, user) : doc;
|
|
505
539
|
const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
|
|
506
|
-
doc,
|
|
540
|
+
doc: responseDoc,
|
|
507
541
|
req: c.req,
|
|
508
542
|
user,
|
|
509
543
|
db: readonlyDb
|
|
@@ -545,7 +579,7 @@ var CollectionController = class {
|
|
|
545
579
|
operation: "update",
|
|
546
580
|
db: readonlyDb
|
|
547
581
|
});
|
|
548
|
-
const doc = await db.update({ collection: this.collection.slug, id, data });
|
|
582
|
+
const doc = this.collection.workflow ? (await saveWorkflowDraft({ config, collection: this.collection, id, originalDoc, data, user })).doc : await db.update({ collection: this.collection.slug, id, data });
|
|
549
583
|
if (this.collection.audit && db) {
|
|
550
584
|
AuditService.log(db, {
|
|
551
585
|
operation: "update",
|
|
@@ -573,6 +607,61 @@ var CollectionController = class {
|
|
|
573
607
|
const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user, readonlyDb);
|
|
574
608
|
return c.json(finalDoc);
|
|
575
609
|
}
|
|
610
|
+
async transition(c) {
|
|
611
|
+
const config = c.get("config");
|
|
612
|
+
if (!config.db) return c.json({ message: "Database not configured" }, 500);
|
|
613
|
+
if (!this.collection.workflow) return c.json({ message: "Workflows are not enabled for this collection" }, 404);
|
|
614
|
+
const id = c.req.param("id");
|
|
615
|
+
const transitionName = c.req.param("transition");
|
|
616
|
+
const body = await c.req.json().catch(() => ({}));
|
|
617
|
+
try {
|
|
618
|
+
const doc = await transitionWorkflow({
|
|
619
|
+
config,
|
|
620
|
+
collection: this.collection,
|
|
621
|
+
id,
|
|
622
|
+
transitionName,
|
|
623
|
+
expectedRevision: body.expectedRevision,
|
|
624
|
+
comment: body.comment,
|
|
625
|
+
user: c.get("user"),
|
|
626
|
+
req: { query: c.req.query(), headers: c.req.header(), raw: c.req.raw }
|
|
627
|
+
});
|
|
628
|
+
return c.json(materializeWorkflowDocument(doc, this.collection.workflow, c.get("user")));
|
|
629
|
+
} catch (error) {
|
|
630
|
+
const status = typeof error.statusCode === "number" ? error.statusCode : 500;
|
|
631
|
+
return c.json({ error: true, message: error instanceof Error ? error.message : String(error) }, status);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
async workflowHistory(c) {
|
|
635
|
+
const config = c.get("config");
|
|
636
|
+
if (!config.db) return c.json({ message: "Database not configured" }, 500);
|
|
637
|
+
if (!this.collection.workflow) return c.json({ message: "Workflows are not enabled for this collection" }, 404);
|
|
638
|
+
const documentId = c.req.param("id");
|
|
639
|
+
if (!documentId) return c.json({ message: "Missing ID" }, 400);
|
|
640
|
+
const document = await config.db.findOne({ collection: this.collection.slug, id: documentId });
|
|
641
|
+
if (!document) return c.json({ message: "Not Found" }, 404);
|
|
642
|
+
const readAccess = this.collection.access?.read;
|
|
643
|
+
if (readAccess !== void 0 && readAccess !== null) {
|
|
644
|
+
const args = { user: c.get("user"), req: c.req, doc: document };
|
|
645
|
+
const result2 = typeof readAccess === "function" ? await readAccess(args) : await evaluateAccess(readAccess, args);
|
|
646
|
+
let allowed = result2 === true;
|
|
647
|
+
if (result2 && typeof result2 === "object") {
|
|
648
|
+
const match = await config.db.find({
|
|
649
|
+
collection: this.collection.slug,
|
|
650
|
+
where: { AND: [{ id: { equals: documentId } }, result2] },
|
|
651
|
+
limit: 1
|
|
652
|
+
});
|
|
653
|
+
allowed = match.total > 0;
|
|
654
|
+
}
|
|
655
|
+
if (!allowed) return c.json({ error: true, message: `Access denied: read on ${this.collection.slug}` }, 403);
|
|
656
|
+
}
|
|
657
|
+
const result = await config.db.find({
|
|
658
|
+
collection: WORKFLOW_HISTORY_COLLECTION,
|
|
659
|
+
where: { collection: { equals: this.collection.slug }, documentId: { equals: documentId } },
|
|
660
|
+
sort: "-createdAt",
|
|
661
|
+
limit: Math.min(Number(c.req.query("limit")) || 50, 100)
|
|
662
|
+
});
|
|
663
|
+
return c.json(result);
|
|
664
|
+
}
|
|
576
665
|
/**
|
|
577
666
|
* POST /api/collections/:slug/:id/change-password
|
|
578
667
|
*
|
|
@@ -1040,6 +1129,96 @@ async function verifyCollectionToken(token) {
|
|
|
1040
1129
|
return payload;
|
|
1041
1130
|
}
|
|
1042
1131
|
|
|
1132
|
+
// src/services/email-template.ts
|
|
1133
|
+
var emailTokens = {
|
|
1134
|
+
colors: {
|
|
1135
|
+
canvas: "#f6f7f2",
|
|
1136
|
+
surface: "#ffffff",
|
|
1137
|
+
text: "#171717",
|
|
1138
|
+
muted: "#62665b",
|
|
1139
|
+
subtle: "#8a8f82",
|
|
1140
|
+
border: "#dde0d7",
|
|
1141
|
+
accent: "#b6ff2e",
|
|
1142
|
+
code: "#f1f3ec",
|
|
1143
|
+
dangerSurface: "#fff2f0",
|
|
1144
|
+
dangerBorder: "#ffc9c2",
|
|
1145
|
+
dangerText: "#9f251b"
|
|
1146
|
+
},
|
|
1147
|
+
font: "Arial, 'Helvetica Neue', Helvetica, sans-serif",
|
|
1148
|
+
mono: "'Courier New', Courier, monospace",
|
|
1149
|
+
radius: { card: "12px", control: "6px" },
|
|
1150
|
+
width: "600px"
|
|
1151
|
+
};
|
|
1152
|
+
function escapeHtml(value) {
|
|
1153
|
+
return value.replace(/[&<>'"]/g, (character) => ({
|
|
1154
|
+
"&": "&",
|
|
1155
|
+
"<": "<",
|
|
1156
|
+
">": ">",
|
|
1157
|
+
"'": "'",
|
|
1158
|
+
'"': """
|
|
1159
|
+
})[character]);
|
|
1160
|
+
}
|
|
1161
|
+
function safeHttpUrl(value) {
|
|
1162
|
+
try {
|
|
1163
|
+
const url = new URL(value);
|
|
1164
|
+
return url.protocol === "https:" || url.protocol === "http:" ? escapeHtml(url.toString()) : void 0;
|
|
1165
|
+
} catch {
|
|
1166
|
+
return void 0;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
function heading(content) {
|
|
1170
|
+
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>`;
|
|
1171
|
+
}
|
|
1172
|
+
function paragraph(content, margin = "0 0 16px") {
|
|
1173
|
+
return `<p style="margin:${margin};font-family:${emailTokens.font};font-size:15px;line-height:1.6;color:${emailTokens.colors.muted}">${escapeHtml(content)}</p>`;
|
|
1174
|
+
}
|
|
1175
|
+
function sectionLabel(content) {
|
|
1176
|
+
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>`;
|
|
1177
|
+
}
|
|
1178
|
+
function divider() {
|
|
1179
|
+
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>`;
|
|
1180
|
+
}
|
|
1181
|
+
function spacer(height = 16) {
|
|
1182
|
+
return table(row(" ", `height:${height}px;font-size:0;line-height:0`));
|
|
1183
|
+
}
|
|
1184
|
+
function row(content, cellStyle = "") {
|
|
1185
|
+
return `<tr><td style="${cellStyle}">${content}</td></tr>`;
|
|
1186
|
+
}
|
|
1187
|
+
function table(content, style = "width:100%") {
|
|
1188
|
+
return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="${style}">${content}</table>`;
|
|
1189
|
+
}
|
|
1190
|
+
function detailBox(content, monospace = false) {
|
|
1191
|
+
const font = monospace ? emailTokens.mono : emailTokens.font;
|
|
1192
|
+
return table(row(escapeHtml(content), `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`), `width:100%;background:${emailTokens.colors.code};border-radius:${emailTokens.radius.control}`);
|
|
1193
|
+
}
|
|
1194
|
+
function ctaButton(label, href) {
|
|
1195
|
+
const safeHref = safeHttpUrl(href);
|
|
1196
|
+
if (!safeHref) return "";
|
|
1197
|
+
return table(row(
|
|
1198
|
+
`<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>`,
|
|
1199
|
+
"padding:8px 0 24px"
|
|
1200
|
+
), "width:auto");
|
|
1201
|
+
}
|
|
1202
|
+
function alertBox(content) {
|
|
1203
|
+
return table(row(escapeHtml(content), `padding:13px 16px;font-family:${emailTokens.font};font-size:13px;line-height:1.5;color:${emailTokens.colors.dangerText}`), `width:100%;background:${emailTokens.colors.dangerSurface};border:1px solid ${emailTokens.colors.dangerBorder};border-radius:${emailTokens.radius.control}`);
|
|
1204
|
+
}
|
|
1205
|
+
function layout({ preheader, title, content, footer }) {
|
|
1206
|
+
return `<!doctype html>
|
|
1207
|
+
<html lang="en">
|
|
1208
|
+
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"></head>
|
|
1209
|
+
<body style="margin:0;padding:0;background:${emailTokens.colors.canvas}">
|
|
1210
|
+
<div style="display:none;max-height:0;overflow:hidden;opacity:0;color:transparent">${escapeHtml(preheader)}</div>
|
|
1211
|
+
${table(row(
|
|
1212
|
+
table(
|
|
1213
|
+
row(" ", `height:5px;background:${emailTokens.colors.accent};font-size:0;line-height:0`) + row(`${sectionLabel("Dyrected")}${heading(title)}`, "padding:30px 32px 24px") + row(content, "padding:0 32px 32px") + row(`${divider()}${paragraph(footer, "20px 0 6px")}${paragraph("Privacy: this message contains account-related information; please avoid forwarding it.", "0")}`, "padding:0 32px 28px"),
|
|
1214
|
+
`width:100%;max-width:${emailTokens.width};background:${emailTokens.colors.surface};border:1px solid ${emailTokens.colors.border};border-radius:${emailTokens.radius.card};overflow:hidden`
|
|
1215
|
+
),
|
|
1216
|
+
"padding:32px 12px"
|
|
1217
|
+
), `width:100%;background:${emailTokens.colors.canvas}`)}
|
|
1218
|
+
</body>
|
|
1219
|
+
</html>`;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1043
1222
|
// src/services/email.service.ts
|
|
1044
1223
|
var _devSend = null;
|
|
1045
1224
|
var _devSendPromise = null;
|
|
@@ -1083,77 +1262,24 @@ function buildWelcomeEmail(config, args) {
|
|
|
1083
1262
|
const custom = config.email?.templates?.welcome?.(args);
|
|
1084
1263
|
return {
|
|
1085
1264
|
subject: custom?.subject ?? "Welcome \u2014 your account is ready",
|
|
1086
|
-
html: custom?.html ??
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
<td style="padding:32px 32px 0">
|
|
1093
|
-
<p style="margin:0 0 4px;font-size:12px;font-weight:600;color:#6b7280;font-family:sans-serif;text-transform:uppercase;letter-spacing:0.05em">Dyrected</p>
|
|
1094
|
-
<h1 style="margin:0 0 24px;font-size:22px;font-weight:700;color:#111827;font-family:sans-serif">Welcome!</h1>
|
|
1095
|
-
</td>
|
|
1096
|
-
</tr>
|
|
1097
|
-
<tr>
|
|
1098
|
-
<td style="padding:0 32px">
|
|
1099
|
-
<p style="margin:0 0 12px;font-size:14px;color:#4b5563;line-height:1.6;font-family:sans-serif">Your account has been created. You can now log in with:</p>
|
|
1100
|
-
<table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f3f4f6;border-radius:6px;table-layout:fixed">
|
|
1101
|
-
<tr>
|
|
1102
|
-
<td style="padding:12px 16px;font-size:14px;font-weight:600;color:#111827;font-family:sans-serif;word-break:break-all">
|
|
1103
|
-
${args.email}
|
|
1104
|
-
</td>
|
|
1105
|
-
</tr>
|
|
1106
|
-
</table>
|
|
1107
|
-
</td>
|
|
1108
|
-
</tr>
|
|
1109
|
-
<tr>
|
|
1110
|
-
<td style="padding:32px">
|
|
1111
|
-
<p style="margin:0;font-size:12px;color:#9ca3af;font-family:sans-serif">If you didn't create this account, you can safely ignore this email.</p>
|
|
1112
|
-
</td>
|
|
1113
|
-
</tr>
|
|
1114
|
-
</table>
|
|
1115
|
-
</td>
|
|
1116
|
-
</tr>
|
|
1117
|
-
</table>`
|
|
1265
|
+
html: custom?.html ?? layout({
|
|
1266
|
+
preheader: "Your Dyrected account is ready.",
|
|
1267
|
+
title: "Welcome \u2014 your account is ready",
|
|
1268
|
+
content: `${paragraph("Your account has been created. You can now log in with:")}${detailBox(args.email)}`,
|
|
1269
|
+
footer: "If you didn't create this account, you can safely ignore this email."
|
|
1270
|
+
})
|
|
1118
1271
|
};
|
|
1119
1272
|
}
|
|
1120
1273
|
function buildInviteEmail(config, args) {
|
|
1121
1274
|
const custom = config.email?.templates?.invite?.(args);
|
|
1122
1275
|
return {
|
|
1123
1276
|
subject: custom?.subject ?? "You've been invited",
|
|
1124
|
-
html: custom?.html ??
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
<td style="padding:32px 32px 0">
|
|
1131
|
-
<p style="margin:0 0 4px;font-size:12px;font-weight:600;color:#6b7280;font-family:sans-serif;text-transform:uppercase;letter-spacing:0.05em">Dyrected</p>
|
|
1132
|
-
<h1 style="margin:0 0 24px;font-size:22px;font-weight:700;color:#111827;font-family:sans-serif">You've been invited</h1>
|
|
1133
|
-
</td>
|
|
1134
|
-
</tr>
|
|
1135
|
-
<tr>
|
|
1136
|
-
<td style="padding:0 32px">
|
|
1137
|
-
${args.invitedByEmail ? `<p style="margin:0 0 12px;font-size:14px;color:#4b5563;line-height:1.6;font-family:sans-serif">You were invited by <strong style="color:#111827">${args.invitedByEmail}</strong>.</p>` : ""}
|
|
1138
|
-
<p style="margin:0 0 16px;font-size:14px;color:#4b5563;line-height:1.6;font-family:sans-serif">Use the token below to accept your invitation. It expires in 7 days.</p>
|
|
1139
|
-
<table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f3f4f6;border-radius:6px;table-layout:fixed">
|
|
1140
|
-
<tr>
|
|
1141
|
-
<td style="padding:12px 16px;font-family:monospace;font-size:12px;color:#374151;word-break:break-all;white-space:normal;line-height:1.4">
|
|
1142
|
-
${args.token}
|
|
1143
|
-
</td>
|
|
1144
|
-
</tr>
|
|
1145
|
-
</table>
|
|
1146
|
-
</td>
|
|
1147
|
-
</tr>
|
|
1148
|
-
<tr>
|
|
1149
|
-
<td style="padding:32px">
|
|
1150
|
-
<p style="margin:0;font-size:12px;color:#9ca3af;font-family:sans-serif">If you weren't expecting this invitation, you can safely ignore this email.</p>
|
|
1151
|
-
</td>
|
|
1152
|
-
</tr>
|
|
1153
|
-
</table>
|
|
1154
|
-
</td>
|
|
1155
|
-
</tr>
|
|
1156
|
-
</table>`
|
|
1277
|
+
html: custom?.html ?? layout({
|
|
1278
|
+
preheader: "You've been invited to join a Dyrected account.",
|
|
1279
|
+
title: "You've been invited",
|
|
1280
|
+
content: `${args.invitedByEmail ? paragraph(`You were invited by ${args.invitedByEmail}.`) : ""}${paragraph("Use the token below to accept your invitation. It expires in 7 days.")}${sectionLabel("Invitation token")}${detailBox(args.token, true)}`,
|
|
1281
|
+
footer: "If you weren't expecting this invitation, you can safely ignore this email."
|
|
1282
|
+
})
|
|
1157
1283
|
};
|
|
1158
1284
|
}
|
|
1159
1285
|
function buildResetPasswordEmail(config, args) {
|
|
@@ -1161,95 +1287,24 @@ function buildResetPasswordEmail(config, args) {
|
|
|
1161
1287
|
const resetLink = args.url;
|
|
1162
1288
|
return {
|
|
1163
1289
|
subject: custom?.subject ?? "Reset your password",
|
|
1164
|
-
html: custom?.html ??
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
<td style="padding:32px 32px 0">
|
|
1171
|
-
<p style="margin:0 0 4px;font-size:12px;font-weight:600;color:#6b7280;font-family:sans-serif;text-transform:uppercase;letter-spacing:0.05em">Dyrected</p>
|
|
1172
|
-
<h1 style="margin:0 0 24px;font-size:22px;font-weight:700;color:#111827;font-family:sans-serif">Reset your password</h1>
|
|
1173
|
-
</td>
|
|
1174
|
-
</tr>
|
|
1175
|
-
<tr>
|
|
1176
|
-
<td style="padding:0 32px">
|
|
1177
|
-
<p style="margin:0 0 24px;font-size:14px;color:#4b5563;line-height:1.6;font-family:sans-serif">We received a request to reset your password. Use the button below to set a new password. It will expire in 1 hour.</p>
|
|
1178
|
-
${resetLink ? `
|
|
1179
|
-
<table cellpadding="0" cellspacing="0" border="0" style="margin-bottom:24px">
|
|
1180
|
-
<tr>
|
|
1181
|
-
<td style="border-radius:6px;background-color:#111827">
|
|
1182
|
-
<a href="${resetLink}" style="display:inline-block;padding:12px 28px;font-size:14px;font-weight:600;color:#ffffff;text-decoration:none;font-family:sans-serif;border-radius:6px">
|
|
1183
|
-
Reset Password
|
|
1184
|
-
</a>
|
|
1185
|
-
</td>
|
|
1186
|
-
</tr>
|
|
1187
|
-
</table>
|
|
1188
|
-
` : ""}
|
|
1189
|
-
<p style="margin:0 0 8px;font-size:12px;color:#9ca3af;font-family:sans-serif">Or copy and paste this token manually in the admin dashboard:</p>
|
|
1190
|
-
<table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f3f4f6;border-radius:6px;table-layout:fixed">
|
|
1191
|
-
<tr>
|
|
1192
|
-
<td style="padding:12px 16px;font-family:monospace;font-size:12px;color:#374151;word-break:break-all;white-space:normal;line-height:1.4">
|
|
1193
|
-
${args.token}
|
|
1194
|
-
</td>
|
|
1195
|
-
</tr>
|
|
1196
|
-
</table>
|
|
1197
|
-
</td>
|
|
1198
|
-
</tr>
|
|
1199
|
-
<tr>
|
|
1200
|
-
<td style="padding:32px">
|
|
1201
|
-
<p style="margin:0;font-size:12px;color:#9ca3af;font-family:sans-serif">If you didn't request a password reset, you can safely ignore this email.</p>
|
|
1202
|
-
</td>
|
|
1203
|
-
</tr>
|
|
1204
|
-
</table>
|
|
1205
|
-
</td>
|
|
1206
|
-
</tr>
|
|
1207
|
-
</table>`
|
|
1290
|
+
html: custom?.html ?? layout({
|
|
1291
|
+
preheader: "Reset your Dyrected password.",
|
|
1292
|
+
title: "Reset your password",
|
|
1293
|
+
content: `${paragraph("We received a request to reset your password. The reset link and token expire in 1 hour.")}${resetLink ? ctaButton("Reset password", resetLink) : ""}${sectionLabel(resetLink ? "Or use this token" : "Reset token")}${detailBox(args.token, true)}`,
|
|
1294
|
+
footer: "If you didn't request a password reset, you can safely ignore this email."
|
|
1295
|
+
})
|
|
1208
1296
|
};
|
|
1209
1297
|
}
|
|
1210
1298
|
function buildPasswordChangedEmail(config, args) {
|
|
1211
1299
|
const custom = config.email?.templates?.passwordChanged?.(args);
|
|
1212
1300
|
return {
|
|
1213
1301
|
subject: custom?.subject ?? "Your password has been changed",
|
|
1214
|
-
html: custom?.html ??
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
<td style="padding:32px 32px 0">
|
|
1221
|
-
<p style="margin:0 0 4px;font-size:12px;font-weight:600;color:#6b7280;font-family:sans-serif;text-transform:uppercase;letter-spacing:0.05em">Dyrected</p>
|
|
1222
|
-
<h1 style="margin:0 0 24px;font-size:22px;font-weight:700;color:#111827;font-family:sans-serif">Password changed</h1>
|
|
1223
|
-
</td>
|
|
1224
|
-
</tr>
|
|
1225
|
-
<tr>
|
|
1226
|
-
<td style="padding:0 32px">
|
|
1227
|
-
<p style="margin:0 0 12px;font-size:14px;color:#4b5563;line-height:1.6;font-family:sans-serif">The password for your account was just changed:</p>
|
|
1228
|
-
<table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f3f4f6;border-radius:6px;table-layout:fixed">
|
|
1229
|
-
<tr>
|
|
1230
|
-
<td style="padding:12px 16px;font-size:14px;font-weight:600;color:#111827;font-family:sans-serif;word-break:break-all">
|
|
1231
|
-
${args.email}
|
|
1232
|
-
</td>
|
|
1233
|
-
</tr>
|
|
1234
|
-
</table>
|
|
1235
|
-
<table cellpadding="0" cellspacing="0" border="0" style="width:100%;margin-top:16px;background-color:#fef2f2;border-radius:6px;border:1px solid #fecaca;table-layout:fixed">
|
|
1236
|
-
<tr>
|
|
1237
|
-
<td style="padding:12px 16px;font-size:13px;color:#b91c1c;line-height:1.5;font-family:sans-serif">
|
|
1238
|
-
If you did not make this change, please contact support immediately.
|
|
1239
|
-
</td>
|
|
1240
|
-
</tr>
|
|
1241
|
-
</table>
|
|
1242
|
-
</td>
|
|
1243
|
-
</tr>
|
|
1244
|
-
<tr>
|
|
1245
|
-
<td style="padding:32px">
|
|
1246
|
-
<p style="margin:0;font-size:12px;color:#9ca3af;font-family:sans-serif">This is an automated security notification.</p>
|
|
1247
|
-
</td>
|
|
1248
|
-
</tr>
|
|
1249
|
-
</table>
|
|
1250
|
-
</td>
|
|
1251
|
-
</tr>
|
|
1252
|
-
</table>`
|
|
1302
|
+
html: custom?.html ?? layout({
|
|
1303
|
+
preheader: "Your Dyrected password was changed.",
|
|
1304
|
+
title: "Password changed",
|
|
1305
|
+
content: `${paragraph("The password for this account was just changed:")}${detailBox(args.email)}${spacer()}${alertBox("If you did not make this change, please contact support immediately.")}`,
|
|
1306
|
+
footer: "This is an automated security notification."
|
|
1307
|
+
})
|
|
1253
1308
|
};
|
|
1254
1309
|
}
|
|
1255
1310
|
|
|
@@ -1623,328 +1678,6 @@ function optionalAuth() {
|
|
|
1623
1678
|
};
|
|
1624
1679
|
}
|
|
1625
1680
|
|
|
1626
|
-
// src/utils/openapi.ts
|
|
1627
|
-
function generateOpenApi(config) {
|
|
1628
|
-
const spec = {
|
|
1629
|
-
openapi: "3.0.0",
|
|
1630
|
-
info: {
|
|
1631
|
-
title: "Dyrected API",
|
|
1632
|
-
version: "1.0.0",
|
|
1633
|
-
description: "Automatically generated OpenAPI specification for the Dyrected project."
|
|
1634
|
-
},
|
|
1635
|
-
components: {
|
|
1636
|
-
schemas: {},
|
|
1637
|
-
securitySchemes: {
|
|
1638
|
-
ApiKeyAuth: {
|
|
1639
|
-
type: "apiKey",
|
|
1640
|
-
in: "header",
|
|
1641
|
-
name: "x-api-key"
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
},
|
|
1645
|
-
paths: {},
|
|
1646
|
-
security: [{ ApiKeyAuth: [] }]
|
|
1647
|
-
};
|
|
1648
|
-
for (const collection of config.collections) {
|
|
1649
|
-
spec.components.schemas[collection.slug] = collectionToSchema(collection);
|
|
1650
|
-
}
|
|
1651
|
-
for (const global of config.globals) {
|
|
1652
|
-
spec.components.schemas[global.slug] = globalToSchema(global);
|
|
1653
|
-
}
|
|
1654
|
-
for (const collection of config.collections) {
|
|
1655
|
-
const slug = collection.slug;
|
|
1656
|
-
const path = `/api/collections/${slug}`;
|
|
1657
|
-
const labels = collection.labels || { singular: slug, plural: `${slug}s` };
|
|
1658
|
-
spec.paths[path] = {
|
|
1659
|
-
get: {
|
|
1660
|
-
tags: ["Collections"],
|
|
1661
|
-
summary: `Find ${labels.plural}`,
|
|
1662
|
-
parameters: [
|
|
1663
|
-
{ name: "limit", in: "query", schema: { type: "integer", default: 10 } },
|
|
1664
|
-
{ name: "page", in: "query", schema: { type: "integer", default: 1 } },
|
|
1665
|
-
{ name: "where", in: "query", schema: { type: "string" }, description: "JSON filter" },
|
|
1666
|
-
{ name: "sort", in: "query", schema: { type: "string" }, description: "Sort field (e.g. -createdAt)" }
|
|
1667
|
-
],
|
|
1668
|
-
responses: {
|
|
1669
|
-
200: {
|
|
1670
|
-
description: "Success",
|
|
1671
|
-
content: {
|
|
1672
|
-
"application/json": {
|
|
1673
|
-
schema: {
|
|
1674
|
-
type: "object",
|
|
1675
|
-
properties: {
|
|
1676
|
-
docs: { type: "array", items: { $ref: `#/components/schemas/${slug}` } },
|
|
1677
|
-
total: { type: "integer" },
|
|
1678
|
-
limit: { type: "integer" },
|
|
1679
|
-
page: { type: "integer" }
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
},
|
|
1687
|
-
post: {
|
|
1688
|
-
tags: ["Collections"],
|
|
1689
|
-
summary: `Create ${labels.singular}`,
|
|
1690
|
-
requestBody: {
|
|
1691
|
-
required: true,
|
|
1692
|
-
content: {
|
|
1693
|
-
"application/json": {
|
|
1694
|
-
schema: { $ref: `#/components/schemas/${slug}` }
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
},
|
|
1698
|
-
responses: {
|
|
1699
|
-
201: {
|
|
1700
|
-
description: "Created",
|
|
1701
|
-
content: {
|
|
1702
|
-
"application/json": {
|
|
1703
|
-
schema: { $ref: `#/components/schemas/${slug}` }
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
};
|
|
1710
|
-
spec.paths[`${path}/{id}`] = {
|
|
1711
|
-
get: {
|
|
1712
|
-
tags: ["Collections"],
|
|
1713
|
-
summary: `Get a single ${labels.singular}`,
|
|
1714
|
-
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
1715
|
-
responses: {
|
|
1716
|
-
200: {
|
|
1717
|
-
description: "Success",
|
|
1718
|
-
content: {
|
|
1719
|
-
"application/json": {
|
|
1720
|
-
schema: { $ref: `#/components/schemas/${slug}` }
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
},
|
|
1726
|
-
patch: {
|
|
1727
|
-
tags: ["Collections"],
|
|
1728
|
-
summary: `Update ${labels.singular}`,
|
|
1729
|
-
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
1730
|
-
requestBody: {
|
|
1731
|
-
required: true,
|
|
1732
|
-
content: {
|
|
1733
|
-
"application/json": {
|
|
1734
|
-
schema: { $ref: `#/components/schemas/${slug}` }
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
},
|
|
1738
|
-
responses: {
|
|
1739
|
-
200: {
|
|
1740
|
-
description: "Updated",
|
|
1741
|
-
content: {
|
|
1742
|
-
"application/json": {
|
|
1743
|
-
schema: { $ref: `#/components/schemas/${slug}` }
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
},
|
|
1749
|
-
delete: {
|
|
1750
|
-
tags: ["Collections"],
|
|
1751
|
-
summary: `Delete ${labels.singular}`,
|
|
1752
|
-
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
1753
|
-
responses: {
|
|
1754
|
-
204: { description: "Deleted" }
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
};
|
|
1758
|
-
}
|
|
1759
|
-
for (const global of config.globals) {
|
|
1760
|
-
const slug = global.slug;
|
|
1761
|
-
const path = `/api/globals/${slug}`;
|
|
1762
|
-
spec.paths[path] = {
|
|
1763
|
-
get: {
|
|
1764
|
-
tags: ["Globals"],
|
|
1765
|
-
summary: `Get ${global.label || slug}`,
|
|
1766
|
-
responses: {
|
|
1767
|
-
200: {
|
|
1768
|
-
description: "Success",
|
|
1769
|
-
content: {
|
|
1770
|
-
"application/json": {
|
|
1771
|
-
schema: { $ref: `#/components/schemas/${slug}` }
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
},
|
|
1777
|
-
patch: {
|
|
1778
|
-
tags: ["Globals"],
|
|
1779
|
-
summary: `Update ${global.label || slug}`,
|
|
1780
|
-
requestBody: {
|
|
1781
|
-
required: true,
|
|
1782
|
-
content: {
|
|
1783
|
-
"application/json": {
|
|
1784
|
-
schema: { $ref: `#/components/schemas/${slug}` }
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
},
|
|
1788
|
-
responses: {
|
|
1789
|
-
200: {
|
|
1790
|
-
description: "Updated",
|
|
1791
|
-
content: {
|
|
1792
|
-
"application/json": {
|
|
1793
|
-
schema: { $ref: `#/components/schemas/${slug}` }
|
|
1794
|
-
}
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
};
|
|
1800
|
-
}
|
|
1801
|
-
if (config.storage) {
|
|
1802
|
-
spec.paths["/api/media"] = {
|
|
1803
|
-
get: {
|
|
1804
|
-
tags: ["Media"],
|
|
1805
|
-
summary: "List Media",
|
|
1806
|
-
responses: {
|
|
1807
|
-
200: {
|
|
1808
|
-
description: "Success",
|
|
1809
|
-
content: {
|
|
1810
|
-
"application/json": {
|
|
1811
|
-
schema: {
|
|
1812
|
-
type: "object",
|
|
1813
|
-
properties: {
|
|
1814
|
-
docs: { type: "array", items: { type: "object", additionalProperties: true } }
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
},
|
|
1822
|
-
post: {
|
|
1823
|
-
tags: ["Media"],
|
|
1824
|
-
summary: "Upload Media",
|
|
1825
|
-
requestBody: {
|
|
1826
|
-
content: {
|
|
1827
|
-
"multipart/form-data": {
|
|
1828
|
-
schema: {
|
|
1829
|
-
type: "object",
|
|
1830
|
-
properties: {
|
|
1831
|
-
file: { type: "string", format: "binary" }
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
},
|
|
1837
|
-
responses: {
|
|
1838
|
-
201: { description: "Uploaded" }
|
|
1839
|
-
}
|
|
1840
|
-
}
|
|
1841
|
-
};
|
|
1842
|
-
}
|
|
1843
|
-
return spec;
|
|
1844
|
-
}
|
|
1845
|
-
function collectionToSchema(collection) {
|
|
1846
|
-
const { properties, required } = fieldsToProperties(collection.fields);
|
|
1847
|
-
return {
|
|
1848
|
-
type: "object",
|
|
1849
|
-
properties: {
|
|
1850
|
-
id: { type: "string" },
|
|
1851
|
-
createdAt: { type: "string", format: "date-time" },
|
|
1852
|
-
updatedAt: { type: "string", format: "date-time" },
|
|
1853
|
-
...properties
|
|
1854
|
-
},
|
|
1855
|
-
required: ["id", ...required]
|
|
1856
|
-
};
|
|
1857
|
-
}
|
|
1858
|
-
function globalToSchema(global) {
|
|
1859
|
-
const { properties, required } = fieldsToProperties(global.fields);
|
|
1860
|
-
return {
|
|
1861
|
-
type: "object",
|
|
1862
|
-
properties,
|
|
1863
|
-
required
|
|
1864
|
-
};
|
|
1865
|
-
}
|
|
1866
|
-
function fieldsToProperties(fields) {
|
|
1867
|
-
const props = {};
|
|
1868
|
-
const required = [];
|
|
1869
|
-
for (const field of fields) {
|
|
1870
|
-
if (!field.name || field.type === "join" || field.type === "row") continue;
|
|
1871
|
-
props[field.name] = fieldToSchema(field);
|
|
1872
|
-
if (field.required) {
|
|
1873
|
-
required.push(field.name);
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
return { properties: props, required };
|
|
1877
|
-
}
|
|
1878
|
-
function fieldToSchema(field) {
|
|
1879
|
-
let schema = {};
|
|
1880
|
-
switch (field.type) {
|
|
1881
|
-
case "text":
|
|
1882
|
-
case "textarea":
|
|
1883
|
-
case "email":
|
|
1884
|
-
case "url":
|
|
1885
|
-
schema = { type: "string" };
|
|
1886
|
-
break;
|
|
1887
|
-
case "number":
|
|
1888
|
-
schema = { type: "number" };
|
|
1889
|
-
break;
|
|
1890
|
-
case "boolean":
|
|
1891
|
-
schema = { type: "boolean" };
|
|
1892
|
-
break;
|
|
1893
|
-
case "date":
|
|
1894
|
-
schema = { type: "string", format: "date-time" };
|
|
1895
|
-
break;
|
|
1896
|
-
case "select":
|
|
1897
|
-
case "radio":
|
|
1898
|
-
schema = { type: "string", enum: Array.isArray(field.options) ? field.options.map((o) => typeof o === "string" ? o : o.value) : void 0 };
|
|
1899
|
-
break;
|
|
1900
|
-
case "multiSelect":
|
|
1901
|
-
schema = {
|
|
1902
|
-
type: "array",
|
|
1903
|
-
items: { type: "string", enum: Array.isArray(field.options) ? field.options.map((o) => typeof o === "string" ? o : o.value) : void 0 }
|
|
1904
|
-
};
|
|
1905
|
-
break;
|
|
1906
|
-
case "relationship":
|
|
1907
|
-
schema = { type: "string", description: `ID of a ${field.relationTo} record` };
|
|
1908
|
-
break;
|
|
1909
|
-
case "object": {
|
|
1910
|
-
const { properties, required } = fieldsToProperties(field.fields || []);
|
|
1911
|
-
schema = { type: "object", properties, required };
|
|
1912
|
-
break;
|
|
1913
|
-
}
|
|
1914
|
-
case "array": {
|
|
1915
|
-
const { properties, required } = fieldsToProperties(field.fields || []);
|
|
1916
|
-
schema = { type: "array", items: { type: "object", properties, required } };
|
|
1917
|
-
break;
|
|
1918
|
-
}
|
|
1919
|
-
case "json":
|
|
1920
|
-
case "richText":
|
|
1921
|
-
schema = { type: "object", additionalProperties: true };
|
|
1922
|
-
break;
|
|
1923
|
-
case "blocks":
|
|
1924
|
-
schema = {
|
|
1925
|
-
type: "array",
|
|
1926
|
-
items: {
|
|
1927
|
-
oneOf: field.blocks?.map((block) => {
|
|
1928
|
-
const { properties, required } = fieldsToProperties(block.fields);
|
|
1929
|
-
return {
|
|
1930
|
-
type: "object",
|
|
1931
|
-
properties: {
|
|
1932
|
-
blockType: { type: "string", enum: [block.slug] },
|
|
1933
|
-
...properties
|
|
1934
|
-
},
|
|
1935
|
-
required: ["blockType", ...required]
|
|
1936
|
-
};
|
|
1937
|
-
})
|
|
1938
|
-
}
|
|
1939
|
-
};
|
|
1940
|
-
break;
|
|
1941
|
-
default:
|
|
1942
|
-
schema = { type: "string" };
|
|
1943
|
-
}
|
|
1944
|
-
if (field.label) schema.description = field.label;
|
|
1945
|
-
return schema;
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
1681
|
// src/utils/swagger.ts
|
|
1949
1682
|
function getSwaggerHtml(specUrl = "/api/openapi.json") {
|
|
1950
1683
|
return `
|
|
@@ -1994,20 +1727,6 @@ function getSwaggerHtml(specUrl = "/api/openapi.json") {
|
|
|
1994
1727
|
`;
|
|
1995
1728
|
}
|
|
1996
1729
|
|
|
1997
|
-
// src/auth/jexl.ts
|
|
1998
|
-
import jexl from "jexl";
|
|
1999
|
-
async function evaluateAccess(expression, context) {
|
|
2000
|
-
if (expression === void 0 || expression === null) return false;
|
|
2001
|
-
if (typeof expression === "boolean") return expression;
|
|
2002
|
-
try {
|
|
2003
|
-
const result = await jexl.eval(expression, context);
|
|
2004
|
-
return !!result;
|
|
2005
|
-
} catch (err) {
|
|
2006
|
-
console.error("[dyrected/core] Jexl evaluation failed:", err);
|
|
2007
|
-
return false;
|
|
2008
|
-
}
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
1730
|
// src/router.ts
|
|
2012
1731
|
function accessGate(target, action) {
|
|
2013
1732
|
return async (c, next) => {
|
|
@@ -2118,7 +1837,22 @@ function registerRoutes(app, config) {
|
|
|
2118
1837
|
}))),
|
|
2119
1838
|
upload: !!col.upload,
|
|
2120
1839
|
auth: !!col.auth,
|
|
2121
|
-
admin: col.admin
|
|
1840
|
+
admin: col.admin,
|
|
1841
|
+
workflow: col.workflow ? {
|
|
1842
|
+
initialState: col.workflow.initialState,
|
|
1843
|
+
draftState: col.workflow.draftState,
|
|
1844
|
+
states: col.workflow.states,
|
|
1845
|
+
transitions: col.workflow.transitions.map((t) => ({
|
|
1846
|
+
name: t.name,
|
|
1847
|
+
label: t.label,
|
|
1848
|
+
from: t.from,
|
|
1849
|
+
to: t.to,
|
|
1850
|
+
requiredCapabilities: t.requiredCapabilities,
|
|
1851
|
+
requireComment: t.requireComment,
|
|
1852
|
+
unpublish: t.unpublish
|
|
1853
|
+
})),
|
|
1854
|
+
roles: col.workflow.roles
|
|
1855
|
+
} : void 0
|
|
2122
1856
|
})));
|
|
2123
1857
|
const filteredGlobals = await Promise.all(globals.filter((glb) => !siteId || glb.shared || !glb.siteId || glb.siteId === siteId).map(async (glb) => ({
|
|
2124
1858
|
slug: glb.slug,
|
|
@@ -2312,6 +2046,10 @@ function registerRoutes(app, config) {
|
|
|
2312
2046
|
if (collection.auth) {
|
|
2313
2047
|
app.post(`${path}/:id/change-password`, requireAuth(), (c) => controller.changePassword(c));
|
|
2314
2048
|
}
|
|
2049
|
+
if (collection.workflow) {
|
|
2050
|
+
app.post(`${path}/:id/transitions/:transition`, requireAuth(), (c) => controller.transition(c));
|
|
2051
|
+
app.get(`${path}/:id/workflow-history`, requireAuth(), (c) => controller.workflowHistory(c));
|
|
2052
|
+
}
|
|
2315
2053
|
}
|
|
2316
2054
|
for (const global of config.globals) {
|
|
2317
2055
|
const path = `/api/globals/${global.slug}`;
|
|
@@ -2323,6 +2061,41 @@ function registerRoutes(app, config) {
|
|
|
2323
2061
|
const previewController = new PreviewController();
|
|
2324
2062
|
app.post("/api/preview-token", requireAuth(), (c) => previewController.createToken(c));
|
|
2325
2063
|
app.get("/api/preview-data", (c) => previewController.getData(c));
|
|
2064
|
+
app.all("/api/collections/:slug/:id/*", requireAuth(), async (c) => {
|
|
2065
|
+
const slug = c.req.param("slug");
|
|
2066
|
+
const id = c.req.param("id");
|
|
2067
|
+
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
2068
|
+
const config2 = c.get("config");
|
|
2069
|
+
if (config2.collections.some((col) => col.slug === slug)) {
|
|
2070
|
+
return c.json({ message: "Not Found" }, 404);
|
|
2071
|
+
}
|
|
2072
|
+
if (!config2.onSchemaFetch || !siteId) {
|
|
2073
|
+
return c.json({ message: `Collection "${slug}" not found` }, 404);
|
|
2074
|
+
}
|
|
2075
|
+
const dynamic = await config2.onSchemaFetch(siteId);
|
|
2076
|
+
const collection = dynamic.collections?.find((col) => col.slug === slug);
|
|
2077
|
+
if (!collection?.workflow) {
|
|
2078
|
+
return c.json({ message: `Collection "${slug}" not found or has no workflow` }, 404);
|
|
2079
|
+
}
|
|
2080
|
+
const controller = new CollectionController(collection);
|
|
2081
|
+
const rawPath = c.req.path;
|
|
2082
|
+
const method = c.req.method;
|
|
2083
|
+
const transitionMatch = rawPath.match(/\/transitions\/([^/]+)$/);
|
|
2084
|
+
if (transitionMatch && method === "POST") {
|
|
2085
|
+
c.req.raw.__transition = transitionMatch[1];
|
|
2086
|
+
const origParam = c.req.param.bind(c.req);
|
|
2087
|
+
c.req.param = (key) => {
|
|
2088
|
+
if (key === "transition") return transitionMatch[1];
|
|
2089
|
+
if (key === "id") return id;
|
|
2090
|
+
return origParam(key);
|
|
2091
|
+
};
|
|
2092
|
+
return controller.transition(c);
|
|
2093
|
+
}
|
|
2094
|
+
if (rawPath.endsWith("/workflow-history") && method === "GET") {
|
|
2095
|
+
return controller.workflowHistory(c);
|
|
2096
|
+
}
|
|
2097
|
+
return c.json({ message: "Not Found" }, 404);
|
|
2098
|
+
});
|
|
2326
2099
|
app.all("/api/collections/:slug/:id?", async (c) => {
|
|
2327
2100
|
const slug = c.req.param("slug");
|
|
2328
2101
|
const id = c.req.param("id");
|