@dyrected/core 2.5.11 → 2.5.13

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/index.cjs CHANGED
@@ -908,23 +908,102 @@ function normalizeConfig(config) {
908
908
  const globals = config?.globals || [];
909
909
  const needsAudit = collections.some((col) => col.audit);
910
910
  const normalizedCollections = collections.map((col) => {
911
- const fields = col.fields || [];
911
+ let fields = col.fields || [];
912
912
  const existingFieldNames = new Set(fields.map((f) => f.name));
913
- const fieldsToInject = SYSTEM_FIELDS.filter((f) => !existingFieldNames.has(f.name));
913
+ if (col.auth) {
914
+ if (!existingFieldNames.has("email")) {
915
+ fields = [
916
+ ...fields,
917
+ {
918
+ name: "email",
919
+ type: "email",
920
+ label: "Email",
921
+ required: true,
922
+ unique: true,
923
+ promoted: true,
924
+ access: {
925
+ update: "!id"
926
+ }
927
+ }
928
+ ];
929
+ }
930
+ if (!existingFieldNames.has("password")) {
931
+ fields = [
932
+ ...fields,
933
+ {
934
+ name: "password",
935
+ type: "text",
936
+ label: "Password",
937
+ required: true,
938
+ access: {
939
+ update: "!id || user.id == id"
940
+ }
941
+ }
942
+ ];
943
+ }
944
+ if (!existingFieldNames.has("roles")) {
945
+ fields = [
946
+ ...fields,
947
+ {
948
+ name: "roles",
949
+ type: "select",
950
+ label: "Roles",
951
+ defaultValue: [],
952
+ options: [
953
+ { value: "admin", label: "Admin" },
954
+ { value: "editor", label: "Editor" },
955
+ { value: "viewer", label: "Viewer" }
956
+ ],
957
+ access: {
958
+ update: "user.roles && 'admin' in user.roles"
959
+ }
960
+ }
961
+ ];
962
+ }
963
+ fields = fields.map((field) => {
964
+ if (field.name === "email") {
965
+ return {
966
+ ...field,
967
+ access: {
968
+ ...field.access || {},
969
+ update: "!id"
970
+ }
971
+ };
972
+ }
973
+ if (field.name === "password") {
974
+ return {
975
+ ...field,
976
+ admin: { ...field.admin || {} },
977
+ access: {
978
+ ...field.access || {},
979
+ update: "!id || user.id == id"
980
+ }
981
+ };
982
+ }
983
+ if (field.name === "roles") {
984
+ return {
985
+ ...field,
986
+ access: {
987
+ ...field.access || {},
988
+ // Must be an admin; cannot edit own roles (no self-elevation).
989
+ update: "user.roles && 'admin' in user.roles && user.id != id"
990
+ }
991
+ };
992
+ }
993
+ return field;
994
+ });
995
+ }
996
+ const updatedFieldNames = new Set(fields.map((f) => f.name));
997
+ const fieldsToInject = SYSTEM_FIELDS.filter((f) => !updatedFieldNames.has(f.name));
914
998
  return {
915
999
  ...col,
916
1000
  fields: [...fields, ...fieldsToInject]
917
1001
  };
918
1002
  });
919
- const hasAuditCollection = normalizedCollections.some(
920
- (col) => col.slug === AUDIT_COLLECTION_SLUG
921
- );
1003
+ const hasAuditCollection = normalizedCollections.some((col) => col.slug === AUDIT_COLLECTION_SLUG);
922
1004
  return {
923
1005
  ...config,
924
- collections: [
925
- ...normalizedCollections,
926
- ...needsAudit && !hasAuditCollection ? [AUDIT_COLLECTION] : []
927
- ],
1006
+ collections: [...normalizedCollections, ...needsAudit && !hasAuditCollection ? [AUDIT_COLLECTION] : []],
928
1007
  globals
929
1008
  };
930
1009
  }
@@ -1295,6 +1374,26 @@ var AuditService = class {
1295
1374
  }
1296
1375
  };
1297
1376
 
1377
+ // src/auth/password.ts
1378
+ var import_node_util = require("util");
1379
+ var import_node_crypto = require("crypto");
1380
+ var scryptAsync = (0, import_node_util.promisify)(import_node_crypto.scrypt);
1381
+ var SALT_LEN = 16;
1382
+ var KEY_LEN = 64;
1383
+ async function hashPassword(plain) {
1384
+ const salt = (0, import_node_crypto.randomBytes)(SALT_LEN).toString("hex");
1385
+ const derivedKey = await scryptAsync(plain, salt, KEY_LEN);
1386
+ return `${salt}:${derivedKey.toString("hex")}`;
1387
+ }
1388
+ async function verifyPassword(plain, stored) {
1389
+ const [salt, storedHash] = stored.split(":");
1390
+ if (!salt || !storedHash) return false;
1391
+ const derivedKey = await scryptAsync(plain, salt, KEY_LEN);
1392
+ const storedBuffer = Buffer.from(storedHash, "hex");
1393
+ if (derivedKey.length !== storedBuffer.length) return false;
1394
+ return (0, import_node_crypto.timingSafeEqual)(derivedKey, storedBuffer);
1395
+ }
1396
+
1298
1397
  // src/controllers/collection.controller.ts
1299
1398
  var CollectionController = class {
1300
1399
  collection;
@@ -1384,6 +1483,9 @@ var CollectionController = class {
1384
1483
  createdBy: user?.sub ?? null,
1385
1484
  updatedBy: user?.sub ?? null
1386
1485
  };
1486
+ if (this.collection.auth && data.password) {
1487
+ data.password = await hashPassword(data.password);
1488
+ }
1387
1489
  const doc = await db.create({ collection: this.collection.slug, data });
1388
1490
  if (this.collection.audit && db) {
1389
1491
  AuditService.log(db, {
@@ -1443,11 +1545,16 @@ var CollectionController = class {
1443
1545
  if (!id) return c.json({ message: "Missing ID" }, 400);
1444
1546
  const body = await c.req.json();
1445
1547
  const user = c.get("user");
1446
- const data = {
1447
- ...body,
1548
+ const data = { ...body };
1549
+ if (this.collection.auth) {
1550
+ delete data.password;
1551
+ delete data.oldPassword;
1552
+ delete data.confirmPassword;
1553
+ }
1554
+ Object.assign(data, {
1448
1555
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1449
1556
  updatedBy: user?.sub ?? null
1450
- };
1557
+ });
1451
1558
  let before = null;
1452
1559
  if (this.collection.audit) {
1453
1560
  before = await db.findOne({ collection: this.collection.slug, id });
@@ -1465,6 +1572,77 @@ var CollectionController = class {
1465
1572
  }
1466
1573
  return c.json(doc);
1467
1574
  }
1575
+ /**
1576
+ * POST /api/collections/:slug/:id/change-password
1577
+ *
1578
+ * Dedicated endpoint for password changes. Requires the caller to supply:
1579
+ * { oldPassword, newPassword, confirmPassword }
1580
+ *
1581
+ * Rules:
1582
+ * - Only the account owner or an admin may change the password.
1583
+ * - Non-admin callers MUST provide a valid oldPassword.
1584
+ * - newPassword and confirmPassword must match.
1585
+ */
1586
+ async changePassword(c) {
1587
+ const config = c.get("config");
1588
+ const db = config.db;
1589
+ if (!db) return c.json({ message: "Database not configured" }, 500);
1590
+ if (!this.collection.auth) {
1591
+ return c.json({ message: "This collection does not support authentication" }, 400);
1592
+ }
1593
+ const id = c.req.param("id");
1594
+ if (!id) return c.json({ message: "Missing ID" }, 400);
1595
+ const user = c.get("user");
1596
+ if (!user) return c.json({ message: "Authentication required" }, 401);
1597
+ const body = await c.req.json().catch(() => null);
1598
+ const { oldPassword, newPassword, confirmPassword } = body ?? {};
1599
+ if (!newPassword) {
1600
+ return c.json({ message: "newPassword is required" }, 400);
1601
+ }
1602
+ if (newPassword !== confirmPassword) {
1603
+ return c.json({ message: "Passwords do not match" }, 400);
1604
+ }
1605
+ if (newPassword.length < 8) {
1606
+ return c.json({ message: "Password must be at least 8 characters" }, 400);
1607
+ }
1608
+ const isAdmin = Array.isArray(user.roles) && user.roles.includes("admin");
1609
+ const isSelf = user.sub === id;
1610
+ if (!isAdmin && !isSelf) {
1611
+ return c.json({ message: "You are not authorised to change this password" }, 403);
1612
+ }
1613
+ if (!isAdmin) {
1614
+ if (!oldPassword) {
1615
+ return c.json({ message: "Current password is required" }, 400);
1616
+ }
1617
+ const existing = await db.findOne({ collection: this.collection.slug, id });
1618
+ if (!existing) return c.json({ message: "User not found" }, 404);
1619
+ const valid = await verifyPassword(oldPassword, existing.password);
1620
+ if (!valid) {
1621
+ return c.json({ message: "Invalid current password" }, 400);
1622
+ }
1623
+ }
1624
+ const hashed = await hashPassword(newPassword);
1625
+ const doc = await db.update({
1626
+ collection: this.collection.slug,
1627
+ id,
1628
+ data: {
1629
+ password: hashed,
1630
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1631
+ updatedBy: user.sub
1632
+ }
1633
+ });
1634
+ if (this.collection.audit) {
1635
+ AuditService.log(db, {
1636
+ operation: "update",
1637
+ collection: this.collection.slug,
1638
+ documentId: id,
1639
+ user: { id: user.sub, collection: user.collection, email: user.email },
1640
+ before: null,
1641
+ after: { id }
1642
+ });
1643
+ }
1644
+ return c.json({ success: true, message: "Password updated successfully" });
1645
+ }
1468
1646
  async delete(c) {
1469
1647
  const config = c.get("config");
1470
1648
  const db = config.db;
@@ -1756,26 +1934,6 @@ var MediaController = class {
1756
1934
  }
1757
1935
  };
1758
1936
 
1759
- // src/auth/password.ts
1760
- var import_node_util = require("util");
1761
- var import_node_crypto = require("crypto");
1762
- var scryptAsync = (0, import_node_util.promisify)(import_node_crypto.scrypt);
1763
- var SALT_LEN = 16;
1764
- var KEY_LEN = 64;
1765
- async function hashPassword(plain) {
1766
- const salt = (0, import_node_crypto.randomBytes)(SALT_LEN).toString("hex");
1767
- const derivedKey = await scryptAsync(plain, salt, KEY_LEN);
1768
- return `${salt}:${derivedKey.toString("hex")}`;
1769
- }
1770
- async function verifyPassword(plain, stored) {
1771
- const [salt, storedHash] = stored.split(":");
1772
- if (!salt || !storedHash) return false;
1773
- const derivedKey = await scryptAsync(plain, salt, KEY_LEN);
1774
- const storedBuffer = Buffer.from(storedHash, "hex");
1775
- if (derivedKey.length !== storedBuffer.length) return false;
1776
- return (0, import_node_crypto.timingSafeEqual)(derivedKey, storedBuffer);
1777
- }
1778
-
1779
1937
  // src/auth/token.ts
1780
1938
  var import_jose = require("jose");
1781
1939
  var import_node_util2 = require("util");
@@ -2685,14 +2843,19 @@ function registerRoutes(app, config) {
2685
2843
  }
2686
2844
  return true;
2687
2845
  };
2846
+ const serializeAccess = async (access) => {
2847
+ if (typeof access === "string") return access;
2848
+ if (typeof access === "boolean") return access;
2849
+ return resolveAccess(access);
2850
+ };
2688
2851
  const filteredCollections = await Promise.all(collections.filter((col) => !siteId || col.shared || !col.siteId || col.siteId === siteId).map(async (col) => ({
2689
2852
  slug: col.slug,
2690
2853
  labels: col.labels,
2691
2854
  access: {
2692
- read: await resolveAccess(col.access?.read),
2693
- create: await resolveAccess(col.access?.create),
2694
- update: await resolveAccess(col.access?.update),
2695
- delete: await resolveAccess(col.access?.delete)
2855
+ read: await serializeAccess(col.access?.read),
2856
+ create: await serializeAccess(col.access?.create),
2857
+ update: await serializeAccess(col.access?.update),
2858
+ delete: await serializeAccess(col.access?.delete)
2696
2859
  },
2697
2860
  fields: await Promise.all(col.fields.map(async (f) => ({
2698
2861
  name: f.name,
@@ -2707,8 +2870,8 @@ function registerRoutes(app, config) {
2707
2870
  blocks: f.blocks,
2708
2871
  admin: f.admin,
2709
2872
  access: {
2710
- read: await resolveAccess(f.access?.read),
2711
- update: await resolveAccess(f.access?.update)
2873
+ read: await serializeAccess(f.access?.read),
2874
+ update: await serializeAccess(f.access?.update)
2712
2875
  }
2713
2876
  }))),
2714
2877
  upload: !!col.upload,
@@ -2719,8 +2882,8 @@ function registerRoutes(app, config) {
2719
2882
  slug: glb.slug,
2720
2883
  label: glb.label,
2721
2884
  access: {
2722
- read: await resolveAccess(glb.access?.read),
2723
- update: await resolveAccess(glb.access?.update)
2885
+ read: await serializeAccess(glb.access?.read),
2886
+ update: await serializeAccess(glb.access?.update)
2724
2887
  },
2725
2888
  fields: await Promise.all(glb.fields.map(async (f) => ({
2726
2889
  name: f.name,
@@ -2735,8 +2898,8 @@ function registerRoutes(app, config) {
2735
2898
  blocks: f.blocks,
2736
2899
  admin: f.admin,
2737
2900
  access: {
2738
- read: await resolveAccess(f.access?.read),
2739
- update: await resolveAccess(f.access?.update)
2901
+ read: await serializeAccess(f.access?.read),
2902
+ update: await serializeAccess(f.access?.update)
2740
2903
  }
2741
2904
  }))),
2742
2905
  admin: glb.admin
@@ -2798,6 +2961,9 @@ function registerRoutes(app, config) {
2798
2961
  app.patch(`${path}/:id`, accessGate(collection, "update"), (c) => controller.update(c));
2799
2962
  app.delete(`${path}/:id`, accessGate(collection, "delete"), (c) => controller.delete(c));
2800
2963
  app.post(`${path}/seed`, (c) => controller.seed(c));
2964
+ if (collection.auth) {
2965
+ app.post(`${path}/:id/change-password`, requireAuth(), (c) => controller.changePassword(c));
2966
+ }
2801
2967
  }
2802
2968
  for (const global of config.globals) {
2803
2969
  const path = `/api/globals/${global.slug}`;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './app-Dk3SzV1a.cjs';
2
- export { A as AccessFunction, a as AdminConfig, B as Block, b as DatabaseAdapter, c as DyrectedContext, F as Field, d as FieldHook, e as FieldType, f as FileData, H as HookFunction, I as ImageService, P as PaginatedResult, S as StorageAdapter, U as UploadConfig, g as createDyrectedApp } from './app-Dk3SzV1a.cjs';
1
+ import { D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './app-DnX9mNsh.cjs';
2
+ export { A as AccessFunction, a as AdminConfig, B as Block, b as DatabaseAdapter, c as DyrectedContext, F as Field, d as FieldHook, e as FieldType, f as FileData, H as HookFunction, I as ImageService, P as PaginatedResult, S as StorageAdapter, U as UploadConfig, g as createDyrectedApp } from './app-DnX9mNsh.cjs';
3
3
  import 'hono/types';
4
4
  import 'hono';
5
5
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './app-Dk3SzV1a.js';
2
- export { A as AccessFunction, a as AdminConfig, B as Block, b as DatabaseAdapter, c as DyrectedContext, F as Field, d as FieldHook, e as FieldType, f as FileData, H as HookFunction, I as ImageService, P as PaginatedResult, S as StorageAdapter, U as UploadConfig, g as createDyrectedApp } from './app-Dk3SzV1a.js';
1
+ import { D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './app-DnX9mNsh.js';
2
+ export { A as AccessFunction, a as AdminConfig, B as Block, b as DatabaseAdapter, c as DyrectedContext, F as Field, d as FieldHook, e as FieldType, f as FileData, H as HookFunction, I as ImageService, P as PaginatedResult, S as StorageAdapter, U as UploadConfig, g as createDyrectedApp } from './app-DnX9mNsh.js';
3
3
  import 'hono/types';
4
4
  import 'hono';
5
5
 
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createDyrectedApp,
3
3
  normalizeConfig
4
- } from "./chunk-3VUH2MNW.js";
4
+ } from "./chunk-JJN4J5NS.js";
5
5
 
6
6
  // src/utils/setup-prompt.ts
7
7
  function buildEnvironmentSection(frameworkLabel, isSelfHosted, config) {
@@ -0,0 +1,8 @@
1
+ import {
2
+ hashPassword,
3
+ verifyPassword
4
+ } from "./chunk-G2QQLBHW.js";
5
+ export {
6
+ hashPassword,
7
+ verifyPassword
8
+ };