domma-cms 0.3.0 → 0.5.2

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 (150) hide show
  1. package/README.md +3 -3
  2. package/admin/css/admin.css +1 -1
  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 -1
  7. package/admin/js/app.js +8 -4
  8. package/admin/js/config/sidebar-config.js +1 -1
  9. package/admin/js/lib/markdown-toolbar.js +18 -10
  10. package/admin/js/templates/action-editor.html +171 -0
  11. package/admin/js/templates/actions-list.html +19 -0
  12. package/admin/js/templates/api-reference.html +1411 -0
  13. package/admin/js/templates/block-editor.html +158 -0
  14. package/admin/js/templates/blocks.html +8 -0
  15. package/admin/js/templates/collection-editor.html +47 -0
  16. package/admin/js/templates/collection-entries.html +3 -0
  17. package/admin/js/templates/collections.html +51 -4
  18. package/admin/js/templates/documentation.html +258 -0
  19. package/{plugins/form-builder/admin → admin/js}/templates/form-editor.html +238 -199
  20. package/{plugins/form-builder/admin → admin/js}/templates/form-submissions.html +30 -30
  21. package/{plugins/form-builder/admin/templates/forms-list.html → admin/js/templates/forms.html} +17 -17
  22. package/admin/js/templates/login.html +29 -4
  23. package/admin/js/templates/my-profile.html +17 -0
  24. package/admin/js/templates/page-editor.html +39 -0
  25. package/admin/js/templates/pages.html +6 -1
  26. package/admin/js/templates/pro-docs.html +259 -0
  27. package/admin/js/templates/role-editor.html +59 -0
  28. package/admin/js/templates/roles.html +10 -0
  29. package/admin/js/templates/settings.html +167 -23
  30. package/admin/js/templates/tutorials.html +81 -0
  31. package/admin/js/templates/user-editor.html +7 -0
  32. package/admin/js/templates/users.html +3 -26
  33. package/admin/js/templates/view-editor.html +201 -0
  34. package/admin/js/templates/view-preview.html +51 -0
  35. package/admin/js/templates/views-list.html +19 -0
  36. package/admin/js/views/action-editor.js +1 -0
  37. package/admin/js/views/actions-list.js +1 -0
  38. package/admin/js/views/api-reference.js +1 -0
  39. package/admin/js/views/block-editor.js +8 -0
  40. package/admin/js/views/blocks.js +4 -0
  41. package/admin/js/views/collection-editor.js +3 -3
  42. package/admin/js/views/collection-entries.js +1 -1
  43. package/admin/js/views/collections.js +1 -1
  44. package/admin/js/views/dashboard.js +1 -1
  45. package/admin/js/views/form-editor.js +8 -0
  46. package/admin/js/views/form-submissions.js +1 -0
  47. package/admin/js/views/forms.js +1 -0
  48. package/admin/js/views/index.js +1 -1
  49. package/admin/js/views/login.js +2 -2
  50. package/admin/js/views/media.js +1 -1
  51. package/admin/js/views/my-profile.js +1 -0
  52. package/admin/js/views/page-editor.js +34 -15
  53. package/admin/js/views/pages.js +5 -5
  54. package/admin/js/views/plugins.js +10 -10
  55. package/admin/js/views/pro-docs.js +1 -0
  56. package/admin/js/views/role-editor.js +1 -0
  57. package/admin/js/views/roles.js +4 -0
  58. package/admin/js/views/settings.js +3 -1
  59. package/admin/js/views/user-editor.js +1 -1
  60. package/admin/js/views/users.js +4 -7
  61. package/admin/js/views/view-editor.js +1 -0
  62. package/admin/js/views/view-preview.js +1 -0
  63. package/admin/js/views/views-list.js +1 -0
  64. package/bin/cli.js +1 -1
  65. package/config/auth.json +1 -0
  66. package/config/connections.json.bak +9 -0
  67. package/config/connections.json.example +9 -0
  68. package/config/navigation.json +5 -15
  69. package/config/plugins.json +19 -29
  70. package/config/server.json +6 -6
  71. package/config/site.json +16 -6
  72. package/package.json +25 -10
  73. package/plugins/example-analytics/stats.json +17 -12
  74. package/plugins/form-builder/data/forms/contacts.json +62 -62
  75. package/plugins/form-builder/data/forms/enquiries.json +103 -0
  76. package/plugins/form-builder/data/forms/feedback.json +17 -16
  77. package/plugins/form-builder/data/forms/notes.json +79 -0
  78. package/plugins/form-builder/data/forms/to-do.json +100 -0
  79. package/plugins/form-builder/data/submissions/contacts.json +1 -26
  80. package/plugins/form-builder/data/submissions/notes.json +1 -0
  81. package/plugins/form-builder/data/submissions/to-do.json +1 -0
  82. package/plugins/theme-roller/admin/templates/theme-roller.html +71 -0
  83. package/plugins/theme-roller/admin/views/theme-roller-view.js +403 -0
  84. package/plugins/theme-roller/config.js +1 -0
  85. package/plugins/theme-roller/plugin.js +233 -0
  86. package/plugins/theme-roller/plugin.json +31 -0
  87. package/plugins/theme-roller/public/active-theme.css +0 -0
  88. package/plugins/theme-roller/public/inject-head-late.html +1 -0
  89. package/public/css/forms.css +1 -0
  90. package/public/css/site.css +1 -1
  91. package/public/js/forms.js +1 -0
  92. package/public/js/site.js +1 -1
  93. package/scripts/build.js +194 -129
  94. package/scripts/pro.js +254 -0
  95. package/scripts/reset.js +33 -8
  96. package/scripts/seed.js +677 -128
  97. package/scripts/setup.js +1 -0
  98. package/server/middleware/auth.js +136 -120
  99. package/server/routes/api/actions.js +200 -0
  100. package/server/routes/api/auth.js +292 -146
  101. package/server/routes/api/blocks.js +84 -0
  102. package/server/routes/api/collections.js +79 -27
  103. package/{plugins/form-builder/plugin.js → server/routes/api/forms.js} +491 -505
  104. package/server/routes/api/layouts.js +49 -39
  105. package/server/routes/api/media.js +118 -92
  106. package/server/routes/api/navigation.js +40 -36
  107. package/server/routes/api/pages.js +132 -118
  108. package/server/routes/api/plugins.js +6 -3
  109. package/server/routes/api/settings.js +104 -88
  110. package/server/routes/api/users.js +27 -19
  111. package/server/routes/api/views.js +148 -0
  112. package/server/routes/public.js +124 -108
  113. package/server/server.js +269 -181
  114. package/server/services/actions.js +387 -0
  115. package/server/services/adapterRegistry.js +98 -0
  116. package/server/services/adapters/FileAdapter.js +192 -0
  117. package/server/services/adapters/MongoAdapter.js +220 -0
  118. package/server/services/blocks.js +162 -0
  119. package/server/services/collections.js +74 -86
  120. package/server/services/connectionManager.js +102 -0
  121. package/server/services/content.js +312 -307
  122. package/server/services/email.js +126 -0
  123. package/server/services/forms.js +173 -0
  124. package/server/services/markdown.js +1378 -747
  125. package/server/services/permissionRegistry.js +173 -0
  126. package/server/services/presetCollections.js +251 -0
  127. package/server/services/renderer.js +98 -2
  128. package/server/services/roles.js +227 -0
  129. package/server/services/rowAccess.js +104 -0
  130. package/server/services/userProfiles.js +199 -0
  131. package/server/services/users.js +281 -212
  132. package/server/services/views.js +280 -0
  133. package/server/templates/page.html +124 -113
  134. package/plugins/form-builder/admin/templates/form-settings.html +0 -29
  135. package/plugins/form-builder/admin/views/form-editor.js +0 -1444
  136. package/plugins/form-builder/admin/views/form-settings.js +0 -38
  137. package/plugins/form-builder/admin/views/form-submissions.js +0 -295
  138. package/plugins/form-builder/admin/views/forms-list.js +0 -164
  139. package/plugins/form-builder/config.js +0 -9
  140. package/plugins/form-builder/data/forms/consent.json +0 -104
  141. package/plugins/form-builder/data/forms/contact-details.json +0 -99
  142. package/plugins/form-builder/data/submissions/consent.json +0 -13
  143. package/plugins/form-builder/plugin.json +0 -52
  144. package/plugins/form-builder/public/inject-body.html +0 -352
  145. package/plugins/form-builder/public/inject-head.html +0 -58
  146. package/plugins/form-builder/public/package.json +0 -1
  147. package/scripts/copy-domma.js +0 -48
  148. package/server/services/userTypes.js +0 -167
  149. /package/plugins/form-builder/data/submissions/{contact-details.json → enquiries.json} +0 -0
  150. /package/{plugins/form-builder/public → public/js}/form-logic-engine.js +0 -0
package/scripts/setup.js CHANGED
@@ -39,6 +39,7 @@ const THEMES = [
39
39
  'lemon-dark', 'lemon-light',
40
40
  'silver-dark', 'silver-light',
41
41
  'grayve-dark', 'grayve-light',
42
+ 'mint-dark', 'mint-light',
42
43
  'christmas-dark', 'christmas-light',
43
44
  'unicorn-dark', 'unicorn-light',
44
45
  'dreamy-dark', 'dreamy-light',
@@ -1,120 +1,136 @@
1
- /**
2
- * Authentication Middleware
3
- * JWT-based authentication with role guards for Domma CMS.
4
- * Role data is read from the userTypes cache (not config.auth.roles).
5
- */
6
- import { getRoleLevel, getRoleHierarchy, getPermissionsFor } from '../services/userTypes.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
- await request.jwtVerify();
18
- } catch {
19
- return reply.code(401).send({
20
- statusCode: 401,
21
- error: 'Unauthorised',
22
- message: 'Invalid or missing authentication token'
23
- });
24
- }
25
- }
26
-
27
- /**
28
- * Return a preHandler that enforces one of the specified roles.
29
- * Must be used after authenticate.
30
- *
31
- * @param {string[]} allowedRoles
32
- * @returns {Function}
33
- */
34
- export function requireRole(allowedRoles) {
35
- const allowed = Array.isArray(allowedRoles) ? allowedRoles : [allowedRoles];
36
-
37
- return async (request, reply) => {
38
- if (!request.user) {
39
- return reply.code(401).send({
40
- statusCode: 401,
41
- error: 'Unauthorised',
42
- message: 'Authentication required'
43
- });
44
- }
45
-
46
- if (!allowed.includes(request.user.role)) {
47
- return reply.code(403).send({
48
- statusCode: 403,
49
- error: 'Forbidden',
50
- message: `Access denied. Required role: ${allowed.join(' or ')}`
51
- });
52
- }
53
- };
54
- }
55
-
56
- /**
57
- * Return a preHandler that checks the current user's role has access to a resource.
58
- * Reads from the userTypes cache at request time — reflects live role changes.
59
- *
60
- * @param {string} resource - Resource key (e.g. 'pages', 'users')
61
- * @returns {Function}
62
- */
63
- export function requirePermission(resource) {
64
- return async (request, reply) => {
65
- if (!request.user) {
66
- return reply.code(401).send({
67
- statusCode: 401,
68
- error: 'Unauthorised',
69
- message: 'Authentication required'
70
- });
71
- }
72
-
73
- const allowed = getPermissionsFor(resource);
74
- if (!allowed.includes(request.user.role)) {
75
- return reply.code(403).send({
76
- statusCode: 403,
77
- error: 'Forbidden',
78
- message: 'Insufficient permissions'
79
- });
80
- }
81
- };
82
- }
83
-
84
- /**
85
- * Shorthand preHandler — level-0 role (admin) only.
86
- *
87
- * @param {FastifyRequest} request
88
- * @param {FastifyReply} reply
89
- * @returns {Promise<void>}
90
- */
91
- export async function requireAdmin(request, reply) {
92
- if (!request.user) {
93
- return reply.code(401).send({ statusCode: 401, error: 'Unauthorised', message: 'Authentication required' });
94
- }
95
- if (getRoleLevel(request.user.role) !== 0) {
96
- return reply.code(403).send({ statusCode: 403, error: 'Forbidden', message: 'Admin access required' });
97
- }
98
- }
99
-
100
- /**
101
- * Determine whether an actor can manage a target user.
102
- * Managers cannot create, edit, or delete users with a lower level number (higher privilege).
103
- *
104
- * @param {string} actorRole - Role of the user performing the action
105
- * @param {string} targetRole - Role of the user being acted upon
106
- * @returns {boolean}
107
- */
108
- export function canManageUser(actorRole, targetRole) {
109
- return getRoleLevel(actorRole) < getRoleLevel(targetRole);
110
- }
111
-
112
- /**
113
- * Return role names ordered from most to least privileged.
114
- * Computed from the userTypes cache.
115
- *
116
- * @returns {string[]}
117
- */
118
- export function getRoleHierarchyList() {
119
- return getRoleHierarchy();
120
- }
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
+ }