domma-cms 0.2.1 → 0.5.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.
Files changed (166) hide show
  1. package/README.md +3 -3
  2. package/admin/css/admin.css +1 -1200
  3. package/admin/dist/domma/domma-tools.css +2313 -0
  4. package/admin/dist/domma/domma-tools.min.js +10 -0
  5. package/admin/index.html +4 -0
  6. package/admin/js/api.js +1 -242
  7. package/admin/js/app.js +9 -279
  8. package/admin/js/config/sidebar-config.js +1 -115
  9. package/admin/js/lib/card.js +1 -63
  10. package/admin/js/lib/image-editor.js +1 -869
  11. package/admin/js/lib/markdown-toolbar.js +54 -421
  12. package/admin/js/templates/action-editor.html +171 -0
  13. package/admin/js/templates/actions-list.html +19 -0
  14. package/admin/js/templates/api-reference.html +1411 -0
  15. package/admin/js/templates/block-editor.html +158 -0
  16. package/admin/js/templates/blocks.html +8 -0
  17. package/admin/js/templates/collection-editor.html +47 -0
  18. package/admin/js/templates/collection-entries.html +3 -0
  19. package/admin/js/templates/collections.html +51 -4
  20. package/admin/js/templates/documentation.html +258 -0
  21. package/admin/js/templates/form-editor.html +238 -0
  22. package/{plugins/form-builder/admin → admin/js}/templates/form-submissions.html +30 -30
  23. package/{plugins/form-builder/admin/templates/forms-list.html → admin/js/templates/forms.html} +17 -17
  24. package/admin/js/templates/layouts.html +44 -7
  25. package/admin/js/templates/login.html +29 -4
  26. package/admin/js/templates/my-profile.html +17 -0
  27. package/admin/js/templates/page-editor.html +48 -0
  28. package/admin/js/templates/pages.html +6 -1
  29. package/admin/js/templates/pro-docs.html +259 -0
  30. package/admin/js/templates/role-editor.html +59 -0
  31. package/admin/js/templates/roles.html +10 -0
  32. package/admin/js/templates/settings.html +137 -18
  33. package/admin/js/templates/tutorials.html +81 -0
  34. package/admin/js/templates/user-editor.html +7 -0
  35. package/admin/js/templates/users.html +3 -1
  36. package/admin/js/templates/view-editor.html +201 -0
  37. package/admin/js/templates/view-preview.html +51 -0
  38. package/admin/js/templates/views-list.html +19 -0
  39. package/admin/js/views/action-editor.js +1 -0
  40. package/admin/js/views/actions-list.js +1 -0
  41. package/admin/js/views/api-reference.js +1 -0
  42. package/admin/js/views/block-editor.js +8 -0
  43. package/admin/js/views/blocks.js +4 -0
  44. package/admin/js/views/collection-editor.js +3 -487
  45. package/admin/js/views/collection-entries.js +1 -484
  46. package/admin/js/views/collections.js +1 -153
  47. package/admin/js/views/dashboard.js +1 -56
  48. package/admin/js/views/documentation.js +1 -12
  49. package/admin/js/views/form-editor.js +8 -0
  50. package/admin/js/views/form-submissions.js +1 -0
  51. package/admin/js/views/forms.js +1 -0
  52. package/admin/js/views/index.js +1 -39
  53. package/admin/js/views/layouts.js +9 -42
  54. package/admin/js/views/login.js +7 -251
  55. package/admin/js/views/media.js +1 -240
  56. package/admin/js/views/my-profile.js +1 -0
  57. package/admin/js/views/navigation.js +14 -212
  58. package/admin/js/views/page-editor.js +72 -661
  59. package/admin/js/views/pages.js +5 -72
  60. package/admin/js/views/plugins.js +13 -90
  61. package/admin/js/views/pro-docs.js +1 -0
  62. package/admin/js/views/role-editor.js +1 -0
  63. package/admin/js/views/roles.js +4 -0
  64. package/admin/js/views/settings.js +3 -199
  65. package/admin/js/views/tutorials.js +1 -12
  66. package/admin/js/views/user-editor.js +1 -88
  67. package/admin/js/views/users.js +4 -76
  68. package/admin/js/views/view-editor.js +1 -0
  69. package/admin/js/views/view-preview.js +1 -0
  70. package/admin/js/views/views-list.js +1 -0
  71. package/bin/cli.js +1 -1
  72. package/config/auth.json +2 -17
  73. package/config/connections.json.bak +9 -0
  74. package/config/connections.json.example +9 -0
  75. package/config/navigation.json +15 -0
  76. package/config/plugins.json +19 -29
  77. package/config/server.json +6 -6
  78. package/config/site.json +17 -6
  79. package/package.json +24 -10
  80. package/plugins/domma-effects/public/celebrations/core/canvas.js +2 -104
  81. package/plugins/domma-effects/public/celebrations/core/particles.js +1 -144
  82. package/plugins/domma-effects/public/celebrations/core/physics.js +1 -166
  83. package/plugins/domma-effects/public/celebrations/index.js +1 -535
  84. package/plugins/domma-effects/public/celebrations/themes/christmas.js +1 -1805
  85. package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1 -1477
  86. package/plugins/domma-effects/public/celebrations/themes/halloween.js +1 -1837
  87. package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1 -1175
  88. package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1 -1258
  89. package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1 -1754
  90. package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1 -1290
  91. package/plugins/domma-effects/public/celebrations/themes/valentines.js +1 -1361
  92. package/plugins/example-analytics/stats.json +21 -12
  93. package/plugins/theme-roller/admin/templates/theme-roller.html +71 -0
  94. package/plugins/theme-roller/admin/views/theme-roller-view.js +403 -0
  95. package/plugins/theme-roller/config.js +1 -0
  96. package/plugins/theme-roller/plugin.js +233 -0
  97. package/plugins/theme-roller/plugin.json +31 -0
  98. package/plugins/theme-roller/public/active-theme.css +0 -0
  99. package/plugins/theme-roller/public/inject-head-late.html +1 -0
  100. package/public/css/forms.css +1 -0
  101. package/public/css/site.css +1 -302
  102. package/public/js/btt.js +1 -90
  103. package/public/js/cookie-consent.js +1 -61
  104. package/public/js/form-logic-engine.js +1 -0
  105. package/public/js/forms.js +1 -0
  106. package/public/js/site.js +1 -204
  107. package/scripts/build.js +194 -129
  108. package/scripts/pro.js +254 -0
  109. package/scripts/reset.js +33 -8
  110. package/scripts/seed.js +343 -78
  111. package/scripts/setup.js +5 -4
  112. package/server/middleware/auth.js +136 -97
  113. package/server/routes/api/actions.js +200 -0
  114. package/server/routes/api/auth.js +292 -116
  115. package/server/routes/api/blocks.js +84 -0
  116. package/server/routes/api/collections.js +88 -23
  117. package/{plugins/form-builder/plugin.js → server/routes/api/forms.js} +483 -505
  118. package/server/routes/api/layouts.js +49 -25
  119. package/server/routes/api/media.js +118 -93
  120. package/server/routes/api/navigation.js +40 -37
  121. package/server/routes/api/pages.js +132 -118
  122. package/server/routes/api/plugins.js +6 -3
  123. package/server/routes/api/settings.js +104 -89
  124. package/server/routes/api/users.js +27 -21
  125. package/server/routes/api/views.js +148 -0
  126. package/server/routes/public.js +124 -108
  127. package/server/server.js +269 -173
  128. package/server/services/actions.js +387 -0
  129. package/server/services/adapterRegistry.js +98 -0
  130. package/server/services/adapters/FileAdapter.js +192 -0
  131. package/server/services/adapters/MongoAdapter.js +220 -0
  132. package/server/services/blocks.js +162 -0
  133. package/server/services/collections.js +74 -86
  134. package/server/services/connectionManager.js +102 -0
  135. package/server/services/content.js +312 -307
  136. package/{plugins/form-builder → server/services}/email.js +126 -103
  137. package/server/services/forms.js +173 -0
  138. package/server/services/markdown.js +1378 -648
  139. package/server/services/permissionRegistry.js +173 -0
  140. package/server/services/presetCollections.js +251 -0
  141. package/server/services/renderer.js +75 -1
  142. package/server/services/roles.js +227 -0
  143. package/server/services/rowAccess.js +104 -0
  144. package/server/services/userProfiles.js +199 -0
  145. package/server/services/users.js +281 -212
  146. package/server/services/views.js +280 -0
  147. package/server/templates/page.html +119 -113
  148. package/plugins/form-builder/admin/templates/form-editor.html +0 -171
  149. package/plugins/form-builder/admin/templates/form-settings.html +0 -29
  150. package/plugins/form-builder/admin/views/form-editor.js +0 -1442
  151. package/plugins/form-builder/admin/views/form-settings.js +0 -38
  152. package/plugins/form-builder/admin/views/form-submissions.js +0 -295
  153. package/plugins/form-builder/admin/views/forms-list.js +0 -164
  154. package/plugins/form-builder/config.js +0 -9
  155. package/plugins/form-builder/data/forms/consent.json +0 -104
  156. package/plugins/form-builder/data/forms/contact-details.json +0 -63
  157. package/plugins/form-builder/data/forms/contacts.json +0 -66
  158. package/plugins/form-builder/data/submissions/consent.json +0 -13
  159. package/plugins/form-builder/data/submissions/contact-details.json +0 -1
  160. package/plugins/form-builder/data/submissions/contacts.json +0 -26
  161. package/plugins/form-builder/plugin.json +0 -52
  162. package/plugins/form-builder/public/form-logic-engine.js +0 -568
  163. package/plugins/form-builder/public/inject-body.html +0 -352
  164. package/plugins/form-builder/public/inject-head.html +0 -58
  165. package/plugins/form-builder/public/package.json +0 -1
  166. package/scripts/copy-domma.js +0 -48
@@ -1,97 +1,136 @@
1
- /**
2
- * Authentication Middleware
3
- * JWT-based authentication with role guards for Domma CMS.
4
- */
5
- import { config } from '../config.js';
6
-
7
- const { roles } = config.auth;
8
-
9
- /**
10
- * Role hierarchy ordered from most to least privileged.
11
- * Used by canManageUser() to compare privilege levels.
12
- */
13
- export const ROLE_HIERARCHY = Object.entries(roles)
14
- .sort((a, b) => a[1].level - b[1].level)
15
- .map(([key]) => key);
16
-
17
- export const ROLES = Object.fromEntries(
18
- Object.entries(roles).map(([key]) => [key.toUpperCase(), key])
19
- );
20
-
21
- /**
22
- * Verify JWT Bearer token. Populates request.user on success.
23
- *
24
- * @param {FastifyRequest} request
25
- * @param {FastifyReply} reply
26
- * @returns {Promise<void>}
27
- */
28
- export async function authenticate(request, reply) {
29
- try {
30
- await request.jwtVerify();
31
- } catch {
32
- return reply.code(401).send({
33
- statusCode: 401,
34
- error: 'Unauthorised',
35
- message: 'Invalid or missing authentication token'
36
- });
37
- }
38
- }
39
-
40
- /**
41
- * Return a preHandler that enforces one of the specified roles.
42
- * Must be used after authenticate.
43
- *
44
- * @param {string[]} allowedRoles
45
- * @returns {Function}
46
- */
47
- export function requireRole(allowedRoles) {
48
- const allowed = Array.isArray(allowedRoles) ? allowedRoles : [allowedRoles];
49
-
50
- return async (request, reply) => {
51
- if (!request.user) {
52
- return reply.code(401).send({
53
- statusCode: 401,
54
- error: 'Unauthorised',
55
- message: 'Authentication required'
56
- });
57
- }
58
-
59
- if (!allowed.includes(request.user.role)) {
60
- return reply.code(403).send({
61
- statusCode: 403,
62
- error: 'Forbidden',
63
- message: `Access denied. Required role: ${allowed.join(' or ')}`
64
- });
65
- }
66
- };
67
- }
68
-
69
- /**
70
- * Shorthand preHandler — admin only.
71
- *
72
- * @param {FastifyRequest} request
73
- * @param {FastifyReply} reply
74
- * @returns {Promise<void>}
75
- */
76
- export async function requireAdmin(request, reply) {
77
- if (!request.user) {
78
- return reply.code(401).send({ statusCode: 401, error: 'Unauthorised', message: 'Authentication required' });
79
- }
80
- if (request.user.role !== 'admin') {
81
- return reply.code(403).send({ statusCode: 403, error: 'Forbidden', message: 'Admin access required' });
82
- }
83
- }
84
-
85
- /**
86
- * Determine whether an actor can manage a target user.
87
- * Managers cannot create, edit, or delete admins.
88
- *
89
- * @param {string} actorRole - Role of the user performing the action
90
- * @param {string} targetRole - Role of the user being acted upon
91
- * @returns {boolean}
92
- */
93
- export function canManageUser(actorRole, targetRole) {
94
- const actorLevel = roles[actorRole]?.level ?? Infinity;
95
- const targetLevel = roles[targetRole]?.level ?? Infinity;
96
- return actorLevel < targetLevel;
97
- }
1
+ /**
2
+ * Authentication Middleware
3
+ * JWT-based authentication with role guards for Domma CMS.
4
+ * Role data is read from the roles cache (not config.auth.roles).
5
+ */
6
+ import {getPermissionsFor, getPermissionsForRole, getRoleHierarchy, getRoleLevel} from '../services/roles.js';
7
+
8
+ /**
9
+ * Verify JWT Bearer token. Populates request.user on success.
10
+ *
11
+ * @param {FastifyRequest} request
12
+ * @param {FastifyReply} reply
13
+ * @returns {Promise<void>}
14
+ */
15
+ export async function authenticate(request, reply) {
16
+ try {
17
+ const decoded = await request.jwtVerify();
18
+ if (decoded.type !== 'access') {
19
+ return reply.code(401).send({
20
+ statusCode: 401,
21
+ error: 'Unauthorised',
22
+ message: 'Invalid token type'
23
+ });
24
+ }
25
+ } catch {
26
+ return reply.code(401).send({
27
+ statusCode: 401,
28
+ error: 'Unauthorised',
29
+ message: 'Invalid or missing authentication token'
30
+ });
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Return a preHandler that enforces one of the specified roles.
36
+ * Must be used after authenticate.
37
+ *
38
+ * @param {string[]} allowedRoles
39
+ * @returns {Function}
40
+ */
41
+ export function requireRole(allowedRoles) {
42
+ const allowed = Array.isArray(allowedRoles) ? allowedRoles : [allowedRoles];
43
+
44
+ return async (request, reply) => {
45
+ if (!request.user) {
46
+ return reply.code(401).send({
47
+ statusCode: 401,
48
+ error: 'Unauthorised',
49
+ message: 'Authentication required'
50
+ });
51
+ }
52
+
53
+ if (!allowed.includes(request.user.role)) {
54
+ return reply.code(403).send({
55
+ statusCode: 403,
56
+ error: 'Forbidden',
57
+ message: `Access denied. Required role: ${allowed.join(' or ')}`
58
+ });
59
+ }
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Return a preHandler that checks the current user's role has access to a resource.
65
+ * Reads from the roles cache at request time — reflects live role changes.
66
+ *
67
+ * @param {string} resource - Resource key (e.g. 'pages', 'users')
68
+ * @param {string} [action] - Optional action (read | create | update | delete)
69
+ * @returns {Function}
70
+ */
71
+ export function requirePermission(resource, action) {
72
+ return async (request, reply) => {
73
+ if (!request.user) {
74
+ return reply.code(401).send({
75
+ statusCode: 401,
76
+ error: 'Unauthorised',
77
+ message: 'Authentication required'
78
+ });
79
+ }
80
+
81
+ const allowed = getPermissionsFor(resource, action);
82
+ if (!allowed.includes(request.user.role)) {
83
+ return reply.code(403).send({
84
+ statusCode: 403,
85
+ error: 'Forbidden',
86
+ message: 'Insufficient permissions'
87
+ });
88
+ }
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Export getPermissionsForRole for use in route handlers.
94
+ *
95
+ * @param {string} roleName
96
+ * @returns {string[]}
97
+ */
98
+ export {getPermissionsForRole};
99
+
100
+ /**
101
+ * Shorthand preHandler — level-0 role (admin) only.
102
+ *
103
+ * @param {FastifyRequest} request
104
+ * @param {FastifyReply} reply
105
+ * @returns {Promise<void>}
106
+ */
107
+ export async function requireAdmin(request, reply) {
108
+ if (!request.user) {
109
+ return reply.code(401).send({ statusCode: 401, error: 'Unauthorised', message: 'Authentication required' });
110
+ }
111
+ if (getRoleLevel(request.user.role) !== 0) {
112
+ return reply.code(403).send({ statusCode: 403, error: 'Forbidden', message: 'Admin access required' });
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Determine whether an actor can manage a target user.
118
+ * Managers cannot create, edit, or delete users with a lower level number (higher privilege).
119
+ *
120
+ * @param {string} actorRole - Role of the user performing the action
121
+ * @param {string} targetRole - Role of the user being acted upon
122
+ * @returns {boolean}
123
+ */
124
+ export function canManageUser(actorRole, targetRole) {
125
+ return getRoleLevel(actorRole) < getRoleLevel(targetRole);
126
+ }
127
+
128
+ /**
129
+ * Return role names ordered from most to least privileged.
130
+ * Computed from the roles cache.
131
+ *
132
+ * @returns {string[]}
133
+ */
134
+ export function getRoleHierarchyList() {
135
+ return getRoleHierarchy();
136
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Actions API (Pro — requires MongoDB)
3
+ *
4
+ * Admin endpoints (authenticated + actions permission):
5
+ * GET /actions - List all action configs
6
+ * POST /actions - Create action config
7
+ * GET /actions/:slug - Get action config
8
+ * PUT /actions/:slug - Update action config
9
+ * DELETE /actions/:slug - Delete action config
10
+ * POST /actions/:slug/execute - Execute action on an entry (admin)
11
+ * GET /actions/collection/:slug - List actions for a collection
12
+ *
13
+ * Public endpoint (role-checked per action access config):
14
+ * POST /actions/:slug/public - Execute action (role-restricted)
15
+ */
16
+ import {
17
+ createAction,
18
+ deleteAction,
19
+ executeAction,
20
+ getAction,
21
+ listActions,
22
+ listActionsForCollection,
23
+ updateAction
24
+ } from '../../services/actions.js';
25
+ import {getEntry} from '../../services/collections.js';
26
+ import {authenticate, requirePermission} from '../../middleware/auth.js';
27
+ import {getRoleLevel} from '../../services/roles.js';
28
+ import {checkEntryAccess} from '../../services/rowAccess.js';
29
+
30
+ export async function actionsRoutes(fastify) {
31
+ const canRead = {preHandler: [authenticate, requirePermission('actions', 'read')]};
32
+ const canCreate = {preHandler: [authenticate, requirePermission('actions', 'create')]};
33
+ const canUpdate = {preHandler: [authenticate, requirePermission('actions', 'update')]};
34
+ const canDelete = {preHandler: [authenticate, requirePermission('actions', 'delete')]};
35
+
36
+ // -------------------------------------------------------------------------
37
+ // Admin CRUD
38
+ // -------------------------------------------------------------------------
39
+
40
+ fastify.get('/actions', canRead, async (request, reply) => {
41
+ try {
42
+ return await listActions();
43
+ } catch (err) {
44
+ return reply.status(503).send({ error: err.message });
45
+ }
46
+ });
47
+
48
+ fastify.post('/actions', canCreate, async (request, reply) => {
49
+ try {
50
+ const action = await createAction(request.body || {}, request.user?.id || null);
51
+ return reply.status(201).send(action);
52
+ } catch (err) {
53
+ const status = err.message.includes('already exists') ? 409 : 400;
54
+ return reply.status(status).send({ error: err.message });
55
+ }
56
+ });
57
+
58
+ // Static sub-route declared before parameterised :slug for radix-tree priority
59
+ fastify.get('/actions/collection/:collectionSlug', canRead, async (request, reply) => {
60
+ try {
61
+ return await listActionsForCollection(request.params.collectionSlug);
62
+ } catch (err) {
63
+ return reply.status(503).send({ error: err.message });
64
+ }
65
+ });
66
+
67
+ fastify.get('/actions/:slug', canRead, async (request, reply) => {
68
+ try {
69
+ const action = await getAction(request.params.slug);
70
+ if (!action) return reply.status(404).send({ error: 'Action not found' });
71
+ return action;
72
+ } catch (err) {
73
+ return reply.status(503).send({ error: err.message });
74
+ }
75
+ });
76
+
77
+ fastify.put('/actions/:slug', canUpdate, async (request, reply) => {
78
+ try {
79
+ return await updateAction(request.params.slug, request.body || {});
80
+ } catch (err) {
81
+ const status = err.message.includes('not found') ? 404 : 400;
82
+ return reply.status(status).send({ error: err.message });
83
+ }
84
+ });
85
+
86
+ fastify.delete('/actions/:slug', canDelete, async (request, reply) => {
87
+ try {
88
+ await deleteAction(request.params.slug);
89
+ return { success: true };
90
+ } catch (err) {
91
+ const status = err.message.includes('not found') ? 404 : 400;
92
+ return reply.status(status).send({ error: err.message });
93
+ }
94
+ });
95
+
96
+ // -------------------------------------------------------------------------
97
+ // Execute (admin)
98
+ // -------------------------------------------------------------------------
99
+
100
+ fastify.post('/actions/:slug/execute', canRead, async (request, reply) => {
101
+ const { entryId } = request.body || {};
102
+ if (!entryId) return reply.status(400).send({ error: 'entryId is required' });
103
+
104
+ try {
105
+ const result = await executeAction(
106
+ request.params.slug,
107
+ entryId,
108
+ { user: request.user || null }
109
+ );
110
+ return result;
111
+ } catch (err) {
112
+ const status = err.statusCode === 403 ? 403
113
+ : err.message.includes('not found') ? 404 : 400;
114
+ return reply.status(status).send({ error: err.message });
115
+ }
116
+ });
117
+
118
+ // -------------------------------------------------------------------------
119
+ // Batch access check (used by admin UI to filter visible action buttons)
120
+ // -------------------------------------------------------------------------
121
+
122
+ fastify.post('/actions/:slug/check-access', canRead, async (request, reply) => {
123
+ const {entryIds} = request.body || {};
124
+ if (!Array.isArray(entryIds) || entryIds.length === 0) {
125
+ return reply.status(400).send({error: 'entryIds must be a non-empty array'});
126
+ }
127
+
128
+ let action;
129
+ try {
130
+ action = await getAction(request.params.slug);
131
+ } catch (err) {
132
+ return reply.status(503).send({error: err.message});
133
+ }
134
+ if (!action) return reply.status(404).send({error: 'Action not found'});
135
+
136
+ const rowLevel = action.access?.rowLevel;
137
+ const user = request.user;
138
+
139
+ // Admin or no row-level config → all IDs allowed
140
+ if (!rowLevel || getRoleLevel(user?.role) === 0) {
141
+ return {allowed: entryIds};
142
+ }
143
+
144
+ // Fetch entries and check access for each
145
+ const results = await Promise.all(
146
+ entryIds.map(async (id) => {
147
+ try {
148
+ const entry = await getEntry(action.collection, id);
149
+ return entry && checkEntryAccess(entry, user, rowLevel) ? id : null;
150
+ } catch {
151
+ return null;
152
+ }
153
+ })
154
+ );
155
+
156
+ return {allowed: results.filter(Boolean)};
157
+ });
158
+
159
+ // -------------------------------------------------------------------------
160
+ // Public execute (role-checked per action access config)
161
+ // -------------------------------------------------------------------------
162
+
163
+ fastify.post('/actions/:slug/public', async (request, reply) => {
164
+ let action;
165
+ try {
166
+ action = await getAction(request.params.slug);
167
+ } catch (err) {
168
+ return reply.status(503).send({ error: err.message });
169
+ }
170
+
171
+ if (!action) return reply.status(404).send({ error: 'Action not found' });
172
+
173
+ // Always require authentication for public action endpoints
174
+ try {
175
+ await request.jwtVerify();
176
+ } catch {
177
+ return reply.status(401).send({ error: 'Unauthorised' });
178
+ }
179
+
180
+ const user = request.user;
181
+ const allowedRoles = action.access?.roles || ['admin'];
182
+ const userLevel = getRoleLevel(user?.role);
183
+ const minAllowed = Math.min(...allowedRoles.map(r => getRoleLevel(r)));
184
+
185
+ if (userLevel > minAllowed) {
186
+ return reply.status(403).send({ error: 'Insufficient permissions' });
187
+ }
188
+
189
+ const { entryId } = request.body || {};
190
+ if (!entryId) return reply.status(400).send({ error: 'entryId is required' });
191
+
192
+ try {
193
+ return await executeAction(action.slug, entryId, { user });
194
+ } catch (err) {
195
+ const status = err.statusCode === 403 ? 403
196
+ : err.message.includes('not found') ? 404 : 400;
197
+ return reply.status(status).send({ error: err.message });
198
+ }
199
+ });
200
+ }