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.
- package/README.md +3 -3
- package/admin/css/admin.css +1 -1
- package/admin/dist/domma/domma-tools.css +2313 -0
- package/admin/dist/domma/domma-tools.min.js +10 -0
- package/admin/index.html +4 -0
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +8 -4
- package/admin/js/config/sidebar-config.js +1 -1
- package/admin/js/lib/markdown-toolbar.js +18 -10
- package/admin/js/templates/action-editor.html +171 -0
- package/admin/js/templates/actions-list.html +19 -0
- package/admin/js/templates/api-reference.html +1411 -0
- package/admin/js/templates/block-editor.html +158 -0
- package/admin/js/templates/blocks.html +8 -0
- package/admin/js/templates/collection-editor.html +47 -0
- package/admin/js/templates/collection-entries.html +3 -0
- package/admin/js/templates/collections.html +51 -4
- package/admin/js/templates/documentation.html +258 -0
- package/{plugins/form-builder/admin → admin/js}/templates/form-editor.html +238 -199
- package/{plugins/form-builder/admin → admin/js}/templates/form-submissions.html +30 -30
- package/{plugins/form-builder/admin/templates/forms-list.html → admin/js/templates/forms.html} +17 -17
- package/admin/js/templates/login.html +29 -4
- package/admin/js/templates/my-profile.html +17 -0
- package/admin/js/templates/page-editor.html +39 -0
- package/admin/js/templates/pages.html +6 -1
- package/admin/js/templates/pro-docs.html +259 -0
- package/admin/js/templates/role-editor.html +59 -0
- package/admin/js/templates/roles.html +10 -0
- package/admin/js/templates/settings.html +167 -23
- package/admin/js/templates/tutorials.html +81 -0
- package/admin/js/templates/user-editor.html +7 -0
- package/admin/js/templates/users.html +3 -26
- package/admin/js/templates/view-editor.html +201 -0
- package/admin/js/templates/view-preview.html +51 -0
- package/admin/js/templates/views-list.html +19 -0
- package/admin/js/views/action-editor.js +1 -0
- package/admin/js/views/actions-list.js +1 -0
- package/admin/js/views/api-reference.js +1 -0
- package/admin/js/views/block-editor.js +8 -0
- package/admin/js/views/blocks.js +4 -0
- package/admin/js/views/collection-editor.js +3 -3
- package/admin/js/views/collection-entries.js +1 -1
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/dashboard.js +1 -1
- package/admin/js/views/form-editor.js +8 -0
- package/admin/js/views/form-submissions.js +1 -0
- package/admin/js/views/forms.js +1 -0
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/login.js +2 -2
- package/admin/js/views/media.js +1 -1
- package/admin/js/views/my-profile.js +1 -0
- package/admin/js/views/page-editor.js +34 -15
- package/admin/js/views/pages.js +5 -5
- package/admin/js/views/plugins.js +10 -10
- package/admin/js/views/pro-docs.js +1 -0
- package/admin/js/views/role-editor.js +1 -0
- package/admin/js/views/roles.js +4 -0
- package/admin/js/views/settings.js +3 -1
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +4 -7
- package/admin/js/views/view-editor.js +1 -0
- package/admin/js/views/view-preview.js +1 -0
- package/admin/js/views/views-list.js +1 -0
- package/bin/cli.js +1 -1
- package/config/auth.json +1 -0
- package/config/connections.json.bak +9 -0
- package/config/connections.json.example +9 -0
- package/config/navigation.json +5 -15
- package/config/plugins.json +19 -29
- package/config/server.json +6 -6
- package/config/site.json +16 -6
- package/package.json +25 -10
- package/plugins/example-analytics/stats.json +17 -12
- package/plugins/form-builder/data/forms/contacts.json +62 -62
- package/plugins/form-builder/data/forms/enquiries.json +103 -0
- package/plugins/form-builder/data/forms/feedback.json +17 -16
- package/plugins/form-builder/data/forms/notes.json +79 -0
- package/plugins/form-builder/data/forms/to-do.json +100 -0
- package/plugins/form-builder/data/submissions/contacts.json +1 -26
- package/plugins/form-builder/data/submissions/notes.json +1 -0
- package/plugins/form-builder/data/submissions/to-do.json +1 -0
- package/plugins/theme-roller/admin/templates/theme-roller.html +71 -0
- package/plugins/theme-roller/admin/views/theme-roller-view.js +403 -0
- package/plugins/theme-roller/config.js +1 -0
- package/plugins/theme-roller/plugin.js +233 -0
- package/plugins/theme-roller/plugin.json +31 -0
- package/plugins/theme-roller/public/active-theme.css +0 -0
- package/plugins/theme-roller/public/inject-head-late.html +1 -0
- package/public/css/forms.css +1 -0
- package/public/css/site.css +1 -1
- package/public/js/forms.js +1 -0
- package/public/js/site.js +1 -1
- package/scripts/build.js +194 -129
- package/scripts/pro.js +254 -0
- package/scripts/reset.js +33 -8
- package/scripts/seed.js +677 -128
- package/scripts/setup.js +1 -0
- package/server/middleware/auth.js +136 -120
- package/server/routes/api/actions.js +200 -0
- package/server/routes/api/auth.js +292 -146
- package/server/routes/api/blocks.js +84 -0
- package/server/routes/api/collections.js +79 -27
- package/{plugins/form-builder/plugin.js → server/routes/api/forms.js} +491 -505
- package/server/routes/api/layouts.js +49 -39
- package/server/routes/api/media.js +118 -92
- package/server/routes/api/navigation.js +40 -36
- package/server/routes/api/pages.js +132 -118
- package/server/routes/api/plugins.js +6 -3
- package/server/routes/api/settings.js +104 -88
- package/server/routes/api/users.js +27 -19
- package/server/routes/api/views.js +148 -0
- package/server/routes/public.js +124 -108
- package/server/server.js +269 -181
- package/server/services/actions.js +387 -0
- package/server/services/adapterRegistry.js +98 -0
- package/server/services/adapters/FileAdapter.js +192 -0
- package/server/services/adapters/MongoAdapter.js +220 -0
- package/server/services/blocks.js +162 -0
- package/server/services/collections.js +74 -86
- package/server/services/connectionManager.js +102 -0
- package/server/services/content.js +312 -307
- package/server/services/email.js +126 -0
- package/server/services/forms.js +173 -0
- package/server/services/markdown.js +1378 -747
- package/server/services/permissionRegistry.js +173 -0
- package/server/services/presetCollections.js +251 -0
- package/server/services/renderer.js +98 -2
- package/server/services/roles.js +227 -0
- package/server/services/rowAccess.js +104 -0
- package/server/services/userProfiles.js +199 -0
- package/server/services/users.js +281 -212
- package/server/services/views.js +280 -0
- package/server/templates/page.html +124 -113
- package/plugins/form-builder/admin/templates/form-settings.html +0 -29
- package/plugins/form-builder/admin/views/form-editor.js +0 -1444
- package/plugins/form-builder/admin/views/form-settings.js +0 -38
- package/plugins/form-builder/admin/views/form-submissions.js +0 -295
- package/plugins/form-builder/admin/views/forms-list.js +0 -164
- package/plugins/form-builder/config.js +0 -9
- package/plugins/form-builder/data/forms/consent.json +0 -104
- package/plugins/form-builder/data/forms/contact-details.json +0 -99
- package/plugins/form-builder/data/submissions/consent.json +0 -13
- package/plugins/form-builder/plugin.json +0 -52
- package/plugins/form-builder/public/inject-body.html +0 -352
- package/plugins/form-builder/public/inject-head.html +0 -58
- package/plugins/form-builder/public/package.json +0 -1
- package/scripts/copy-domma.js +0 -48
- package/server/services/userTypes.js +0 -167
- /package/plugins/form-builder/data/submissions/{contact-details.json → enquiries.json} +0 -0
- /package/{plugins/form-builder/public → public/js}/form-logic-engine.js +0 -0
package/scripts/setup.js
CHANGED
|
@@ -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
|
|
5
|
-
*/
|
|
6
|
-
import {
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* @param {
|
|
105
|
-
* @
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
+
}
|