domma-cms 0.17.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 +39 -3
- package/admin/css/admin.css +1 -1
- package/admin/css/dashboard.css +1 -1
- package/admin/index.html +2 -2
- 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/card-builder.js +3 -3
- package/admin/js/lib/crud-tutorial.js +1 -0
- package/admin/js/lib/effects-builder.js +1 -1
- package/admin/js/lib/markdown-toolbar.js +6 -6
- 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/dashboard/cache.html +32 -0
- package/admin/js/templates/dashboard.html +4 -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/settings.html +26 -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-enhance.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/dashboard/widgets/activity-feed.js +1 -1
- package/admin/js/views/dashboard/widgets/cache.js +1 -0
- package/admin/js/views/dashboard/widgets/journeys.js +1 -1
- package/admin/js/views/dashboard/widgets/spike-feed.js +1 -1
- package/admin/js/views/dashboard/widgets/top-pages.js +1 -1
- package/admin/js/views/dashboard.js +1 -1
- package/admin/js/views/form-editor.js +6 -6
- 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 +41 -36
- 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/settings.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/cache.json +4 -0
- package/config/cache.json.example +12 -0
- 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/config.js +12 -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/cache.js +57 -0
- 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/navigation.js +2 -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/settings.js +3 -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 +88 -7
- package/server/server.js +54 -3
- 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 +23 -8
- package/server/services/cache/drivers/MemoryDriver.js +118 -0
- package/server/services/cache/drivers/NoneDriver.js +12 -0
- package/server/services/cache/index.js +229 -0
- package/server/services/cache/lru.js +61 -0
- package/server/services/collections.js +102 -12
- package/server/services/content.js +25 -6
- package/server/services/filterEngine.js +281 -0
- package/server/services/forms.js +3 -0
- package/server/services/hooks.js +48 -0
- package/server/services/markdown.js +711 -124
- 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 +19 -4
- package/server/templates/page.html +135 -130
- /package/config/{navigation.json → navigation.json.bak} +0 -0
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Role data is read from the roles cache (not config.auth.roles).
|
|
5
5
|
*/
|
|
6
6
|
import {getPermissionsFor, getPermissionsForRole, getRoleHierarchy, getRoleLevel} from '../services/roles.js';
|
|
7
|
+
import {getEffectiveLevel, getEffectiveRoles} from '../services/userRoles.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Verify JWT Bearer token. Populates request.user on success.
|
|
@@ -78,8 +79,12 @@ export function requirePermission(resource, action) {
|
|
|
78
79
|
});
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
// Check ANY of the user's effective roles (primary + additional) against
|
|
83
|
+
// the resource's permission list — union semantics so a user with both
|
|
84
|
+
// candidate and recruiter roles can do either role's actions.
|
|
81
85
|
const allowed = getPermissionsFor(resource, action);
|
|
82
|
-
|
|
86
|
+
const roles = getEffectiveRoles(request.user);
|
|
87
|
+
if (!roles.some(r => allowed.includes(r))) {
|
|
83
88
|
return reply.code(403).send({
|
|
84
89
|
statusCode: 403,
|
|
85
90
|
error: 'Forbidden',
|
|
@@ -111,7 +116,9 @@ export async function requireAdmin(request, reply) {
|
|
|
111
116
|
if (!request.user) {
|
|
112
117
|
return reply.code(401).send({ statusCode: 401, error: 'Unauthorised', message: 'Authentication required' });
|
|
113
118
|
}
|
|
114
|
-
|
|
119
|
+
// Use effective level — a user with "additionalRoles: ['admin']" can do admin work
|
|
120
|
+
// even if their primary role is something lower-privilege.
|
|
121
|
+
if (getEffectiveLevel(request.user) > 1) {
|
|
115
122
|
return reply.code(403).send({ statusCode: 403, error: 'Forbidden', message: 'Admin access required' });
|
|
116
123
|
}
|
|
117
124
|
}
|
|
@@ -120,53 +127,112 @@ export async function requireAdmin(request, reply) {
|
|
|
120
127
|
* Determine whether an actor can manage a target user.
|
|
121
128
|
* Managers cannot create, edit, or delete users with a lower level number (higher privilege).
|
|
122
129
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
130
|
+
* Accepts either user objects (preferred — uses effective level across all roles)
|
|
131
|
+
* or bare role-name strings (legacy compat). Strings are looked up via
|
|
132
|
+
* `getRoleLevel`; objects via `getEffectiveLevel` so multi-role actors and
|
|
133
|
+
* targets are compared by their HIGHEST-privilege role.
|
|
134
|
+
*
|
|
135
|
+
* @param {string|object} actor - Role name OR user object
|
|
136
|
+
* @param {string|object} target - Role name OR user object
|
|
125
137
|
* @returns {boolean}
|
|
126
138
|
*/
|
|
127
|
-
export function canManageUser(
|
|
128
|
-
|
|
139
|
+
export function canManageUser(actor, target) {
|
|
140
|
+
const actorLevel = typeof actor === 'string' ? getRoleLevel(actor) : getEffectiveLevel(actor);
|
|
141
|
+
const targetLevel = typeof target === 'string' ? getRoleLevel(target) : getEffectiveLevel(target);
|
|
142
|
+
return actorLevel < targetLevel;
|
|
129
143
|
}
|
|
130
144
|
|
|
131
145
|
/**
|
|
132
146
|
* Check whether a user role satisfies a visibility requirement.
|
|
133
147
|
* Used by both requireVisibility() and the public page renderer.
|
|
134
148
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
149
|
+
* Visibility may be either:
|
|
150
|
+
* - A single string ('public', 'private', or a role name)
|
|
151
|
+
* - An array of role names — granted if ANY entry passes the per-role check
|
|
152
|
+
*
|
|
153
|
+
* Per-role semantics are unchanged: each role check passes if the visitor's
|
|
154
|
+
* role level is at or above the required role (lower or equal level number).
|
|
155
|
+
* 'private' resolves to super-admin only (Infinity → level 0).
|
|
156
|
+
*
|
|
157
|
+
* The "any of" semantics for arrays means siblings at different tiers of the
|
|
158
|
+
* hierarchy are all granted access — e.g. `visibility: [candidate, employer]`
|
|
159
|
+
* lets both roles in, plus anyone more privileged than either (typically
|
|
160
|
+
* admins inherit access automatically via the level comparison).
|
|
161
|
+
*
|
|
162
|
+
* @param {string|null} userRole - The visitor's role, or null if unauthenticated
|
|
163
|
+
* @param {string|string[]} visibility - Required visibility — single value or array
|
|
137
164
|
* @returns {boolean} true if access is granted
|
|
138
165
|
*/
|
|
139
|
-
export function checkVisibility(
|
|
140
|
-
if (!visibility
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
166
|
+
export function checkVisibility(userRoleOrObj, visibility) {
|
|
167
|
+
if (!visibility) return true;
|
|
168
|
+
|
|
169
|
+
// Accept either a bare role name (legacy) or a user object with multi-role
|
|
170
|
+
// support. When given an object we walk every effective role and grant
|
|
171
|
+
// access if ANY satisfies — same union semantics as permissions.
|
|
172
|
+
const roles = typeof userRoleOrObj === 'string'
|
|
173
|
+
? (userRoleOrObj ? [userRoleOrObj] : [])
|
|
174
|
+
: getEffectiveRoles(userRoleOrObj);
|
|
175
|
+
|
|
176
|
+
if (Array.isArray(visibility)) {
|
|
177
|
+
if (visibility.length === 0) return true;
|
|
178
|
+
if (visibility.includes('public')) return true;
|
|
179
|
+
if (!roles.length) return false;
|
|
180
|
+
return roles.some(r => visibility.some(v => checkSingleVisibility(r, v)));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (visibility === 'public') return true;
|
|
184
|
+
if (!roles.length) return false;
|
|
185
|
+
return roles.some(r => checkSingleVisibility(r, visibility));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Internal helper — single-role visibility check.
|
|
190
|
+
* Returns true if the user role is at or above the required role's level.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} userRole - Must not be null
|
|
193
|
+
* @param {string} visibility - Single visibility token (role name or 'private')
|
|
194
|
+
* @returns {boolean}
|
|
195
|
+
*/
|
|
196
|
+
function checkSingleVisibility(userRole, visibility) {
|
|
197
|
+
const userLevel = getRoleLevel(userRole);
|
|
198
|
+
const requiredLevel = getRoleLevel(visibility);
|
|
199
|
+
const threshold = requiredLevel === Infinity ? 0 : requiredLevel;
|
|
145
200
|
return userLevel <= threshold;
|
|
146
201
|
}
|
|
147
202
|
|
|
148
203
|
/**
|
|
149
204
|
* Fastify preHandler factory — gates a route by visibility level.
|
|
150
|
-
* Works identically to the content-page visibility system
|
|
151
|
-
*
|
|
205
|
+
* Works identically to the content-page visibility system; accepts the same
|
|
206
|
+
* single-string or array-of-roles syntax as checkVisibility().
|
|
152
207
|
*
|
|
153
|
-
*
|
|
208
|
+
* Returns a no-op for 'public' (or any array containing 'public') so it is
|
|
209
|
+
* safe to apply unconditionally.
|
|
210
|
+
*
|
|
211
|
+
* @param {string|string[]} visibility - 'public' | 'private' | role name | array of role names
|
|
154
212
|
* @returns {Function} Fastify preHandler
|
|
155
213
|
*/
|
|
156
214
|
export function requireVisibility(visibility) {
|
|
157
|
-
|
|
215
|
+
const isPublic = !visibility
|
|
216
|
+
|| visibility === 'public'
|
|
217
|
+
|| (Array.isArray(visibility) && (visibility.length === 0 || visibility.includes('public')));
|
|
218
|
+
|
|
219
|
+
if (isPublic) {
|
|
158
220
|
return (_request, _reply, done) => { if (done) done(); };
|
|
159
221
|
}
|
|
160
222
|
|
|
161
223
|
return async (request, reply) => {
|
|
162
|
-
|
|
224
|
+
// Build a user-shaped object so checkVisibility can see multi-role
|
|
225
|
+
// (primary + additional). Unauthenticated → null → public-only access.
|
|
226
|
+
let userObj = null;
|
|
163
227
|
try {
|
|
164
228
|
const decoded = await request.jwtVerify();
|
|
165
|
-
if (decoded.type === 'access')
|
|
229
|
+
if (decoded.type === 'access') {
|
|
230
|
+
userObj = { role: decoded.role, additionalRoles: decoded.additionalRoles || [] };
|
|
231
|
+
}
|
|
166
232
|
} catch { /* unauthenticated */ }
|
|
167
233
|
|
|
168
|
-
if (!checkVisibility(
|
|
169
|
-
const code =
|
|
234
|
+
if (!checkVisibility(userObj, visibility)) {
|
|
235
|
+
const code = userObj ? 403 : 401;
|
|
170
236
|
return reply.code(code).send({
|
|
171
237
|
statusCode: code,
|
|
172
238
|
error: code === 403 ? 'Forbidden' : 'Unauthorised',
|
|
@@ -20,11 +20,13 @@ import {
|
|
|
20
20
|
getAction,
|
|
21
21
|
listActions,
|
|
22
22
|
listActionsForCollection,
|
|
23
|
+
listTransitionsForEntry,
|
|
23
24
|
updateAction
|
|
24
25
|
} from '../../services/actions.js';
|
|
25
26
|
import {getEntry} from '../../services/collections.js';
|
|
26
27
|
import {authenticate, requirePermission} from '../../middleware/auth.js';
|
|
27
28
|
import {getRoleLevel} from '../../services/roles.js';
|
|
29
|
+
import {getEffectiveLevel} from '../../services/userRoles.js';
|
|
28
30
|
import {checkEntryAccess} from '../../services/rowAccess.js';
|
|
29
31
|
|
|
30
32
|
export async function actionsRoutes(fastify) {
|
|
@@ -39,7 +41,10 @@ export async function actionsRoutes(fastify) {
|
|
|
39
41
|
|
|
40
42
|
fastify.get('/actions', canRead, async (request, reply) => {
|
|
41
43
|
try {
|
|
42
|
-
|
|
44
|
+
const {canSeeArtefact} = await import('../../services/projects.js');
|
|
45
|
+
const all = await listActions();
|
|
46
|
+
// listActions returns full records with meta inline — filter directly
|
|
47
|
+
return all.filter(a => canSeeArtefact(request.user, a));
|
|
43
48
|
} catch (err) {
|
|
44
49
|
return reply.status(503).send({ error: err.message });
|
|
45
50
|
}
|
|
@@ -68,6 +73,10 @@ export async function actionsRoutes(fastify) {
|
|
|
68
73
|
try {
|
|
69
74
|
const action = await getAction(request.params.slug);
|
|
70
75
|
if (!action) return reply.status(404).send({ error: 'Action not found' });
|
|
76
|
+
const {canSeeArtefact} = await import('../../services/projects.js');
|
|
77
|
+
if (!canSeeArtefact(request.user, action)) {
|
|
78
|
+
return reply.status(403).send({ error: 'Access denied for this project' });
|
|
79
|
+
}
|
|
71
80
|
return action;
|
|
72
81
|
} catch (err) {
|
|
73
82
|
return reply.status(503).send({ error: err.message });
|
|
@@ -136,8 +145,8 @@ export async function actionsRoutes(fastify) {
|
|
|
136
145
|
const rowLevel = action.access?.rowLevel;
|
|
137
146
|
const user = request.user;
|
|
138
147
|
|
|
139
|
-
// Admin or no row-level config → all IDs allowed
|
|
140
|
-
if (!rowLevel ||
|
|
148
|
+
// Admin (across primary OR additional roles) or no row-level config → all IDs allowed
|
|
149
|
+
if (!rowLevel || getEffectiveLevel(user) === 0) {
|
|
141
150
|
return {allowed: entryIds};
|
|
142
151
|
}
|
|
143
152
|
|
|
@@ -146,7 +155,7 @@ export async function actionsRoutes(fastify) {
|
|
|
146
155
|
entryIds.map(async (id) => {
|
|
147
156
|
try {
|
|
148
157
|
const entry = await getEntry(action.collection, id);
|
|
149
|
-
return entry && checkEntryAccess(entry, user, rowLevel) ? id : null;
|
|
158
|
+
return (entry && (await checkEntryAccess(entry, user, rowLevel))) ? id : null;
|
|
150
159
|
} catch {
|
|
151
160
|
return null;
|
|
152
161
|
}
|
|
@@ -179,7 +188,8 @@ export async function actionsRoutes(fastify) {
|
|
|
179
188
|
|
|
180
189
|
const user = request.user;
|
|
181
190
|
const allowedRoles = action.access?.roles || ['admin', 'super-admin'];
|
|
182
|
-
|
|
191
|
+
// Use effective level — a candidate-with-also-admin can run admin-tier actions.
|
|
192
|
+
const userLevel = getEffectiveLevel(user);
|
|
183
193
|
const minAllowed = Math.min(...allowedRoles.map(r => getRoleLevel(r)));
|
|
184
194
|
|
|
185
195
|
if (userLevel > minAllowed) {
|
|
@@ -197,4 +207,47 @@ export async function actionsRoutes(fastify) {
|
|
|
197
207
|
return reply.status(status).send({ error: err.message });
|
|
198
208
|
}
|
|
199
209
|
});
|
|
210
|
+
|
|
211
|
+
/*
|
|
212
|
+
* GET /api/actions/transitions?collection=<slug>&entryId=<id>
|
|
213
|
+
*
|
|
214
|
+
* Returns the actions valid as *next-state transitions* for the given
|
|
215
|
+
* entry, filtered by the requesting user's role. Used by the per-row
|
|
216
|
+
* transitions UI in the Collection Browser and the `[transitions]`
|
|
217
|
+
* shortcode to render only the buttons that will actually succeed.
|
|
218
|
+
*
|
|
219
|
+
* Auth: optional. Anonymous calls return actions accessible to anonymous
|
|
220
|
+
* users (rare — most transitions require a role). With a JWT we narrow
|
|
221
|
+
* to what the signed-in user can do.
|
|
222
|
+
*
|
|
223
|
+
* Response: { transitions: [{slug, title, trigger, transition}, ...] }
|
|
224
|
+
* Returns 503 if MongoDB is unavailable (actions are a Pro feature).
|
|
225
|
+
*/
|
|
226
|
+
fastify.get('/actions/transitions', async (request, reply) => {
|
|
227
|
+
const { collection: collectionSlug, entryId } = request.query;
|
|
228
|
+
if (!collectionSlug || !entryId) {
|
|
229
|
+
return reply.status(400).send({ error: 'collection and entryId are required' });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let user = null;
|
|
233
|
+
try {
|
|
234
|
+
const decoded = await request.jwtVerify();
|
|
235
|
+
if (decoded.type === 'access') user = {id: decoded.id, role: decoded.role};
|
|
236
|
+
} catch { /* anonymous */ }
|
|
237
|
+
|
|
238
|
+
let entry;
|
|
239
|
+
try {
|
|
240
|
+
entry = await getEntry(collectionSlug, entryId);
|
|
241
|
+
} catch {
|
|
242
|
+
return reply.status(404).send({ error: 'Entry not found' });
|
|
243
|
+
}
|
|
244
|
+
if (!entry) return reply.status(404).send({ error: 'Entry not found' });
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const transitions = await listTransitionsForEntry(collectionSlug, entry, user);
|
|
248
|
+
return { transitions };
|
|
249
|
+
} catch (err) {
|
|
250
|
+
return reply.status(503).send({ error: err.message, transitions: [] });
|
|
251
|
+
}
|
|
252
|
+
});
|
|
200
253
|
}
|
|
@@ -108,7 +108,7 @@ export async function authRoutes(fastify) {
|
|
|
108
108
|
|
|
109
109
|
await touchLastLogin(user.id);
|
|
110
110
|
|
|
111
|
-
const safeUser = { id: user.id, name: user.name, email: user.email, role: user.role, level: getRoleLevel(user.role) };
|
|
111
|
+
const safeUser = { id: user.id, name: user.name, email: user.email, role: user.role, additionalRoles: user.additionalRoles || [], level: getRoleLevel(user.role) };
|
|
112
112
|
hooks.emit('user:loggedIn', {userId: user.id, email: user.email, role: user.role});
|
|
113
113
|
const { token, refreshToken } = signTokens(fastify, safeUser);
|
|
114
114
|
return { token, refreshToken, user: safeUser };
|
|
@@ -271,7 +271,7 @@ export async function authRoutes(fastify) {
|
|
|
271
271
|
return reply.status(401).send({ error: 'User not found or inactive' });
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
const safeUser = { id: user.id, name: user.name, email: user.email, role: user.role, level: getRoleLevel(user.role) };
|
|
274
|
+
const safeUser = { id: user.id, name: user.name, email: user.email, role: user.role, additionalRoles: user.additionalRoles || [], level: getRoleLevel(user.role) };
|
|
275
275
|
const token = fastify.jwt.sign({ ...safeUser, type: 'access' }, { expiresIn: accessTokenExpiry });
|
|
276
276
|
return { token };
|
|
277
277
|
});
|
|
@@ -20,20 +20,35 @@ export async function blocksRoutes(fastify) {
|
|
|
20
20
|
const canDelete = {preHandler: [authenticate, requirePermission('pages', 'delete')]};
|
|
21
21
|
|
|
22
22
|
// List all blocks
|
|
23
|
-
fastify.get('/blocks', canRead, async () => {
|
|
24
|
-
|
|
23
|
+
fastify.get('/blocks', canRead, async (request) => {
|
|
24
|
+
const {canSeeArtefact} = await import('../../services/projects.js');
|
|
25
|
+
const all = await listBlocks();
|
|
26
|
+
const filtered = [];
|
|
27
|
+
for (const b of all) {
|
|
28
|
+
// listBlocks returns metadata only; getBlock returns full record with meta
|
|
29
|
+
let full = null;
|
|
30
|
+
try { full = await getBlock(b.name); } catch { /* skip */ }
|
|
31
|
+
if (canSeeArtefact(request.user, full)) filtered.push(b);
|
|
32
|
+
}
|
|
33
|
+
return filtered;
|
|
25
34
|
});
|
|
26
35
|
|
|
27
36
|
// Get single block
|
|
28
37
|
fastify.get('/blocks/:name', canRead, async (request, reply) => {
|
|
29
38
|
const {name} = request.params;
|
|
39
|
+
let block;
|
|
30
40
|
try {
|
|
31
|
-
|
|
41
|
+
block = await getBlock(name);
|
|
32
42
|
} catch (err) {
|
|
33
43
|
if (err.code === 'INVALID_NAME') return reply.status(400).send({error: err.message});
|
|
34
44
|
if (err.code === 'ENOENT') return reply.status(404).send({error: 'Block not found'});
|
|
35
45
|
throw err;
|
|
36
46
|
}
|
|
47
|
+
const {canSeeArtefact} = await import('../../services/projects.js');
|
|
48
|
+
if (!canSeeArtefact(request.user, block)) {
|
|
49
|
+
return reply.status(403).send({error: 'Access denied for this project'});
|
|
50
|
+
}
|
|
51
|
+
return block;
|
|
37
52
|
});
|
|
38
53
|
|
|
39
54
|
// Export single block as downloadable .dmblock.json bundle
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache API
|
|
3
|
+
* GET /api/cache/status - report enabled flag plus live stats
|
|
4
|
+
* GET /api/cache/keys - list cached keys with their tags (no values)
|
|
5
|
+
* POST /api/cache/toggle - flip enabled on/off, persist to config/cache.json
|
|
6
|
+
* POST /api/cache/clear - flush every cached entry
|
|
7
|
+
*
|
|
8
|
+
* All endpoints are admin-only.
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'fs/promises';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import {fileURLToPath} from 'url';
|
|
13
|
+
import * as cache from '../../services/cache/index.js';
|
|
14
|
+
import {authenticate, requireAdmin} from '../../middleware/auth.js';
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const CACHE_CONFIG_FILE = path.resolve(__dirname, '../../../config/cache.json');
|
|
18
|
+
|
|
19
|
+
const CACHE_DEFAULTS = {
|
|
20
|
+
enabled: process.env.NODE_ENV === 'production',
|
|
21
|
+
driver: 'memory',
|
|
22
|
+
memory: {maxItems: 1000, defaultTtlSeconds: 3600}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
async function readCacheConfig() {
|
|
26
|
+
try {
|
|
27
|
+
return {...CACHE_DEFAULTS, ...JSON.parse(await fs.readFile(CACHE_CONFIG_FILE, 'utf8'))};
|
|
28
|
+
} catch {
|
|
29
|
+
return {...CACHE_DEFAULTS};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function writeCacheConfig(cfg) {
|
|
34
|
+
await fs.writeFile(CACHE_CONFIG_FILE, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function cacheRoutes(fastify) {
|
|
38
|
+
const adminOnly = {preHandler: [authenticate, requireAdmin]};
|
|
39
|
+
|
|
40
|
+
fastify.get('/cache/status', adminOnly, async () => cache.getStats());
|
|
41
|
+
|
|
42
|
+
fastify.get('/cache/keys', adminOnly, async () => ({entries: cache.listEntries()}));
|
|
43
|
+
|
|
44
|
+
fastify.post('/cache/toggle', adminOnly, async (request, reply) => {
|
|
45
|
+
const enabled = !!request.body?.enabled;
|
|
46
|
+
const cfg = await readCacheConfig();
|
|
47
|
+
cfg.enabled = enabled;
|
|
48
|
+
await writeCacheConfig(cfg);
|
|
49
|
+
await cache.setEnabled(enabled);
|
|
50
|
+
return reply.send(cache.getStats());
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
fastify.post('/cache/clear', adminOnly, async () => {
|
|
54
|
+
await cache.clear();
|
|
55
|
+
return cache.getStats();
|
|
56
|
+
});
|
|
57
|
+
}
|