domma-cms 0.44.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +53 -53
- package/README.md +27 -27
- package/admin/css/admin.css +1 -1
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/index.html +2 -2
- package/admin/js/app.js +1 -1
- package/admin/js/lib/card-builder.js +2 -2
- package/admin/js/lib/crud-tutorial.js +1 -1
- package/admin/js/lib/effect-defs.js +1 -1
- package/admin/js/lib/effects-builder.js +1 -1
- package/admin/js/lib/image-editor.js +1 -1
- package/admin/js/lib/markdown-toolbar.js +2 -2
- package/admin/js/lib/project-context.js +1 -1
- package/admin/js/lib/project-quick-create.js +1 -1
- package/admin/js/lib/scribe-composer.js +1 -1
- package/admin/js/lib/server-restart.js +1 -1
- package/admin/js/lib/shortcode-context-menu.js +2 -2
- package/admin/js/lib/simple-editor.js +1 -1
- package/admin/js/lib/themes.js +1 -1
- package/admin/js/lib/timeline-builder.js +1 -1
- package/admin/js/templates/action-editor.html +8 -8
- package/admin/js/templates/api-endpoint-editor.html +7 -7
- package/admin/js/templates/api-endpoints.html +1 -1
- package/admin/js/templates/api-reference.html +1 -1
- package/admin/js/templates/block-editor.html +6 -6
- package/admin/js/templates/collection-editor.html +1 -1
- package/admin/js/templates/component-editor.html +2 -2
- package/admin/js/templates/dashboard/cache.html +3 -3
- package/admin/js/templates/dashboard/journeys.html +3 -3
- package/admin/js/templates/dashboard/kpi-strip.html +3 -3
- package/admin/js/templates/dashboard/top-pages.html +1 -1
- package/admin/js/templates/dashboard/traffic-chart.html +1 -1
- package/admin/js/templates/docs/api-actions.html +1 -1
- package/admin/js/templates/docs/api-collections.html +4 -4
- package/admin/js/templates/docs/api-pages.html +2 -2
- package/admin/js/templates/docs/api-plugins.html +1 -1
- package/admin/js/templates/docs/api-settings.html +2 -2
- package/admin/js/templates/docs/api-users.html +1 -1
- package/admin/js/templates/docs/components-howto.html +4 -4
- package/admin/js/templates/docs/components-reference.html +21 -21
- package/admin/js/templates/docs/components-rules.html +14 -14
- package/admin/js/templates/docs/components-walkthrough.html +13 -13
- package/admin/js/templates/docs/tutorial-crud.html +49 -49
- package/admin/js/templates/docs/tutorial-forms.html +4 -4
- package/admin/js/templates/docs/tutorial-plugin.html +10 -10
- package/admin/js/templates/docs/usage-actions.html +6 -6
- package/admin/js/templates/docs/usage-cta-shortcode.html +9 -9
- package/admin/js/templates/docs/usage-dconfig.html +4 -4
- package/admin/js/templates/docs/usage-navigation.html +1 -1
- package/admin/js/templates/docs/usage-pages.html +1 -1
- package/admin/js/templates/docs/usage-shortcodes.html +24 -24
- package/admin/js/templates/docs/usage-users-roles.html +3 -3
- package/admin/js/templates/docs/usage-views.html +4 -4
- package/admin/js/templates/documentation.html +1 -1
- package/admin/js/templates/effects.html +752 -752
- package/admin/js/templates/form-editor.html +5 -5
- package/admin/js/templates/forms.html +17 -17
- package/admin/js/templates/login.html +5 -5
- package/admin/js/templates/menu-editor.html +10 -10
- package/admin/js/templates/my-profile.html +17 -17
- package/admin/js/templates/navigation.html +11 -11
- package/admin/js/templates/notifications.html +1 -1
- package/admin/js/templates/page-editor.html +4 -4
- package/admin/js/templates/pro-docs.html +14 -14
- package/admin/js/templates/role-editor.html +70 -70
- package/admin/js/templates/roles.html +10 -10
- package/admin/js/templates/settings.html +10 -10
- package/admin/js/templates/tutorials.html +3 -3
- package/admin/js/templates/view-editor.html +15 -15
- package/admin/js/views/action-editor.js +1 -1
- package/admin/js/views/actions-list.js +1 -1
- package/admin/js/views/api-endpoint-editor.js +2 -2
- package/admin/js/views/api-endpoints.js +2 -2
- package/admin/js/views/api-tokens.js +3 -3
- package/admin/js/views/blocks.js +2 -2
- package/admin/js/views/collection-editor.js +1 -1
- package/admin/js/views/collection-entries.js +1 -1
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/component-editor.js +1 -1
- package/admin/js/views/components.js +4 -4
- package/admin/js/views/dashboard/widgets/cache.js +1 -1
- package/admin/js/views/dashboard/widgets/journeys.js +1 -1
- package/admin/js/views/dashboard/widgets/kpi-strip.js +1 -1
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/form-submissions.js +1 -1
- package/admin/js/views/forms.js +1 -1
- package/admin/js/views/layouts.js +1 -1
- package/admin/js/views/login.js +2 -2
- package/admin/js/views/menu-editor.js +3 -3
- package/admin/js/views/menu-locations.js +1 -1
- package/admin/js/views/menus.js +3 -3
- package/admin/js/views/navigation.js +8 -8
- package/admin/js/views/page-editor.js +40 -40
- package/admin/js/views/pages.js +3 -3
- package/admin/js/views/plugin-code.js +2 -2
- package/admin/js/views/plugins.js +1 -1
- package/admin/js/views/project-settings.js +1 -1
- package/admin/js/views/projects.js +1 -1
- package/admin/js/views/roles.js +1 -1
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +3 -3
- package/admin/js/views/view-editor.js +1 -1
- package/admin/js/views/view-preview.js +1 -1
- package/admin/js/views/views-list.js +1 -1
- package/bin/cli.js +8 -8
- package/bin/lib/config-merge.js +44 -44
- package/bin/update.js +16 -16
- package/config/plugins.json +1 -1
- package/package.json +2 -2
- package/plugins/_template/admin/views/index.js +1 -1
- package/plugins/_template/config.js +1 -1
- package/plugins/_template/plugin.js +1 -1
- package/plugins/analytics/admin/templates/analytics.html +3 -3
- package/plugins/analytics/admin/views/analytics.js +1 -1
- package/plugins/analytics/config.js +1 -1
- package/plugins/analytics/plugin.js +4 -4
- package/plugins/analytics/plugin.json +1 -1
- package/plugins/analytics/public/inject-body.html +2 -2
- package/plugins/analytics/public/inject-head.html +1 -1
- package/plugins/blog/admin/templates/blog.html +4 -4
- package/plugins/blog/admin/views/blog.js +3 -3
- package/plugins/blog/admin/views/categories.js +2 -2
- package/plugins/blog/admin/views/comments.js +4 -4
- package/plugins/blog/admin/views/post-editor.js +1 -1
- package/plugins/blog/plugin.js +2 -2
- package/plugins/blog/plugin.json +4 -4
- package/plugins/contacts/admin/views/contacts.js +8 -8
- package/plugins/contacts/plugin.js +4 -4
- package/plugins/contacts/plugin.json +1 -1
- package/plugins/invoice/admin/templates/editor.html +6 -6
- package/plugins/invoice/admin/templates/index.html +4 -4
- package/plugins/invoice/admin/views/editor.js +3 -3
- package/plugins/invoice/admin/views/index.js +4 -4
- package/plugins/invoice/admin/views/issuers.js +1 -1
- package/plugins/invoice/admin/views/party-view.js +3 -3
- package/plugins/invoice/admin/views/receivers.js +1 -1
- package/plugins/invoice/config.js +1 -1
- package/plugins/invoice/plugin.js +1 -1
- package/plugins/invoice/plugin.json +4 -4
- package/plugins/notes/admin/views/notes.js +4 -4
- package/plugins/notes/plugin.js +1 -1
- package/plugins/notes/plugin.json +1 -1
- package/plugins/site-search/admin/templates/site-search.html +3 -3
- package/plugins/site-search/admin/views/site-search.js +1 -1
- package/plugins/site-search/config.js +1 -1
- package/plugins/site-search/plugin.js +4 -4
- package/plugins/site-search/plugin.json +1 -1
- package/plugins/surveys/admin/templates/results.html +1 -1
- package/plugins/surveys/admin/templates/survey-editor.html +4 -4
- package/plugins/surveys/admin/views/audience.js +6 -6
- package/plugins/surveys/admin/views/results.js +3 -3
- package/plugins/surveys/admin/views/survey-editor.js +1 -1
- package/plugins/surveys/admin/views/surveys.js +2 -2
- package/plugins/surveys/plugin.js +1 -1
- package/plugins/surveys/plugin.json +4 -4
- package/plugins/theme-switcher/admin/views/theme-switcher.js +1 -1
- package/plugins/theme-switcher/plugin.json +3 -3
- package/plugins/theme-switcher/public/inject-body.html +28 -28
- package/plugins/theme-switcher/public/inject-head.html +1 -1
- package/plugins/todo/plugin.js +1 -1
- package/public/css/forms.css +1 -1
- package/public/js/collection-browser.js +1 -1
- package/public/js/collection-context.js +1 -1
- package/public/js/form-logic-engine.js +1 -1
- package/public/js/forms.js +2 -1
- package/public/js/site.js +1 -1
- package/scripts/build.js +14 -14
- package/scripts/create-plugin.js +2 -2
- package/scripts/fresh.js +2 -2
- package/scripts/gen-instance-secret.js +1 -1
- package/scripts/pro.js +12 -12
- package/scripts/reset.js +2 -2
- package/scripts/setup.js +15 -15
- package/scripts/users.js +5 -5
- package/scripts/verify-assets.mjs +6 -6
- package/server/config.js +1 -1
- package/server/middleware/auth.js +253 -253
- package/server/middleware/managerAuth.js +3 -3
- package/server/routes/api/actions.js +4 -4
- package/server/routes/api/api-endpoints.js +7 -7
- package/server/routes/api/api-tokens.js +5 -5
- package/server/routes/api/auth.js +309 -309
- package/server/routes/api/blocks.js +3 -3
- package/server/routes/api/collections.js +18 -18
- package/server/routes/api/dashboard.js +5 -5
- package/server/routes/api/effects.js +2 -2
- package/server/routes/api/endpoints-public.js +5 -5
- package/server/routes/api/forms.js +47 -47
- package/server/routes/api/menu-locations.js +3 -3
- package/server/routes/api/menus.js +7 -7
- package/server/routes/api/navigation.js +42 -42
- package/server/routes/api/notifications.js +2 -2
- package/server/routes/api/pages.js +14 -2
- package/server/routes/api/plugin-marketplace.js +3 -3
- package/server/routes/api/plugins.js +6 -6
- package/server/routes/api/projects.js +9 -9
- package/server/routes/api/scaffold.js +7 -7
- package/server/routes/api/settings.js +6 -6
- package/server/routes/api/sidebar.js +1 -1
- package/server/routes/api/users.js +5 -5
- package/server/routes/api/versions.js +1 -1
- package/server/routes/api/views.js +1 -1
- package/server/routes/docs-public.js +3 -3
- package/server/routes/public.js +14 -14
- package/server/server.js +34 -16
- package/server/services/actions.js +19 -19
- package/server/services/adapters/FileAdapter.js +2 -2
- package/server/services/adapters/MongoAdapter.js +5 -5
- package/server/services/apiEndpoints.js +15 -15
- package/server/services/apiTokens.js +7 -7
- package/server/services/blocks.js +13 -13
- package/server/services/cache/drivers/MemoryDriver.js +3 -3
- package/server/services/cache/drivers/NoneDriver.js +1 -1
- package/server/services/cache/index.js +5 -5
- package/server/services/collections.js +14 -14
- package/server/services/components.js +9 -9
- package/server/services/connectionManager.js +4 -4
- package/server/services/content.js +25 -2
- package/server/services/docs.js +14 -14
- package/server/services/email.js +167 -167
- package/server/services/filterEngine.js +6 -6
- package/server/services/forms.js +18 -10
- package/server/services/health.js +1 -1
- package/server/services/hooks.js +7 -7
- package/server/services/images.js +4 -4
- package/server/services/managerClient.js +1 -1
- package/server/services/markdown.js +79 -79
- package/server/services/menuRender.js +5 -5
- package/server/services/menus-migration.js +3 -3
- package/server/services/menus.js +31 -31
- package/server/services/permissionRegistry.js +1 -1
- package/server/services/pluginFiles.js +5 -5
- package/server/services/pluginInstaller.js +17 -17
- package/server/services/pluginScaffold.js +1 -1
- package/server/services/plugins.js +17 -17
- package/server/services/presetCollections.js +2 -2
- package/server/services/projects.js +35 -26
- package/server/services/recipes/contact-list.json +1 -1
- package/server/services/recipes/onboarding.json +8 -8
- package/server/services/references.js +8 -8
- package/server/services/renderer.js +9 -9
- package/server/services/roles.js +9 -9
- package/server/services/rowAccess.js +10 -10
- package/server/services/scaffolder.js +30 -30
- package/server/services/sidebar-migration.js +10 -10
- package/server/services/sitemap.js +1 -1
- package/server/services/userProfiles.js +199 -199
- package/server/services/userRoles.js +7 -7
- package/server/services/users.js +302 -302
- package/server/services/versions.js +3 -3
- package/server/services/viewPipeline.js +6 -6
- package/server/services/views.js +11 -11
- package/server/templates/page.html +5 -5
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
* Projects group related artefacts (Pages, Collections, Forms, Actions,
|
|
5
5
|
* Menus, Blocks, Components, Views, Roles, Users, API endpoints) under a
|
|
6
6
|
* named slug. Membership is
|
|
7
|
-
* a metadata tag (`meta.project: '<slug>'`) on each artefact
|
|
7
|
+
* a metadata tag (`meta.project: '<slug>'`) on each artefact - no
|
|
8
8
|
* filesystem isolation. Projects also act as a permission boundary:
|
|
9
9
|
* users with `projects: []` set only see their projects' artefacts plus
|
|
10
10
|
* the core project's artefacts.
|
|
11
11
|
*
|
|
12
12
|
* The built-in **core** project owns every artefact not claimed by another
|
|
13
|
-
* project
|
|
13
|
+
* project - by resolution fallback, not by tagging. It is seeded at boot
|
|
14
14
|
* (`seedCoreProject()`), cannot be deleted, and is never an access
|
|
15
15
|
* boundary: core-resolved artefacts are visible to all authenticated
|
|
16
16
|
* users, regardless of their `projects: []` scope.
|
|
@@ -32,7 +32,7 @@ export const CORE_PROJECT_SLUG = 'core';
|
|
|
32
32
|
const CORE_PROJECT_SEED = {
|
|
33
33
|
slug: CORE_PROJECT_SLUG,
|
|
34
34
|
name: 'Core',
|
|
35
|
-
description: 'The core site
|
|
35
|
+
description: 'The core site - everything not assigned to another project.',
|
|
36
36
|
icon: 'home',
|
|
37
37
|
rootUrl: '/',
|
|
38
38
|
sortOrder: -1,
|
|
@@ -47,7 +47,7 @@ export const DOCS_PROJECT_SLUG = 'domma-docs';
|
|
|
47
47
|
const DOCS_PROJECT_SEED = {
|
|
48
48
|
slug: DOCS_PROJECT_SLUG,
|
|
49
49
|
name: 'Domma Docs',
|
|
50
|
-
description: 'The Domma CMS handbook
|
|
50
|
+
description: 'The Domma CMS handbook - guides, tutorials, component & API ' +
|
|
51
51
|
'reference. Switch on to publish it on your own site.',
|
|
52
52
|
icon: 'document',
|
|
53
53
|
rootUrl: '/domma-docs',
|
|
@@ -132,7 +132,7 @@ export async function createProject(input) {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
|
-
* Seed the built-in core project at boot. Create-if-absent by slug
|
|
135
|
+
* Seed the built-in core project at boot. Create-if-absent by slug - NOT by
|
|
136
136
|
* data.json existence: preset seeding already creates an empty data file, and
|
|
137
137
|
* retrofitted sites have project entries that must be preserved. If a project
|
|
138
138
|
* with the core slug already exists it is adopted: `protected: true` is set
|
|
@@ -145,7 +145,7 @@ export async function seedCoreProject() {
|
|
|
145
145
|
const {entries} = await listEntries(PROJECTS_COLLECTION_SLUG, {limit: 0});
|
|
146
146
|
const existing = entries.find(e => e.data?.slug === CORE_PROJECT_SLUG);
|
|
147
147
|
if (existing) {
|
|
148
|
-
// Core is always protected and always enabled
|
|
148
|
+
// Core is always protected and always enabled - adopt both invariants.
|
|
149
149
|
if (existing.data.protected === true && existing.data.enabled !== false) return normalise(existing);
|
|
150
150
|
const adopted = {...existing.data, protected: true, enabled: true, updatedAt: new Date().toISOString()};
|
|
151
151
|
await updateEntry(PROJECTS_COLLECTION_SLUG, existing.id, adopted);
|
|
@@ -158,7 +158,7 @@ export async function seedCoreProject() {
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
/**
|
|
161
|
-
* Seed the built-in Domma Docs project at boot. Create-if-absent by slug
|
|
161
|
+
* Seed the built-in Domma Docs project at boot. Create-if-absent by slug - an
|
|
162
162
|
* existing record (including the admin's enabled/disabled choice and any edits)
|
|
163
163
|
* is returned untouched. Disabled by default: a fresh site ships with the docs
|
|
164
164
|
* off until an admin enables the project.
|
|
@@ -223,7 +223,7 @@ export async function updateProject(slug, input) {
|
|
|
223
223
|
|
|
224
224
|
/**
|
|
225
225
|
* Delete a project. Returns true if removed, false if not found.
|
|
226
|
-
* Does NOT check for tagged artefacts
|
|
226
|
+
* Does NOT check for tagged artefacts - caller (API route) must do that.
|
|
227
227
|
* The core project can never be deleted.
|
|
228
228
|
*
|
|
229
229
|
* @param {string} slug
|
|
@@ -254,7 +254,7 @@ export function validateProject(project) {
|
|
|
254
254
|
const errors = [];
|
|
255
255
|
if (!project || typeof project !== 'object') return ['Project must be an object'];
|
|
256
256
|
if (!validateSlug(project.slug)) {
|
|
257
|
-
errors.push(`Invalid slug "${project.slug}"
|
|
257
|
+
errors.push(`Invalid slug "${project.slug}" - lowercase alphanumeric + hyphen only`);
|
|
258
258
|
}
|
|
259
259
|
if (!project.name || typeof project.name !== 'string') {
|
|
260
260
|
errors.push('name is required');
|
|
@@ -281,7 +281,7 @@ function normalise(entry) {
|
|
|
281
281
|
|
|
282
282
|
/**
|
|
283
283
|
* Whether the given project is enabled (i.e. not switched off). A disabled
|
|
284
|
-
* project is a hard kill-switch on its PUBLIC surfaces
|
|
284
|
+
* project is a hard kill-switch on its PUBLIC surfaces - public pages 404 and
|
|
285
285
|
* its external API (collections + custom endpoints + tokens) is refused.
|
|
286
286
|
*
|
|
287
287
|
* The core project is always enabled. An unknown slug resolves to core by
|
|
@@ -300,7 +300,7 @@ export async function isProjectEnabled(slug) {
|
|
|
300
300
|
/**
|
|
301
301
|
* List projects the given user is allowed to see. Super-admin sees all;
|
|
302
302
|
* users with empty `projects: []` see all; scoped users see their listed
|
|
303
|
-
* projects plus the core project (whose artefacts they can see anyway
|
|
303
|
+
* projects plus the core project (whose artefacts they can see anyway -
|
|
304
304
|
* see canSeeArtefact).
|
|
305
305
|
*
|
|
306
306
|
* @param {object|null} user
|
|
@@ -321,7 +321,7 @@ export async function listProjectsForUser(user) {
|
|
|
321
321
|
* Rules:
|
|
322
322
|
* - Super-admin (role level 0) bypasses everything.
|
|
323
323
|
* - Core artefacts (no `meta.project`, or tagged `core`) are visible to
|
|
324
|
-
* all
|
|
324
|
+
* all - the core project is organisational, never an access boundary.
|
|
325
325
|
* - Users with no scope set (`projects: []` or missing) have no restriction.
|
|
326
326
|
* - Otherwise: artefact's project must be in user's scope.
|
|
327
327
|
*
|
|
@@ -341,7 +341,7 @@ export function canSeeArtefact(user, artefact) {
|
|
|
341
341
|
/**
|
|
342
342
|
* Resolve the project a non-page artefact belongs to. Artefacts without a
|
|
343
343
|
* `meta.project` tag belong to the core project by exclusion. (Pages resolve
|
|
344
|
-
* via getProjectForPage instead
|
|
344
|
+
* via getProjectForPage instead - they carry a frontmatter `project` field
|
|
345
345
|
* and inherit by rootUrl prefix.)
|
|
346
346
|
*
|
|
347
347
|
* @param {object} artefact - Anything with optional `meta.project`.
|
|
@@ -354,15 +354,22 @@ export function resolveArtefactProject(artefact) {
|
|
|
354
354
|
/**
|
|
355
355
|
* Resolve the project a page belongs to using the inheritance rules:
|
|
356
356
|
* 1. Explicit `project: '<slug>'` → use it
|
|
357
|
-
* 2. Explicit `project: null` → core (legacy opt-out, deprecated
|
|
357
|
+
* 2. Explicit `project: null` → core (legacy opt-out, deprecated - core
|
|
358
358
|
* owns the floor, so "untagged" no longer exists as a state)
|
|
359
359
|
* 3. Missing field → longest matching `rootUrl` prefix wins
|
|
360
360
|
* 4. No match → core
|
|
361
361
|
*
|
|
362
|
-
* The core project does not participate in prefix matching
|
|
362
|
+
* The core project does not participate in prefix matching - it is the
|
|
363
363
|
* fallback when nothing else claims the page. If the core record has not
|
|
364
364
|
* been seeded yet, falls back to null (pre-seed behaviour).
|
|
365
365
|
*
|
|
366
|
+
* A trailing slash on `rootUrl` is ignored: '/shop/' claims '/shop/items'
|
|
367
|
+
* exactly as '/shop' does. (The naive prefix test built `rootUrl + '/'` =
|
|
368
|
+
* '/shop//', which nothing starts with, so one stray keystroke in the project
|
|
369
|
+
* editor silently switched inheritance off.) '/' itself is rejected at
|
|
370
|
+
* validation - it belongs to core - so a whole-site project inherits nothing
|
|
371
|
+
* by URL and its pages must carry an explicit `project:` tag.
|
|
372
|
+
*
|
|
366
373
|
* @param {string} urlPath
|
|
367
374
|
* @param {string|null|undefined} explicitProject
|
|
368
375
|
* @returns {Promise<string|null>}
|
|
@@ -376,11 +383,13 @@ export async function getProjectForPage(urlPath, explicitProject) {
|
|
|
376
383
|
let bestLen = -1;
|
|
377
384
|
for (const p of projects) {
|
|
378
385
|
if (!p.rootUrl || p.slug === CORE_PROJECT_SLUG) continue;
|
|
379
|
-
|
|
380
|
-
|
|
386
|
+
const root = p.rootUrl.replace(/\/+$/, '');
|
|
387
|
+
if (!root) continue;
|
|
388
|
+
if (urlPath === root || urlPath.startsWith(root + '/')) {
|
|
389
|
+
if (root.length > bestLen) {
|
|
381
390
|
best = p.slug;
|
|
382
|
-
bestLen =
|
|
383
|
-
} else if (
|
|
391
|
+
bestLen = root.length;
|
|
392
|
+
} else if (root.length === bestLen && p.slug < best) {
|
|
384
393
|
best = p.slug;
|
|
385
394
|
}
|
|
386
395
|
}
|
|
@@ -390,7 +399,7 @@ export async function getProjectForPage(urlPath, explicitProject) {
|
|
|
390
399
|
|
|
391
400
|
/**
|
|
392
401
|
* Enumerate every artefact belonging to the given project. Returns a flat
|
|
393
|
-
* grouping by type
|
|
402
|
+
* grouping by type - each value is an array of artefact summary objects.
|
|
394
403
|
* Used by the project detail page (counts + per-type tabs).
|
|
395
404
|
*
|
|
396
405
|
* Membership is by resolution, not tag equality: artefacts without a
|
|
@@ -497,7 +506,7 @@ export async function getArtefactsForProject(projectSlug) {
|
|
|
497
506
|
|
|
498
507
|
try {
|
|
499
508
|
// Custom API endpoints store their project in data.project (required
|
|
500
|
-
// field
|
|
509
|
+
// field - no untagged-→core fallback applies, so match directly).
|
|
501
510
|
const {entries} = await listEntries('api-endpoints', {limit: 0});
|
|
502
511
|
for (const e of entries) {
|
|
503
512
|
if (e.data?.project === projectSlug) {
|
|
@@ -514,11 +523,11 @@ export async function getArtefactsForProject(projectSlug) {
|
|
|
514
523
|
* Returns a count map of how many of each type were untagged. Used as the
|
|
515
524
|
* "Untag all" escape hatch before delete.
|
|
516
525
|
*
|
|
517
|
-
* Per-service plumbing (Tasks 9
|
|
526
|
+
* Per-service plumbing (Tasks 9-16) is required for non-menu artefacts to
|
|
518
527
|
* carry meta.project through their update path; without that plumbing, this
|
|
519
528
|
* function simply returns zero counts for those types.
|
|
520
529
|
*
|
|
521
|
-
* Refused for the core project
|
|
530
|
+
* Refused for the core project - membership there is by resolution, so
|
|
522
531
|
* untagging would be a no-op that resolution immediately reverses.
|
|
523
532
|
*
|
|
524
533
|
* @param {string} projectSlug
|
|
@@ -527,11 +536,11 @@ export async function getArtefactsForProject(projectSlug) {
|
|
|
527
536
|
*/
|
|
528
537
|
export async function untagAllForProject(projectSlug) {
|
|
529
538
|
if (projectSlug === CORE_PROJECT_SLUG) {
|
|
530
|
-
throw new Error('Cannot untag the core project
|
|
539
|
+
throw new Error('Cannot untag the core project - artefacts would immediately resolve back to it');
|
|
531
540
|
}
|
|
532
541
|
const proj = await getProject(projectSlug);
|
|
533
542
|
if (proj?.protected === true) {
|
|
534
|
-
throw new Error(`Cannot untag the protected "${projectSlug}" project
|
|
543
|
+
throw new Error(`Cannot untag the protected "${projectSlug}" project - its pages would resolve back to core`);
|
|
535
544
|
}
|
|
536
545
|
const counts = {
|
|
537
546
|
pages: 0, collections: 0, forms: 0, actions: 0,
|
|
@@ -629,7 +638,7 @@ export async function untagAllForProject(projectSlug) {
|
|
|
629
638
|
// tasks may revisit.
|
|
630
639
|
//
|
|
631
640
|
// API endpoints: intentionally skipped. An endpoint's project IS its URL
|
|
632
|
-
// namespace (/api/x/<project>/...)
|
|
641
|
+
// namespace (/api/x/<project>/...) - silently untagging would move live
|
|
633
642
|
// endpoints to /api/x/core/... and break external callers. Delete or
|
|
634
643
|
// recreate them explicitly instead.
|
|
635
644
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"slug": "contact-list",
|
|
3
3
|
"name": "Contact list (CRM-lite)",
|
|
4
|
-
"description": "A simple address book
|
|
4
|
+
"description": "A simple address book - contacts collection with email/phone/company/tags, a quick-add form, and a single status transition for follow-ups.",
|
|
5
5
|
"icon": "users",
|
|
6
6
|
"options": [
|
|
7
7
|
{ "name": "collectionSlug", "label": "Collection slug", "default": "contacts", "hint": "Where contact records are stored" },
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"name": "namespace",
|
|
20
20
|
"label": "Namespace",
|
|
21
21
|
"default": "onboarding",
|
|
22
|
-
"hint": "Single name that cascades into every slug below. Change this once and the collection, form, and actions all rename in lockstep
|
|
22
|
+
"hint": "Single name that cascades into every slug below. Change this once and the collection, form, and actions all rename in lockstep - or override the individual slugs below to break the chain."
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"name": "collectionSlug",
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
"label": "Reference contacts",
|
|
130
130
|
"type": "textarea",
|
|
131
131
|
"required": false,
|
|
132
|
-
"placeholder": "Name, email, relationship
|
|
132
|
+
"placeholder": "Name, email, relationship - one per line"
|
|
133
133
|
},
|
|
134
134
|
{
|
|
135
135
|
"name": "notes",
|
|
@@ -194,7 +194,7 @@
|
|
|
194
194
|
},
|
|
195
195
|
"form": {
|
|
196
196
|
"title": "Employee Onboarding",
|
|
197
|
-
"description": "Welcome aboard
|
|
197
|
+
"description": "Welcome aboard - let's get you set up.",
|
|
198
198
|
"fields": [
|
|
199
199
|
{
|
|
200
200
|
"name": "fullName",
|
|
@@ -267,19 +267,19 @@
|
|
|
267
267
|
"accept": "application/pdf,image/jpeg,image/png",
|
|
268
268
|
"maxSize": 5242880
|
|
269
269
|
},
|
|
270
|
-
"helper": "PDF, JPEG or PNG
|
|
270
|
+
"helper": "PDF, JPEG or PNG - up to 5 MB"
|
|
271
271
|
},
|
|
272
272
|
{
|
|
273
273
|
"name": "references",
|
|
274
274
|
"label": "Reference contacts",
|
|
275
275
|
"type": "textarea",
|
|
276
276
|
"required": false,
|
|
277
|
-
"placeholder": "Name, email, relationship
|
|
277
|
+
"placeholder": "Name, email, relationship - one per line"
|
|
278
278
|
}
|
|
279
279
|
],
|
|
280
280
|
"settings": {
|
|
281
281
|
"submitText": "Send to HR",
|
|
282
|
-
"successMessage": "Thanks
|
|
282
|
+
"successMessage": "Thanks - HR will review your details shortly.",
|
|
283
283
|
"honeypot": true,
|
|
284
284
|
"rateLimitPerMinute": 3,
|
|
285
285
|
"actionSlug": ""
|
|
@@ -378,8 +378,8 @@
|
|
|
378
378
|
"type": "email",
|
|
379
379
|
"config": {
|
|
380
380
|
"to": "{{entry.data.email}}",
|
|
381
|
-
"subject": "Welcome
|
|
382
|
-
"template": "Hi {{entry.data.fullName}},\n\nGreat news
|
|
381
|
+
"subject": "Welcome - you're cleared to start",
|
|
382
|
+
"template": "Hi {{entry.data.fullName}},\n\nGreat news - your onboarding paperwork has been approved. Your first day is {{entry.data.startDate}}.\n\nIf you have any questions before then, just reply to this email.\n\nThe HR team"
|
|
383
383
|
}
|
|
384
384
|
}
|
|
385
385
|
]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Reference resolver
|
|
2
|
+
* Reference resolver - turn raw foreign-key ids stored on entries into
|
|
3
3
|
* displayable values from the target collections.
|
|
4
4
|
*
|
|
5
5
|
* Reference field schema shape:
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* collection: "jobs", // target collection slug (required)
|
|
12
12
|
* displayField: "title", // field on the target to show (default: "title")
|
|
13
13
|
* searchField: "title", // field to search on in picker UI (default: displayField)
|
|
14
|
-
* linkTemplate: "/jobs/{{id}}" // optional link template
|
|
14
|
+
* linkTemplate: "/jobs/{{id}}" // optional link template - `{{id}}` substituted
|
|
15
15
|
* }
|
|
16
16
|
* }
|
|
17
17
|
*
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*
|
|
23
23
|
* Batching: when resolving N entries that each have a `jobId`, we collect
|
|
24
24
|
* all distinct ids once, fetch the target collection in a single pass, then
|
|
25
|
-
* decorate every entry from the lookup map
|
|
25
|
+
* decorate every entry from the lookup map - one adapter call per *target
|
|
26
26
|
* collection* per resolve, not per entry.
|
|
27
27
|
*
|
|
28
28
|
* Missing-target handling: if a referenced id no longer exists (the target
|
|
@@ -59,7 +59,7 @@ export function referenceFields(schema) {
|
|
|
59
59
|
* @param {object} schema - Source collection schema
|
|
60
60
|
* @param {object[]} entries - Source entries; each may be mutated
|
|
61
61
|
* @param {object} [opts]
|
|
62
|
-
* @param {Function} [opts.adapterResolver] - Optional async (slug) → adapter
|
|
62
|
+
* @param {Function} [opts.adapterResolver] - Optional async (slug) → adapter - for tests
|
|
63
63
|
* @returns {Promise<object[]>}
|
|
64
64
|
*/
|
|
65
65
|
export async function resolveReferences(schema, entries, opts = {}) {
|
|
@@ -95,17 +95,17 @@ export async function resolveReferences(schema, entries, opts = {}) {
|
|
|
95
95
|
const adapter = await resolver(collectionSlug);
|
|
96
96
|
// FileAdapter: cheap to read all then filter; MongoAdapter: $in is the
|
|
97
97
|
// right call but we don't have a list-by-ids API yet. For now, get()
|
|
98
|
-
// each id in parallel
|
|
98
|
+
// each id in parallel - N requests but small N (distinct refs only).
|
|
99
99
|
const map = new Map();
|
|
100
100
|
await Promise.all([...idSet].map(async id => {
|
|
101
101
|
try {
|
|
102
102
|
const target = await adapter.get(collectionSlug, id);
|
|
103
103
|
if (target) map.set(id, target);
|
|
104
|
-
} catch { /* missing target
|
|
104
|
+
} catch { /* missing target - left out of the map */ }
|
|
105
105
|
}));
|
|
106
106
|
lookups.set(collectionSlug, map);
|
|
107
107
|
} catch {
|
|
108
|
-
lookups.set(collectionSlug, new Map()); // collection gone
|
|
108
|
+
lookups.set(collectionSlug, new Map()); // collection gone - all refs render as missing
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
@@ -151,7 +151,7 @@ function buildRef(field, id, lookupMap) {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
/**
|
|
154
|
-
* Convenience helper for renderers
|
|
154
|
+
* Convenience helper for renderers - get the display label for a reference
|
|
155
155
|
* field on a single entry, falling back to the raw value when no resolution
|
|
156
156
|
* was performed (e.g. a caller skipped `resolveRefs: true`).
|
|
157
157
|
*
|
|
@@ -36,7 +36,7 @@ let _templateCache = null;
|
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Strip hidden items from the navigation config before public injection.
|
|
39
|
-
* Returns a shallow clone
|
|
39
|
+
* Returns a shallow clone - never mutates the cached config object.
|
|
40
40
|
*
|
|
41
41
|
* @param {object} nav - Raw navigation config from getConfig('navigation')
|
|
42
42
|
* @returns {object} Filtered navigation object safe for public injection
|
|
@@ -59,7 +59,7 @@ function filterHiddenNavItems(nav) {
|
|
|
59
59
|
* whose binding matches, each drawn by the same builder the `[menu]` shortcode
|
|
60
60
|
* uses so an overlay and a shortcode of the same menu look identical.
|
|
61
61
|
*
|
|
62
|
-
* Emitted as bare <nav> elements with no wrapper
|
|
62
|
+
* Emitted as bare <nav> elements with no wrapper - a wrapping element would
|
|
63
63
|
* risk becoming a containing block for their position:fixed pinning.
|
|
64
64
|
*
|
|
65
65
|
* @param {{urlPath?: string, project?: string|null}} menuCtx
|
|
@@ -88,7 +88,7 @@ async function buildOverlayMenus(menuCtx, user) {
|
|
|
88
88
|
* @returns {Promise<string>}
|
|
89
89
|
*/
|
|
90
90
|
export async function renderPage(page, opts = {}) {
|
|
91
|
-
// Page context for menu bindings
|
|
91
|
+
// Page context for menu bindings - a menu bound to this URL or this page's
|
|
92
92
|
// project takes its slot over. `resolvedProject` is stamped by the content
|
|
93
93
|
// service; fall back to resolving it here for callers that skip that path.
|
|
94
94
|
const menuCtx = {
|
|
@@ -144,7 +144,7 @@ export async function renderPage(page, opts = {}) {
|
|
|
144
144
|
const pageWidth = VALID_LAYOUT_WIDTHS.has(page.width) ? page.width : null;
|
|
145
145
|
const layoutWidth = pageWidth || (VALID_LAYOUT_WIDTHS.has(preset.width) ? preset.width : 'normal');
|
|
146
146
|
// A vertical navbar docks as a side rail, so the body needs its gutter from
|
|
147
|
-
// the first paint
|
|
147
|
+
// the first paint - stamping the class client-side would shift the layout.
|
|
148
148
|
const verticalNavClass = navigation.orientation === 'vertical'
|
|
149
149
|
? `dm-vnav dm-vnav-${navigation.side === 'right' ? 'right' : 'left'}`
|
|
150
150
|
: '';
|
|
@@ -242,7 +242,7 @@ export async function renderPage(page, opts = {}) {
|
|
|
242
242
|
headInject: [injection.head, navbarFontLink].filter(Boolean).join('\n'),
|
|
243
243
|
headInjectLate: [injection.headLate, customCssTag, navbarStyleTag].filter(Boolean).join('\n'),
|
|
244
244
|
bodyEndInject: [
|
|
245
|
-
// Overlay menus
|
|
245
|
+
// Overlay menus - bound to the `overlay` slot and pinned by their own
|
|
246
246
|
// float config, so they belong at the end of the body, clear of the
|
|
247
247
|
// content flow.
|
|
248
248
|
overlayMenus,
|
|
@@ -385,7 +385,7 @@ function buildSeoTags({page, site, baseUrl, seoTitle, seoDescription, ogImage})
|
|
|
385
385
|
if (ogImage) tags.push(`<meta name="twitter:image" content="${ogImage}">`);
|
|
386
386
|
if (twitterHandle) tags.push(`<meta name="twitter:site" content="${twitterHandle}">`);
|
|
387
387
|
|
|
388
|
-
// JSON-LD WebPage schema
|
|
388
|
+
// JSON-LD WebPage schema - gives search engines a structured summary.
|
|
389
389
|
// Omit `url` when no baseUrl, since a relative URL in JSON-LD is invalid.
|
|
390
390
|
const jsonLd = {
|
|
391
391
|
'@context': 'https://schema.org',
|
|
@@ -400,7 +400,7 @@ function buildSeoTags({page, site, baseUrl, seoTitle, seoDescription, ogImage})
|
|
|
400
400
|
const jsonLdString = JSON.stringify(jsonLd).replace(/<\/script>/gi, '<\\/script>');
|
|
401
401
|
tags.push(`<script type="application/ld+json">${jsonLdString}</script>`);
|
|
402
402
|
|
|
403
|
-
// BreadcrumbList JSON-LD
|
|
403
|
+
// BreadcrumbList JSON-LD - emit whenever the page has a non-trivial path
|
|
404
404
|
// and an origin is known (Schema.org requires absolute URLs in `item`).
|
|
405
405
|
// Independent of the visual breadcrumb setting so SERPs can show the trail
|
|
406
406
|
// even when the on-page nav hides it.
|
|
@@ -523,7 +523,7 @@ function buildFontVars(fontFamily, fontSize) {
|
|
|
523
523
|
*/
|
|
524
524
|
export async function renderBlogPage(templatePath, data = {}, seoMeta = {}) {
|
|
525
525
|
const user = seoMeta.user || null;
|
|
526
|
-
// Plugin fragments know their own URL via seoMeta.urlPath
|
|
526
|
+
// Plugin fragments know their own URL via seoMeta.urlPath - enough for a
|
|
527
527
|
// menu bound to that page or to the project the URL falls under.
|
|
528
528
|
const menuCtx = seoMeta.urlPath
|
|
529
529
|
? {urlPath: seoMeta.urlPath, project: await getProjectForPage(seoMeta.urlPath)}
|
|
@@ -673,7 +673,7 @@ async function readLegacyNavBackup() {
|
|
|
673
673
|
const here = fileURLToPath(import.meta.url);
|
|
674
674
|
const bak = path.resolve(path.dirname(here), '..', '..', 'config', 'navigation.json.bak');
|
|
675
675
|
const raw = await fs.readFile(bak, 'utf8');
|
|
676
|
-
console.warn('[menus] navbar location unmapped
|
|
676
|
+
console.warn('[menus] navbar location unmapped - falling back to navigation.json.bak (deprecated)');
|
|
677
677
|
return JSON.parse(raw);
|
|
678
678
|
} catch {
|
|
679
679
|
return null;
|
package/server/services/roles.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Roles Service
|
|
3
|
-
* Preset Collection
|
|
3
|
+
* Preset Collection - seeds roles on first startup, caches them in memory.
|
|
4
4
|
* Auth middleware calls getRoleMap() / getPermissionsFor() at request time.
|
|
5
5
|
*
|
|
6
6
|
* Base roles: super-admin (0), admin (1), user (2).
|
|
@@ -18,13 +18,13 @@ const DIR = path.join(COLLECTIONS_DIR, SLUG);
|
|
|
18
18
|
const SCHEMA_PATH = path.join(DIR, 'schema.json');
|
|
19
19
|
const DATA_PATH = path.join(DIR, 'data.json');
|
|
20
20
|
|
|
21
|
-
/** Users dir
|
|
21
|
+
/** Users dir - derived from collections dir parent (content/users/) */
|
|
22
22
|
const USERS_DIR = path.resolve(path.dirname(COLLECTIONS_DIR), 'users');
|
|
23
23
|
|
|
24
24
|
/** Base role slugs that should always exist */
|
|
25
25
|
const BASE_ROLE_NAMES = ['super-admin', 'admin', 'user'];
|
|
26
26
|
|
|
27
|
-
/** Role slugs that were seeded by the old 4-role system
|
|
27
|
+
/** Role slugs that were seeded by the old 4-role system - used to detect legacy data */
|
|
28
28
|
const DEFUNCT_ROLE_NAMES = ['manager', 'editor', 'subscriber'];
|
|
29
29
|
|
|
30
30
|
export {RESOURCES, ACTIONS};
|
|
@@ -32,7 +32,7 @@ export {RESOURCES, ACTIONS};
|
|
|
32
32
|
const PRESET_SCHEMA = {
|
|
33
33
|
slug: SLUG,
|
|
34
34
|
title: 'Roles',
|
|
35
|
-
description: 'CMS role definitions
|
|
35
|
+
description: 'CMS role definitions - managed by the system.',
|
|
36
36
|
preset: true,
|
|
37
37
|
fields: [
|
|
38
38
|
{name: 'name', label: 'Name (slug)', type: 'text', required: true},
|
|
@@ -194,7 +194,7 @@ export async function seed() {
|
|
|
194
194
|
await writeData(entries);
|
|
195
195
|
}
|
|
196
196
|
} catch {
|
|
197
|
-
// data.json doesn't exist
|
|
197
|
+
// data.json doesn't exist - fresh install
|
|
198
198
|
entries = SEED_ENTRIES.map(data => makeEntry(data));
|
|
199
199
|
await writeData(entries);
|
|
200
200
|
}
|
|
@@ -203,7 +203,7 @@ export async function seed() {
|
|
|
203
203
|
// definitions in an update. Role permissions are a persisted snapshot
|
|
204
204
|
// taken at seed time, so a new family (e.g. api-endpoints) would
|
|
205
205
|
// otherwise be invisible on existing installs until granted by hand.
|
|
206
|
-
// Append-only
|
|
206
|
+
// Append-only - permissions an admin ADDED are preserved; note that a
|
|
207
207
|
// seeded permission deliberately removed from a base role is re-added on
|
|
208
208
|
// boot (use a custom role to run with less than the base set). The root
|
|
209
209
|
// role's seed list is the full registry, so it always carries every
|
|
@@ -241,7 +241,7 @@ async function migrateUserRoles(roleEntries) {
|
|
|
241
241
|
try {
|
|
242
242
|
files = await fs.readdir(USERS_DIR);
|
|
243
243
|
} catch {
|
|
244
|
-
return; // users dir doesn't exist yet
|
|
244
|
+
return; // users dir doesn't exist yet - nothing to migrate
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
for (const file of files) {
|
|
@@ -277,7 +277,7 @@ export async function load() {
|
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
/**
|
|
280
|
-
* Reload from disk
|
|
280
|
+
* Reload from disk - call after any CRUD on the roles collection.
|
|
281
281
|
*
|
|
282
282
|
* @returns {Promise<void>}
|
|
283
283
|
*/
|
|
@@ -287,7 +287,7 @@ export async function invalidate() {
|
|
|
287
287
|
|
|
288
288
|
/**
|
|
289
289
|
* Persist a new role entry and rebuild the cache.
|
|
290
|
-
* Idempotent
|
|
290
|
+
* Idempotent - if a role with the same name exists, it is updated in place.
|
|
291
291
|
*
|
|
292
292
|
* @param {{name:string,label:string,level:number,permissions:string[],badgeClass?:string,plugin?:string}} data
|
|
293
293
|
* @returns {Promise<void>}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* Resolve `data.<field>` against `targetCollection` and check that target
|
|
19
19
|
* entry's `meta.createdBy` (or `data.<targetField>` if `targetField` is a
|
|
20
20
|
* data field name) equals `user[userKey]`. This is "user can access THIS
|
|
21
|
-
* entry because they own the entry it points at"
|
|
21
|
+
* entry because they own the entry it points at" - the recruiter/employer
|
|
22
22
|
* pattern: applications scoped to the jobs the user posted.
|
|
23
23
|
*
|
|
24
24
|
* `targetField` accepts:
|
|
@@ -76,13 +76,13 @@ function isAdmin(user) {
|
|
|
76
76
|
* satisfies an `owner`/`field` rule, OR the entry's referenced target
|
|
77
77
|
* satisfies the `reference` rule. Returns `false` only when a rule is set
|
|
78
78
|
* AND the entry fails it. Misconfigured rules (missing fields, unknown
|
|
79
|
-
* targets) fail closed
|
|
79
|
+
* targets) fail closed - return `false` to avoid leaking data through typos.
|
|
80
80
|
*
|
|
81
81
|
* @param {object} entry - Full entry object ({ id, data, meta })
|
|
82
82
|
* @param {object|null} user - Authenticated user object
|
|
83
83
|
* @param {object|null} rowLevel - rowLevel config from action/view access
|
|
84
84
|
* @param {object} [opts]
|
|
85
|
-
* @param {Function} [opts.adapterResolver] - Optional async (slug) → adapter
|
|
85
|
+
* @param {Function} [opts.adapterResolver] - Optional async (slug) → adapter - for tests
|
|
86
86
|
* @returns {Promise<boolean>}
|
|
87
87
|
*/
|
|
88
88
|
export async function checkEntryAccess(entry, user, rowLevel, opts = {}) {
|
|
@@ -98,7 +98,7 @@ export async function checkEntryAccess(entry, user, rowLevel, opts = {}) {
|
|
|
98
98
|
|
|
99
99
|
if (rowLevel.mode === 'field') {
|
|
100
100
|
const field = rowLevel.field;
|
|
101
|
-
if (!field) return false; // misconfigured
|
|
101
|
+
if (!field) return false; // misconfigured - fail closed
|
|
102
102
|
return entry?.data?.[field] === userVal;
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -106,10 +106,10 @@ export async function checkEntryAccess(entry, user, rowLevel, opts = {}) {
|
|
|
106
106
|
const field = rowLevel.field;
|
|
107
107
|
const targetCollection = rowLevel.targetCollection;
|
|
108
108
|
const targetField = rowLevel.targetField || 'createdBy';
|
|
109
|
-
if (!field || !targetCollection) return false; // misconfigured
|
|
109
|
+
if (!field || !targetCollection) return false; // misconfigured - fail closed
|
|
110
110
|
|
|
111
111
|
// The referenced id lives at data.<field>. Arrays of ids are supported
|
|
112
|
-
//
|
|
112
|
+
// - access is granted when ANY of the targets is owned by the user
|
|
113
113
|
// (e.g. "I can see this entry because I own at least one of its parents").
|
|
114
114
|
const raw = entry?.data?.[field];
|
|
115
115
|
if (raw == null || raw === '') return false;
|
|
@@ -126,7 +126,7 @@ export async function checkEntryAccess(entry, user, rowLevel, opts = {}) {
|
|
|
126
126
|
for (const id of ids) {
|
|
127
127
|
try {
|
|
128
128
|
const target = await adapter.get(targetCollection, id);
|
|
129
|
-
if (!target) continue; // dangling reference
|
|
129
|
+
if (!target) continue; // dangling reference - skip
|
|
130
130
|
const value = targetField === 'createdBy'
|
|
131
131
|
? target.meta?.createdBy
|
|
132
132
|
: target.data?.[targetField];
|
|
@@ -138,7 +138,7 @@ export async function checkEntryAccess(entry, user, rowLevel, opts = {}) {
|
|
|
138
138
|
return false;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
return false; // unknown mode
|
|
141
|
+
return false; // unknown mode - fail closed
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
/**
|
|
@@ -148,7 +148,7 @@ export async function checkEntryAccess(entry, user, rowLevel, opts = {}) {
|
|
|
148
148
|
* The returned object is ready to use as `{ $match: buildRowLevelMatch(...) }`.
|
|
149
149
|
*
|
|
150
150
|
* Note: `mode: 'reference'` can't be expressed as a flat $match without a
|
|
151
|
-
* `$lookup`
|
|
151
|
+
* `$lookup` - for that mode this function returns `null` and callers must
|
|
152
152
|
* fall back to per-entry `checkEntryAccess()` filtering (slower but correct).
|
|
153
153
|
*
|
|
154
154
|
* @param {object|null} user
|
|
@@ -171,7 +171,7 @@ export function buildRowLevelMatch(user, rowLevel) {
|
|
|
171
171
|
return {[`data.${field}`]: userVal};
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
// 'reference' mode needs a join
|
|
174
|
+
// 'reference' mode needs a join - caller should use checkEntryAccess()
|
|
175
175
|
// per-entry instead. Return null so the upstream code knows to do that.
|
|
176
176
|
return null;
|
|
177
177
|
}
|