@dyrected/core 2.5.28 → 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/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-Bp7PDOYG.cjs';
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-Bp7PDOYG.js';
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
- } from "./chunk-SUGK7UYL.js";
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-DQIWTQZW.js");
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
- const doc = await db.findOne({ collection: this.collection.slug, id });
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
+ "&": "&amp;",
1120
+ "<": "&lt;",
1121
+ ">": "&gt;",
1122
+ "'": "&#39;",
1123
+ '"': "&quot;"
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">&nbsp;</td></tr></table>`;
1145
+ }
1146
+ function spacer(height = 16) {
1147
+ return table(row("&nbsp;", `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("&nbsp;", `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
- <table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f9fafb;table-layout:fixed">
1088
- <tr>
1089
- <td align="center" style="padding:40px 16px">
1090
- <table cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:600px;background-color:#ffffff;border-radius:12px;border:1px solid #e5e7eb;table-layout:fixed">
1091
- <tr>
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
- <table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f9fafb;table-layout:fixed">
1126
- <tr>
1127
- <td align="center" style="padding:40px 16px">
1128
- <table cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:600px;background-color:#ffffff;border-radius:12px;border:1px solid #e5e7eb;table-layout:fixed">
1129
- <tr>
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
- <table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f9fafb;table-layout:fixed">
1166
- <tr>
1167
- <td align="center" style="padding:40px 16px">
1168
- <table cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:600px;background-color:#ffffff;border-radius:12px;border:1px solid #e5e7eb;table-layout:fixed">
1169
- <tr>
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
- <table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f9fafb;table-layout:fixed">
1216
- <tr>
1217
- <td align="center" style="padding:40px 16px">
1218
- <table cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:600px;background-color:#ffffff;border-radius:12px;border:1px solid #e5e7eb;table-layout:fixed">
1219
- <tr>
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
+ };