domma-cms 0.44.0 → 0.45.1
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
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
* One builder, three callers: the `[menu]` shortcode, the overlay menus the
|
|
5
5
|
* renderer injects into the page shell, and anything a plugin wants to render
|
|
6
6
|
* itself. Keeping it here means a menu looks the same however it reaches the
|
|
7
|
-
* page
|
|
7
|
+
* page - the shortcode and an overlay binding of the same menu produce byte-
|
|
8
8
|
* identical markup.
|
|
9
9
|
*
|
|
10
10
|
* Item filtering (visibility, `hidden`), depth capping and badge-count
|
|
11
|
-
* resolution happen before this
|
|
11
|
+
* resolution happen before this - callers hand in the items they want drawn.
|
|
12
12
|
*/
|
|
13
13
|
import {colourToCss, floatToCss} from './menus.js';
|
|
14
14
|
|
|
15
|
-
/** Attribute-safe escaping
|
|
15
|
+
/** Attribute-safe escaping - mirrors escapeAttr in markdown.js. */
|
|
16
16
|
function esc(str) {
|
|
17
17
|
return String(str ?? '')
|
|
18
18
|
.replace(/&/g, '&')
|
|
@@ -43,7 +43,7 @@ export function renderMenuItemsAsUl(items) {
|
|
|
43
43
|
const colourCss = it.colour ? colourToCss(it.colour) : '';
|
|
44
44
|
const styleAttr = colourCss ? ` style="color:${colourCss}"` : '';
|
|
45
45
|
|
|
46
|
-
// Badge
|
|
46
|
+
// Badge - static text, or a count stamped on by resolveMenuDecorations.
|
|
47
47
|
let badge = '';
|
|
48
48
|
if (it.badge && it.badge.text != null && it.badge.text !== '') {
|
|
49
49
|
const bg = it.badge.variant ? colourToCss(it.badge.variant) : '';
|
|
@@ -60,7 +60,7 @@ export function renderMenuItemsAsUl(items) {
|
|
|
60
60
|
* Build the full `<nav>` for a menu.
|
|
61
61
|
*
|
|
62
62
|
* @param {object} menu The menu record (slug, orientation, position, float)
|
|
63
|
-
* @param {Array} items Items to draw
|
|
63
|
+
* @param {Array} items Items to draw - already filtered/capped/decorated
|
|
64
64
|
* @param {object} [opts]
|
|
65
65
|
* @param {string} [opts.orientation] Override the menu's own orientation for this placement
|
|
66
66
|
* @param {string} [opts.variant] Adds `dm-menu--variant-<x>`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Menus auto-migration.
|
|
3
3
|
*
|
|
4
|
-
* Runs once on server boot. Idempotent
|
|
4
|
+
* Runs once on server boot. Idempotent - guarded by the existence of
|
|
5
5
|
* config/menu-locations.json. Converts:
|
|
6
6
|
* - config/navigation.json → config/menus/main.json + locations.navbar
|
|
7
7
|
* (brand moves into site.json.brand)
|
|
@@ -28,7 +28,7 @@ async function readJson(p) {
|
|
|
28
28
|
|
|
29
29
|
async function writeJson(p, data) {
|
|
30
30
|
await fs.writeFile(p, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
31
|
-
// Menu files written outside the menus service
|
|
31
|
+
// Menu files written outside the menus service - drop its cached index so
|
|
32
32
|
// binding resolution sees the migrated menus.
|
|
33
33
|
invalidateMenuIndex();
|
|
34
34
|
}
|
|
@@ -104,7 +104,7 @@ export async function runMigration(opts = {}) {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
// 4. Write the locations map (even if empty
|
|
107
|
+
// 4. Write the locations map (even if empty - its existence is the migration guard)
|
|
108
108
|
await writeJson(path.join(configDir, 'menu-locations.json'), locations);
|
|
109
109
|
|
|
110
110
|
return {migrated: true};
|
package/server/services/menus.js
CHANGED
|
@@ -18,13 +18,13 @@ import * as cache from './cache/index.js';
|
|
|
18
18
|
/**
|
|
19
19
|
* Bust the response-cache tags that menu changes affect. Public pages are
|
|
20
20
|
* cached with the `nav` and `site` tags (see routes/public.js), so any menu
|
|
21
|
-
* mutation
|
|
21
|
+
* mutation - content, behaviour/style overrides, or slot mapping - must clear
|
|
22
22
|
* them or the navbar/footer/[menu] output stays stale until TTL or restart.
|
|
23
23
|
* No-op when the cache is disabled (e.g. dev / boot-time seeding).
|
|
24
24
|
*/
|
|
25
25
|
async function invalidateNavCache() {
|
|
26
26
|
try { await cache.invalidateTags(['nav', 'site']); }
|
|
27
|
-
catch { /* cache not ready (boot)
|
|
27
|
+
catch { /* cache not ready (boot) - safe to ignore */ }
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -52,7 +52,7 @@ export function validateSlug(slug) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// ---------------------------------------------------------------------------
|
|
55
|
-
// Decoration colour convention
|
|
55
|
+
// Decoration colour convention - preset key → theme CSS var, or #rrggbb hex.
|
|
56
56
|
// A "colour value" is one of these preset keys or a 6-digit hex.
|
|
57
57
|
// Keep PRESET_COLOUR_VARS in sync with the copy in public/js/menu-decor.mjs.
|
|
58
58
|
// ---------------------------------------------------------------------------
|
|
@@ -82,7 +82,7 @@ export function colourToCss(value) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// ---------------------------------------------------------------------------
|
|
85
|
-
// Floating menus
|
|
85
|
+
// Floating menus - a menu with position "floating" detaches from the document
|
|
86
86
|
// flow and pins itself to a viewport corner or edge, the same way the
|
|
87
87
|
// breadcrumb trail does. The anchor/offset shape deliberately mirrors
|
|
88
88
|
// site.json's `breadcrumbs` config so the two read alike in the admin.
|
|
@@ -112,7 +112,7 @@ function floatNumber(value, fallback, min, max) {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
|
-
* Surface declarations for a floating panel
|
|
115
|
+
* Surface declarations for a floating panel - corner rounding, drop shadow and
|
|
116
116
|
* accent colour. Everything is optional: an omitted field falls through to the
|
|
117
117
|
* `.dm-menu-floating` defaults in site.css rather than being frozen inline.
|
|
118
118
|
*
|
|
@@ -135,7 +135,7 @@ function floatSurfaceCss(f) {
|
|
|
135
135
|
decls.push(`--dm-menu-accent:${css}`, `border-color:${css}`);
|
|
136
136
|
}
|
|
137
137
|
if (f.opacity != null && f.opacity !== '') {
|
|
138
|
-
// Panel translucency, not item translucency
|
|
138
|
+
// Panel translucency, not item translucency - the backdrop filter in
|
|
139
139
|
// site.css keeps the text readable over busy page content.
|
|
140
140
|
decls.push(`--dm-menu-panel-opacity:${floatNumber(f.opacity, 100, 20, 100) / 100}`);
|
|
141
141
|
}
|
|
@@ -176,7 +176,7 @@ export function floatToCss(menu) {
|
|
|
176
176
|
decls.push(`width:${floatNumber(f.width, 260, 120, 600)}px`);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
// A pinned panel must never grow past the viewport
|
|
179
|
+
// A pinned panel must never grow past the viewport - it scrolls instead.
|
|
180
180
|
decls.push(`max-height:calc(100vh - ${(anchor === 'L' || anchor === 'R') ? 32 : y * 2}px)`);
|
|
181
181
|
|
|
182
182
|
decls.push(...floatSurfaceCss(f));
|
|
@@ -185,19 +185,19 @@ export function floatToCss(menu) {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
// ---------------------------------------------------------------------------
|
|
188
|
-
// Orientation
|
|
188
|
+
// Orientation - how the menu flows where it renders. `horizontal` is the
|
|
189
189
|
// historic behaviour and stays the default for every menu without the field.
|
|
190
190
|
// `vertical` stacks the items into a column: a side rail for the navbar slot,
|
|
191
191
|
// a stacked list for `[menu]` and floating panels.
|
|
192
192
|
//
|
|
193
|
-
// `side` only means anything for a vertical menu in the navbar slot
|
|
193
|
+
// `side` only means anything for a vertical menu in the navbar slot - it picks
|
|
194
194
|
// the viewport edge the rail docks to.
|
|
195
195
|
// ---------------------------------------------------------------------------
|
|
196
196
|
export const ORIENTATIONS = new Set(['horizontal', 'vertical']);
|
|
197
197
|
export const SIDES = new Set(['left', 'right']);
|
|
198
198
|
|
|
199
199
|
// ---------------------------------------------------------------------------
|
|
200
|
-
// Bindings
|
|
200
|
+
// Bindings - a menu can claim a slot for a subset of the site instead of being
|
|
201
201
|
// mapped to it globally in menu-locations.json. `binding.slot` names the slot
|
|
202
202
|
// it takes over; `binding.projects` and `binding.pages` say where.
|
|
203
203
|
//
|
|
@@ -300,12 +300,12 @@ async function writeMenuFile(menu) {
|
|
|
300
300
|
await ensureMenusDir();
|
|
301
301
|
await fs.writeFile(menuPath(menu.slug), JSON.stringify(menu, null, 2) + '\n', 'utf8');
|
|
302
302
|
invalidateMenuIndex();
|
|
303
|
-
// Menu content / behaviour / style changed
|
|
303
|
+
// Menu content / behaviour / style changed - drop the cached public pages.
|
|
304
304
|
await invalidateNavCache();
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
/**
|
|
308
|
-
* List all menus. Returns metadata only (no items)
|
|
308
|
+
* List all menus. Returns metadata only (no items) - use getMenu(slug) for
|
|
309
309
|
* the full payload. Sorted alphabetically by slug.
|
|
310
310
|
*
|
|
311
311
|
* @returns {Promise<Array<{slug:string,name:string,description:string,itemsCount:number,meta:object}>>}
|
|
@@ -368,7 +368,7 @@ export async function createMenu(input) {
|
|
|
368
368
|
throw new Error(`Menu "${input.slug}" already exists`);
|
|
369
369
|
}
|
|
370
370
|
// Normalise the binding first so validation judges what will actually be
|
|
371
|
-
// written
|
|
371
|
+
// written - surrounding whitespace and duplicates are tidied, not rejected.
|
|
372
372
|
const binding = normaliseBinding(input.binding);
|
|
373
373
|
const errors = validateMenu({...input, binding});
|
|
374
374
|
if (errors.length) {
|
|
@@ -444,7 +444,7 @@ export async function updateMenu(slug, input) {
|
|
|
444
444
|
|
|
445
445
|
/**
|
|
446
446
|
* Delete a menu file. Returns true if removed, false if it didn't exist.
|
|
447
|
-
* Callers must check the locations map first
|
|
447
|
+
* Callers must check the locations map first - service does not.
|
|
448
448
|
*
|
|
449
449
|
* @param {string} slug
|
|
450
450
|
* @returns {Promise<boolean>}
|
|
@@ -531,30 +531,30 @@ export function validateMenu(menu) {
|
|
|
531
531
|
const errors = [];
|
|
532
532
|
if (!menu || typeof menu !== 'object') return ['Menu must be an object'];
|
|
533
533
|
if (!validateSlug(menu.slug)) {
|
|
534
|
-
errors.push(`Invalid slug "${menu.slug}"
|
|
534
|
+
errors.push(`Invalid slug "${menu.slug}" - lowercase alphanumeric + hyphen only`);
|
|
535
535
|
}
|
|
536
536
|
if (!menu.name || typeof menu.name !== 'string') {
|
|
537
537
|
errors.push('name is required');
|
|
538
538
|
}
|
|
539
539
|
if (menu.position != null && !POSITIONS.has(menu.position)) {
|
|
540
|
-
errors.push(`Invalid position "${menu.position}"
|
|
540
|
+
errors.push(`Invalid position "${menu.position}" - expected one of: ${[...POSITIONS].join(', ')}`);
|
|
541
541
|
}
|
|
542
542
|
if (menu.orientation != null && !ORIENTATIONS.has(menu.orientation)) {
|
|
543
|
-
errors.push(`Invalid orientation "${menu.orientation}"
|
|
543
|
+
errors.push(`Invalid orientation "${menu.orientation}" - expected one of: ${[...ORIENTATIONS].join(', ')}`);
|
|
544
544
|
}
|
|
545
545
|
if (menu.side != null && !SIDES.has(menu.side)) {
|
|
546
|
-
errors.push(`Invalid side "${menu.side}"
|
|
546
|
+
errors.push(`Invalid side "${menu.side}" - expected one of: ${[...SIDES].join(', ')}`);
|
|
547
547
|
}
|
|
548
548
|
if (menu.binding != null) {
|
|
549
549
|
if (typeof menu.binding !== 'object' || Array.isArray(menu.binding)) {
|
|
550
550
|
errors.push('binding must be an object');
|
|
551
551
|
} else {
|
|
552
552
|
const {slot, projects, pages} = menu.binding;
|
|
553
|
-
// A binding with no slot is inert rather than invalid
|
|
554
|
-
// sends an empty block when the feature is switched off
|
|
553
|
+
// A binding with no slot is inert rather than invalid - the admin
|
|
554
|
+
// sends an empty block when the feature is switched off - but a
|
|
555
555
|
// slot that IS present has to be a usable slot name.
|
|
556
556
|
if (slot != null && slot !== '' && !validateSlug(slot)) {
|
|
557
|
-
errors.push(`binding.slot "${slot}"
|
|
557
|
+
errors.push(`binding.slot "${slot}" - lowercase alphanumeric + hyphen only`);
|
|
558
558
|
}
|
|
559
559
|
for (const [key, list] of [['projects', projects], ['pages', pages]]) {
|
|
560
560
|
if (list == null) continue;
|
|
@@ -597,7 +597,7 @@ export function validateMenu(menu) {
|
|
|
597
597
|
errors.push('items must be an array');
|
|
598
598
|
} else {
|
|
599
599
|
walkItems(menu.items, (item, path) => {
|
|
600
|
-
// Separator items are purely visual dividers
|
|
600
|
+
// Separator items are purely visual dividers - no text/url required.
|
|
601
601
|
// Renderers translate them into <hr> / structural breaks.
|
|
602
602
|
if (item && item.type === 'separator') return;
|
|
603
603
|
|
|
@@ -605,7 +605,7 @@ export function validateMenu(menu) {
|
|
|
605
605
|
errors.push(`Item ${path}: text is required`);
|
|
606
606
|
}
|
|
607
607
|
// url is only required for LEAF items (no children). Folder items
|
|
608
|
-
//
|
|
608
|
+
// - those with their own `items` array - are pure groupings and
|
|
609
609
|
// don't need a navigation target. The admin sidebar uses this
|
|
610
610
|
// shape extensively (Content / Data / System headings etc.).
|
|
611
611
|
const hasChildren = Array.isArray(item.items) && item.items.length > 0;
|
|
@@ -671,7 +671,7 @@ function walkItems(items, fn, parentPath = '') {
|
|
|
671
671
|
}
|
|
672
672
|
|
|
673
673
|
// ---------------------------------------------------------------------------
|
|
674
|
-
// Locations registry
|
|
674
|
+
// Locations registry - module-level Map, seeded by core, extended by plugins
|
|
675
675
|
// ---------------------------------------------------------------------------
|
|
676
676
|
|
|
677
677
|
const _locationRegistry = new Map(); // slot -> { label, description, maxDepth, source }
|
|
@@ -687,7 +687,7 @@ const _locationRegistry = new Map(); // slot -> { label, description, maxDepth,
|
|
|
687
687
|
export function registerLocation(slot, def) {
|
|
688
688
|
if (!validateSlug(slot)) throw new Error(`Invalid slot name: ${slot}`);
|
|
689
689
|
if (_locationRegistry.has(slot)) {
|
|
690
|
-
console.warn(`[menus] Slot "${slot}" re-registered
|
|
690
|
+
console.warn(`[menus] Slot "${slot}" re-registered - last write wins`);
|
|
691
691
|
}
|
|
692
692
|
_locationRegistry.set(slot, {
|
|
693
693
|
label: def?.label || slot,
|
|
@@ -704,7 +704,7 @@ export function getLocationRegistry() {
|
|
|
704
704
|
return [..._locationRegistry.entries()].map(([slot, def]) => ({slot, ...def}));
|
|
705
705
|
}
|
|
706
706
|
|
|
707
|
-
// Core slots
|
|
707
|
+
// Core slots - registered at module load. Plugins register theirs from
|
|
708
708
|
// their plugin.js setup function via the registerMenuLocation hook (Task 7).
|
|
709
709
|
registerLocation('navbar', {label: 'Navbar', description: 'Site navbar', source: 'core'});
|
|
710
710
|
registerLocation('footer-primary', {label: 'Footer primary', description: 'Primary footer column', source: 'core', maxDepth: 1});
|
|
@@ -713,7 +713,7 @@ registerLocation('admin-sidebar', {label: 'Admin sidebar', description: 'The l
|
|
|
713
713
|
// The overlay slot is the "anywhere" placement: menus bound to it render on
|
|
714
714
|
// matching pages positioned by their own `float` config, with no navbar, no
|
|
715
715
|
// footer and no shortcode involved. Unlike the other slots it is not
|
|
716
|
-
// one-menu-wins
|
|
716
|
+
// one-menu-wins - a page can carry several overlays at once.
|
|
717
717
|
registerLocation('overlay', {label: 'Page overlay', description: 'Floating menus placed on matching pages by their binding', source: 'core', maxDepth: null});
|
|
718
718
|
|
|
719
719
|
// ---------------------------------------------------------------------------
|
|
@@ -768,8 +768,8 @@ export async function setLocations(map) {
|
|
|
768
768
|
|
|
769
769
|
/**
|
|
770
770
|
* Every menu on disk, cached in memory. Bound-menu resolution has to consider
|
|
771
|
-
* all of them on each render, and the alternative
|
|
772
|
-
* three times per uncached page
|
|
771
|
+
* all of them on each render, and the alternative - re-reading the directory
|
|
772
|
+
* three times per uncached page - is wasteful for a set of files that only
|
|
773
773
|
* changes when an admin saves a menu.
|
|
774
774
|
*
|
|
775
775
|
* Every write path in this module invalidates the cache; anything writing menu
|
|
@@ -777,7 +777,7 @@ export async function setLocations(map) {
|
|
|
777
777
|
*/
|
|
778
778
|
let _menuIndex = null;
|
|
779
779
|
|
|
780
|
-
/** Drop the cached menu index
|
|
780
|
+
/** Drop the cached menu index - call after writing menu files out-of-band. */
|
|
781
781
|
export function invalidateMenuIndex() {
|
|
782
782
|
_menuIndex = null;
|
|
783
783
|
}
|
|
@@ -880,7 +880,7 @@ export async function resolveBoundMenu(slot, ctx = {}) {
|
|
|
880
880
|
*
|
|
881
881
|
* A menu bound to the current page or project (see `resolveBoundMenu`) takes
|
|
882
882
|
* the slot over; otherwise the `menu-locations.json` mapping applies. Callers
|
|
883
|
-
* that know the page they are rendering should pass `ctx`
|
|
883
|
+
* that know the page they are rendering should pass `ctx` - without it only the
|
|
884
884
|
* global mapping is considered, which is the pre-binding behaviour.
|
|
885
885
|
*
|
|
886
886
|
* Note: `user` may be null (anonymous). `user.role` and
|
|
@@ -242,7 +242,7 @@ const _pluginContributed = [];
|
|
|
242
242
|
|
|
243
243
|
/**
|
|
244
244
|
* Register a resource contributed by a plugin.
|
|
245
|
-
* Idempotent
|
|
245
|
+
* Idempotent - if the key already exists in the effective registry it is skipped.
|
|
246
246
|
*
|
|
247
247
|
* @param {{key:string,label:string,description?:string,icon?:string,group?:string,actions?:{key:string,label:string,description?:string}[]}} resource
|
|
248
248
|
* @param {string} plugin - Plugin name that owns this resource
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Plugin Files Service
|
|
3
3
|
* Read/write access to a plugin's source files for the in-admin code editor.
|
|
4
4
|
*
|
|
5
|
-
* Every operation is confined to plugins/<name>/
|
|
5
|
+
* Every operation is confined to plugins/<name>/ - names must be discovered
|
|
6
6
|
* plugin directories (underscore-prefixed dirs like _template/_lib are never
|
|
7
7
|
* valid names), paths are normalised and containment-checked after symlink
|
|
8
8
|
* resolution, and only known text extensions are editable.
|
|
@@ -20,7 +20,7 @@ const NAME_PATTERN = /^[a-z][a-z0-9-]{1,39}$/;
|
|
|
20
20
|
// Editable/viewable file types. Everything else is invisible to the editor.
|
|
21
21
|
const TEXT_EXTENSIONS = new Set(['.js', '.json', '.md', '.html', '.css', '.txt', '.svg']);
|
|
22
22
|
|
|
23
|
-
// Never deletable
|
|
23
|
+
// Never deletable - removing any of these breaks discovery of the plugin.
|
|
24
24
|
const REQUIRED_FILES = new Set(['plugin.json', 'plugin.js', 'config.js']);
|
|
25
25
|
|
|
26
26
|
// Directories skipped when listing.
|
|
@@ -86,7 +86,7 @@ async function resolveFilePath(pluginDir, relPath) {
|
|
|
86
86
|
}
|
|
87
87
|
} catch (err) {
|
|
88
88
|
if (err.statusCode) throw err;
|
|
89
|
-
// ENOENT
|
|
89
|
+
// ENOENT - new file, fine
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
return abs;
|
|
@@ -184,7 +184,7 @@ export async function writePluginFile(name, relPath, content) {
|
|
|
184
184
|
await fs.writeFile(abs, content, 'utf8');
|
|
185
185
|
|
|
186
186
|
const rel = path.relative(pluginDir, abs).split(path.sep).join('/');
|
|
187
|
-
// admin/** is fetched by the SPA at runtime
|
|
187
|
+
// admin/** is fetched by the SPA at runtime - a tab refresh picks it up.
|
|
188
188
|
// Everything else (plugin.js, config.js, plugin.json, public/ snippets)
|
|
189
189
|
// is read or imported server-side and needs a restart.
|
|
190
190
|
const restartRequired = !rel.startsWith('admin/');
|
|
@@ -204,7 +204,7 @@ export async function deletePluginFile(name, relPath) {
|
|
|
204
204
|
const rel = path.relative(pluginDir, abs).split(path.sep).join('/');
|
|
205
205
|
|
|
206
206
|
if (REQUIRED_FILES.has(rel)) {
|
|
207
|
-
throw fail(400, `${rel} is required
|
|
207
|
+
throw fail(400, `${rel} is required - deleting it would break the plugin.`);
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
try {
|
|
@@ -27,7 +27,7 @@ const CONFIG_PATH = path.join(ROOT, 'config', 'plugins.json');
|
|
|
27
27
|
const AUDIT_LOG = path.join(ROOT, 'content', 'instances_log.json');
|
|
28
28
|
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
30
|
-
// Trusted public keys
|
|
30
|
+
// Trusted public keys - rotate by adding new keyId + bumping CMS version
|
|
31
31
|
// Private key counterpart: domma-cms-manager/config/keys/plugin-signing-2026.pem
|
|
32
32
|
// ---------------------------------------------------------------------------
|
|
33
33
|
|
|
@@ -109,12 +109,12 @@ async function extractTar(tarBuffer, slug, destDir) {
|
|
|
109
109
|
zeroBlockCount = 0;
|
|
110
110
|
offset += 512;
|
|
111
111
|
|
|
112
|
-
// Filename: bytes 0
|
|
112
|
+
// Filename: bytes 0-99, null-terminated
|
|
113
113
|
const nameEnd = header.indexOf(0, 0);
|
|
114
114
|
const name = header.slice(0, nameEnd === -1 ? 100 : Math.min(nameEnd, 100)).toString('utf8');
|
|
115
115
|
if (!name) break;
|
|
116
116
|
|
|
117
|
-
// File size: bytes 124
|
|
117
|
+
// File size: bytes 124-135, octal ASCII
|
|
118
118
|
const sizeStr = header.slice(124, 136).toString('utf8').replace(/\0/g, '').trim();
|
|
119
119
|
const size = parseInt(sizeStr, 8) || 0;
|
|
120
120
|
|
|
@@ -177,36 +177,36 @@ export async function installPlugin(slug, { tarball, signature, publicKeyId }) {
|
|
|
177
177
|
throw new Error(`Invalid plugin slug: ${slug}`);
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
// Step 1
|
|
180
|
+
// Step 1 - Verify signature
|
|
181
181
|
const pubKeyPem = TRUSTED_KEYS[publicKeyId];
|
|
182
182
|
if (!pubKeyPem) {
|
|
183
183
|
throw new Error(`Unknown public key: ${publicKeyId}`);
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
if (pubKeyPem.includes('PLACEHOLDER')) {
|
|
187
|
-
throw new Error(`pluginInstaller: TRUSTED_KEYS['${publicKeyId}'] is a placeholder
|
|
187
|
+
throw new Error(`pluginInstaller: TRUSTED_KEYS['${publicKeyId}'] is a placeholder - not usable for verification`);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
const signatureBuffer = Buffer.from(signature, 'base64');
|
|
191
191
|
const valid = crypto.verify('sha256', tarball, pubKeyPem, signatureBuffer);
|
|
192
192
|
if (!valid) {
|
|
193
|
-
throw new Error('Plugin signature invalid
|
|
193
|
+
throw new Error('Plugin signature invalid - refusing to install');
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
// Step 2
|
|
196
|
+
// Step 2 - Reject if already installed
|
|
197
197
|
const destDir = path.join(PLUGINS_DIR, slug);
|
|
198
198
|
let alreadyExists = false;
|
|
199
199
|
try {
|
|
200
200
|
await fs.access(destDir);
|
|
201
201
|
alreadyExists = true;
|
|
202
202
|
} catch {
|
|
203
|
-
// Not present
|
|
203
|
+
// Not present - proceed
|
|
204
204
|
}
|
|
205
205
|
if (alreadyExists) {
|
|
206
206
|
throw new Error(`Plugin already installed: ${slug}`);
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
// Step 3
|
|
209
|
+
// Step 3 - Decompress + extract
|
|
210
210
|
await fs.mkdir(destDir, { recursive: true });
|
|
211
211
|
try {
|
|
212
212
|
const tarBuffer = zlib.gunzipSync(tarball);
|
|
@@ -217,7 +217,7 @@ export async function installPlugin(slug, { tarball, signature, publicKeyId }) {
|
|
|
217
217
|
throw err;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
// Step 4
|
|
220
|
+
// Step 4 - Validate manifest
|
|
221
221
|
const manifests = await discoverPlugins();
|
|
222
222
|
const manifest = manifests.find(m => m.name === slug);
|
|
223
223
|
if (!manifest) {
|
|
@@ -225,14 +225,14 @@ export async function installPlugin(slug, { tarball, signature, publicKeyId }) {
|
|
|
225
225
|
throw new Error(`Plugin manifest validation failed after extraction: ${slug}`);
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
// Step 5
|
|
228
|
+
// Step 5 - Update config/plugins.json
|
|
229
229
|
const cfg = await readPluginsConfig();
|
|
230
230
|
if (!cfg[slug]) {
|
|
231
231
|
cfg[slug] = { enabled: false, settings: {} };
|
|
232
232
|
await writePluginsConfig(cfg);
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
// Step 6
|
|
235
|
+
// Step 6 - Run onEnable lifecycle hook (warn on failure, do not throw)
|
|
236
236
|
try {
|
|
237
237
|
const pluginJsPath = path.join(destDir, 'plugin.js');
|
|
238
238
|
const mod = await import(pluginJsPath);
|
|
@@ -243,7 +243,7 @@ export async function installPlugin(slug, { tarball, signature, publicKeyId }) {
|
|
|
243
243
|
console.warn(`[pluginInstaller] onEnable hook for "${slug}" failed: ${err.message}`);
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
-
// Step 7
|
|
246
|
+
// Step 7 - Audit log
|
|
247
247
|
await writeAuditLog({
|
|
248
248
|
action: 'install',
|
|
249
249
|
slug,
|
|
@@ -271,7 +271,7 @@ export async function uninstallPlugin(slug) {
|
|
|
271
271
|
|
|
272
272
|
const destDir = path.join(PLUGINS_DIR, slug);
|
|
273
273
|
|
|
274
|
-
// Step 1
|
|
274
|
+
// Step 1 - Run onDisable lifecycle hook (warn on failure, do not throw)
|
|
275
275
|
try {
|
|
276
276
|
const pluginJsPath = path.join(destDir, 'plugin.js');
|
|
277
277
|
const mod = await import(pluginJsPath);
|
|
@@ -282,17 +282,17 @@ export async function uninstallPlugin(slug) {
|
|
|
282
282
|
console.warn(`[pluginInstaller] onDisable hook for "${slug}" failed: ${err.message}`);
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
// Step 2
|
|
285
|
+
// Step 2 - Remove plugin directory
|
|
286
286
|
await fs.rm(destDir, { recursive: true, force: true });
|
|
287
287
|
|
|
288
|
-
// Step 3
|
|
288
|
+
// Step 3 - Remove from config/plugins.json
|
|
289
289
|
const cfg = await readPluginsConfig();
|
|
290
290
|
if (slug in cfg) {
|
|
291
291
|
delete cfg[slug];
|
|
292
292
|
await writePluginsConfig(cfg);
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
// Step 4
|
|
295
|
+
// Step 4 - Audit log
|
|
296
296
|
await writeAuditLog({
|
|
297
297
|
action: 'uninstall',
|
|
298
298
|
slug,
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* Discovers, loads, and manages CMS plugins.
|
|
4
4
|
*
|
|
5
5
|
* Plugin structure:
|
|
6
|
-
* plugins/{name}/plugin.json
|
|
7
|
-
* plugins/{name}/plugin.js
|
|
8
|
-
* plugins/{name}/config.js
|
|
9
|
-
* plugins/{name}/admin/
|
|
10
|
-
* plugins/{name}/public/
|
|
11
|
-
* plugins/{name}/data/
|
|
6
|
+
* plugins/{name}/plugin.json - manifest (required)
|
|
7
|
+
* plugins/{name}/plugin.js - Fastify plugin, default export (required)
|
|
8
|
+
* plugins/{name}/config.js - default settings, default export (required)
|
|
9
|
+
* plugins/{name}/admin/ - optional admin views/templates
|
|
10
|
+
* plugins/{name}/public/ - optional injection HTML snippets
|
|
11
|
+
* plugins/{name}/data/ - optional plugin-specific data store
|
|
12
12
|
*/
|
|
13
13
|
import fs from 'fs/promises';
|
|
14
14
|
import path from 'path';
|
|
@@ -61,7 +61,7 @@ export function getPluginLoadFailures() {
|
|
|
61
61
|
const _loadedPlugins = {};
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* Core plugins
|
|
64
|
+
* Core plugins - always loaded regardless of plugins.json enabled state.
|
|
65
65
|
* These are considered first-class CMS features, not optional add-ons.
|
|
66
66
|
*/
|
|
67
67
|
const CORE_PLUGINS = new Set(['analytics']);
|
|
@@ -101,7 +101,7 @@ export async function discoverPlugins() {
|
|
|
101
101
|
for (const field of REQUIRED_MANIFEST_FIELDS) {
|
|
102
102
|
if (!manifest[field]) {
|
|
103
103
|
const reason = `missing required field "${field}" in plugin.json`;
|
|
104
|
-
console.warn(`Plugin "${dir}": ${reason}
|
|
104
|
+
console.warn(`Plugin "${dir}": ${reason} - skipping`);
|
|
105
105
|
recordPluginLoadFailure(dir, reason);
|
|
106
106
|
valid = false;
|
|
107
107
|
break;
|
|
@@ -117,7 +117,7 @@ export async function discoverPlugins() {
|
|
|
117
117
|
await fs.access(pluginJsPath);
|
|
118
118
|
} catch {
|
|
119
119
|
const reason = 'missing required file plugin.js';
|
|
120
|
-
console.warn(`Plugin "${dir}": ${reason}
|
|
120
|
+
console.warn(`Plugin "${dir}": ${reason} - skipping`);
|
|
121
121
|
recordPluginLoadFailure(dir, reason);
|
|
122
122
|
continue;
|
|
123
123
|
}
|
|
@@ -126,7 +126,7 @@ export async function discoverPlugins() {
|
|
|
126
126
|
await fs.access(configJsPath);
|
|
127
127
|
} catch {
|
|
128
128
|
const reason = 'missing required file config.js';
|
|
129
|
-
console.warn(`Plugin "${dir}": ${reason}
|
|
129
|
+
console.warn(`Plugin "${dir}": ${reason} - skipping`);
|
|
130
130
|
recordPluginLoadFailure(dir, reason);
|
|
131
131
|
continue;
|
|
132
132
|
}
|
|
@@ -153,7 +153,7 @@ async function loadConfigDefaults(name) {
|
|
|
153
153
|
const {default: exported} = await import(configJsPath);
|
|
154
154
|
defaults = exported || {};
|
|
155
155
|
} catch (err) {
|
|
156
|
-
const reason = `failed to load config.js
|
|
156
|
+
const reason = `failed to load config.js - ${err.message}`;
|
|
157
157
|
console.warn(`Plugin "${name}": ${reason}`);
|
|
158
158
|
recordPluginLoadFailure(name, reason);
|
|
159
159
|
}
|
|
@@ -233,7 +233,7 @@ export async function registerPlugins(fastify) {
|
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
// Ensure plugin collections exist (idempotent
|
|
236
|
+
// Ensure plugin collections exist (idempotent - skips if already present)
|
|
237
237
|
await setupPlugin(manifest.name, {collections: {getCollection, createCollection}})
|
|
238
238
|
.catch(err => fastify.log.warn(`[plugins] Collection setup for "${manifest.name}" failed: ${err.message}`));
|
|
239
239
|
|
|
@@ -244,7 +244,7 @@ export async function registerPlugins(fastify) {
|
|
|
244
244
|
await fs.access(publicEntryPath);
|
|
245
245
|
_loadedPlugins[manifest.name].publicEntry = publicEntryPath;
|
|
246
246
|
} catch {
|
|
247
|
-
// no public entry
|
|
247
|
+
// no public entry - that's fine
|
|
248
248
|
}
|
|
249
249
|
} catch (err) {
|
|
250
250
|
const reason = `server failed to load: ${err.message}`;
|
|
@@ -306,7 +306,7 @@ export async function getInjectionSnippets() {
|
|
|
306
306
|
if (!manifest.inject[key]) continue;
|
|
307
307
|
const filePath = path.resolve(PLUGINS_DIR, manifest.name, manifest.inject[key]);
|
|
308
308
|
if (!filePath.startsWith(pluginRoot + path.sep)) {
|
|
309
|
-
console.warn(`Plugin "${manifest.name}": inject.${key} path escapes plugin directory
|
|
309
|
+
console.warn(`Plugin "${manifest.name}": inject.${key} path escapes plugin directory - skipping`);
|
|
310
310
|
continue;
|
|
311
311
|
}
|
|
312
312
|
try {
|
|
@@ -314,7 +314,7 @@ export async function getInjectionSnippets() {
|
|
|
314
314
|
if (target === 'head') head += content + '\n';
|
|
315
315
|
if (target === 'headLate') headLate += content + '\n';
|
|
316
316
|
if (target === 'bodyEnd') bodyEnd += content + '\n';
|
|
317
|
-
} catch { /* snippet not found
|
|
317
|
+
} catch { /* snippet not found - skip */
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
320
|
}
|
|
@@ -431,7 +431,7 @@ export async function setupPlugin(pluginName, services) {
|
|
|
431
431
|
}
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
-
// Roles
|
|
434
|
+
// Roles - read plugins/<name>/roles/*.json, register resources + persist roles
|
|
435
435
|
if (services.roles) {
|
|
436
436
|
const rolesDir = path.join(pluginDir, 'roles');
|
|
437
437
|
let roleFiles;
|
|
@@ -511,7 +511,7 @@ export async function teardownPlugin(pluginName, services, fastify) {
|
|
|
511
511
|
}
|
|
512
512
|
}
|
|
513
513
|
|
|
514
|
-
// Roles
|
|
514
|
+
// Roles - remove plugin-contributed roles and unregister their resources
|
|
515
515
|
if (services.roles) {
|
|
516
516
|
try {
|
|
517
517
|
await services.roles.removeRolesByPlugin(pluginName);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Preset Collections
|
|
3
3
|
* Built-in collections whose schemas are defined in code and overwritten on boot.
|
|
4
|
-
* Data files are never touched
|
|
4
|
+
* Data files are never touched - only schema.json is reset each startup.
|
|
5
5
|
*/
|
|
6
6
|
import fs from 'fs/promises';
|
|
7
7
|
import path from 'path';
|
|
@@ -10,7 +10,7 @@ import {config} from '../config.js';
|
|
|
10
10
|
const COLLECTIONS_DIR = path.resolve(config.content.collectionsDir);
|
|
11
11
|
|
|
12
12
|
// contacts, enquiries, feedback, notes, categories, posts, comments, to-do
|
|
13
|
-
// have been removed
|
|
13
|
+
// have been removed - they belong to plugins and are seeded on plugin enable.
|
|
14
14
|
|
|
15
15
|
const PRESETS = [
|
|
16
16
|
{
|