domma-cms 0.18.0 → 0.21.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 +37 -3
- package/admin/css/admin.css +1 -1
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/config/sidebar-config.js +1 -1
- package/admin/js/lib/crud-tutorial.js +1 -0
- package/admin/js/lib/markdown-toolbar.js +5 -5
- package/admin/js/lib/project-context.js +1 -0
- package/admin/js/lib/sidebar-renderer.js +4 -0
- package/admin/js/templates/action-editor.html +7 -0
- package/admin/js/templates/block-editor.html +7 -0
- package/admin/js/templates/collection-editor.html +9 -0
- package/admin/js/templates/form-editor.html +9 -0
- package/admin/js/templates/menu-editor.html +98 -0
- package/admin/js/templates/menu-locations.html +14 -0
- package/admin/js/templates/menus.html +14 -0
- package/admin/js/templates/page-editor.html +9 -2
- package/admin/js/templates/project-detail.html +50 -0
- package/admin/js/templates/project-editor.html +45 -0
- package/admin/js/templates/project-settings.html +60 -0
- package/admin/js/templates/projects.html +13 -0
- package/admin/js/templates/role-editor.html +11 -0
- package/admin/js/templates/tutorials.html +335 -2
- package/admin/js/templates/view-editor.html +7 -0
- package/admin/js/views/action-editor.js +1 -1
- package/admin/js/views/actions-list.js +1 -1
- package/admin/js/views/block-editor.js +8 -8
- package/admin/js/views/blocks.js +2 -2
- package/admin/js/views/collection-editor.js +4 -4
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/form-editor.js +5 -5
- package/admin/js/views/forms.js +1 -1
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/menu-editor.js +19 -0
- package/admin/js/views/menu-locations.js +1 -0
- package/admin/js/views/menus.js +5 -0
- package/admin/js/views/page-editor.js +24 -24
- package/admin/js/views/pages.js +3 -3
- package/admin/js/views/project-detail.js +4 -0
- package/admin/js/views/project-editor.js +1 -0
- package/admin/js/views/project-settings.js +1 -0
- package/admin/js/views/projects.js +7 -0
- package/admin/js/views/role-editor.js +1 -1
- package/admin/js/views/roles.js +3 -3
- package/admin/js/views/tutorials.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/views-list.js +1 -1
- package/config/menu-locations.json +5 -0
- package/config/menus/admin-sidebar.json +185 -0
- package/config/menus/footer.json +33 -0
- package/config/menus/main.json +35 -0
- package/config/menus/sproj-1779696558011-menu.json +17 -0
- package/config/menus/sproj-1779696960337-menu.json +18 -0
- package/config/menus/sproj-1779696985353-menu.json +18 -0
- package/config/site.json +6 -22
- package/package.json +4 -3
- package/plugins/analytics/daily.json +3 -0
- package/plugins/analytics/journeys.json +8 -0
- package/plugins/analytics/lifetime.json +1 -1
- package/public/css/site.css +1 -1
- package/public/js/collection-browser.js +4 -0
- package/public/js/forms.js +1 -1
- package/public/js/site.js +1 -1
- package/server/middleware/auth.js +88 -22
- package/server/routes/api/actions.js +58 -5
- package/server/routes/api/auth.js +2 -2
- package/server/routes/api/blocks.js +18 -3
- package/server/routes/api/collections.js +201 -8
- package/server/routes/api/forms.js +266 -21
- package/server/routes/api/menu-locations.js +46 -0
- package/server/routes/api/menus.js +115 -0
- package/server/routes/api/pages.js +1 -1
- package/server/routes/api/projects.js +107 -0
- package/server/routes/api/scaffold.js +86 -0
- package/server/routes/api/sidebar.js +23 -0
- package/server/routes/api/users.js +32 -7
- package/server/routes/api/views.js +10 -2
- package/server/routes/public.js +79 -6
- package/server/server.js +38 -0
- package/server/services/actions.js +137 -8
- package/server/services/adapters/FileAdapter.js +23 -8
- package/server/services/adapters/MongoAdapter.js +36 -18
- package/server/services/blocks.js +20 -8
- package/server/services/collections.js +85 -8
- package/server/services/content.js +23 -9
- package/server/services/filterEngine.js +281 -0
- package/server/services/hooks.js +48 -0
- package/server/services/markdown.js +686 -109
- package/server/services/menus-migration.js +107 -0
- package/server/services/menus.js +422 -0
- package/server/services/permissionRegistry.js +26 -0
- package/server/services/plugins.js +9 -2
- package/server/services/presetCollections.js +22 -0
- package/server/services/projects.js +429 -0
- package/server/services/recipes/contact-list.json +78 -0
- package/server/services/recipes/onboarding.json +426 -0
- package/server/services/references.js +174 -0
- package/server/services/renderer.js +237 -40
- package/server/services/roles.js +6 -1
- package/server/services/rowAccess.js +86 -13
- package/server/services/scaffolder.js +465 -0
- package/server/services/sidebar-migration.js +117 -0
- package/server/services/sitemap.js +112 -0
- package/server/services/userRoles.js +86 -0
- package/server/services/users.js +23 -2
- package/server/services/views.js +15 -4
- package/server/templates/page.html +7 -2
- /package/config/{navigation.json → navigation.json.bak} +0 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Roles — multi-role support helpers.
|
|
3
|
+
*
|
|
4
|
+
* A user has ONE primary role (`user.role`) and an optional array of
|
|
5
|
+
* additional roles (`user.additionalRoles`). Permission, visibility, and
|
|
6
|
+
* row-access checks consult ALL of them and grant access if any one role
|
|
7
|
+
* satisfies — "any path wins" semantics that matches how most apps actually
|
|
8
|
+
* model multi-membership (you can be both a candidate AND a recruiter).
|
|
9
|
+
*
|
|
10
|
+
* Hierarchical-level comparisons (`canManageUser`, `requireAdmin`, the
|
|
11
|
+
* level-clamp in `checkSingleVisibility`) use `getEffectiveLevel` which
|
|
12
|
+
* returns the LOWEST level number across all the user's roles — i.e. the
|
|
13
|
+
* highest privilege the user has access to. This is what makes "additional
|
|
14
|
+
* admin" meaningfully grant admin-tier access without rewriting the level
|
|
15
|
+
* arithmetic everywhere.
|
|
16
|
+
*
|
|
17
|
+
* Backward compatibility: users with no `additionalRoles` field behave
|
|
18
|
+
* exactly as before — `getEffectiveRoles` returns `[user.role]`, `getEffectiveLevel`
|
|
19
|
+
* returns `getRoleLevel(user.role)`. Existing callers that read `user.role`
|
|
20
|
+
* directly continue to work; the new helpers are opt-in for callers that
|
|
21
|
+
* want the union semantics.
|
|
22
|
+
*/
|
|
23
|
+
import {getRoleLevel} from './roles.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Return all roles a user holds, primary first, deduplicated. Empty array
|
|
27
|
+
* if the user is null/has no role at all.
|
|
28
|
+
*
|
|
29
|
+
* @param {object|null} user
|
|
30
|
+
* @returns {string[]}
|
|
31
|
+
*/
|
|
32
|
+
export function getEffectiveRoles(user) {
|
|
33
|
+
if (!user || !user.role) return [];
|
|
34
|
+
const additional = Array.isArray(user.additionalRoles) ? user.additionalRoles : [];
|
|
35
|
+
const out = [user.role];
|
|
36
|
+
for (const r of additional) {
|
|
37
|
+
if (r && r !== user.role && !out.includes(r)) out.push(r);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Return the effective role level for a user — the LOWEST level number
|
|
44
|
+
* across all roles. Lower number = higher privilege, so the user's most
|
|
45
|
+
* privileged role wins all hierarchical checks.
|
|
46
|
+
*
|
|
47
|
+
* Returns `Infinity` for users with no roles (denies all level-gated checks).
|
|
48
|
+
*
|
|
49
|
+
* @param {object|null} user
|
|
50
|
+
* @returns {number}
|
|
51
|
+
*/
|
|
52
|
+
export function getEffectiveLevel(user) {
|
|
53
|
+
const roles = getEffectiveRoles(user);
|
|
54
|
+
if (!roles.length) return Infinity;
|
|
55
|
+
return Math.min(...roles.map(r => getRoleLevel(r)));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check whether a user holds a specific role (primary or additional).
|
|
60
|
+
* Exact-name match — does NOT walk the hierarchy. For hierarchical "user
|
|
61
|
+
* is at least this level" checks use `getEffectiveLevel` against the target
|
|
62
|
+
* role's level instead.
|
|
63
|
+
*
|
|
64
|
+
* @param {object|null} user
|
|
65
|
+
* @param {string} role
|
|
66
|
+
* @returns {boolean}
|
|
67
|
+
*/
|
|
68
|
+
export function userHasRole(user, role) {
|
|
69
|
+
if (!user || !role) return false;
|
|
70
|
+
return getEffectiveRoles(user).includes(role);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check whether a user holds ANY of the given roles. Convenience helper
|
|
75
|
+
* for "anyone in this set can do X" patterns — equivalent to OR-ing
|
|
76
|
+
* `userHasRole` over the list.
|
|
77
|
+
*
|
|
78
|
+
* @param {object|null} user
|
|
79
|
+
* @param {string[]} roles
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
export function userHasAnyRole(user, roles) {
|
|
83
|
+
if (!user || !Array.isArray(roles) || !roles.length) return false;
|
|
84
|
+
const userRoles = new Set(getEffectiveRoles(user));
|
|
85
|
+
return roles.some(r => userRoles.has(r));
|
|
86
|
+
}
|
package/server/services/users.js
CHANGED
|
@@ -9,6 +9,7 @@ import bcrypt from 'bcryptjs';
|
|
|
9
9
|
import {v4 as uuidv4} from 'uuid';
|
|
10
10
|
import {config} from '../config.js';
|
|
11
11
|
import {deleteProfile, ensureProfile} from './userProfiles.js';
|
|
12
|
+
import * as cache from './cache/index.js';
|
|
12
13
|
|
|
13
14
|
const USERS_DIR = path.resolve(config.content.usersDir);
|
|
14
15
|
|
|
@@ -122,7 +123,7 @@ export async function getUserByEmail(email) {
|
|
|
122
123
|
* @param {object} data - { name, email, password, role }
|
|
123
124
|
* @returns {Promise<object>} Created user (password stripped)
|
|
124
125
|
*/
|
|
125
|
-
export async function createUser({ name, email, password, role = 'user' }) {
|
|
126
|
+
export async function createUser({ name, email, password, role = 'user', additionalRoles = [], meta, projects }) {
|
|
126
127
|
const existing = await getUserByEmail(email);
|
|
127
128
|
if (existing) throw new Error('A user with that email already exists');
|
|
128
129
|
|
|
@@ -134,10 +135,19 @@ export async function createUser({ name, email, password, role = 'user' }) {
|
|
|
134
135
|
name: name.trim(),
|
|
135
136
|
password: hash,
|
|
136
137
|
role,
|
|
138
|
+
// Additional roles beyond the primary — permission/visibility checks
|
|
139
|
+
// grant access if ANY role satisfies. The primary `role` is what
|
|
140
|
+
// appears in UI badges and is used for hierarchical level comparisons
|
|
141
|
+
// when a single value is needed.
|
|
142
|
+
additionalRoles: Array.isArray(additionalRoles) ? additionalRoles.filter(r => r && r !== role) : [],
|
|
143
|
+
// Project access-scope: when non-empty, the user is restricted to artefacts
|
|
144
|
+
// tagged with one of these project slugs (see canSeeArtefact in projects.js).
|
|
145
|
+
projects: Array.isArray(projects) ? projects.filter(Boolean) : [],
|
|
137
146
|
isActive: true,
|
|
138
147
|
createdAt: now,
|
|
139
148
|
updatedAt: now,
|
|
140
|
-
lastLogin: null
|
|
149
|
+
lastLogin: null,
|
|
150
|
+
...(meta != null && {meta})
|
|
141
151
|
};
|
|
142
152
|
|
|
143
153
|
await writeUserFile(user);
|
|
@@ -169,6 +179,17 @@ export async function updateUser(id, updates) {
|
|
|
169
179
|
};
|
|
170
180
|
|
|
171
181
|
await writeUserFile(updated);
|
|
182
|
+
|
|
183
|
+
// Invalidate per-user cache when the project access-scope changes — downstream
|
|
184
|
+
// request handlers cache scope-filtered responses keyed by `user:<id>`.
|
|
185
|
+
if (updates.projects !== undefined) {
|
|
186
|
+
const before = JSON.stringify(user.projects || []);
|
|
187
|
+
const after = JSON.stringify(updated.projects || []);
|
|
188
|
+
if (before !== after) {
|
|
189
|
+
await cache.invalidateTags([`user:${id}`]);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
172
193
|
return stripPassword(updated);
|
|
173
194
|
}
|
|
174
195
|
|
package/server/services/views.js
CHANGED
|
@@ -137,7 +137,7 @@ export async function getView(slug) {
|
|
|
137
137
|
*/
|
|
138
138
|
export async function createView(data, userId = null) {
|
|
139
139
|
const db = await getMetaDb();
|
|
140
|
-
const { title, description = '', connection = 'default', pipeline, display, access } = data;
|
|
140
|
+
const { title, description = '', connection = 'default', pipeline, display, access, meta: inputMeta } = data;
|
|
141
141
|
|
|
142
142
|
if (!title) throw new Error('title is required');
|
|
143
143
|
if (!pipeline?.source) throw new Error('pipeline.source is required');
|
|
@@ -170,7 +170,12 @@ export async function createView(data, userId = null) {
|
|
|
170
170
|
public: access?.public || false,
|
|
171
171
|
rowLevel: access?.rowLevel || null
|
|
172
172
|
},
|
|
173
|
-
meta: {
|
|
173
|
+
meta: {
|
|
174
|
+
createdAt: now,
|
|
175
|
+
updatedAt: now,
|
|
176
|
+
createdBy: userId,
|
|
177
|
+
...(inputMeta?.project != null && {project: inputMeta.project})
|
|
178
|
+
}
|
|
174
179
|
};
|
|
175
180
|
|
|
176
181
|
await db.collection(VIEWS_COLLECTION).insertOne({ ...view });
|
|
@@ -194,7 +199,7 @@ export async function updateView(slug, data) {
|
|
|
194
199
|
const stages = data.pipeline?.stages ?? existing.pipeline?.stages ?? [];
|
|
195
200
|
validateStages(stages);
|
|
196
201
|
|
|
197
|
-
const { _id, id, meta, ...rest } = data;
|
|
202
|
+
const { _id, id, meta: inputMeta, ...rest } = data;
|
|
198
203
|
const updated = {
|
|
199
204
|
...existing,
|
|
200
205
|
...rest,
|
|
@@ -204,7 +209,13 @@ export async function updateView(slug, data) {
|
|
|
204
209
|
...data.pipeline,
|
|
205
210
|
stages
|
|
206
211
|
},
|
|
207
|
-
meta: {
|
|
212
|
+
meta: {
|
|
213
|
+
...existing.meta,
|
|
214
|
+
...(inputMeta && typeof inputMeta === 'object'
|
|
215
|
+
? ('project' in inputMeta ? {project: inputMeta.project} : {})
|
|
216
|
+
: {}),
|
|
217
|
+
updatedAt: new Date().toISOString()
|
|
218
|
+
}
|
|
208
219
|
};
|
|
209
220
|
|
|
210
221
|
await db.collection(VIEWS_COLLECTION).replaceOne({ slug }, { ...updated });
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>{{seoTitle}}</title>
|
|
7
7
|
<meta name="description" content="{{seoDescription}}">
|
|
8
|
-
{{
|
|
8
|
+
{{seoTags}}
|
|
9
9
|
|
|
10
10
|
<!-- Fonts -->
|
|
11
11
|
{{#if fontLink}}{{fontLink}}{{/if}}
|
|
@@ -103,6 +103,7 @@
|
|
|
103
103
|
}
|
|
104
104
|
window.__CMS_NAV__ = {{navJson}};
|
|
105
105
|
window.__CMS_SITE__ = {{siteJson}};
|
|
106
|
+
{{footerScript}}
|
|
106
107
|
(function () {
|
|
107
108
|
var c = window.__CMS_SITE__ && window.__CMS_SITE__.autoTheme;
|
|
108
109
|
if (!c || !c.enabled) return;
|
|
@@ -115,7 +116,11 @@
|
|
|
115
116
|
</script>
|
|
116
117
|
|
|
117
118
|
<!-- Site initialisation -->
|
|
118
|
-
<script src="/public/js/site.js?v=
|
|
119
|
+
<script src="/public/js/site.js?v=20260524-formerror" type="module"></script>
|
|
120
|
+
|
|
121
|
+
<!-- Interactive Collection Browser (used by [collection] shortcodes with
|
|
122
|
+
searchable / filterable / sortable / paginate attributes) -->
|
|
123
|
+
<script src="/public/js/collection-browser.js?v=20260523-browser"></script>
|
|
119
124
|
|
|
120
125
|
<!-- Core Forms (logic engine must load before renderer) -->
|
|
121
126
|
<script src="/public/js/form-logic-engine.js?v=20260509-chooser"></script>
|
|
File without changes
|