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
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
* PUT /api/plugins/:name - enable/disable, update settings (admin only)
|
|
5
5
|
* GET /api/plugins/admin-config - sidebar/routes/views for enabled plugins (authenticated)
|
|
6
6
|
*/
|
|
7
|
-
import { authenticate, requireAdmin } from '../../middleware/auth.js';
|
|
7
|
+
import { authenticate, requireAdmin, requirePermission } from '../../middleware/auth.js';
|
|
8
8
|
import { discoverPlugins, getPluginStates, savePluginState, getAdminPluginConfig } from '../../services/plugins.js';
|
|
9
9
|
|
|
10
10
|
export async function pluginsRoutes(fastify) {
|
|
11
|
+
const canRead = { preHandler: [authenticate, requirePermission('plugins', 'read')] };
|
|
12
|
+
const canUpdate = { preHandler: [authenticate, requirePermission('plugins', 'update')] };
|
|
13
|
+
|
|
11
14
|
// List all plugins with their current state
|
|
12
|
-
fastify.get('/plugins',
|
|
15
|
+
fastify.get('/plugins', canRead, async () => {
|
|
13
16
|
const manifests = await discoverPlugins();
|
|
14
17
|
const states = getPluginStates();
|
|
15
18
|
|
|
@@ -27,7 +30,7 @@ export async function pluginsRoutes(fastify) {
|
|
|
27
30
|
});
|
|
28
31
|
|
|
29
32
|
// Enable/disable or update settings for a plugin
|
|
30
|
-
fastify.put('/plugins/:name',
|
|
33
|
+
fastify.put('/plugins/:name', canUpdate, async (request, reply) => {
|
|
31
34
|
const { name } = request.params;
|
|
32
35
|
const { enabled, settings } = request.body || {};
|
|
33
36
|
|
|
@@ -1,88 +1,104 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Settings API
|
|
3
|
-
* GET /api/settings - get site settings
|
|
4
|
-
* PUT /api/settings - save site settings
|
|
5
|
-
* POST /api/settings/test-email - send a test email using stored SMTP config
|
|
6
|
-
*/
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import nodemailer from 'nodemailer';
|
|
10
|
-
import fs from 'fs/promises';
|
|
11
|
-
import path from 'path';
|
|
12
|
-
import {fileURLToPath} from 'url';
|
|
13
|
-
|
|
14
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
-
const CUSTOM_CSS_FILE = path.resolve(__dirname, '../../../content/custom.css');
|
|
16
|
-
const CUSTOM_CSS_MAX = 100 * 1024; // 100 KB
|
|
17
|
-
|
|
18
|
-
export async function settingsRoutes(fastify) {
|
|
19
|
-
const
|
|
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
|
-
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Settings API
|
|
3
|
+
* GET /api/settings - get site settings
|
|
4
|
+
* PUT /api/settings - save site settings
|
|
5
|
+
* POST /api/settings/test-email - send a test email using stored SMTP config
|
|
6
|
+
*/
|
|
7
|
+
import {getConfig, saveConfig} from '../../config.js';
|
|
8
|
+
import {authenticate, requirePermission} from '../../middleware/auth.js';
|
|
9
|
+
import nodemailer from 'nodemailer';
|
|
10
|
+
import fs from 'fs/promises';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import {fileURLToPath} from 'url';
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const CUSTOM_CSS_FILE = path.resolve(__dirname, '../../../content/custom.css');
|
|
16
|
+
const CUSTOM_CSS_MAX = 100 * 1024; // 100 KB
|
|
17
|
+
|
|
18
|
+
export async function settingsRoutes(fastify) {
|
|
19
|
+
const canRead = {preHandler: [authenticate, requirePermission('settings', 'read')]};
|
|
20
|
+
const canUpdate = {preHandler: [authenticate, requirePermission('settings', 'update')]};
|
|
21
|
+
|
|
22
|
+
fastify.get('/settings', canRead, async (request, reply) => {
|
|
23
|
+
const config = getConfig('site');
|
|
24
|
+
const safeConfig = { ...config };
|
|
25
|
+
if (safeConfig.smtp) {
|
|
26
|
+
safeConfig.smtp = { ...safeConfig.smtp, pass: '' };
|
|
27
|
+
}
|
|
28
|
+
return reply.send(safeConfig);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
fastify.put('/settings', canUpdate, async (request, reply) => {
|
|
32
|
+
const data = request.body;
|
|
33
|
+
if (!data || typeof data !== 'object') {
|
|
34
|
+
return reply.status(400).send({ error: 'Invalid settings data' });
|
|
35
|
+
}
|
|
36
|
+
const ALLOWED_KEYS = new Set([
|
|
37
|
+
'title', 'tagline', 'description', 'logo', 'favicon',
|
|
38
|
+
'theme', 'adminTheme', 'fontFamily', 'fontSize',
|
|
39
|
+
'smtp', 'footer', 'analytics', 'social', 'locale',
|
|
40
|
+
'layoutOptions', 'seo', 'backToTop', 'cookieConsent', 'breadcrumbs', 'autoTheme'
|
|
41
|
+
]);
|
|
42
|
+
const unknownKeys = Object.keys(data).filter(k => !ALLOWED_KEYS.has(k));
|
|
43
|
+
if (unknownKeys.length > 0) {
|
|
44
|
+
return reply.status(400).send({ error: `Unknown settings keys: ${unknownKeys.join(', ')}` });
|
|
45
|
+
}
|
|
46
|
+
saveConfig('site', data);
|
|
47
|
+
return { success: true };
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
fastify.post('/settings/test-email', canRead, async (request, reply) => {
|
|
51
|
+
const smtp = getConfig('site')?.smtp;
|
|
52
|
+
if (!smtp?.host) {
|
|
53
|
+
return reply.status(400).send({ error: 'SMTP is not configured. Save your SMTP settings first.' });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const transporter = nodemailer.createTransport({
|
|
57
|
+
host: smtp.host,
|
|
58
|
+
port: smtp.port || 587,
|
|
59
|
+
secure: smtp.secure || false,
|
|
60
|
+
auth: smtp.user ? { user: smtp.user, pass: smtp.pass } : undefined
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const to = request.body?.to || smtp.fromAddress;
|
|
64
|
+
if (!to) {
|
|
65
|
+
return reply.status(400).send({ error: 'No recipient address. Provide "to" in the request body or set a From Address.' });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
await transporter.sendMail({
|
|
70
|
+
from: smtp.fromName ? `"${smtp.fromName}" <${smtp.fromAddress}>` : smtp.fromAddress,
|
|
71
|
+
to,
|
|
72
|
+
subject: 'Domma CMS — Test Email',
|
|
73
|
+
text: 'This is a test email sent from Domma CMS to verify your SMTP configuration.',
|
|
74
|
+
html: '<p>This is a test email sent from <strong>Domma CMS</strong> to verify your SMTP configuration.</p>'
|
|
75
|
+
});
|
|
76
|
+
return { success: true, message: `Test email sent to ${to}` };
|
|
77
|
+
} catch (err) {
|
|
78
|
+
return reply.status(500).send({ error: `Failed to send email: ${err.message}` });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// GET /api/settings/custom-css — return current CSS as JSON
|
|
83
|
+
fastify.get('/settings/custom-css', canUpdate, async () => {
|
|
84
|
+
try {
|
|
85
|
+
const css = await fs.readFile(CUSTOM_CSS_FILE, 'utf8');
|
|
86
|
+
return { css };
|
|
87
|
+
} catch {
|
|
88
|
+
return { css: '' };
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// PUT /api/settings/custom-css — save CSS to content/custom.css
|
|
93
|
+
fastify.put('/settings/custom-css', canUpdate, async (request, reply) => {
|
|
94
|
+
const { css } = request.body || {};
|
|
95
|
+
if (typeof css !== 'string') {
|
|
96
|
+
return reply.status(400).send({ error: 'css must be a string.' });
|
|
97
|
+
}
|
|
98
|
+
if (Buffer.byteLength(css, 'utf8') > CUSTOM_CSS_MAX) {
|
|
99
|
+
return reply.status(400).send({ error: 'CSS exceeds the 100 KB limit.' });
|
|
100
|
+
}
|
|
101
|
+
await fs.writeFile(CUSTOM_CSS_FILE, css, 'utf8');
|
|
102
|
+
return { success: true };
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -6,22 +6,21 @@
|
|
|
6
6
|
* PUT /api/users/:id - update user (admin, manager — manager cannot edit admin)
|
|
7
7
|
* DELETE /api/users/:id - delete user (admin, manager — manager cannot delete admin, no self-delete)
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
getUserById,
|
|
14
|
-
createUser,
|
|
15
|
-
updateUser,
|
|
16
|
-
deleteUser
|
|
17
|
-
} from '../../services/users.js';
|
|
9
|
+
import {authenticate, canManageUser, requirePermission} from '../../middleware/auth.js';
|
|
10
|
+
import {getPermissionsFor} from '../../services/roles.js';
|
|
11
|
+
import {createUser, deleteUser, getUserById, listUsers, updateUser} from '../../services/users.js';
|
|
12
|
+
import {getProfile, listProfiles, updateProfile} from '../../services/userProfiles.js';
|
|
18
13
|
|
|
19
14
|
export async function usersRoutes(fastify) {
|
|
20
|
-
const
|
|
15
|
+
const canRead = [authenticate, requirePermission('users', 'read')];
|
|
16
|
+
const canCreate = [authenticate, requirePermission('users', 'create')];
|
|
17
|
+
const canUpdate = [authenticate, requirePermission('users', 'update')];
|
|
18
|
+
const canDelete = [authenticate, requirePermission('users', 'delete')];
|
|
21
19
|
|
|
22
20
|
// List all users
|
|
23
|
-
fastify.get('/users', {
|
|
24
|
-
|
|
21
|
+
fastify.get('/users', {preHandler: canRead}, async () => {
|
|
22
|
+
const [users, profiles] = await Promise.all([listUsers(), listProfiles()]);
|
|
23
|
+
return users.map(u => ({...u, profile: profiles.get(u.id) || {}}));
|
|
25
24
|
});
|
|
26
25
|
|
|
27
26
|
// Get single user (admin, manager, or the user themselves)
|
|
@@ -30,7 +29,7 @@ export async function usersRoutes(fastify) {
|
|
|
30
29
|
const actor = request.user;
|
|
31
30
|
|
|
32
31
|
const isSelf = actor.id === id;
|
|
33
|
-
const canManage = getPermissionsFor('users').includes(actor.role);
|
|
32
|
+
const canManage = getPermissionsFor('users', 'read').includes(actor.role);
|
|
34
33
|
|
|
35
34
|
if (!isSelf && !canManage) {
|
|
36
35
|
return reply.code(403).send({ error: 'Forbidden' });
|
|
@@ -38,11 +37,12 @@ export async function usersRoutes(fastify) {
|
|
|
38
37
|
|
|
39
38
|
const user = await getUserById(id);
|
|
40
39
|
if (!user) return reply.status(404).send({ error: 'User not found' });
|
|
41
|
-
|
|
40
|
+
const profileEntry = await getProfile(id);
|
|
41
|
+
return {...user, profile: profileEntry?.data || {}};
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
// Create user
|
|
45
|
-
fastify.post('/users', {
|
|
45
|
+
fastify.post('/users', {preHandler: canCreate}, async (request, reply) => {
|
|
46
46
|
const { name, email, password, role } = request.body || {};
|
|
47
47
|
if (!name || !email || !password) {
|
|
48
48
|
return reply.status(400).send({ error: 'name, email and password are required' });
|
|
@@ -65,7 +65,7 @@ export async function usersRoutes(fastify) {
|
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
// Update user
|
|
68
|
-
fastify.put('/users/:id', {
|
|
68
|
+
fastify.put('/users/:id', {preHandler: canUpdate}, async (request, reply) => {
|
|
69
69
|
const { id } = request.params;
|
|
70
70
|
const actor = request.user;
|
|
71
71
|
|
|
@@ -82,12 +82,20 @@ export async function usersRoutes(fastify) {
|
|
|
82
82
|
return reply.code(403).send({ error: 'You cannot assign that role' });
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
// Strip profile from core updates and handle separately
|
|
86
|
+
const {profile, ...coreUpdates} = updates;
|
|
87
|
+
const user = await updateUser(id, coreUpdates);
|
|
88
|
+
|
|
89
|
+
if (profile && typeof profile === 'object') {
|
|
90
|
+
await updateProfile(id, profile);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const profileEntry = await getProfile(id);
|
|
94
|
+
return {...user, profile: profileEntry?.data || {}};
|
|
87
95
|
});
|
|
88
96
|
|
|
89
97
|
// Delete user
|
|
90
|
-
fastify.delete('/users/:id', {
|
|
98
|
+
fastify.delete('/users/:id', {preHandler: canDelete}, async (request, reply) => {
|
|
91
99
|
const { id } = request.params;
|
|
92
100
|
const actor = request.user;
|
|
93
101
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Views API (Pro — requires MongoDB)
|
|
3
|
+
*
|
|
4
|
+
* Admin endpoints (authenticated + views permission):
|
|
5
|
+
* GET /views - List all view configs
|
|
6
|
+
* POST /views - Create view config
|
|
7
|
+
* GET /views/:slug - Get view config
|
|
8
|
+
* PUT /views/:slug - Update view config
|
|
9
|
+
* DELETE /views/:slug - Delete view config
|
|
10
|
+
* GET /views/:slug/execute - Execute view (admin)
|
|
11
|
+
* GET /views/collection/:slug - List views targeting a collection
|
|
12
|
+
*
|
|
13
|
+
* Public endpoint (role-checked per view access config):
|
|
14
|
+
* GET /views/:slug/public - Execute view publicly
|
|
15
|
+
*/
|
|
16
|
+
import {createView, deleteView, executeView, getView, listViews, updateView} from '../../services/views.js';
|
|
17
|
+
import {authenticate, requirePermission} from '../../middleware/auth.js';
|
|
18
|
+
import {getRoleLevel} from '../../services/roles.js';
|
|
19
|
+
|
|
20
|
+
export async function viewsRoutes(fastify) {
|
|
21
|
+
const canRead = {preHandler: [authenticate, requirePermission('views', 'read')]};
|
|
22
|
+
const canCreate = {preHandler: [authenticate, requirePermission('views', 'create')]};
|
|
23
|
+
const canUpdate = {preHandler: [authenticate, requirePermission('views', 'update')]};
|
|
24
|
+
const canDelete = {preHandler: [authenticate, requirePermission('views', 'delete')]};
|
|
25
|
+
|
|
26
|
+
// -------------------------------------------------------------------------
|
|
27
|
+
// Admin CRUD
|
|
28
|
+
// -------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
fastify.get('/views', canRead, async (request, reply) => {
|
|
31
|
+
try {
|
|
32
|
+
return await listViews();
|
|
33
|
+
} catch (err) {
|
|
34
|
+
return reply.status(503).send({ error: err.message });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
fastify.post('/views', canCreate, async (request, reply) => {
|
|
39
|
+
try {
|
|
40
|
+
const view = await createView(request.body || {}, request.user?.id || null);
|
|
41
|
+
return reply.status(201).send(view);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
const status = err.message.includes('already exists') ? 409 : 400;
|
|
44
|
+
return reply.status(status).send({ error: err.message });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Static sub-routes must be declared before the parameterised :slug route
|
|
49
|
+
// so Fastify's radix router gives them priority.
|
|
50
|
+
fastify.get('/views/collection/:collectionSlug', canRead, async (request, reply) => {
|
|
51
|
+
try {
|
|
52
|
+
const allViews = await listViews();
|
|
53
|
+
return allViews.filter(v => v.pipeline?.source === request.params.collectionSlug);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
return reply.status(503).send({ error: err.message });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
fastify.get('/views/:slug', canRead, async (request, reply) => {
|
|
60
|
+
try {
|
|
61
|
+
const view = await getView(request.params.slug);
|
|
62
|
+
if (!view) return reply.status(404).send({ error: 'View not found' });
|
|
63
|
+
return view;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
return reply.status(503).send({ error: err.message });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
fastify.put('/views/:slug', canUpdate, async (request, reply) => {
|
|
70
|
+
try {
|
|
71
|
+
return await updateView(request.params.slug, request.body || {});
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const status = err.message.includes('not found') ? 404 : 400;
|
|
74
|
+
return reply.status(status).send({ error: err.message });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
fastify.delete('/views/:slug', canDelete, async (request, reply) => {
|
|
79
|
+
try {
|
|
80
|
+
await deleteView(request.params.slug);
|
|
81
|
+
return { success: true };
|
|
82
|
+
} catch (err) {
|
|
83
|
+
const status = err.message.includes('not found') ? 404 : 400;
|
|
84
|
+
return reply.status(status).send({ error: err.message });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// -------------------------------------------------------------------------
|
|
89
|
+
// Execute (admin)
|
|
90
|
+
// -------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
fastify.get('/views/:slug/execute', canRead, async (request, reply) => {
|
|
93
|
+
const { page, limit } = request.query;
|
|
94
|
+
try {
|
|
95
|
+
return await executeView(request.params.slug, {
|
|
96
|
+
page: parseInt(page, 10) || 1,
|
|
97
|
+
limit: parseInt(limit, 10) || 25,
|
|
98
|
+
user: request.user || null
|
|
99
|
+
});
|
|
100
|
+
} catch (err) {
|
|
101
|
+
const status = err.message.includes('not found') ? 404 : 400;
|
|
102
|
+
return reply.status(status).send({ error: err.message });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// -------------------------------------------------------------------------
|
|
107
|
+
// Public execute (role-checked per view access config)
|
|
108
|
+
// -------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
fastify.get('/views/:slug/public', async (request, reply) => {
|
|
111
|
+
let view;
|
|
112
|
+
try {
|
|
113
|
+
view = await getView(request.params.slug);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return reply.status(503).send({ error: err.message });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!view) return reply.status(404).send({ error: 'View not found' });
|
|
119
|
+
|
|
120
|
+
if (!view.access?.public) {
|
|
121
|
+
try {
|
|
122
|
+
await request.jwtVerify();
|
|
123
|
+
} catch {
|
|
124
|
+
return reply.status(401).send({ error: 'Unauthorised' });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const user = request.user;
|
|
128
|
+
const allowedRoles = view.access?.roles || ['admin'];
|
|
129
|
+
const userLevel = getRoleLevel(user?.role);
|
|
130
|
+
const minAllowed = Math.min(...allowedRoles.map(r => getRoleLevel(r)));
|
|
131
|
+
|
|
132
|
+
if (userLevel > minAllowed) {
|
|
133
|
+
return reply.status(403).send({ error: 'Insufficient permissions' });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const { page, limit } = request.query;
|
|
138
|
+
try {
|
|
139
|
+
return await executeView(view.slug, {
|
|
140
|
+
page: parseInt(page, 10) || 1,
|
|
141
|
+
limit: parseInt(limit, 10) || 25,
|
|
142
|
+
user: request.user || null
|
|
143
|
+
});
|
|
144
|
+
} catch (err) {
|
|
145
|
+
return reply.status(400).send({ error: err.message });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|