@dyrected/core 2.5.29 → 2.5.30
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-CQDVPMEU.js +613 -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-qhLus8ZO.d.cts +1881 -0
- package/dist/index-qhLus8ZO.d.ts +1881 -0
- package/dist/index.cjs +330 -4
- package/dist/index.d.cts +56 -3
- package/dist/index.d.ts +56 -3
- package/dist/index.js +40 -4
- package/dist/server.cjs +482 -156
- package/dist/server.d.cts +28 -1
- package/dist/server.d.ts +28 -1
- package/dist/server.js +232 -158
- package/dist/where-sanitizer-WJ2W2QCE.js +54 -0
- package/package.json +2 -1
package/dist/server.d.cts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as hono_types from 'hono/types';
|
|
2
2
|
import * as hono from 'hono';
|
|
3
3
|
import { Hono, Context } from 'hono';
|
|
4
|
-
import { D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './index-
|
|
4
|
+
import { D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './index-DTrpTru7.cjs';
|
|
5
5
|
import * as hono_utils_http_status from 'hono/utils/http-status';
|
|
6
6
|
import * as hono_utils_types from 'hono/utils/types';
|
|
7
|
+
import 'lucide-react';
|
|
7
8
|
|
|
8
9
|
interface DyrectedContext {
|
|
9
10
|
Variables: {
|
|
@@ -191,6 +192,32 @@ declare class CollectionController {
|
|
|
191
192
|
}, 400, "json">) | (Response & hono.TypedResponse<{
|
|
192
193
|
message: string;
|
|
193
194
|
}, 404, "json">)>;
|
|
195
|
+
transition(c: Context<DyrectedContext>): Promise<(Response & hono.TypedResponse<{
|
|
196
|
+
message: string;
|
|
197
|
+
}, 500, "json">) | (Response & hono.TypedResponse<{
|
|
198
|
+
message: string;
|
|
199
|
+
}, 404, "json">) | (Response & hono.TypedResponse<{
|
|
200
|
+
[x: string]: any;
|
|
201
|
+
id: string;
|
|
202
|
+
} | null, hono_utils_http_status.ContentfulStatusCode, "json">) | (Response & hono.TypedResponse<{
|
|
203
|
+
error: true;
|
|
204
|
+
message: string;
|
|
205
|
+
}, 400, "json">)>;
|
|
206
|
+
workflowHistory(c: Context<DyrectedContext>): Promise<(Response & hono.TypedResponse<{
|
|
207
|
+
docs: {
|
|
208
|
+
[x: string]: any;
|
|
209
|
+
}[];
|
|
210
|
+
total: number;
|
|
211
|
+
limit: number;
|
|
212
|
+
page: number;
|
|
213
|
+
totalPages: number;
|
|
214
|
+
hasNextPage: boolean;
|
|
215
|
+
hasPrevPage: boolean;
|
|
216
|
+
}, hono_utils_http_status.ContentfulStatusCode, "json">) | (Response & hono.TypedResponse<{
|
|
217
|
+
message: string;
|
|
218
|
+
}, 500, "json">) | (Response & hono.TypedResponse<{
|
|
219
|
+
message: string;
|
|
220
|
+
}, 404, "json">)>;
|
|
194
221
|
/**
|
|
195
222
|
* POST /api/collections/:slug/:id/change-password
|
|
196
223
|
*
|
package/dist/server.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as hono_types from 'hono/types';
|
|
2
2
|
import * as hono from 'hono';
|
|
3
3
|
import { Hono, Context } from 'hono';
|
|
4
|
-
import { D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './index-
|
|
4
|
+
import { D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './index-DTrpTru7.js';
|
|
5
5
|
import * as hono_utils_http_status from 'hono/utils/http-status';
|
|
6
6
|
import * as hono_utils_types from 'hono/utils/types';
|
|
7
|
+
import 'lucide-react';
|
|
7
8
|
|
|
8
9
|
interface DyrectedContext {
|
|
9
10
|
Variables: {
|
|
@@ -191,6 +192,32 @@ declare class CollectionController {
|
|
|
191
192
|
}, 400, "json">) | (Response & hono.TypedResponse<{
|
|
192
193
|
message: string;
|
|
193
194
|
}, 404, "json">)>;
|
|
195
|
+
transition(c: Context<DyrectedContext>): Promise<(Response & hono.TypedResponse<{
|
|
196
|
+
message: string;
|
|
197
|
+
}, 500, "json">) | (Response & hono.TypedResponse<{
|
|
198
|
+
message: string;
|
|
199
|
+
}, 404, "json">) | (Response & hono.TypedResponse<{
|
|
200
|
+
[x: string]: any;
|
|
201
|
+
id: string;
|
|
202
|
+
} | null, hono_utils_http_status.ContentfulStatusCode, "json">) | (Response & hono.TypedResponse<{
|
|
203
|
+
error: true;
|
|
204
|
+
message: string;
|
|
205
|
+
}, 400, "json">)>;
|
|
206
|
+
workflowHistory(c: Context<DyrectedContext>): Promise<(Response & hono.TypedResponse<{
|
|
207
|
+
docs: {
|
|
208
|
+
[x: string]: any;
|
|
209
|
+
}[];
|
|
210
|
+
total: number;
|
|
211
|
+
limit: number;
|
|
212
|
+
page: number;
|
|
213
|
+
totalPages: number;
|
|
214
|
+
hasNextPage: boolean;
|
|
215
|
+
hasPrevPage: boolean;
|
|
216
|
+
}, hono_utils_http_status.ContentfulStatusCode, "json">) | (Response & hono.TypedResponse<{
|
|
217
|
+
message: string;
|
|
218
|
+
}, 500, "json">) | (Response & hono.TypedResponse<{
|
|
219
|
+
message: string;
|
|
220
|
+
}, 404, "json">)>;
|
|
194
221
|
/**
|
|
195
222
|
* POST /api/collections/:slug/:id/change-password
|
|
196
223
|
*
|
package/dist/server.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
|
+
WORKFLOW_HISTORY_COLLECTION,
|
|
3
|
+
createWorkflowDocument,
|
|
2
4
|
executeFieldAfterRead,
|
|
3
5
|
executeFieldBeforeChange,
|
|
6
|
+
initializeWorkflowDocument,
|
|
7
|
+
materializeWorkflowDocument,
|
|
4
8
|
normalizeConfig,
|
|
5
|
-
runCollectionHooks
|
|
6
|
-
|
|
9
|
+
runCollectionHooks,
|
|
10
|
+
saveWorkflowDraft,
|
|
11
|
+
transitionWorkflow
|
|
12
|
+
} from "./chunk-CQDVPMEU.js";
|
|
7
13
|
|
|
8
14
|
// src/app.ts
|
|
9
15
|
import { Hono } from "hono";
|
|
@@ -304,7 +310,7 @@ var CollectionController = class {
|
|
|
304
310
|
if (this.collection.admin?.filterable === false) {
|
|
305
311
|
where = void 0;
|
|
306
312
|
} else {
|
|
307
|
-
const { sanitizeWhereClause } = await import("./where-sanitizer-
|
|
313
|
+
const { sanitizeWhereClause } = await import("./where-sanitizer-WJ2W2QCE.js");
|
|
308
314
|
where = sanitizeWhereClause(where, this.collection.fields);
|
|
309
315
|
if (Object.keys(where).length === 0) {
|
|
310
316
|
where = void 0;
|
|
@@ -320,6 +326,9 @@ var CollectionController = class {
|
|
|
320
326
|
if (beforeReadResult !== void 0) {
|
|
321
327
|
where = beforeReadResult;
|
|
322
328
|
}
|
|
329
|
+
if (this.collection.workflow && !user) {
|
|
330
|
+
where = where ? { AND: [where, { __published: { exists: true } }] } : { __published: { exists: true } };
|
|
331
|
+
}
|
|
323
332
|
let result = await db.find({
|
|
324
333
|
collection: this.collection.slug,
|
|
325
334
|
limit,
|
|
@@ -340,7 +349,7 @@ var CollectionController = class {
|
|
|
340
349
|
where
|
|
341
350
|
});
|
|
342
351
|
}
|
|
343
|
-
result.docs = result.docs.map((doc) => DefaultsService.apply(this.collection.fields, doc));
|
|
352
|
+
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
353
|
const processedDocs = [];
|
|
345
354
|
for (const doc of result.docs) {
|
|
346
355
|
const docWithCollectionHooks = await runCollectionHooks(this.collection.hooks?.afterRead, {
|
|
@@ -368,8 +377,12 @@ var CollectionController = class {
|
|
|
368
377
|
const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 10;
|
|
369
378
|
const user = c.get("user");
|
|
370
379
|
if (!id) return c.json({ message: "Missing ID" }, 400);
|
|
371
|
-
|
|
380
|
+
let doc = await db.findOne({ collection: this.collection.slug, id });
|
|
372
381
|
if (!doc) return c.json({ message: "Not Found" }, 404);
|
|
382
|
+
if (this.collection.workflow) {
|
|
383
|
+
doc = materializeWorkflowDocument(doc, this.collection.workflow, user);
|
|
384
|
+
if (!doc) return c.json({ message: "Not Found" }, 404);
|
|
385
|
+
}
|
|
373
386
|
const docWithDefaults = DefaultsService.apply(this.collection.fields, doc);
|
|
374
387
|
const docWithCollectionHooks = await runCollectionHooks(this.collection.hooks?.afterRead, {
|
|
375
388
|
doc: docWithDefaults,
|
|
@@ -409,6 +422,9 @@ var CollectionController = class {
|
|
|
409
422
|
createdBy: user?.sub ?? null,
|
|
410
423
|
updatedBy: user?.sub ?? null
|
|
411
424
|
};
|
|
425
|
+
if (this.collection.workflow) {
|
|
426
|
+
data = initializeWorkflowDocument(data, this.collection.workflow);
|
|
427
|
+
}
|
|
412
428
|
if (this.collection.auth && data.password) {
|
|
413
429
|
data.password = await hashPassword(data.password);
|
|
414
430
|
}
|
|
@@ -420,7 +436,7 @@ var CollectionController = class {
|
|
|
420
436
|
operation: "create",
|
|
421
437
|
db: readonlyDb
|
|
422
438
|
});
|
|
423
|
-
const doc = await db.create({ collection: this.collection.slug, data });
|
|
439
|
+
const doc = this.collection.workflow ? (await createWorkflowDocument({ config, collection: this.collection, data, user })).doc : await db.create({ collection: this.collection.slug, data });
|
|
424
440
|
if (this.collection.audit && db) {
|
|
425
441
|
AuditService.log(db, {
|
|
426
442
|
operation: "create",
|
|
@@ -438,8 +454,9 @@ var CollectionController = class {
|
|
|
438
454
|
operation: "create",
|
|
439
455
|
db
|
|
440
456
|
}, { isolated: true });
|
|
457
|
+
const responseDoc = this.collection.workflow ? materializeWorkflowDocument(doc, this.collection.workflow, user) : doc;
|
|
441
458
|
const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
|
|
442
|
-
doc,
|
|
459
|
+
doc: responseDoc,
|
|
443
460
|
req: c.req,
|
|
444
461
|
user,
|
|
445
462
|
db: readonlyDb
|
|
@@ -502,8 +519,9 @@ var CollectionController = class {
|
|
|
502
519
|
operation: "create",
|
|
503
520
|
db
|
|
504
521
|
}, { isolated: true });
|
|
522
|
+
const responseDoc = this.collection.workflow ? materializeWorkflowDocument(doc, this.collection.workflow, user) : doc;
|
|
505
523
|
const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
|
|
506
|
-
doc,
|
|
524
|
+
doc: responseDoc,
|
|
507
525
|
req: c.req,
|
|
508
526
|
user,
|
|
509
527
|
db: readonlyDb
|
|
@@ -545,7 +563,7 @@ var CollectionController = class {
|
|
|
545
563
|
operation: "update",
|
|
546
564
|
db: readonlyDb
|
|
547
565
|
});
|
|
548
|
-
const doc = await db.update({ collection: this.collection.slug, id, data });
|
|
566
|
+
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
567
|
if (this.collection.audit && db) {
|
|
550
568
|
AuditService.log(db, {
|
|
551
569
|
operation: "update",
|
|
@@ -573,6 +591,42 @@ var CollectionController = class {
|
|
|
573
591
|
const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user, readonlyDb);
|
|
574
592
|
return c.json(finalDoc);
|
|
575
593
|
}
|
|
594
|
+
async transition(c) {
|
|
595
|
+
const config = c.get("config");
|
|
596
|
+
if (!config.db) return c.json({ message: "Database not configured" }, 500);
|
|
597
|
+
if (!this.collection.workflow) return c.json({ message: "Workflows are not enabled for this collection" }, 404);
|
|
598
|
+
const id = c.req.param("id");
|
|
599
|
+
const transitionName = c.req.param("transition");
|
|
600
|
+
const body = await c.req.json().catch(() => ({}));
|
|
601
|
+
try {
|
|
602
|
+
const doc = await transitionWorkflow({
|
|
603
|
+
config,
|
|
604
|
+
collection: this.collection,
|
|
605
|
+
id,
|
|
606
|
+
transitionName,
|
|
607
|
+
expectedRevision: body.expectedRevision,
|
|
608
|
+
comment: body.comment,
|
|
609
|
+
user: c.get("user"),
|
|
610
|
+
req: { query: c.req.query(), headers: c.req.header(), raw: c.req.raw }
|
|
611
|
+
});
|
|
612
|
+
return c.json(materializeWorkflowDocument(doc, this.collection.workflow, c.get("user")));
|
|
613
|
+
} catch (error) {
|
|
614
|
+
const status = typeof error.statusCode === "number" ? error.statusCode : 500;
|
|
615
|
+
return c.json({ error: true, message: error instanceof Error ? error.message : String(error) }, status);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async workflowHistory(c) {
|
|
619
|
+
const config = c.get("config");
|
|
620
|
+
if (!config.db) return c.json({ message: "Database not configured" }, 500);
|
|
621
|
+
if (!this.collection.workflow) return c.json({ message: "Workflows are not enabled for this collection" }, 404);
|
|
622
|
+
const result = await config.db.find({
|
|
623
|
+
collection: WORKFLOW_HISTORY_COLLECTION,
|
|
624
|
+
where: { collection: { equals: this.collection.slug }, documentId: { equals: c.req.param("id") } },
|
|
625
|
+
sort: "-createdAt",
|
|
626
|
+
limit: Math.min(Number(c.req.query("limit")) || 50, 100)
|
|
627
|
+
});
|
|
628
|
+
return c.json(result);
|
|
629
|
+
}
|
|
576
630
|
/**
|
|
577
631
|
* POST /api/collections/:slug/:id/change-password
|
|
578
632
|
*
|
|
@@ -1040,6 +1094,96 @@ async function verifyCollectionToken(token) {
|
|
|
1040
1094
|
return payload;
|
|
1041
1095
|
}
|
|
1042
1096
|
|
|
1097
|
+
// src/services/email-template.ts
|
|
1098
|
+
var emailTokens = {
|
|
1099
|
+
colors: {
|
|
1100
|
+
canvas: "#f6f7f2",
|
|
1101
|
+
surface: "#ffffff",
|
|
1102
|
+
text: "#171717",
|
|
1103
|
+
muted: "#62665b",
|
|
1104
|
+
subtle: "#8a8f82",
|
|
1105
|
+
border: "#dde0d7",
|
|
1106
|
+
accent: "#b6ff2e",
|
|
1107
|
+
code: "#f1f3ec",
|
|
1108
|
+
dangerSurface: "#fff2f0",
|
|
1109
|
+
dangerBorder: "#ffc9c2",
|
|
1110
|
+
dangerText: "#9f251b"
|
|
1111
|
+
},
|
|
1112
|
+
font: "Arial, 'Helvetica Neue', Helvetica, sans-serif",
|
|
1113
|
+
mono: "'Courier New', Courier, monospace",
|
|
1114
|
+
radius: { card: "12px", control: "6px" },
|
|
1115
|
+
width: "600px"
|
|
1116
|
+
};
|
|
1117
|
+
function escapeHtml(value) {
|
|
1118
|
+
return value.replace(/[&<>'"]/g, (character) => ({
|
|
1119
|
+
"&": "&",
|
|
1120
|
+
"<": "<",
|
|
1121
|
+
">": ">",
|
|
1122
|
+
"'": "'",
|
|
1123
|
+
'"': """
|
|
1124
|
+
})[character]);
|
|
1125
|
+
}
|
|
1126
|
+
function safeHttpUrl(value) {
|
|
1127
|
+
try {
|
|
1128
|
+
const url = new URL(value);
|
|
1129
|
+
return url.protocol === "https:" || url.protocol === "http:" ? escapeHtml(url.toString()) : void 0;
|
|
1130
|
+
} catch {
|
|
1131
|
+
return void 0;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
function heading(content) {
|
|
1135
|
+
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>`;
|
|
1136
|
+
}
|
|
1137
|
+
function paragraph(content, margin = "0 0 16px") {
|
|
1138
|
+
return `<p style="margin:${margin};font-family:${emailTokens.font};font-size:15px;line-height:1.6;color:${emailTokens.colors.muted}">${escapeHtml(content)}</p>`;
|
|
1139
|
+
}
|
|
1140
|
+
function sectionLabel(content) {
|
|
1141
|
+
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>`;
|
|
1142
|
+
}
|
|
1143
|
+
function divider() {
|
|
1144
|
+
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>`;
|
|
1145
|
+
}
|
|
1146
|
+
function spacer(height = 16) {
|
|
1147
|
+
return table(row(" ", `height:${height}px;font-size:0;line-height:0`));
|
|
1148
|
+
}
|
|
1149
|
+
function row(content, cellStyle = "") {
|
|
1150
|
+
return `<tr><td style="${cellStyle}">${content}</td></tr>`;
|
|
1151
|
+
}
|
|
1152
|
+
function table(content, style = "width:100%") {
|
|
1153
|
+
return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="${style}">${content}</table>`;
|
|
1154
|
+
}
|
|
1155
|
+
function detailBox(content, monospace = false) {
|
|
1156
|
+
const font = monospace ? emailTokens.mono : emailTokens.font;
|
|
1157
|
+
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}`);
|
|
1158
|
+
}
|
|
1159
|
+
function ctaButton(label, href) {
|
|
1160
|
+
const safeHref = safeHttpUrl(href);
|
|
1161
|
+
if (!safeHref) return "";
|
|
1162
|
+
return table(row(
|
|
1163
|
+
`<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>`,
|
|
1164
|
+
"padding:8px 0 24px"
|
|
1165
|
+
), "width:auto");
|
|
1166
|
+
}
|
|
1167
|
+
function alertBox(content) {
|
|
1168
|
+
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}`);
|
|
1169
|
+
}
|
|
1170
|
+
function layout({ preheader, title, content, footer }) {
|
|
1171
|
+
return `<!doctype html>
|
|
1172
|
+
<html lang="en">
|
|
1173
|
+
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"></head>
|
|
1174
|
+
<body style="margin:0;padding:0;background:${emailTokens.colors.canvas}">
|
|
1175
|
+
<div style="display:none;max-height:0;overflow:hidden;opacity:0;color:transparent">${escapeHtml(preheader)}</div>
|
|
1176
|
+
${table(row(
|
|
1177
|
+
table(
|
|
1178
|
+
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"),
|
|
1179
|
+
`width:100%;max-width:${emailTokens.width};background:${emailTokens.colors.surface};border:1px solid ${emailTokens.colors.border};border-radius:${emailTokens.radius.card};overflow:hidden`
|
|
1180
|
+
),
|
|
1181
|
+
"padding:32px 12px"
|
|
1182
|
+
), `width:100%;background:${emailTokens.colors.canvas}`)}
|
|
1183
|
+
</body>
|
|
1184
|
+
</html>`;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1043
1187
|
// src/services/email.service.ts
|
|
1044
1188
|
var _devSend = null;
|
|
1045
1189
|
var _devSendPromise = null;
|
|
@@ -1083,77 +1227,24 @@ function buildWelcomeEmail(config, args) {
|
|
|
1083
1227
|
const custom = config.email?.templates?.welcome?.(args);
|
|
1084
1228
|
return {
|
|
1085
1229
|
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>`
|
|
1230
|
+
html: custom?.html ?? layout({
|
|
1231
|
+
preheader: "Your Dyrected account is ready.",
|
|
1232
|
+
title: "Welcome \u2014 your account is ready",
|
|
1233
|
+
content: `${paragraph("Your account has been created. You can now log in with:")}${detailBox(args.email)}`,
|
|
1234
|
+
footer: "If you didn't create this account, you can safely ignore this email."
|
|
1235
|
+
})
|
|
1118
1236
|
};
|
|
1119
1237
|
}
|
|
1120
1238
|
function buildInviteEmail(config, args) {
|
|
1121
1239
|
const custom = config.email?.templates?.invite?.(args);
|
|
1122
1240
|
return {
|
|
1123
1241
|
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>`
|
|
1242
|
+
html: custom?.html ?? layout({
|
|
1243
|
+
preheader: "You've been invited to join a Dyrected account.",
|
|
1244
|
+
title: "You've been invited",
|
|
1245
|
+
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)}`,
|
|
1246
|
+
footer: "If you weren't expecting this invitation, you can safely ignore this email."
|
|
1247
|
+
})
|
|
1157
1248
|
};
|
|
1158
1249
|
}
|
|
1159
1250
|
function buildResetPasswordEmail(config, args) {
|
|
@@ -1161,95 +1252,24 @@ function buildResetPasswordEmail(config, args) {
|
|
|
1161
1252
|
const resetLink = args.url;
|
|
1162
1253
|
return {
|
|
1163
1254
|
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>`
|
|
1255
|
+
html: custom?.html ?? layout({
|
|
1256
|
+
preheader: "Reset your Dyrected password.",
|
|
1257
|
+
title: "Reset your password",
|
|
1258
|
+
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)}`,
|
|
1259
|
+
footer: "If you didn't request a password reset, you can safely ignore this email."
|
|
1260
|
+
})
|
|
1208
1261
|
};
|
|
1209
1262
|
}
|
|
1210
1263
|
function buildPasswordChangedEmail(config, args) {
|
|
1211
1264
|
const custom = config.email?.templates?.passwordChanged?.(args);
|
|
1212
1265
|
return {
|
|
1213
1266
|
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>`
|
|
1267
|
+
html: custom?.html ?? layout({
|
|
1268
|
+
preheader: "Your Dyrected password was changed.",
|
|
1269
|
+
title: "Password changed",
|
|
1270
|
+
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.")}`,
|
|
1271
|
+
footer: "This is an automated security notification."
|
|
1272
|
+
})
|
|
1253
1273
|
};
|
|
1254
1274
|
}
|
|
1255
1275
|
|
|
@@ -2118,7 +2138,22 @@ function registerRoutes(app, config) {
|
|
|
2118
2138
|
}))),
|
|
2119
2139
|
upload: !!col.upload,
|
|
2120
2140
|
auth: !!col.auth,
|
|
2121
|
-
admin: col.admin
|
|
2141
|
+
admin: col.admin,
|
|
2142
|
+
workflow: col.workflow ? {
|
|
2143
|
+
initialState: col.workflow.initialState,
|
|
2144
|
+
draftState: col.workflow.draftState,
|
|
2145
|
+
states: col.workflow.states,
|
|
2146
|
+
transitions: col.workflow.transitions.map((t) => ({
|
|
2147
|
+
name: t.name,
|
|
2148
|
+
label: t.label,
|
|
2149
|
+
from: t.from,
|
|
2150
|
+
to: t.to,
|
|
2151
|
+
requiredCapabilities: t.requiredCapabilities,
|
|
2152
|
+
requireComment: t.requireComment,
|
|
2153
|
+
unpublish: t.unpublish
|
|
2154
|
+
})),
|
|
2155
|
+
roles: col.workflow.roles
|
|
2156
|
+
} : void 0
|
|
2122
2157
|
})));
|
|
2123
2158
|
const filteredGlobals = await Promise.all(globals.filter((glb) => !siteId || glb.shared || !glb.siteId || glb.siteId === siteId).map(async (glb) => ({
|
|
2124
2159
|
slug: glb.slug,
|
|
@@ -2312,6 +2347,10 @@ function registerRoutes(app, config) {
|
|
|
2312
2347
|
if (collection.auth) {
|
|
2313
2348
|
app.post(`${path}/:id/change-password`, requireAuth(), (c) => controller.changePassword(c));
|
|
2314
2349
|
}
|
|
2350
|
+
if (collection.workflow) {
|
|
2351
|
+
app.post(`${path}/:id/transitions/:transition`, requireAuth(), (c) => controller.transition(c));
|
|
2352
|
+
app.get(`${path}/:id/workflow-history`, requireAuth(), (c) => controller.workflowHistory(c));
|
|
2353
|
+
}
|
|
2315
2354
|
}
|
|
2316
2355
|
for (const global of config.globals) {
|
|
2317
2356
|
const path = `/api/globals/${global.slug}`;
|
|
@@ -2323,6 +2362,41 @@ function registerRoutes(app, config) {
|
|
|
2323
2362
|
const previewController = new PreviewController();
|
|
2324
2363
|
app.post("/api/preview-token", requireAuth(), (c) => previewController.createToken(c));
|
|
2325
2364
|
app.get("/api/preview-data", (c) => previewController.getData(c));
|
|
2365
|
+
app.all("/api/collections/:slug/:id/*", requireAuth(), async (c) => {
|
|
2366
|
+
const slug = c.req.param("slug");
|
|
2367
|
+
const id = c.req.param("id");
|
|
2368
|
+
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
2369
|
+
const config2 = c.get("config");
|
|
2370
|
+
if (config2.collections.some((col) => col.slug === slug)) {
|
|
2371
|
+
return c.json({ message: "Not Found" }, 404);
|
|
2372
|
+
}
|
|
2373
|
+
if (!config2.onSchemaFetch || !siteId) {
|
|
2374
|
+
return c.json({ message: `Collection "${slug}" not found` }, 404);
|
|
2375
|
+
}
|
|
2376
|
+
const dynamic = await config2.onSchemaFetch(siteId);
|
|
2377
|
+
const collection = dynamic.collections?.find((col) => col.slug === slug);
|
|
2378
|
+
if (!collection?.workflow) {
|
|
2379
|
+
return c.json({ message: `Collection "${slug}" not found or has no workflow` }, 404);
|
|
2380
|
+
}
|
|
2381
|
+
const controller = new CollectionController(collection);
|
|
2382
|
+
const rawPath = c.req.path;
|
|
2383
|
+
const method = c.req.method;
|
|
2384
|
+
const transitionMatch = rawPath.match(/\/transitions\/([^/]+)$/);
|
|
2385
|
+
if (transitionMatch && method === "POST") {
|
|
2386
|
+
c.req.raw.__transition = transitionMatch[1];
|
|
2387
|
+
const origParam = c.req.param.bind(c.req);
|
|
2388
|
+
c.req.param = (key) => {
|
|
2389
|
+
if (key === "transition") return transitionMatch[1];
|
|
2390
|
+
if (key === "id") return id;
|
|
2391
|
+
return origParam(key);
|
|
2392
|
+
};
|
|
2393
|
+
return controller.transition(c);
|
|
2394
|
+
}
|
|
2395
|
+
if (rawPath.endsWith("/workflow-history") && method === "GET") {
|
|
2396
|
+
return controller.workflowHistory(c);
|
|
2397
|
+
}
|
|
2398
|
+
return c.json({ message: "Not Found" }, 404);
|
|
2399
|
+
});
|
|
2326
2400
|
app.all("/api/collections/:slug/:id?", async (c) => {
|
|
2327
2401
|
const slug = c.req.param("slug");
|
|
2328
2402
|
const id = c.req.param("id");
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// src/utils/where-sanitizer.ts
|
|
2
|
+
var NEVER_FILTERABLE_TYPES = [
|
|
3
|
+
"password",
|
|
4
|
+
"richText",
|
|
5
|
+
"json",
|
|
6
|
+
"file",
|
|
7
|
+
"image",
|
|
8
|
+
"join",
|
|
9
|
+
"collapsible"
|
|
10
|
+
];
|
|
11
|
+
function sanitizeWhereClause(where, fields) {
|
|
12
|
+
const fieldMap = /* @__PURE__ */ new Map();
|
|
13
|
+
function addFieldsToMap(fList, prefix = "") {
|
|
14
|
+
for (const f of fList) {
|
|
15
|
+
if ("name" in f && f.name) {
|
|
16
|
+
fieldMap.set(prefix + f.name, f);
|
|
17
|
+
}
|
|
18
|
+
if (f.type === "object" || f.type === "row" || f.type === "array") {
|
|
19
|
+
if ("fields" in f && Array.isArray(f.fields)) {
|
|
20
|
+
addFieldsToMap(f.fields, f.type === "object" || f.type === "array" ? `${prefix}${f.name}.` : prefix);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
addFieldsToMap(fields);
|
|
26
|
+
function walk(node) {
|
|
27
|
+
const result = {};
|
|
28
|
+
for (const [key, value] of Object.entries(node)) {
|
|
29
|
+
if (key === "AND" || key === "OR") {
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
const processed = value.map((v) => walk(v)).filter((v) => Object.keys(v).length > 0);
|
|
32
|
+
if (processed.length > 0) {
|
|
33
|
+
result[key] = processed;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (key === "id") {
|
|
39
|
+
result[key] = value;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const fieldDef = fieldMap.get(key);
|
|
43
|
+
if (!fieldDef) continue;
|
|
44
|
+
if (fieldDef.admin?.filterable === false) continue;
|
|
45
|
+
if (NEVER_FILTERABLE_TYPES.includes(fieldDef.type)) continue;
|
|
46
|
+
result[key] = value;
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
return walk(where);
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
sanitizeWhereClause
|
|
54
|
+
};
|