domma-cms 0.44.0 → 0.45.0

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.
Files changed (254) hide show
  1. package/CLAUDE.md +53 -53
  2. package/README.md +27 -27
  3. package/admin/css/admin.css +1 -1
  4. package/admin/dist/domma/domma-tools.css +3 -3
  5. package/admin/dist/domma/domma-tools.min.js +3 -3
  6. package/admin/index.html +2 -2
  7. package/admin/js/app.js +1 -1
  8. package/admin/js/lib/card-builder.js +2 -2
  9. package/admin/js/lib/crud-tutorial.js +1 -1
  10. package/admin/js/lib/effect-defs.js +1 -1
  11. package/admin/js/lib/effects-builder.js +1 -1
  12. package/admin/js/lib/image-editor.js +1 -1
  13. package/admin/js/lib/markdown-toolbar.js +2 -2
  14. package/admin/js/lib/project-context.js +1 -1
  15. package/admin/js/lib/project-quick-create.js +1 -1
  16. package/admin/js/lib/scribe-composer.js +1 -1
  17. package/admin/js/lib/server-restart.js +1 -1
  18. package/admin/js/lib/shortcode-context-menu.js +2 -2
  19. package/admin/js/lib/simple-editor.js +1 -1
  20. package/admin/js/lib/themes.js +1 -1
  21. package/admin/js/lib/timeline-builder.js +1 -1
  22. package/admin/js/templates/action-editor.html +8 -8
  23. package/admin/js/templates/api-endpoint-editor.html +7 -7
  24. package/admin/js/templates/api-endpoints.html +1 -1
  25. package/admin/js/templates/api-reference.html +1 -1
  26. package/admin/js/templates/block-editor.html +6 -6
  27. package/admin/js/templates/collection-editor.html +1 -1
  28. package/admin/js/templates/component-editor.html +2 -2
  29. package/admin/js/templates/dashboard/cache.html +3 -3
  30. package/admin/js/templates/dashboard/journeys.html +3 -3
  31. package/admin/js/templates/dashboard/kpi-strip.html +3 -3
  32. package/admin/js/templates/dashboard/top-pages.html +1 -1
  33. package/admin/js/templates/dashboard/traffic-chart.html +1 -1
  34. package/admin/js/templates/docs/api-actions.html +1 -1
  35. package/admin/js/templates/docs/api-collections.html +4 -4
  36. package/admin/js/templates/docs/api-pages.html +2 -2
  37. package/admin/js/templates/docs/api-plugins.html +1 -1
  38. package/admin/js/templates/docs/api-settings.html +2 -2
  39. package/admin/js/templates/docs/api-users.html +1 -1
  40. package/admin/js/templates/docs/components-howto.html +4 -4
  41. package/admin/js/templates/docs/components-reference.html +21 -21
  42. package/admin/js/templates/docs/components-rules.html +14 -14
  43. package/admin/js/templates/docs/components-walkthrough.html +13 -13
  44. package/admin/js/templates/docs/tutorial-crud.html +49 -49
  45. package/admin/js/templates/docs/tutorial-forms.html +4 -4
  46. package/admin/js/templates/docs/tutorial-plugin.html +10 -10
  47. package/admin/js/templates/docs/usage-actions.html +6 -6
  48. package/admin/js/templates/docs/usage-cta-shortcode.html +9 -9
  49. package/admin/js/templates/docs/usage-dconfig.html +4 -4
  50. package/admin/js/templates/docs/usage-navigation.html +1 -1
  51. package/admin/js/templates/docs/usage-pages.html +1 -1
  52. package/admin/js/templates/docs/usage-shortcodes.html +24 -24
  53. package/admin/js/templates/docs/usage-users-roles.html +3 -3
  54. package/admin/js/templates/docs/usage-views.html +4 -4
  55. package/admin/js/templates/documentation.html +1 -1
  56. package/admin/js/templates/effects.html +752 -752
  57. package/admin/js/templates/form-editor.html +5 -5
  58. package/admin/js/templates/forms.html +17 -17
  59. package/admin/js/templates/login.html +5 -5
  60. package/admin/js/templates/menu-editor.html +10 -10
  61. package/admin/js/templates/my-profile.html +17 -17
  62. package/admin/js/templates/navigation.html +11 -11
  63. package/admin/js/templates/notifications.html +1 -1
  64. package/admin/js/templates/page-editor.html +4 -4
  65. package/admin/js/templates/pro-docs.html +14 -14
  66. package/admin/js/templates/role-editor.html +70 -70
  67. package/admin/js/templates/roles.html +10 -10
  68. package/admin/js/templates/settings.html +10 -10
  69. package/admin/js/templates/tutorials.html +3 -3
  70. package/admin/js/templates/view-editor.html +15 -15
  71. package/admin/js/views/action-editor.js +1 -1
  72. package/admin/js/views/actions-list.js +1 -1
  73. package/admin/js/views/api-endpoint-editor.js +2 -2
  74. package/admin/js/views/api-endpoints.js +2 -2
  75. package/admin/js/views/api-tokens.js +3 -3
  76. package/admin/js/views/blocks.js +2 -2
  77. package/admin/js/views/collection-editor.js +1 -1
  78. package/admin/js/views/collection-entries.js +1 -1
  79. package/admin/js/views/collections.js +1 -1
  80. package/admin/js/views/component-editor.js +1 -1
  81. package/admin/js/views/components.js +4 -4
  82. package/admin/js/views/dashboard/widgets/cache.js +1 -1
  83. package/admin/js/views/dashboard/widgets/journeys.js +1 -1
  84. package/admin/js/views/dashboard/widgets/kpi-strip.js +1 -1
  85. package/admin/js/views/form-editor.js +7 -7
  86. package/admin/js/views/form-submissions.js +1 -1
  87. package/admin/js/views/forms.js +1 -1
  88. package/admin/js/views/layouts.js +1 -1
  89. package/admin/js/views/login.js +2 -2
  90. package/admin/js/views/menu-editor.js +3 -3
  91. package/admin/js/views/menu-locations.js +1 -1
  92. package/admin/js/views/menus.js +3 -3
  93. package/admin/js/views/navigation.js +8 -8
  94. package/admin/js/views/page-editor.js +40 -40
  95. package/admin/js/views/pages.js +3 -3
  96. package/admin/js/views/plugin-code.js +2 -2
  97. package/admin/js/views/plugins.js +1 -1
  98. package/admin/js/views/project-settings.js +1 -1
  99. package/admin/js/views/projects.js +1 -1
  100. package/admin/js/views/roles.js +1 -1
  101. package/admin/js/views/user-editor.js +1 -1
  102. package/admin/js/views/users.js +3 -3
  103. package/admin/js/views/view-editor.js +1 -1
  104. package/admin/js/views/view-preview.js +1 -1
  105. package/admin/js/views/views-list.js +1 -1
  106. package/bin/cli.js +8 -8
  107. package/bin/lib/config-merge.js +44 -44
  108. package/bin/update.js +16 -16
  109. package/config/plugins.json +1 -1
  110. package/package.json +2 -2
  111. package/plugins/_template/admin/views/index.js +1 -1
  112. package/plugins/_template/config.js +1 -1
  113. package/plugins/_template/plugin.js +1 -1
  114. package/plugins/analytics/admin/templates/analytics.html +3 -3
  115. package/plugins/analytics/admin/views/analytics.js +1 -1
  116. package/plugins/analytics/config.js +1 -1
  117. package/plugins/analytics/plugin.js +4 -4
  118. package/plugins/analytics/plugin.json +1 -1
  119. package/plugins/analytics/public/inject-body.html +2 -2
  120. package/plugins/analytics/public/inject-head.html +1 -1
  121. package/plugins/blog/admin/templates/blog.html +4 -4
  122. package/plugins/blog/admin/views/blog.js +3 -3
  123. package/plugins/blog/admin/views/categories.js +2 -2
  124. package/plugins/blog/admin/views/comments.js +4 -4
  125. package/plugins/blog/admin/views/post-editor.js +1 -1
  126. package/plugins/blog/plugin.js +2 -2
  127. package/plugins/blog/plugin.json +4 -4
  128. package/plugins/contacts/admin/views/contacts.js +8 -8
  129. package/plugins/contacts/plugin.js +4 -4
  130. package/plugins/contacts/plugin.json +1 -1
  131. package/plugins/invoice/admin/templates/editor.html +6 -6
  132. package/plugins/invoice/admin/templates/index.html +4 -4
  133. package/plugins/invoice/admin/views/editor.js +3 -3
  134. package/plugins/invoice/admin/views/index.js +4 -4
  135. package/plugins/invoice/admin/views/issuers.js +1 -1
  136. package/plugins/invoice/admin/views/party-view.js +3 -3
  137. package/plugins/invoice/admin/views/receivers.js +1 -1
  138. package/plugins/invoice/config.js +1 -1
  139. package/plugins/invoice/plugin.js +1 -1
  140. package/plugins/invoice/plugin.json +4 -4
  141. package/plugins/notes/admin/views/notes.js +4 -4
  142. package/plugins/notes/plugin.js +1 -1
  143. package/plugins/notes/plugin.json +1 -1
  144. package/plugins/site-search/admin/templates/site-search.html +3 -3
  145. package/plugins/site-search/admin/views/site-search.js +1 -1
  146. package/plugins/site-search/config.js +1 -1
  147. package/plugins/site-search/plugin.js +4 -4
  148. package/plugins/site-search/plugin.json +1 -1
  149. package/plugins/surveys/admin/templates/results.html +1 -1
  150. package/plugins/surveys/admin/templates/survey-editor.html +4 -4
  151. package/plugins/surveys/admin/views/audience.js +6 -6
  152. package/plugins/surveys/admin/views/results.js +3 -3
  153. package/plugins/surveys/admin/views/survey-editor.js +1 -1
  154. package/plugins/surveys/admin/views/surveys.js +2 -2
  155. package/plugins/surveys/plugin.js +1 -1
  156. package/plugins/surveys/plugin.json +4 -4
  157. package/plugins/theme-switcher/admin/views/theme-switcher.js +1 -1
  158. package/plugins/theme-switcher/plugin.json +3 -3
  159. package/plugins/theme-switcher/public/inject-body.html +28 -28
  160. package/plugins/theme-switcher/public/inject-head.html +1 -1
  161. package/plugins/todo/plugin.js +1 -1
  162. package/public/css/forms.css +1 -1
  163. package/public/js/collection-browser.js +1 -1
  164. package/public/js/collection-context.js +1 -1
  165. package/public/js/form-logic-engine.js +1 -1
  166. package/public/js/forms.js +2 -1
  167. package/public/js/site.js +1 -1
  168. package/scripts/build.js +14 -14
  169. package/scripts/create-plugin.js +2 -2
  170. package/scripts/fresh.js +2 -2
  171. package/scripts/gen-instance-secret.js +1 -1
  172. package/scripts/pro.js +12 -12
  173. package/scripts/reset.js +2 -2
  174. package/scripts/setup.js +15 -15
  175. package/scripts/users.js +5 -5
  176. package/scripts/verify-assets.mjs +6 -6
  177. package/server/config.js +1 -1
  178. package/server/middleware/auth.js +253 -253
  179. package/server/middleware/managerAuth.js +3 -3
  180. package/server/routes/api/actions.js +4 -4
  181. package/server/routes/api/api-endpoints.js +7 -7
  182. package/server/routes/api/api-tokens.js +5 -5
  183. package/server/routes/api/auth.js +309 -309
  184. package/server/routes/api/blocks.js +3 -3
  185. package/server/routes/api/collections.js +18 -18
  186. package/server/routes/api/dashboard.js +5 -5
  187. package/server/routes/api/effects.js +2 -2
  188. package/server/routes/api/endpoints-public.js +5 -5
  189. package/server/routes/api/forms.js +47 -47
  190. package/server/routes/api/menu-locations.js +3 -3
  191. package/server/routes/api/menus.js +7 -7
  192. package/server/routes/api/navigation.js +42 -42
  193. package/server/routes/api/notifications.js +2 -2
  194. package/server/routes/api/pages.js +14 -2
  195. package/server/routes/api/plugin-marketplace.js +3 -3
  196. package/server/routes/api/plugins.js +6 -6
  197. package/server/routes/api/projects.js +9 -9
  198. package/server/routes/api/scaffold.js +7 -7
  199. package/server/routes/api/settings.js +6 -6
  200. package/server/routes/api/sidebar.js +1 -1
  201. package/server/routes/api/users.js +5 -5
  202. package/server/routes/api/versions.js +1 -1
  203. package/server/routes/api/views.js +1 -1
  204. package/server/routes/docs-public.js +3 -3
  205. package/server/routes/public.js +14 -14
  206. package/server/server.js +34 -16
  207. package/server/services/actions.js +19 -19
  208. package/server/services/adapters/FileAdapter.js +2 -2
  209. package/server/services/adapters/MongoAdapter.js +5 -5
  210. package/server/services/apiEndpoints.js +15 -15
  211. package/server/services/apiTokens.js +7 -7
  212. package/server/services/blocks.js +13 -13
  213. package/server/services/cache/drivers/MemoryDriver.js +3 -3
  214. package/server/services/cache/drivers/NoneDriver.js +1 -1
  215. package/server/services/cache/index.js +5 -5
  216. package/server/services/collections.js +14 -14
  217. package/server/services/components.js +9 -9
  218. package/server/services/connectionManager.js +4 -4
  219. package/server/services/content.js +25 -2
  220. package/server/services/docs.js +14 -14
  221. package/server/services/email.js +167 -167
  222. package/server/services/filterEngine.js +6 -6
  223. package/server/services/forms.js +18 -10
  224. package/server/services/health.js +1 -1
  225. package/server/services/hooks.js +7 -7
  226. package/server/services/images.js +4 -4
  227. package/server/services/managerClient.js +1 -1
  228. package/server/services/markdown.js +79 -79
  229. package/server/services/menuRender.js +5 -5
  230. package/server/services/menus-migration.js +3 -3
  231. package/server/services/menus.js +31 -31
  232. package/server/services/permissionRegistry.js +1 -1
  233. package/server/services/pluginFiles.js +5 -5
  234. package/server/services/pluginInstaller.js +17 -17
  235. package/server/services/pluginScaffold.js +1 -1
  236. package/server/services/plugins.js +17 -17
  237. package/server/services/presetCollections.js +2 -2
  238. package/server/services/projects.js +35 -26
  239. package/server/services/recipes/contact-list.json +1 -1
  240. package/server/services/recipes/onboarding.json +8 -8
  241. package/server/services/references.js +8 -8
  242. package/server/services/renderer.js +9 -9
  243. package/server/services/roles.js +9 -9
  244. package/server/services/rowAccess.js +10 -10
  245. package/server/services/scaffolder.js +30 -30
  246. package/server/services/sidebar-migration.js +10 -10
  247. package/server/services/sitemap.js +1 -1
  248. package/server/services/userProfiles.js +199 -199
  249. package/server/services/userRoles.js +7 -7
  250. package/server/services/users.js +302 -302
  251. package/server/services/versions.js +3 -3
  252. package/server/services/viewPipeline.js +6 -6
  253. package/server/services/views.js +11 -11
  254. package/server/templates/page.html +5 -5
@@ -1,42 +1,42 @@
1
- /**
2
- * Navigation API
3
- * GET /api/navigation - get navigation config
4
- * PUT /api/navigation - save navigation config
5
- */
6
- import {getConfig, saveConfig} from '../../config.js';
7
- import {authenticate, requirePermission} from '../../middleware/auth.js';
8
- import * as cache from '../../services/cache/index.js';
9
-
10
- export async function navigationRoutes(fastify) {
11
- const canRead = {preHandler: [authenticate, requirePermission('navigation', 'read')]};
12
- const canUpdate = {preHandler: [authenticate, requirePermission('navigation', 'update')]};
13
-
14
- fastify.get('/navigation', canRead, async () => {
15
- return getConfig('navigation');
16
- });
17
-
18
- fastify.put('/navigation', canUpdate, async (request, reply) => {
19
- const data = request.body;
20
- if (!data || typeof data !== 'object') {
21
- return reply.status(400).send({ error: 'Invalid navigation data' });
22
- }
23
- if (!Array.isArray(data.items) && !Array.isArray(data)) {
24
- return reply.status(400).send({ error: 'Navigation must be an array of items' });
25
- }
26
- // Normalise child key: Domma navbar expects `items`, not `children`
27
- if (Array.isArray(data.items)) {
28
- data.items = data.items.map(item => {
29
- const children = item.items || item.children;
30
- if (children?.length) {
31
- const { children: _c, ...rest } = item;
32
- return { ...rest, items: children };
33
- }
34
- const { children: _c, ...rest } = item;
35
- return rest;
36
- });
37
- }
38
- saveConfig('navigation', data);
39
- await cache.invalidateTags(['nav']);
40
- return { success: true };
41
- });
42
- }
1
+ /**
2
+ * Navigation API
3
+ * GET /api/navigation - get navigation config
4
+ * PUT /api/navigation - save navigation config
5
+ */
6
+ import {getConfig, saveConfig} from '../../config.js';
7
+ import {authenticate, requirePermission} from '../../middleware/auth.js';
8
+ import * as cache from '../../services/cache/index.js';
9
+
10
+ export async function navigationRoutes(fastify) {
11
+ const canRead = {preHandler: [authenticate, requirePermission('navigation', 'read')]};
12
+ const canUpdate = {preHandler: [authenticate, requirePermission('navigation', 'update')]};
13
+
14
+ fastify.get('/navigation', canRead, async () => {
15
+ return getConfig('navigation');
16
+ });
17
+
18
+ fastify.put('/navigation', canUpdate, async (request, reply) => {
19
+ const data = request.body;
20
+ if (!data || typeof data !== 'object') {
21
+ return reply.status(400).send({ error: 'Invalid navigation data' });
22
+ }
23
+ if (!Array.isArray(data.items) && !Array.isArray(data)) {
24
+ return reply.status(400).send({ error: 'Navigation must be an array of items' });
25
+ }
26
+ // Normalise child key: Domma navbar expects `items`, not `children`
27
+ if (Array.isArray(data.items)) {
28
+ data.items = data.items.map(item => {
29
+ const children = item.items || item.children;
30
+ if (children?.length) {
31
+ const { children: _c, ...rest } = item;
32
+ return { ...rest, items: children };
33
+ }
34
+ const { children: _c, ...rest } = item;
35
+ return rest;
36
+ });
37
+ }
38
+ saveConfig('navigation', data);
39
+ await cache.invalidateTags(['nav']);
40
+ return { success: true };
41
+ });
42
+ }
@@ -83,7 +83,7 @@ export async function notificationsRoutes(fastify) {
83
83
  }));
84
84
  });
85
85
 
86
- // --- Unread count (lightweight — for bell badge) ---
86
+ // --- Unread count (lightweight - for bell badge) ---
87
87
  fastify.get('/system/notifications/unread-count', canRead, async (request) => {
88
88
  const userId = request.user.id;
89
89
  const now = new Date().toISOString();
@@ -110,7 +110,7 @@ export async function notificationsRoutes(fastify) {
110
110
  if (!entry) return reply.code(404).send({ error: 'Notification not found.' });
111
111
 
112
112
  const readBy = Array.isArray(entry.data.readBy) ? entry.data.readBy : [];
113
- if (readBy.includes(userId)) return { ok: true }; // already read — idempotent
113
+ if (readBy.includes(userId)) return { ok: true }; // already read - idempotent
114
114
 
115
115
  const updated = await adapter.update(SLUG, id, {
116
116
  ...entry.data,
@@ -26,7 +26,7 @@ export async function pagesRoutes(fastify) {
26
26
  return {html};
27
27
  });
28
28
 
29
- // Aggregate unique tags from all pages — must be registered before the wildcard /pages/* route
29
+ // Aggregate unique tags from all pages - must be registered before the wildcard /pages/* route
30
30
  fastify.get('/pages/tags', canRead, async (request, reply) => {
31
31
  const pages = await listPages();
32
32
  const tagSet = new Set();
@@ -72,8 +72,20 @@ export async function pagesRoutes(fastify) {
72
72
  const { urlPath, frontmatter, body } = request.body;
73
73
  if (!urlPath) return reply.status(400).send({ error: 'urlPath is required' });
74
74
 
75
+ // Name the project in the conflict. A page filed under a project the
76
+ // admin isn't currently browsing is absent from the filtered list, so
77
+ // a bare "already exists" reads as a phantom.
75
78
  const existing = await getPage(urlPath);
76
- if (existing) return reply.status(409).send({ error: 'Page already exists at that path' });
79
+ if (existing) {
80
+ const {getProjectForPage} = await import('../../services/projects.js');
81
+ const project = await getProjectForPage(urlPath, existing.project);
82
+ return reply.status(409).send({
83
+ error: `A page already exists at ${urlPath}`
84
+ + (project ? ` (project: ${project})` : ''),
85
+ urlPath,
86
+ project: project || null
87
+ });
88
+ }
77
89
 
78
90
  const page = await createPage(urlPath, frontmatter || {}, body || '');
79
91
  return reply.status(201).send(page);
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Plugin Marketplace API
3
- * GET /api/plugins/marketplace — fetch catalogue from manager (requires MANAGER_URL)
4
- * POST /api/plugins/marketplace/install — download + install a plugin bundle
5
- * DELETE /api/plugins/marketplace/:slug — uninstall an installed plugin
3
+ * GET /api/plugins/marketplace - fetch catalogue from manager (requires MANAGER_URL)
4
+ * POST /api/plugins/marketplace/install - download + install a plugin bundle
5
+ * DELETE /api/plugins/marketplace/:slug - uninstall an installed plugin
6
6
  *
7
7
  * All routes require authentication + admin role.
8
8
  */
@@ -24,7 +24,7 @@ import {getConfig} from '../../config.js';
24
24
  * True when something will bring the process back after a clean exit.
25
25
  *
26
26
  * Two supervisors matter in practice: pm2 (sets `pm_id` / `PM2_HOME`) and the
27
- * fleet manager, which forks each site with an IPC channel — hence
27
+ * fleet manager, which forks each site with an IPC channel - hence
28
28
  * `process.send`. A bare `node server/server.js` has neither, and exiting there
29
29
  * would leave the site down for good, so restarts are refused.
30
30
  *
@@ -35,7 +35,7 @@ export function hasSupervisor() {
35
35
  }
36
36
 
37
37
  // One pending restart at a time. An admin flipping four plugins in a row should
38
- // cost one restart, not four — each toggle pushes the timer out instead of
38
+ // cost one restart, not four - each toggle pushes the timer out instead of
39
39
  // stacking exits.
40
40
  let _restartTimer = null;
41
41
 
@@ -43,7 +43,7 @@ let _restartTimer = null;
43
43
  * Exit cleanly so the supervisor restarts us on the new plugin set. Debounced,
44
44
  * and long enough after the reply for it to reach the browser.
45
45
  *
46
- * The timer is unref'd so it can never be the thing keeping the process alive —
46
+ * The timer is unref'd so it can never be the thing keeping the process alive -
47
47
  * in production the listening socket does that.
48
48
  *
49
49
  * @param {Function} exit How to exit; injectable so tests don't take the runner down
@@ -98,7 +98,7 @@ export async function pluginsRoutes(fastify, opts = {}) {
98
98
 
99
99
  // Scaffold a new plugin from plugins/_template. The plugin is created
100
100
  // pre-enabled so it goes live on the next server restart (its routes and
101
- // admin views only register at boot — same restart rule as enable/disable).
101
+ // admin views only register at boot - same restart rule as enable/disable).
102
102
  fastify.post('/plugins/scaffold', canCreate, async (request, reply) => {
103
103
  const {slug, displayName, description, author, icon} = request.body || {};
104
104
  try {
@@ -134,7 +134,7 @@ export async function pluginsRoutes(fastify, opts = {}) {
134
134
 
135
135
  // A plugin's routes, hooks and admin views only register at boot, so a
136
136
  // toggle does nothing until the process restarts. Do it for the admin
137
- // rather than leaving a "restart to apply" note they have to action —
137
+ // rather than leaving a "restart to apply" note they have to action -
138
138
  // but only where a supervisor will bring the process back, and only
139
139
  // when the enabled flag actually changed (a settings-only save must not
140
140
  // bounce the site).
@@ -154,7 +154,7 @@ export async function pluginsRoutes(fastify, opts = {}) {
154
154
  return getAdminPluginConfig();
155
155
  });
156
156
 
157
- // ── In-admin code editor (plugins.develop — code execution grade) ──────
157
+ // ── In-admin code editor (plugins.develop - code execution grade) ──────
158
158
 
159
159
  const mapErr = (reply, err) => reply.status(err.statusCode || 500).send({ error: err.message });
160
160
 
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Projects API
3
3
  *
4
- * GET /api/projects — list visible projects
5
- * GET /api/projects/:slug — full project
6
- * POST /api/projects — create
7
- * PUT /api/projects/:slug — edit (slug immutable)
8
- * DELETE /api/projects/:slug — delete (403 for core; 409 if any artefacts tagged)
9
- * GET /api/projects/:slug/artefacts — grouped artefacts
10
- * POST /api/projects/:slug/untag-all — escape hatch before delete (403 for core)
4
+ * GET /api/projects - list visible projects
5
+ * GET /api/projects/:slug - full project
6
+ * POST /api/projects - create
7
+ * PUT /api/projects/:slug - edit (slug immutable)
8
+ * DELETE /api/projects/:slug - delete (403 for core; 409 if any artefacts tagged)
9
+ * GET /api/projects/:slug/artefacts - grouped artefacts
10
+ * POST /api/projects/:slug/untag-all - escape hatch before delete (403 for core)
11
11
  *
12
12
  * Auth middlewares are accepted as DI options so tests can supply no-ops.
13
13
  */
@@ -88,7 +88,7 @@ export async function projectsRoutes(fastify, opts = {}) {
88
88
  const total = Object.values(grouped).reduce((sum, arr) => sum + arr.length, 0);
89
89
  if (total > 0) {
90
90
  return reply.status(409).send({
91
- error: `Cannot delete — ${total} artefacts still tagged with this project`,
91
+ error: `Cannot delete - ${total} artefacts still tagged with this project`,
92
92
  artefacts: Object.fromEntries(Object.entries(grouped).map(([k, v]) => [k, v.length]))
93
93
  });
94
94
  }
@@ -107,7 +107,7 @@ export async function projectsRoutes(fastify, opts = {}) {
107
107
 
108
108
  fastify.post('/projects/:slug/untag-all', canDelete, async (request, reply) => {
109
109
  if (request.params.slug === CORE_PROJECT_SLUG) {
110
- return reply.status(403).send({error: 'Cannot untag the core project — artefacts would immediately resolve back to it'});
110
+ return reply.status(403).send({error: 'Cannot untag the core project - artefacts would immediately resolve back to it'});
111
111
  }
112
112
  const target = await getProject(request.params.slug);
113
113
  if (target?.protected === true) {
@@ -1,15 +1,15 @@
1
1
  /**
2
- * Scaffold API — list bundled recipes and apply one to create a working CRUD
2
+ * Scaffold API - list bundled recipes and apply one to create a working CRUD
3
3
  * system (Collection + Form + Actions) in a single call.
4
4
  *
5
- * Admin-only — these endpoints can create resources, so they require the same
5
+ * Admin-only - these endpoints can create resources, so they require the same
6
6
  * permission tier as collection/form management. The `apply` endpoint is
7
7
  * non-atomic across recipe pieces but returns a clear summary of what was
8
8
  * created vs. skipped so the caller can clean up if needed.
9
9
  *
10
10
  * Endpoints:
11
- * GET /api/scaffold/recipes — list bundled recipes (name, description, options)
12
- * POST /api/scaffold/apply — apply one recipe with caller-provided option overrides
11
+ * GET /api/scaffold/recipes - list bundled recipes (name, description, options)
12
+ * POST /api/scaffold/apply - apply one recipe with caller-provided option overrides
13
13
  */
14
14
  import {applyRecipe, getRecipe, listRecipes} from '../../services/scaffolder.js';
15
15
  import {authenticate, requirePermission} from '../../middleware/auth.js';
@@ -17,7 +17,7 @@ import {authenticate, requirePermission} from '../../middleware/auth.js';
17
17
  export async function scaffoldRoutes(fastify) {
18
18
  // Scaffolding creates collections + forms + actions, so we require both
19
19
  // collections.create and forms.create permissions. The cheap check is to
20
- // gate on collections.create — anyone with that should already be a
20
+ // gate on collections.create - anyone with that should already be a
21
21
  // privileged role; we add forms.create on top as a belt-and-braces.
22
22
  const canScaffold = {preHandler: [authenticate, requirePermission('collections', 'create')]};
23
23
 
@@ -38,7 +38,7 @@ export async function scaffoldRoutes(fastify) {
38
38
  /*
39
39
  * GET /api/scaffold/recipes/:slug
40
40
  *
41
- * Returns the full recipe document — useful for a preview view that wants
41
+ * Returns the full recipe document - useful for a preview view that wants
42
42
  * to show the user exactly what will be created before they click Apply.
43
43
  */
44
44
  fastify.get('/scaffold/recipes/:slug', canScaffold, async (request, reply) => {
@@ -61,7 +61,7 @@ export async function scaffoldRoutes(fastify) {
61
61
  * { created: {collection, form, actions: [...]}, skipped: [...], warnings: [...], snippet: string|null }
62
62
  *
63
63
  * The `snippet` is a Markdown fragment the caller can insert at the
64
- * cursor position in the page editor — typically the [form] + [collection]
64
+ * cursor position in the page editor - typically the [form] + [collection]
65
65
  * combination needed to see the new system on a page immediately.
66
66
  */
67
67
  fastify.post('/scaffold/apply', canScaffold, async (request, reply) => {
@@ -41,7 +41,7 @@ export async function settingsRoutes(fastify) {
41
41
  'smtp', 'footer', 'analytics', 'social', 'locale',
42
42
  'layoutOptions', 'seo', 'backToTop', 'cookieConsent', 'breadcrumbs', 'autoTheme',
43
43
  'adminBrand',
44
- // Keys that legitimately live in site.json (written elsewhere — e.g.
44
+ // Keys that legitimately live in site.json (written elsewhere - e.g.
45
45
  // `brand` by the menus migration) and are echoed back by GET /settings.
46
46
  // They must be accepted here or any client that round-trips the GET
47
47
  // payload into a save gets a 400 for keys it never touched.
@@ -80,7 +80,7 @@ export async function settingsRoutes(fastify) {
80
80
  return reply.status(400).send({ error: 'SMTP is not configured. Save your SMTP settings first.' });
81
81
  }
82
82
 
83
- // Shared transport factory — carries the self-signed-cert TLS relaxation.
83
+ // Shared transport factory - carries the self-signed-cert TLS relaxation.
84
84
  // Do NOT build a raw nodemailer transport here; that regresses on engine sync.
85
85
  const transporter = await createTransport(smtp);
86
86
 
@@ -93,7 +93,7 @@ export async function settingsRoutes(fastify) {
93
93
  await transporter.sendMail({
94
94
  from: smtp.fromName ? `"${smtp.fromName}" <${smtp.fromAddress}>` : smtp.fromAddress,
95
95
  to,
96
- subject: 'Domma CMS — Test Email',
96
+ subject: 'Domma CMS - Test Email',
97
97
  text: 'This is a test email sent from Domma CMS to verify your SMTP configuration.',
98
98
  html: '<p>This is a test email sent from <strong>Domma CMS</strong> to verify your SMTP configuration.</p>'
99
99
  });
@@ -103,7 +103,7 @@ export async function settingsRoutes(fastify) {
103
103
  }
104
104
  });
105
105
 
106
- // GET /api/settings/db-status — returns whether MongoDB connections are configured
106
+ // GET /api/settings/db-status - returns whether MongoDB connections are configured
107
107
  fastify.get('/settings/db-status', canRead, async () => {
108
108
  try {
109
109
  const raw = await fs.readFile(CONNECTIONS_FILE, 'utf8');
@@ -117,7 +117,7 @@ export async function settingsRoutes(fastify) {
117
117
  }
118
118
  });
119
119
 
120
- // GET /api/settings/custom-css — return current CSS as JSON
120
+ // GET /api/settings/custom-css - return current CSS as JSON
121
121
  fastify.get('/settings/custom-css', canUpdate, async () => {
122
122
  try {
123
123
  const css = await fs.readFile(CUSTOM_CSS_FILE, 'utf8');
@@ -127,7 +127,7 @@ export async function settingsRoutes(fastify) {
127
127
  }
128
128
  });
129
129
 
130
- // PUT /api/settings/custom-css — save CSS to content/custom.css
130
+ // PUT /api/settings/custom-css - save CSS to content/custom.css
131
131
  fastify.put('/settings/custom-css', canUpdate, async (request, reply) => {
132
132
  const { css } = request.body || {};
133
133
  if (typeof css !== 'string') {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Admin sidebar API
3
3
  *
4
- * GET /api/sidebar/registered-items — plugin-registered sidebar items
4
+ * GET /api/sidebar/registered-items - plugin-registered sidebar items
5
5
  *
6
6
  * Auth middlewares are accepted as DI options so tests can supply no-ops.
7
7
  */
@@ -2,9 +2,9 @@
2
2
  * Users API
3
3
  * GET /api/users - list all users (admin, manager)
4
4
  * GET /api/users/:id - single user (admin, manager, or self)
5
- * POST /api/users - create user (admin, manager — manager cannot create admin)
6
- * PUT /api/users/:id - update user (admin, manager — manager cannot edit admin)
7
- * DELETE /api/users/:id - delete user (admin, manager — manager cannot delete admin, no self-delete)
5
+ * POST /api/users - create user (admin, manager - manager cannot create admin)
6
+ * PUT /api/users/:id - update user (admin, manager - manager cannot edit admin)
7
+ * DELETE /api/users/:id - delete user (admin, manager - manager cannot delete admin, no self-delete)
8
8
  */
9
9
  import {authenticate, canManageUser, requirePermission} from '../../middleware/auth.js';
10
10
  import {getPermissionsFor} from '../../services/roles.js';
@@ -64,7 +64,7 @@ export async function usersRoutes(fastify) {
64
64
  if (!canManageUser(request.user, targetRole)) {
65
65
  return reply.code(403).send({ error: 'You cannot create a user with that role' });
66
66
  }
67
- // Additional-roles privilege guard — actor must be able to manage EVERY role
67
+ // Additional-roles privilege guard - actor must be able to manage EVERY role
68
68
  // they're trying to assign, not just the primary.
69
69
  const requestedAdditional = Array.isArray(request.body?.additionalRoles) ? request.body.additionalRoles : [];
70
70
  for (const r of requestedAdditional) {
@@ -89,7 +89,7 @@ export async function usersRoutes(fastify) {
89
89
  const target = await getUserById(id);
90
90
  if (!target) return reply.status(404).send({ error: 'User not found' });
91
91
 
92
- // Self-edit is always allowed — the hierarchy guard only applies to
92
+ // Self-edit is always allowed - the hierarchy guard only applies to
93
93
  // OTHER users, otherwise nobody can edit a peer-level account (or
94
94
  // even their own, since canManageUser is a strict level comparison).
95
95
  // Role-escalation guards below still apply to self-edits.
@@ -83,7 +83,7 @@ export async function versionsRoutes(fastify) {
83
83
  }
84
84
  });
85
85
 
86
- // Prune versions — keep the most recent N (policy decided in pruneVersions)
86
+ // Prune versions - keep the most recent N (policy decided in pruneVersions)
87
87
  fastify.post('/versions/prune/*', canDelete, async (request, reply) => {
88
88
  const urlPath = '/' + request.params['*'];
89
89
  const keep = Number.parseInt(request.body?.keep, 10);
@@ -32,7 +32,7 @@ export async function viewsRoutes(fastify) {
32
32
  try {
33
33
  const {canSeeArtefact} = await import('../../services/projects.js');
34
34
  const all = await listViews();
35
- // listViews returns full records with meta inline — filter directly
35
+ // listViews returns full records with meta inline - filter directly
36
36
  return all.filter(v => canSeeArtefact(request.user, v));
37
37
  } catch (err) {
38
38
  return reply.status(503).send({ error: err.message });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Domma Docs — public surface
2
+ * Domma Docs - public surface
3
3
  *
4
4
  * Serves the handbook at request time from the admin doc templates:
5
5
  * GET /domma-docs → home landing
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * This dedicated /domma-docs(/*) prefix out-prioritises public.js's `/*`
13
13
  * catch-all (Fastify ranks static segments above wildcards), exactly as
14
- * routes/api/endpoints-public.js `/x/*` coexists with it — so no per-path
14
+ * routes/api/endpoints-public.js `/x/*` coexists with it - so no per-path
15
15
  * special-casing is needed in public.js. Registered at app root (not behind a
16
16
  * prefixed plugin) so the specificity ranking shares one router scope.
17
17
  */
@@ -38,6 +38,6 @@ export async function docsPublicRoutes(fastify) {
38
38
  return reply.type('text/html').send(html);
39
39
  };
40
40
 
41
- fastify.get('/domma-docs', handle); // exact — params['*'] === '' (home landing)
41
+ fastify.get('/domma-docs', handle); // exact - params['*'] === '' (home landing)
42
42
  fastify.get('/domma-docs/*', handle); // sections + leaves
43
43
  }
@@ -18,7 +18,7 @@ import * as cache from '../services/cache/index.js';
18
18
  *
19
19
  * Honours `site.baseUrl` as an explicit override (useful when the CMS sits
20
20
  * behind infrastructure that doesn't forward Host correctly). Otherwise
21
- * builds it from `request.protocol` + `request.host` — both of which Fastify
21
+ * builds it from `request.protocol` + `request.host` - both of which Fastify
22
22
  * resolves from `X-Forwarded-*` headers when `trustProxy` is on. `request.host`
23
23
  * includes the port for non-default ports (e.g. `localhost:4096` in dev) and
24
24
  * omits it for the standard 80/443.
@@ -58,7 +58,7 @@ export async function publicRoutes(fastify) {
58
58
  // Health check
59
59
  fastify.get('/api/health', async () => ({ status: 'ok' }));
60
60
 
61
- // SEO: sitemap.xml — cached per origin (so a single CMS responding on
61
+ // SEO: sitemap.xml - cached per origin (so a single CMS responding on
62
62
  // multiple domains gets the right absolute URLs); invalidated by page
63
63
  // CRUD via the 'sitemap' tag.
64
64
  fastify.get('/sitemap.xml', async (request, reply) => {
@@ -71,7 +71,7 @@ export async function publicRoutes(fastify) {
71
71
  return reply.type('application/xml').send(xml);
72
72
  });
73
73
 
74
- // SEO: robots.txt — cheap to build, no cache wrap needed.
74
+ // SEO: robots.txt - cheap to build, no cache wrap needed.
75
75
  fastify.get('/robots.txt', async (request, reply) => {
76
76
  return reply.type('text/plain').send(buildRobotsTxt(getBaseUrl(request)));
77
77
  });
@@ -87,7 +87,7 @@ export async function publicRoutes(fastify) {
87
87
 
88
88
  const urlPath = '/' + (rawPath || '');
89
89
 
90
- // First-pass page lookup — used only for the visibility decision
90
+ // First-pass page lookup - used only for the visibility decision
91
91
  // (metadata-only is sufficient). We re-fetch below with `user` once
92
92
  // it is resolved so the body's [menu] shortcode renders against the
93
93
  // correct role.
@@ -110,28 +110,28 @@ export async function publicRoutes(fastify) {
110
110
  }
111
111
 
112
112
  // Hard kill-switch: a disabled project takes its pages off the public
113
- // site entirely — 404 (no oracle), regardless of page visibility.
113
+ // site entirely - 404 (no oracle), regardless of page visibility.
114
114
  const pageProject = await getProjectForPage(page.urlPath || urlPath, page.project);
115
115
  if (!(await isProjectEnabled(pageProject))) {
116
116
  reply.status(404);
117
117
  return reply.type('text/html').send(await render404(urlPath));
118
118
  }
119
119
 
120
- // Enforce page visibility — role only resolved for gated pages,
120
+ // Enforce page visibility - role only resolved for gated pages,
121
121
  // so public pages share a single cache entry keyed `roleanon`.
122
122
  //
123
123
  // `visibility` may be a string ('public' | 'private' | role name) or
124
- // an array of role names — see checkVisibility() for full semantics.
124
+ // an array of role names - see checkVisibility() for full semantics.
125
125
  // Effectively-public values (missing, 'public', or an array containing
126
126
  // 'public') skip JWT verification entirely so anonymous traffic hits
127
127
  // the shared cache without auth cost.
128
- // For per-role cache keying we use the primary role only — multi-role
128
+ // For per-role cache keying we use the primary role only - multi-role
129
129
  // users still see correctly-gated content (checkVisibility consults
130
130
  // additionalRoles too) but the cache key stays bounded to one entry
131
131
  // per primary role rather than 2^N per role combination. The fallback
132
132
  // is correctness: any user whose access depends on an additional role
133
133
  // is granted, but their served HTML is the variant cached for their
134
- // primary role's tier — fine for the public-site use case.
134
+ // primary role's tier - fine for the public-site use case.
135
135
  let userRole = null;
136
136
  let userObj = null;
137
137
  const vis = page.visibility;
@@ -146,7 +146,7 @@ export async function publicRoutes(fastify) {
146
146
  userRole = decoded.role;
147
147
  userObj = { role: decoded.role, additionalRoles: decoded.additionalRoles || [] };
148
148
  }
149
- } catch { /* no token — treat as unauthenticated */ }
149
+ } catch { /* no token - treat as unauthenticated */ }
150
150
 
151
151
  if (!checkVisibility(userObj, vis)) {
152
152
  reply.status(403);
@@ -155,12 +155,12 @@ export async function publicRoutes(fastify) {
155
155
  }
156
156
 
157
157
  const baseUrl = getBaseUrl(request);
158
- // mtime in the key: any change to the backing file — editor save,
159
- // script, git pull — yields a fresh entry even if no invalidation
158
+ // mtime in the key: any change to the backing file - editor save,
159
+ // script, git pull - yields a fresh entry even if no invalidation
160
160
  // hook fired. Stale-mtime entries age out via TTL/LRU.
161
161
  const pageMtime = await getPageMtime(page.urlPath || urlPath);
162
162
  const cacheKey = `page:${urlPath}:m${pageMtime}:role${userRole ?? 'anon'}:o${baseUrl}`;
163
- const cacheTags = [`page:${urlPath}`, ...(page.tags || []), 'nav', 'site'];
163
+ const cacheTags = [`page:${urlPath}`, ...(page.cacheTags || []), 'nav', 'site'];
164
164
  const html = await cache.wrap(
165
165
  cacheKey,
166
166
  async () => {
@@ -179,7 +179,7 @@ export async function publicRoutes(fastify) {
179
179
  }
180
180
 
181
181
  /**
182
- * Render a 404 response — tries content/pages/404.md first, falls back to
182
+ * Render a 404 response - tries content/pages/404.md first, falls back to
183
183
  * a minimal inline page so the site theme is applied when possible.
184
184
  */
185
185
  export async function render404(urlPath) {