@dyrected/core 2.5.12 → 2.5.14

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.cjs CHANGED
@@ -248,6 +248,26 @@ var AuditService = class {
248
248
  }
249
249
  };
250
250
 
251
+ // src/auth/password.ts
252
+ var import_node_util = require("util");
253
+ var import_node_crypto = require("crypto");
254
+ var scryptAsync = (0, import_node_util.promisify)(import_node_crypto.scrypt);
255
+ var SALT_LEN = 16;
256
+ var KEY_LEN = 64;
257
+ async function hashPassword(plain) {
258
+ const salt = (0, import_node_crypto.randomBytes)(SALT_LEN).toString("hex");
259
+ const derivedKey = await scryptAsync(plain, salt, KEY_LEN);
260
+ return `${salt}:${derivedKey.toString("hex")}`;
261
+ }
262
+ async function verifyPassword(plain, stored) {
263
+ const [salt, storedHash] = stored.split(":");
264
+ if (!salt || !storedHash) return false;
265
+ const derivedKey = await scryptAsync(plain, salt, KEY_LEN);
266
+ const storedBuffer = Buffer.from(storedHash, "hex");
267
+ if (derivedKey.length !== storedBuffer.length) return false;
268
+ return (0, import_node_crypto.timingSafeEqual)(derivedKey, storedBuffer);
269
+ }
270
+
251
271
  // src/controllers/collection.controller.ts
252
272
  var CollectionController = class {
253
273
  collection;
@@ -337,6 +357,9 @@ var CollectionController = class {
337
357
  createdBy: user?.sub ?? null,
338
358
  updatedBy: user?.sub ?? null
339
359
  };
360
+ if (this.collection.auth && data.password) {
361
+ data.password = await hashPassword(data.password);
362
+ }
340
363
  const doc = await db.create({ collection: this.collection.slug, data });
341
364
  if (this.collection.audit && db) {
342
365
  AuditService.log(db, {
@@ -396,11 +419,16 @@ var CollectionController = class {
396
419
  if (!id) return c.json({ message: "Missing ID" }, 400);
397
420
  const body = await c.req.json();
398
421
  const user = c.get("user");
399
- const data = {
400
- ...body,
422
+ const data = { ...body };
423
+ if (this.collection.auth) {
424
+ delete data.password;
425
+ delete data.oldPassword;
426
+ delete data.confirmPassword;
427
+ }
428
+ Object.assign(data, {
401
429
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
402
430
  updatedBy: user?.sub ?? null
403
- };
431
+ });
404
432
  let before = null;
405
433
  if (this.collection.audit) {
406
434
  before = await db.findOne({ collection: this.collection.slug, id });
@@ -418,6 +446,77 @@ var CollectionController = class {
418
446
  }
419
447
  return c.json(doc);
420
448
  }
449
+ /**
450
+ * POST /api/collections/:slug/:id/change-password
451
+ *
452
+ * Dedicated endpoint for password changes. Requires the caller to supply:
453
+ * { oldPassword, newPassword, confirmPassword }
454
+ *
455
+ * Rules:
456
+ * - Only the account owner or an admin may change the password.
457
+ * - Non-admin callers MUST provide a valid oldPassword.
458
+ * - newPassword and confirmPassword must match.
459
+ */
460
+ async changePassword(c) {
461
+ const config = c.get("config");
462
+ const db = config.db;
463
+ if (!db) return c.json({ message: "Database not configured" }, 500);
464
+ if (!this.collection.auth) {
465
+ return c.json({ message: "This collection does not support authentication" }, 400);
466
+ }
467
+ const id = c.req.param("id");
468
+ if (!id) return c.json({ message: "Missing ID" }, 400);
469
+ const user = c.get("user");
470
+ if (!user) return c.json({ message: "Authentication required" }, 401);
471
+ const body = await c.req.json().catch(() => null);
472
+ const { oldPassword, newPassword, confirmPassword } = body ?? {};
473
+ if (!newPassword) {
474
+ return c.json({ message: "newPassword is required" }, 400);
475
+ }
476
+ if (newPassword !== confirmPassword) {
477
+ return c.json({ message: "Passwords do not match" }, 400);
478
+ }
479
+ if (newPassword.length < 8) {
480
+ return c.json({ message: "Password must be at least 8 characters" }, 400);
481
+ }
482
+ const isAdmin = Array.isArray(user.roles) && user.roles.includes("admin");
483
+ const isSelf = user.sub === id;
484
+ if (!isAdmin && !isSelf) {
485
+ return c.json({ message: "You are not authorised to change this password" }, 403);
486
+ }
487
+ if (!isAdmin) {
488
+ if (!oldPassword) {
489
+ return c.json({ message: "Current password is required" }, 400);
490
+ }
491
+ const existing = await db.findOne({ collection: this.collection.slug, id });
492
+ if (!existing) return c.json({ message: "User not found" }, 404);
493
+ const valid = await verifyPassword(oldPassword, existing.password);
494
+ if (!valid) {
495
+ return c.json({ message: "Invalid current password" }, 400);
496
+ }
497
+ }
498
+ const hashed = await hashPassword(newPassword);
499
+ const doc = await db.update({
500
+ collection: this.collection.slug,
501
+ id,
502
+ data: {
503
+ password: hashed,
504
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
505
+ updatedBy: user.sub
506
+ }
507
+ });
508
+ if (this.collection.audit) {
509
+ AuditService.log(db, {
510
+ operation: "update",
511
+ collection: this.collection.slug,
512
+ documentId: id,
513
+ user: { id: user.sub, collection: user.collection, email: user.email },
514
+ before: null,
515
+ after: { id }
516
+ });
517
+ }
518
+ return c.json({ success: true, message: "Password updated successfully" });
519
+ }
421
520
  async delete(c) {
422
521
  const config = c.get("config");
423
522
  const db = config.db;
@@ -709,26 +808,6 @@ var MediaController = class {
709
808
  }
710
809
  };
711
810
 
712
- // src/auth/password.ts
713
- var import_node_util = require("util");
714
- var import_node_crypto = require("crypto");
715
- var scryptAsync = (0, import_node_util.promisify)(import_node_crypto.scrypt);
716
- var SALT_LEN = 16;
717
- var KEY_LEN = 64;
718
- async function hashPassword(plain) {
719
- const salt = (0, import_node_crypto.randomBytes)(SALT_LEN).toString("hex");
720
- const derivedKey = await scryptAsync(plain, salt, KEY_LEN);
721
- return `${salt}:${derivedKey.toString("hex")}`;
722
- }
723
- async function verifyPassword(plain, stored) {
724
- const [salt, storedHash] = stored.split(":");
725
- if (!salt || !storedHash) return false;
726
- const derivedKey = await scryptAsync(plain, salt, KEY_LEN);
727
- const storedBuffer = Buffer.from(storedHash, "hex");
728
- if (derivedKey.length !== storedBuffer.length) return false;
729
- return (0, import_node_crypto.timingSafeEqual)(derivedKey, storedBuffer);
730
- }
731
-
732
811
  // src/auth/token.ts
733
812
  var import_jose = require("jose");
734
813
  var import_node_util2 = require("util");
@@ -1638,14 +1717,19 @@ function registerRoutes(app, config) {
1638
1717
  }
1639
1718
  return true;
1640
1719
  };
1720
+ const serializeAccess = async (access) => {
1721
+ if (typeof access === "string") return access;
1722
+ if (typeof access === "boolean") return access;
1723
+ return resolveAccess(access);
1724
+ };
1641
1725
  const filteredCollections = await Promise.all(collections.filter((col) => !siteId || col.shared || !col.siteId || col.siteId === siteId).map(async (col) => ({
1642
1726
  slug: col.slug,
1643
1727
  labels: col.labels,
1644
1728
  access: {
1645
- read: await resolveAccess(col.access?.read),
1646
- create: await resolveAccess(col.access?.create),
1647
- update: await resolveAccess(col.access?.update),
1648
- delete: await resolveAccess(col.access?.delete)
1729
+ read: await serializeAccess(col.access?.read),
1730
+ create: await serializeAccess(col.access?.create),
1731
+ update: await serializeAccess(col.access?.update),
1732
+ delete: await serializeAccess(col.access?.delete)
1649
1733
  },
1650
1734
  fields: await Promise.all(col.fields.map(async (f) => ({
1651
1735
  name: f.name,
@@ -1660,8 +1744,8 @@ function registerRoutes(app, config) {
1660
1744
  blocks: f.blocks,
1661
1745
  admin: f.admin,
1662
1746
  access: {
1663
- read: await resolveAccess(f.access?.read),
1664
- update: await resolveAccess(f.access?.update)
1747
+ read: await serializeAccess(f.access?.read),
1748
+ update: await serializeAccess(f.access?.update)
1665
1749
  }
1666
1750
  }))),
1667
1751
  upload: !!col.upload,
@@ -1672,8 +1756,8 @@ function registerRoutes(app, config) {
1672
1756
  slug: glb.slug,
1673
1757
  label: glb.label,
1674
1758
  access: {
1675
- read: await resolveAccess(glb.access?.read),
1676
- update: await resolveAccess(glb.access?.update)
1759
+ read: await serializeAccess(glb.access?.read),
1760
+ update: await serializeAccess(glb.access?.update)
1677
1761
  },
1678
1762
  fields: await Promise.all(glb.fields.map(async (f) => ({
1679
1763
  name: f.name,
@@ -1688,8 +1772,8 @@ function registerRoutes(app, config) {
1688
1772
  blocks: f.blocks,
1689
1773
  admin: f.admin,
1690
1774
  access: {
1691
- read: await resolveAccess(f.access?.read),
1692
- update: await resolveAccess(f.access?.update)
1775
+ read: await serializeAccess(f.access?.read),
1776
+ update: await serializeAccess(f.access?.update)
1693
1777
  }
1694
1778
  }))),
1695
1779
  admin: glb.admin
@@ -1751,6 +1835,9 @@ function registerRoutes(app, config) {
1751
1835
  app.patch(`${path}/:id`, accessGate(collection, "update"), (c) => controller.update(c));
1752
1836
  app.delete(`${path}/:id`, accessGate(collection, "delete"), (c) => controller.delete(c));
1753
1837
  app.post(`${path}/seed`, (c) => controller.seed(c));
1838
+ if (collection.auth) {
1839
+ app.post(`${path}/:id/change-password`, requireAuth(), (c) => controller.changePassword(c));
1840
+ }
1754
1841
  }
1755
1842
  for (const global of config.globals) {
1756
1843
  const path = `/api/globals/${global.slug}`;
@@ -1875,23 +1962,102 @@ function normalizeConfig(config) {
1875
1962
  const globals = config?.globals || [];
1876
1963
  const needsAudit = collections.some((col) => col.audit);
1877
1964
  const normalizedCollections = collections.map((col) => {
1878
- const fields = col.fields || [];
1965
+ let fields = col.fields || [];
1879
1966
  const existingFieldNames = new Set(fields.map((f) => f.name));
1880
- const fieldsToInject = SYSTEM_FIELDS.filter((f) => !existingFieldNames.has(f.name));
1967
+ if (col.auth) {
1968
+ if (!existingFieldNames.has("email")) {
1969
+ fields = [
1970
+ ...fields,
1971
+ {
1972
+ name: "email",
1973
+ type: "email",
1974
+ label: "Email",
1975
+ required: true,
1976
+ unique: true,
1977
+ promoted: true,
1978
+ access: {
1979
+ update: "!id"
1980
+ }
1981
+ }
1982
+ ];
1983
+ }
1984
+ if (!existingFieldNames.has("password")) {
1985
+ fields = [
1986
+ ...fields,
1987
+ {
1988
+ name: "password",
1989
+ type: "text",
1990
+ label: "Password",
1991
+ required: true,
1992
+ access: {
1993
+ update: "!id || user.id == id"
1994
+ }
1995
+ }
1996
+ ];
1997
+ }
1998
+ if (!existingFieldNames.has("roles")) {
1999
+ fields = [
2000
+ ...fields,
2001
+ {
2002
+ name: "roles",
2003
+ type: "select",
2004
+ label: "Roles",
2005
+ defaultValue: [],
2006
+ options: [
2007
+ { value: "admin", label: "Admin" },
2008
+ { value: "editor", label: "Editor" },
2009
+ { value: "viewer", label: "Viewer" }
2010
+ ],
2011
+ access: {
2012
+ update: "user.roles && 'admin' in user.roles"
2013
+ }
2014
+ }
2015
+ ];
2016
+ }
2017
+ fields = fields.map((field) => {
2018
+ if (field.name === "email") {
2019
+ return {
2020
+ ...field,
2021
+ access: {
2022
+ ...field.access || {},
2023
+ update: "!id"
2024
+ }
2025
+ };
2026
+ }
2027
+ if (field.name === "password") {
2028
+ return {
2029
+ ...field,
2030
+ admin: { ...field.admin || {} },
2031
+ access: {
2032
+ ...field.access || {},
2033
+ update: "!id || user.id == id"
2034
+ }
2035
+ };
2036
+ }
2037
+ if (field.name === "roles") {
2038
+ return {
2039
+ ...field,
2040
+ access: {
2041
+ ...field.access || {},
2042
+ // Must be an admin; cannot edit own roles (no self-elevation).
2043
+ update: "user.roles && 'admin' in user.roles && user.id != id"
2044
+ }
2045
+ };
2046
+ }
2047
+ return field;
2048
+ });
2049
+ }
2050
+ const updatedFieldNames = new Set(fields.map((f) => f.name));
2051
+ const fieldsToInject = SYSTEM_FIELDS.filter((f) => !updatedFieldNames.has(f.name));
1881
2052
  return {
1882
2053
  ...col,
1883
2054
  fields: [...fields, ...fieldsToInject]
1884
2055
  };
1885
2056
  });
1886
- const hasAuditCollection = normalizedCollections.some(
1887
- (col) => col.slug === AUDIT_COLLECTION_SLUG
1888
- );
2057
+ const hasAuditCollection = normalizedCollections.some((col) => col.slug === AUDIT_COLLECTION_SLUG);
1889
2058
  return {
1890
2059
  ...config,
1891
- collections: [
1892
- ...normalizedCollections,
1893
- ...needsAudit && !hasAuditCollection ? [AUDIT_COLLECTION] : []
1894
- ],
2060
+ collections: [...normalizedCollections, ...needsAudit && !hasAuditCollection ? [AUDIT_COLLECTION] : []],
1895
2061
  globals
1896
2062
  };
1897
2063
  }
package/dist/server.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { c as DyrectedContext, D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './app-Dk3SzV1a.cjs';
2
- export { g as createDyrectedApp } from './app-Dk3SzV1a.cjs';
1
+ import { c as DyrectedContext, D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './app-DnX9mNsh.cjs';
2
+ export { g as createDyrectedApp } from './app-DnX9mNsh.cjs';
3
3
  import * as hono from 'hono';
4
4
  import { Hono, Context } from 'hono';
5
5
  import * as hono_utils_http_status from 'hono/utils/http-status';
@@ -140,6 +140,31 @@ declare class CollectionController {
140
140
  message: string;
141
141
  }, 400, "json">) | (Response & hono.TypedResponse<any, 201, "json">)>;
142
142
  update(c: Context<DyrectedContext>): Promise<Response & hono.TypedResponse<any, hono_utils_http_status.ContentfulStatusCode, "json">>;
143
+ /**
144
+ * POST /api/collections/:slug/:id/change-password
145
+ *
146
+ * Dedicated endpoint for password changes. Requires the caller to supply:
147
+ * { oldPassword, newPassword, confirmPassword }
148
+ *
149
+ * Rules:
150
+ * - Only the account owner or an admin may change the password.
151
+ * - Non-admin callers MUST provide a valid oldPassword.
152
+ * - newPassword and confirmPassword must match.
153
+ */
154
+ changePassword(c: Context<DyrectedContext>): Promise<(Response & hono.TypedResponse<{
155
+ message: string;
156
+ }, 500, "json">) | (Response & hono.TypedResponse<{
157
+ message: string;
158
+ }, 400, "json">) | (Response & hono.TypedResponse<{
159
+ message: string;
160
+ }, 401, "json">) | (Response & hono.TypedResponse<{
161
+ message: string;
162
+ }, 403, "json">) | (Response & hono.TypedResponse<{
163
+ message: string;
164
+ }, 404, "json">) | (Response & hono.TypedResponse<{
165
+ success: true;
166
+ message: string;
167
+ }, hono_utils_http_status.ContentfulStatusCode, "json">)>;
143
168
  delete(c: Context<DyrectedContext>): Promise<Response & hono.TypedResponse<{
144
169
  message: string;
145
170
  }, hono_utils_http_status.ContentfulStatusCode, "json">>;
package/dist/server.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { c as DyrectedContext, D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './app-Dk3SzV1a.js';
2
- export { g as createDyrectedApp } from './app-Dk3SzV1a.js';
1
+ import { c as DyrectedContext, D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './app-DnX9mNsh.js';
2
+ export { g as createDyrectedApp } from './app-DnX9mNsh.js';
3
3
  import * as hono from 'hono';
4
4
  import { Hono, Context } from 'hono';
5
5
  import * as hono_utils_http_status from 'hono/utils/http-status';
@@ -140,6 +140,31 @@ declare class CollectionController {
140
140
  message: string;
141
141
  }, 400, "json">) | (Response & hono.TypedResponse<any, 201, "json">)>;
142
142
  update(c: Context<DyrectedContext>): Promise<Response & hono.TypedResponse<any, hono_utils_http_status.ContentfulStatusCode, "json">>;
143
+ /**
144
+ * POST /api/collections/:slug/:id/change-password
145
+ *
146
+ * Dedicated endpoint for password changes. Requires the caller to supply:
147
+ * { oldPassword, newPassword, confirmPassword }
148
+ *
149
+ * Rules:
150
+ * - Only the account owner or an admin may change the password.
151
+ * - Non-admin callers MUST provide a valid oldPassword.
152
+ * - newPassword and confirmPassword must match.
153
+ */
154
+ changePassword(c: Context<DyrectedContext>): Promise<(Response & hono.TypedResponse<{
155
+ message: string;
156
+ }, 500, "json">) | (Response & hono.TypedResponse<{
157
+ message: string;
158
+ }, 400, "json">) | (Response & hono.TypedResponse<{
159
+ message: string;
160
+ }, 401, "json">) | (Response & hono.TypedResponse<{
161
+ message: string;
162
+ }, 403, "json">) | (Response & hono.TypedResponse<{
163
+ message: string;
164
+ }, 404, "json">) | (Response & hono.TypedResponse<{
165
+ success: true;
166
+ message: string;
167
+ }, hono_utils_http_status.ContentfulStatusCode, "json">)>;
143
168
  delete(c: Context<DyrectedContext>): Promise<Response & hono.TypedResponse<{
144
169
  message: string;
145
170
  }, hono_utils_http_status.ContentfulStatusCode, "json">>;
package/dist/server.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  PreviewController,
7
7
  createDyrectedApp,
8
8
  registerRoutes
9
- } from "./chunk-3VUH2MNW.js";
9
+ } from "./chunk-JJN4J5NS.js";
10
10
  export {
11
11
  AuthController,
12
12
  CollectionController,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dyrected/core",
3
- "version": "2.5.12",
3
+ "version": "2.5.14",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",