domma-cms 0.43.1 → 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 +25 -25
- package/admin/js/templates/form-editor.html +5 -5
- package/admin/js/templates/login.html +5 -5
- package/admin/js/templates/menu-editor.html +10 -10
- 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 +1 -1
- 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 +5 -5
- package/bin/update.js +16 -16
- package/config/site.json +86 -86
- 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 +100 -6
- 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 +11 -11
- 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 +10 -10
- 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/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 +1 -1
- 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 +4 -4
- package/server/services/userRoles.js +7 -7
- package/server/services/users.js +5 -5
- 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
- package/config/navigation.json.bak +0 -31
|
@@ -4,18 +4,18 @@
|
|
|
4
4
|
* Submissions are stored exclusively in Collections (no dual-storage).
|
|
5
5
|
*
|
|
6
6
|
* Endpoints (prefix: /api):
|
|
7
|
-
* GET /forms
|
|
8
|
-
* POST /forms
|
|
9
|
-
* GET /forms/:slug
|
|
10
|
-
* GET /forms/:slug/public
|
|
11
|
-
* PUT /forms/:slug
|
|
12
|
-
* DELETE /forms/:slug
|
|
13
|
-
* GET /forms/:slug/submissions
|
|
14
|
-
* GET /forms/:slug/submissions/export
|
|
15
|
-
* DELETE /forms/:slug/submissions
|
|
16
|
-
* DELETE /forms/:slug/submissions/:id
|
|
17
|
-
* POST /forms/submit/:slug
|
|
18
|
-
* POST /forms/test-email
|
|
7
|
+
* GET /forms - admin: list all forms
|
|
8
|
+
* POST /forms - admin: create new form
|
|
9
|
+
* GET /forms/:slug - admin: get form definition
|
|
10
|
+
* GET /forms/:slug/public - public: get form (no actions block)
|
|
11
|
+
* PUT /forms/:slug - admin: update form definition
|
|
12
|
+
* DELETE /forms/:slug - admin: delete form
|
|
13
|
+
* GET /forms/:slug/submissions - admin: list submissions from collection
|
|
14
|
+
* GET /forms/:slug/submissions/export - admin: CSV export from collection
|
|
15
|
+
* DELETE /forms/:slug/submissions - admin: clear all submissions
|
|
16
|
+
* DELETE /forms/:slug/submissions/:id - admin: delete one submission
|
|
17
|
+
* POST /forms/submit/:slug - public: accept submission
|
|
18
|
+
* POST /forms/test-email - admin: send test email
|
|
19
19
|
*/
|
|
20
20
|
import {createRequire} from 'module';
|
|
21
21
|
import {fileURLToPath} from 'url';
|
|
@@ -40,7 +40,7 @@ import {v4 as uuidv4} from 'uuid';
|
|
|
40
40
|
/**
|
|
41
41
|
* Detect form↔collection mismatches at save time so admins know BEFORE
|
|
42
42
|
* users hit silent rejection. Returns an array of human-readable warning
|
|
43
|
-
* strings
|
|
43
|
+
* strings - empty when the form lines up cleanly with its target collection.
|
|
44
44
|
*
|
|
45
45
|
* Checks performed:
|
|
46
46
|
* - Target collection exists (otherwise every submission goes nowhere)
|
|
@@ -63,7 +63,7 @@ async function detectFormCollectionMismatch(form) {
|
|
|
63
63
|
return warnings;
|
|
64
64
|
}
|
|
65
65
|
if (!collection) {
|
|
66
|
-
warnings.push(`Target collection "${targetSlug}" does not exist
|
|
66
|
+
warnings.push(`Target collection "${targetSlug}" does not exist - submissions will fail. Either create the collection or change the form's target.`);
|
|
67
67
|
return warnings;
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -145,7 +145,7 @@ function validateUpload(fieldCfg, part, size) {
|
|
|
145
145
|
* `content/media/`, and replaced by a `{ url, name, size, mime }` reference.
|
|
146
146
|
*
|
|
147
147
|
* Throws on validation failure (caller handles HTTP 400). Files are streamed
|
|
148
|
-
* into memory up to fastify-multipart's configured limit
|
|
148
|
+
* into memory up to fastify-multipart's configured limit - fine for the
|
|
149
149
|
* resume-pdf / portrait-photo scale; large uploads should still use the admin
|
|
150
150
|
* media flow.
|
|
151
151
|
*
|
|
@@ -163,7 +163,7 @@ async function parseMultipartForm(request, form) {
|
|
|
163
163
|
if (part.type === 'file') {
|
|
164
164
|
const fieldCfg = fileFields.get(part.fieldname);
|
|
165
165
|
if (!fieldCfg) {
|
|
166
|
-
// Unknown file field
|
|
166
|
+
// Unknown file field - drain and skip rather than 400, so a
|
|
167
167
|
// form that gains a file input later doesn't reject older
|
|
168
168
|
// submissions in-flight from a stale page.
|
|
169
169
|
for await (const _ of part.file) { /* drain */ }
|
|
@@ -174,7 +174,7 @@ async function parseMultipartForm(request, form) {
|
|
|
174
174
|
const buffer = Buffer.concat(chunks);
|
|
175
175
|
const err = validateUpload(fieldCfg, part, buffer.length);
|
|
176
176
|
if (err) throw new Error(err);
|
|
177
|
-
if (buffer.length === 0) continue; // empty file input
|
|
177
|
+
if (buffer.length === 0) continue; // empty file input - skip
|
|
178
178
|
const saved = await saveMedia(safeFilename(part.filename), buffer);
|
|
179
179
|
out[part.fieldname] = {
|
|
180
180
|
url: saved.url,
|
|
@@ -183,7 +183,7 @@ async function parseMultipartForm(request, form) {
|
|
|
183
183
|
mime: part.mimetype
|
|
184
184
|
};
|
|
185
185
|
} else {
|
|
186
|
-
// Text field
|
|
186
|
+
// Text field - last value wins on duplicates (browser-standard).
|
|
187
187
|
out[part.fieldname] = part.value;
|
|
188
188
|
}
|
|
189
189
|
}
|
|
@@ -237,7 +237,7 @@ export async function formsRoutes(fastify) {
|
|
|
237
237
|
const canDelete = { preHandler: [authenticate, requirePermission('collections', 'delete')] };
|
|
238
238
|
|
|
239
239
|
// -----------------------------------------------------------------------
|
|
240
|
-
// GET /forms
|
|
240
|
+
// GET /forms - list all form definitions with submission counts
|
|
241
241
|
// -----------------------------------------------------------------------
|
|
242
242
|
fastify.get('/forms', canRead, async (request) => {
|
|
243
243
|
const {canSeeArtefact} = await import('../../services/projects.js');
|
|
@@ -245,7 +245,7 @@ export async function formsRoutes(fastify) {
|
|
|
245
245
|
const result = await Promise.all(forms.map(async form => {
|
|
246
246
|
let submissionCount = 0;
|
|
247
247
|
try {
|
|
248
|
-
// listEntries returns { entries, total, page, limit }
|
|
248
|
+
// listEntries returns { entries, total, page, limit } - total is the
|
|
249
249
|
// unpaginated count which is what we want for the badge here.
|
|
250
250
|
const r = await listEntries(form.slug, { limit: 0 });
|
|
251
251
|
submissionCount = r.total ?? (r.entries || []).length;
|
|
@@ -263,7 +263,7 @@ export async function formsRoutes(fastify) {
|
|
|
263
263
|
});
|
|
264
264
|
|
|
265
265
|
// -----------------------------------------------------------------------
|
|
266
|
-
// POST /forms
|
|
266
|
+
// POST /forms - create new form
|
|
267
267
|
// -----------------------------------------------------------------------
|
|
268
268
|
fastify.post('/forms', canCreate, async (request, reply) => {
|
|
269
269
|
const { title, slug: rawSlug } = request.body || {};
|
|
@@ -280,7 +280,7 @@ export async function formsRoutes(fastify) {
|
|
|
280
280
|
await readForm(slug);
|
|
281
281
|
return reply.status(409).send({ error: `A form with slug "${slug}" already exists.` });
|
|
282
282
|
} catch {
|
|
283
|
-
// Does not exist
|
|
283
|
+
// Does not exist - good
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
const now = new Date().toISOString();
|
|
@@ -333,7 +333,7 @@ export async function formsRoutes(fastify) {
|
|
|
333
333
|
});
|
|
334
334
|
|
|
335
335
|
// -----------------------------------------------------------------------
|
|
336
|
-
// GET /forms/:slug
|
|
336
|
+
// GET /forms/:slug - get single form (admin, includes actions)
|
|
337
337
|
// -----------------------------------------------------------------------
|
|
338
338
|
fastify.get('/forms/:slug', canRead, async (request, reply) => {
|
|
339
339
|
let form;
|
|
@@ -350,7 +350,7 @@ export async function formsRoutes(fastify) {
|
|
|
350
350
|
});
|
|
351
351
|
|
|
352
352
|
// -----------------------------------------------------------------------
|
|
353
|
-
// GET /forms/:slug/public
|
|
353
|
+
// GET /forms/:slug/public - get form for public rendering (no actions)
|
|
354
354
|
// -----------------------------------------------------------------------
|
|
355
355
|
fastify.get('/forms/:slug/public', async (request, reply) => {
|
|
356
356
|
try {
|
|
@@ -363,7 +363,7 @@ export async function formsRoutes(fastify) {
|
|
|
363
363
|
});
|
|
364
364
|
|
|
365
365
|
// -----------------------------------------------------------------------
|
|
366
|
-
// PUT /forms/:slug
|
|
366
|
+
// PUT /forms/:slug - update form definition
|
|
367
367
|
// -----------------------------------------------------------------------
|
|
368
368
|
fastify.put('/forms/:slug', canUpdate, async (request, reply) => {
|
|
369
369
|
const { slug } = request.params;
|
|
@@ -386,14 +386,14 @@ export async function formsRoutes(fastify) {
|
|
|
386
386
|
|
|
387
387
|
// Non-blocking warnings: check the form's fields against its target
|
|
388
388
|
// collection's required fields. Missing-required-on-form would cause
|
|
389
|
-
// every submission to fail server-side validation
|
|
389
|
+
// every submission to fail server-side validation - admin gets a
|
|
390
390
|
// heads-up here so they can fix it BEFORE users hit silent rejection.
|
|
391
391
|
const warnings = await detectFormCollectionMismatch(updated);
|
|
392
392
|
return warnings.length ? { ...updated, warnings } : updated;
|
|
393
393
|
});
|
|
394
394
|
|
|
395
395
|
// -----------------------------------------------------------------------
|
|
396
|
-
// DELETE /forms/:slug
|
|
396
|
+
// DELETE /forms/:slug - delete form
|
|
397
397
|
// -----------------------------------------------------------------------
|
|
398
398
|
fastify.delete('/forms/:slug', canDelete, async (request, reply) => {
|
|
399
399
|
const { slug } = request.params;
|
|
@@ -406,7 +406,7 @@ export async function formsRoutes(fastify) {
|
|
|
406
406
|
});
|
|
407
407
|
|
|
408
408
|
// -----------------------------------------------------------------------
|
|
409
|
-
// GET /forms/:slug/submissions
|
|
409
|
+
// GET /forms/:slug/submissions - list submissions from collection
|
|
410
410
|
// -----------------------------------------------------------------------
|
|
411
411
|
fastify.get('/forms/:slug/submissions', canRead, async (request, reply) => {
|
|
412
412
|
const { slug } = request.params;
|
|
@@ -420,19 +420,19 @@ export async function formsRoutes(fastify) {
|
|
|
420
420
|
const targetSlug = (collectionAction?.enabled && collectionAction.slug) ? collectionAction.slug : slug;
|
|
421
421
|
|
|
422
422
|
// A form whose backing collection has not been provisioned yet simply
|
|
423
|
-
// has no submissions
|
|
423
|
+
// has no submissions - return an empty list rather than a 404 so the
|
|
424
424
|
// admin view loads cleanly instead of erroring.
|
|
425
425
|
const collection = await getCollection(targetSlug);
|
|
426
426
|
if (!collection) return [];
|
|
427
427
|
|
|
428
|
-
// listEntries returns { entries, total, page, limit }
|
|
428
|
+
// listEntries returns { entries, total, page, limit } - unwrap to the
|
|
429
429
|
// array. Newest-first is the conventional display order.
|
|
430
430
|
const { entries } = await listEntries(targetSlug, { limit: 0 });
|
|
431
431
|
return [...entries].reverse();
|
|
432
432
|
});
|
|
433
433
|
|
|
434
434
|
// -----------------------------------------------------------------------
|
|
435
|
-
// GET /forms/:slug/submissions/export
|
|
435
|
+
// GET /forms/:slug/submissions/export - CSV download from collection
|
|
436
436
|
// -----------------------------------------------------------------------
|
|
437
437
|
fastify.get('/forms/:slug/submissions/export', canRead, async (request, reply) => {
|
|
438
438
|
const { slug } = request.params;
|
|
@@ -456,7 +456,7 @@ export async function formsRoutes(fastify) {
|
|
|
456
456
|
});
|
|
457
457
|
|
|
458
458
|
// -----------------------------------------------------------------------
|
|
459
|
-
// GET /forms/:slug/submissions/export/json
|
|
459
|
+
// GET /forms/:slug/submissions/export/json - JSON export from collection
|
|
460
460
|
// -----------------------------------------------------------------------
|
|
461
461
|
fastify.get('/forms/:slug/submissions/export/json', canRead, async (request, reply) => {
|
|
462
462
|
const { slug } = request.params;
|
|
@@ -479,7 +479,7 @@ export async function formsRoutes(fastify) {
|
|
|
479
479
|
});
|
|
480
480
|
|
|
481
481
|
// -----------------------------------------------------------------------
|
|
482
|
-
// DELETE /forms/:slug/submissions
|
|
482
|
+
// DELETE /forms/:slug/submissions - clear all submissions
|
|
483
483
|
// -----------------------------------------------------------------------
|
|
484
484
|
fastify.delete('/forms/:slug/submissions', canDelete, async (request, reply) => {
|
|
485
485
|
const { slug } = request.params;
|
|
@@ -492,7 +492,7 @@ export async function formsRoutes(fastify) {
|
|
|
492
492
|
});
|
|
493
493
|
|
|
494
494
|
// -----------------------------------------------------------------------
|
|
495
|
-
// DELETE /forms/:slug/submissions/:id
|
|
495
|
+
// DELETE /forms/:slug/submissions/:id - delete one submission
|
|
496
496
|
// -----------------------------------------------------------------------
|
|
497
497
|
fastify.delete('/forms/:slug/submissions/:id', canDelete, async (request, reply) => {
|
|
498
498
|
const { slug, id } = request.params;
|
|
@@ -505,12 +505,12 @@ export async function formsRoutes(fastify) {
|
|
|
505
505
|
});
|
|
506
506
|
|
|
507
507
|
/*
|
|
508
|
-
* POST /forms/submit/:slug
|
|
508
|
+
* POST /forms/submit/:slug - public form submission.
|
|
509
509
|
*
|
|
510
510
|
* Authentication is OPTIONAL: when a valid JWT is present the submitter
|
|
511
511
|
* is captured as the entry's `createdBy` and passed into any triggered
|
|
512
512
|
* action's template context (as `{{user.id}}`, `{{user.email}}`, etc.).
|
|
513
|
-
* Anonymous submissions still work
|
|
513
|
+
* Anonymous submissions still work - `createdBy` is null and actions
|
|
514
514
|
* receive an empty user. This is what makes the apply/bookmark patterns
|
|
515
515
|
* work without forcing every form to be auth-only:
|
|
516
516
|
*
|
|
@@ -527,7 +527,7 @@ export async function formsRoutes(fastify) {
|
|
|
527
527
|
return reply.status(404).send({ error: 'Form not found.' });
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
-
// Multipart parsing
|
|
530
|
+
// Multipart parsing - when the request is multipart (file uploads),
|
|
531
531
|
// we walk parts ourselves: text fields go into `body`, file parts are
|
|
532
532
|
// validated against the form's field config and saved to /content/media,
|
|
533
533
|
// then their { url, name, size, mime } reference object lands in `body`
|
|
@@ -542,7 +542,7 @@ export async function formsRoutes(fastify) {
|
|
|
542
542
|
}
|
|
543
543
|
const settings = form.settings || {};
|
|
544
544
|
|
|
545
|
-
// Best-effort auth
|
|
545
|
+
// Best-effort auth - never reject, just enrich.
|
|
546
546
|
let submittingUser = null;
|
|
547
547
|
try {
|
|
548
548
|
const decoded = await request.jwtVerify();
|
|
@@ -556,12 +556,12 @@ export async function formsRoutes(fastify) {
|
|
|
556
556
|
}
|
|
557
557
|
} catch { /* anonymous submission */ }
|
|
558
558
|
|
|
559
|
-
// Honeypot check
|
|
559
|
+
// Honeypot check - silently accept if filled (bot detected)
|
|
560
560
|
if (settings.honeypot && body._hp) {
|
|
561
561
|
return { ok: true, message: settings.successMessage, redirect: settings.successRedirect || null };
|
|
562
562
|
}
|
|
563
563
|
|
|
564
|
-
// Timing check
|
|
564
|
+
// Timing check - silently accept if submitted too fast (< 2 s, likely a bot)
|
|
565
565
|
if (settings.honeypot && body._t) {
|
|
566
566
|
const elapsed = Date.now() - Number(body._t);
|
|
567
567
|
if (!Number.isNaN(elapsed) && elapsed < 2000) {
|
|
@@ -614,7 +614,7 @@ export async function formsRoutes(fastify) {
|
|
|
614
614
|
return reply.status(429).send({ error: 'Too many submissions. Please try again later.' });
|
|
615
615
|
}
|
|
616
616
|
|
|
617
|
-
// Build submission data
|
|
617
|
+
// Build submission data - only include visible fields
|
|
618
618
|
const data = {};
|
|
619
619
|
for (const field of form.fields || []) {
|
|
620
620
|
if (field.type === 'page-break' || field.type === 'spacer') continue;
|
|
@@ -630,9 +630,9 @@ export async function formsRoutes(fastify) {
|
|
|
630
630
|
// CRITICAL: collection write failure is the difference between "your
|
|
631
631
|
// submission was saved" and "your submission disappeared into thin air".
|
|
632
632
|
// We MUST surface the error to the user rather than logging a warning
|
|
633
|
-
// and returning success
|
|
633
|
+
// and returning success - anything else is a silent-data-loss bug.
|
|
634
634
|
// (Email / webhook / action failures downstream are still treated as
|
|
635
|
-
// non-fatal
|
|
635
|
+
// non-fatal - they fire AFTER the entry is persisted, so even a partial
|
|
636
636
|
// delivery has the original submission safely stored.)
|
|
637
637
|
const collectionAction = form.actions?.collection;
|
|
638
638
|
const targetSlug = (collectionAction?.enabled && collectionAction.slug) ? collectionAction.slug : slug;
|
|
@@ -653,12 +653,12 @@ export async function formsRoutes(fastify) {
|
|
|
653
653
|
createdBy: submittingUser?.id || null
|
|
654
654
|
});
|
|
655
655
|
} else {
|
|
656
|
-
// Could not resolve or provision a target
|
|
656
|
+
// Could not resolve or provision a target - e.g. collection
|
|
657
657
|
// storage is explicitly disabled with no slug. Fail loudly
|
|
658
658
|
// rather than silently lose submissions.
|
|
659
659
|
fastify.log.warn(`[forms] Submission for "${slug}" had no target collection "${targetSlug}"`);
|
|
660
660
|
return reply.status(500).send({
|
|
661
|
-
error: `Submission not saved
|
|
661
|
+
error: `Submission not saved - target collection "${targetSlug}" does not exist. Please contact the site administrator.`
|
|
662
662
|
});
|
|
663
663
|
}
|
|
664
664
|
} catch (err) {
|
|
@@ -706,7 +706,7 @@ export async function formsRoutes(fastify) {
|
|
|
706
706
|
}
|
|
707
707
|
}
|
|
708
708
|
|
|
709
|
-
// CMS Action trigger
|
|
709
|
+
// CMS Action trigger - forwards the authenticated submitter so
|
|
710
710
|
// action steps can interpolate {{user.id}}, {{user.email}}, etc.
|
|
711
711
|
// (Anonymous submissions still trigger the action with user={}.)
|
|
712
712
|
const actionSlug = form.settings?.actionSlug;
|
|
@@ -735,7 +735,7 @@ export async function formsRoutes(fastify) {
|
|
|
735
735
|
});
|
|
736
736
|
|
|
737
737
|
// -----------------------------------------------------------------------
|
|
738
|
-
// POST /forms/test-email
|
|
738
|
+
// POST /forms/test-email - send a test email
|
|
739
739
|
// -----------------------------------------------------------------------
|
|
740
740
|
fastify.post('/forms/test-email', { preHandler: [authenticate, requireAdmin] }, async (request, reply) => {
|
|
741
741
|
const smtp = getConfig('site').smtp || {};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Menu locations API
|
|
3
3
|
*
|
|
4
|
-
* GET /api/menu-locations
|
|
5
|
-
* PUT /api/menu-locations
|
|
6
|
-
* GET /api/menu-locations/registry
|
|
4
|
+
* GET /api/menu-locations - current slot → menu-slug map
|
|
5
|
+
* PUT /api/menu-locations - replace the map (validates against registry + menus)
|
|
6
|
+
* GET /api/menu-locations/registry - list registered slots (core + plugin-contributed)
|
|
7
7
|
*
|
|
8
8
|
* Auth middlewares are accepted as options so tests can supply no-ops without
|
|
9
9
|
* needing to register `@fastify/jwt` + seed the roles cache. In production the
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Menus API
|
|
3
3
|
*
|
|
4
|
-
* GET /api/menus
|
|
5
|
-
* GET /api/menus/:slug
|
|
6
|
-
* POST /api/menus
|
|
7
|
-
* PUT /api/menus/:slug
|
|
8
|
-
* DELETE /api/menus/:slug
|
|
9
|
-
* POST /api/menus/:slug/duplicate
|
|
4
|
+
* GET /api/menus - list (metadata only)
|
|
5
|
+
* GET /api/menus/:slug - full menu
|
|
6
|
+
* POST /api/menus - create
|
|
7
|
+
* PUT /api/menus/:slug - replace
|
|
8
|
+
* DELETE /api/menus/:slug - delete (409 if mapped to a slot)
|
|
9
|
+
* POST /api/menus/:slug/duplicate - copy as <slug>-copy
|
|
10
10
|
*
|
|
11
11
|
* Auth middlewares are accepted as options so tests can supply no-ops without
|
|
12
12
|
* needing to register `@fastify/jwt` + seed the roles cache. In production the
|
|
@@ -93,7 +93,7 @@ export async function menusRoutes(fastify, opts = {}) {
|
|
|
93
93
|
const slot = Object.entries(map).find(([, slug]) => slug === request.params.slug)?.[0];
|
|
94
94
|
if (slot) {
|
|
95
95
|
return reply.status(409).send({
|
|
96
|
-
error: `Cannot delete
|
|
96
|
+
error: `Cannot delete - menu is mapped to slot "${slot}". Unmap it first.`
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
99
|
const ok = await deleteMenu(request.params.slug);
|
|
@@ -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') {
|