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.
- package/CLAUDE.md +53 -53
- package/README.md +27 -27
- package/admin/css/admin.css +1 -1
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/index.html +2 -2
- package/admin/js/app.js +1 -1
- package/admin/js/lib/card-builder.js +2 -2
- package/admin/js/lib/crud-tutorial.js +1 -1
- package/admin/js/lib/effect-defs.js +1 -1
- package/admin/js/lib/effects-builder.js +1 -1
- package/admin/js/lib/image-editor.js +1 -1
- package/admin/js/lib/markdown-toolbar.js +2 -2
- package/admin/js/lib/project-context.js +1 -1
- package/admin/js/lib/project-quick-create.js +1 -1
- package/admin/js/lib/scribe-composer.js +1 -1
- package/admin/js/lib/server-restart.js +1 -1
- package/admin/js/lib/shortcode-context-menu.js +2 -2
- package/admin/js/lib/simple-editor.js +1 -1
- package/admin/js/lib/themes.js +1 -1
- package/admin/js/lib/timeline-builder.js +1 -1
- package/admin/js/templates/action-editor.html +8 -8
- package/admin/js/templates/api-endpoint-editor.html +7 -7
- package/admin/js/templates/api-endpoints.html +1 -1
- package/admin/js/templates/api-reference.html +1 -1
- package/admin/js/templates/block-editor.html +6 -6
- package/admin/js/templates/collection-editor.html +1 -1
- package/admin/js/templates/component-editor.html +2 -2
- package/admin/js/templates/dashboard/cache.html +3 -3
- package/admin/js/templates/dashboard/journeys.html +3 -3
- package/admin/js/templates/dashboard/kpi-strip.html +3 -3
- package/admin/js/templates/dashboard/top-pages.html +1 -1
- package/admin/js/templates/dashboard/traffic-chart.html +1 -1
- package/admin/js/templates/docs/api-actions.html +1 -1
- package/admin/js/templates/docs/api-collections.html +4 -4
- package/admin/js/templates/docs/api-pages.html +2 -2
- package/admin/js/templates/docs/api-plugins.html +1 -1
- package/admin/js/templates/docs/api-settings.html +2 -2
- package/admin/js/templates/docs/api-users.html +1 -1
- package/admin/js/templates/docs/components-howto.html +4 -4
- package/admin/js/templates/docs/components-reference.html +21 -21
- package/admin/js/templates/docs/components-rules.html +14 -14
- package/admin/js/templates/docs/components-walkthrough.html +13 -13
- package/admin/js/templates/docs/tutorial-crud.html +49 -49
- package/admin/js/templates/docs/tutorial-forms.html +4 -4
- package/admin/js/templates/docs/tutorial-plugin.html +10 -10
- package/admin/js/templates/docs/usage-actions.html +6 -6
- package/admin/js/templates/docs/usage-cta-shortcode.html +9 -9
- package/admin/js/templates/docs/usage-dconfig.html +4 -4
- package/admin/js/templates/docs/usage-navigation.html +1 -1
- package/admin/js/templates/docs/usage-pages.html +1 -1
- package/admin/js/templates/docs/usage-shortcodes.html +24 -24
- package/admin/js/templates/docs/usage-users-roles.html +3 -3
- package/admin/js/templates/docs/usage-views.html +4 -4
- package/admin/js/templates/documentation.html +1 -1
- package/admin/js/templates/effects.html +752 -752
- package/admin/js/templates/form-editor.html +5 -5
- package/admin/js/templates/forms.html +17 -17
- package/admin/js/templates/login.html +5 -5
- package/admin/js/templates/menu-editor.html +10 -10
- package/admin/js/templates/my-profile.html +17 -17
- package/admin/js/templates/navigation.html +11 -11
- package/admin/js/templates/notifications.html +1 -1
- package/admin/js/templates/page-editor.html +4 -4
- package/admin/js/templates/pro-docs.html +14 -14
- package/admin/js/templates/role-editor.html +70 -70
- package/admin/js/templates/roles.html +10 -10
- package/admin/js/templates/settings.html +10 -10
- package/admin/js/templates/tutorials.html +3 -3
- package/admin/js/templates/view-editor.html +15 -15
- package/admin/js/views/action-editor.js +1 -1
- package/admin/js/views/actions-list.js +1 -1
- package/admin/js/views/api-endpoint-editor.js +2 -2
- package/admin/js/views/api-endpoints.js +2 -2
- package/admin/js/views/api-tokens.js +3 -3
- package/admin/js/views/blocks.js +2 -2
- package/admin/js/views/collection-editor.js +1 -1
- package/admin/js/views/collection-entries.js +1 -1
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/component-editor.js +1 -1
- package/admin/js/views/components.js +4 -4
- package/admin/js/views/dashboard/widgets/cache.js +1 -1
- package/admin/js/views/dashboard/widgets/journeys.js +1 -1
- package/admin/js/views/dashboard/widgets/kpi-strip.js +1 -1
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/form-submissions.js +1 -1
- package/admin/js/views/forms.js +1 -1
- package/admin/js/views/layouts.js +1 -1
- package/admin/js/views/login.js +2 -2
- package/admin/js/views/menu-editor.js +3 -3
- package/admin/js/views/menu-locations.js +1 -1
- package/admin/js/views/menus.js +3 -3
- package/admin/js/views/navigation.js +8 -8
- package/admin/js/views/page-editor.js +40 -40
- package/admin/js/views/pages.js +3 -3
- package/admin/js/views/plugin-code.js +2 -2
- package/admin/js/views/plugins.js +1 -1
- package/admin/js/views/project-settings.js +1 -1
- package/admin/js/views/projects.js +1 -1
- package/admin/js/views/roles.js +1 -1
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +3 -3
- package/admin/js/views/view-editor.js +1 -1
- package/admin/js/views/view-preview.js +1 -1
- package/admin/js/views/views-list.js +1 -1
- package/bin/cli.js +8 -8
- package/bin/lib/config-merge.js +44 -44
- package/bin/update.js +16 -16
- package/config/plugins.json +1 -1
- package/package.json +2 -2
- package/plugins/_template/admin/views/index.js +1 -1
- package/plugins/_template/config.js +1 -1
- package/plugins/_template/plugin.js +1 -1
- package/plugins/analytics/admin/templates/analytics.html +3 -3
- package/plugins/analytics/admin/views/analytics.js +1 -1
- package/plugins/analytics/config.js +1 -1
- package/plugins/analytics/plugin.js +4 -4
- package/plugins/analytics/plugin.json +1 -1
- package/plugins/analytics/public/inject-body.html +2 -2
- package/plugins/analytics/public/inject-head.html +1 -1
- package/plugins/blog/admin/templates/blog.html +4 -4
- package/plugins/blog/admin/views/blog.js +3 -3
- package/plugins/blog/admin/views/categories.js +2 -2
- package/plugins/blog/admin/views/comments.js +4 -4
- package/plugins/blog/admin/views/post-editor.js +1 -1
- package/plugins/blog/plugin.js +2 -2
- package/plugins/blog/plugin.json +4 -4
- package/plugins/contacts/admin/views/contacts.js +8 -8
- package/plugins/contacts/plugin.js +4 -4
- package/plugins/contacts/plugin.json +1 -1
- package/plugins/invoice/admin/templates/editor.html +6 -6
- package/plugins/invoice/admin/templates/index.html +4 -4
- package/plugins/invoice/admin/views/editor.js +3 -3
- package/plugins/invoice/admin/views/index.js +4 -4
- package/plugins/invoice/admin/views/issuers.js +1 -1
- package/plugins/invoice/admin/views/party-view.js +3 -3
- package/plugins/invoice/admin/views/receivers.js +1 -1
- package/plugins/invoice/config.js +1 -1
- package/plugins/invoice/plugin.js +1 -1
- package/plugins/invoice/plugin.json +4 -4
- package/plugins/notes/admin/views/notes.js +4 -4
- package/plugins/notes/plugin.js +1 -1
- package/plugins/notes/plugin.json +1 -1
- package/plugins/site-search/admin/templates/site-search.html +3 -3
- package/plugins/site-search/admin/views/site-search.js +1 -1
- package/plugins/site-search/config.js +1 -1
- package/plugins/site-search/plugin.js +4 -4
- package/plugins/site-search/plugin.json +1 -1
- package/plugins/surveys/admin/templates/results.html +1 -1
- package/plugins/surveys/admin/templates/survey-editor.html +4 -4
- package/plugins/surveys/admin/views/audience.js +6 -6
- package/plugins/surveys/admin/views/results.js +3 -3
- package/plugins/surveys/admin/views/survey-editor.js +1 -1
- package/plugins/surveys/admin/views/surveys.js +2 -2
- package/plugins/surveys/plugin.js +1 -1
- package/plugins/surveys/plugin.json +4 -4
- package/plugins/theme-switcher/admin/views/theme-switcher.js +1 -1
- package/plugins/theme-switcher/plugin.json +3 -3
- package/plugins/theme-switcher/public/inject-body.html +28 -28
- package/plugins/theme-switcher/public/inject-head.html +1 -1
- package/plugins/todo/plugin.js +1 -1
- package/public/css/forms.css +1 -1
- package/public/js/collection-browser.js +1 -1
- package/public/js/collection-context.js +1 -1
- package/public/js/form-logic-engine.js +1 -1
- package/public/js/forms.js +2 -1
- package/public/js/site.js +1 -1
- package/scripts/build.js +14 -14
- package/scripts/create-plugin.js +2 -2
- package/scripts/fresh.js +2 -2
- package/scripts/gen-instance-secret.js +1 -1
- package/scripts/pro.js +12 -12
- package/scripts/reset.js +2 -2
- package/scripts/setup.js +15 -15
- package/scripts/users.js +5 -5
- package/scripts/verify-assets.mjs +6 -6
- package/server/config.js +1 -1
- package/server/middleware/auth.js +253 -253
- package/server/middleware/managerAuth.js +3 -3
- package/server/routes/api/actions.js +4 -4
- package/server/routes/api/api-endpoints.js +7 -7
- package/server/routes/api/api-tokens.js +5 -5
- package/server/routes/api/auth.js +309 -309
- package/server/routes/api/blocks.js +3 -3
- package/server/routes/api/collections.js +18 -18
- package/server/routes/api/dashboard.js +5 -5
- package/server/routes/api/effects.js +2 -2
- package/server/routes/api/endpoints-public.js +5 -5
- package/server/routes/api/forms.js +47 -47
- package/server/routes/api/menu-locations.js +3 -3
- package/server/routes/api/menus.js +7 -7
- package/server/routes/api/navigation.js +42 -42
- package/server/routes/api/notifications.js +2 -2
- package/server/routes/api/pages.js +14 -2
- package/server/routes/api/plugin-marketplace.js +3 -3
- package/server/routes/api/plugins.js +6 -6
- package/server/routes/api/projects.js +9 -9
- package/server/routes/api/scaffold.js +7 -7
- package/server/routes/api/settings.js +6 -6
- package/server/routes/api/sidebar.js +1 -1
- package/server/routes/api/users.js +5 -5
- package/server/routes/api/versions.js +1 -1
- package/server/routes/api/views.js +1 -1
- package/server/routes/docs-public.js +3 -3
- package/server/routes/public.js +14 -14
- package/server/server.js +34 -16
- package/server/services/actions.js +19 -19
- package/server/services/adapters/FileAdapter.js +2 -2
- package/server/services/adapters/MongoAdapter.js +5 -5
- package/server/services/apiEndpoints.js +15 -15
- package/server/services/apiTokens.js +7 -7
- package/server/services/blocks.js +13 -13
- package/server/services/cache/drivers/MemoryDriver.js +3 -3
- package/server/services/cache/drivers/NoneDriver.js +1 -1
- package/server/services/cache/index.js +5 -5
- package/server/services/collections.js +14 -14
- package/server/services/components.js +9 -9
- package/server/services/connectionManager.js +4 -4
- package/server/services/content.js +25 -2
- package/server/services/docs.js +14 -14
- package/server/services/email.js +167 -167
- package/server/services/filterEngine.js +6 -6
- package/server/services/forms.js +18 -10
- package/server/services/health.js +1 -1
- package/server/services/hooks.js +7 -7
- package/server/services/images.js +4 -4
- package/server/services/managerClient.js +1 -1
- package/server/services/markdown.js +79 -79
- package/server/services/menuRender.js +5 -5
- package/server/services/menus-migration.js +3 -3
- package/server/services/menus.js +31 -31
- package/server/services/permissionRegistry.js +1 -1
- package/server/services/pluginFiles.js +5 -5
- package/server/services/pluginInstaller.js +17 -17
- package/server/services/pluginScaffold.js +1 -1
- package/server/services/plugins.js +17 -17
- package/server/services/presetCollections.js +2 -2
- package/server/services/projects.js +35 -26
- package/server/services/recipes/contact-list.json +1 -1
- package/server/services/recipes/onboarding.json +8 -8
- package/server/services/references.js +8 -8
- package/server/services/renderer.js +9 -9
- package/server/services/roles.js +9 -9
- package/server/services/rowAccess.js +10 -10
- package/server/services/scaffolder.js +30 -30
- package/server/services/sidebar-migration.js +10 -10
- package/server/services/sitemap.js +1 -1
- package/server/services/userProfiles.js +199 -199
- package/server/services/userRoles.js +7 -7
- package/server/services/users.js +302 -302
- package/server/services/versions.js +3 -3
- package/server/services/viewPipeline.js +6 -6
- package/server/services/views.js +11 -11
- 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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
4
|
-
* POST /api/plugins/marketplace/install
|
|
5
|
-
* DELETE /api/plugins/marketplace/:slug
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5
|
-
* GET /api/projects/:slug
|
|
6
|
-
* POST /api/projects
|
|
7
|
-
* PUT /api/projects/:slug
|
|
8
|
-
* DELETE /api/projects/:slug
|
|
9
|
-
* GET /api/projects/:slug/artefacts
|
|
10
|
-
* POST /api/projects/:slug/untag-all
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
12
|
-
* POST /api/scaffold/apply
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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') {
|
|
@@ -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
|
|
6
|
-
* PUT /api/users/:id - update user (admin, manager
|
|
7
|
-
* DELETE /api/users/:id - delete user (admin, manager
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
41
|
+
fastify.get('/domma-docs', handle); // exact - params['*'] === '' (home landing)
|
|
42
42
|
fastify.get('/domma-docs/*', handle); // sections + leaves
|
|
43
43
|
}
|
package/server/routes/public.js
CHANGED
|
@@ -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`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
159
|
-
// script, git pull
|
|
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.
|
|
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
|
|
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) {
|