domma-cms 0.1.0 → 0.2.1

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 (89) hide show
  1. package/README.md +2 -3
  2. package/admin/css/admin.css +78 -1
  3. package/admin/js/api.js +32 -0
  4. package/admin/js/app.js +24 -7
  5. package/admin/js/config/sidebar-config.js +8 -0
  6. package/admin/js/templates/collection-editor.html +80 -0
  7. package/admin/js/templates/collection-entries.html +36 -0
  8. package/admin/js/templates/collections.html +12 -0
  9. package/admin/js/templates/documentation.html +136 -0
  10. package/admin/js/templates/navigation.html +26 -4
  11. package/admin/js/templates/page-editor.html +91 -85
  12. package/admin/js/templates/settings.html +433 -172
  13. package/admin/js/views/collection-editor.js +487 -0
  14. package/admin/js/views/collection-entries.js +484 -0
  15. package/admin/js/views/collections.js +153 -0
  16. package/admin/js/views/dashboard.js +14 -6
  17. package/admin/js/views/index.js +9 -3
  18. package/admin/js/views/login.js +3 -2
  19. package/admin/js/views/navigation.js +77 -11
  20. package/admin/js/views/page-editor.js +207 -25
  21. package/admin/js/views/pages.js +14 -6
  22. package/admin/js/views/settings.js +137 -2
  23. package/admin/js/views/users.js +10 -7
  24. package/bin/cli.js +53 -17
  25. package/config/auth.json +2 -1
  26. package/config/content.json +1 -0
  27. package/config/navigation.json +14 -4
  28. package/config/plugins.json +0 -18
  29. package/config/presets.json +4 -8
  30. package/config/site.json +44 -3
  31. package/package.json +6 -2
  32. package/plugins/domma-effects/admin/templates/domma-effects.html +92 -3
  33. package/plugins/domma-effects/plugin.js +125 -0
  34. package/plugins/domma-effects/public/inject-body.html +19 -0
  35. package/plugins/example-analytics/admin/views/analytics.js +2 -2
  36. package/plugins/example-analytics/plugin.json +8 -0
  37. package/plugins/example-analytics/stats.json +15 -1
  38. package/plugins/form-builder/admin/templates/form-editor.html +19 -6
  39. package/plugins/form-builder/admin/views/form-editor.js +634 -9
  40. package/plugins/form-builder/admin/views/form-submissions.js +4 -4
  41. package/plugins/form-builder/admin/views/forms-list.js +5 -5
  42. package/plugins/form-builder/data/forms/consent.json +104 -0
  43. package/plugins/form-builder/data/forms/contacts.json +66 -0
  44. package/plugins/form-builder/data/submissions/consent.json +13 -0
  45. package/plugins/form-builder/data/submissions/contacts.json +26 -0
  46. package/plugins/form-builder/plugin.js +62 -11
  47. package/plugins/form-builder/plugin.json +12 -16
  48. package/plugins/form-builder/public/form-logic-engine.js +568 -0
  49. package/plugins/form-builder/public/inject-body.html +88 -6
  50. package/plugins/form-builder/public/inject-head.html +16 -0
  51. package/plugins/form-builder/public/package.json +1 -0
  52. package/public/css/site.css +113 -0
  53. package/public/js/btt.js +90 -0
  54. package/public/js/cookie-consent.js +61 -0
  55. package/public/js/site.js +129 -34
  56. package/scripts/build.js +129 -0
  57. package/scripts/seed.js +517 -7
  58. package/scripts/setup.js +12 -9
  59. package/server/routes/api/collections.js +301 -0
  60. package/server/routes/api/settings.js +66 -2
  61. package/server/server.js +19 -15
  62. package/server/services/collections.js +430 -0
  63. package/server/services/content.js +11 -2
  64. package/server/services/hooks.js +109 -0
  65. package/server/services/markdown.js +500 -149
  66. package/server/services/plugins.js +6 -1
  67. package/server/services/renderer.js +73 -7
  68. package/server/templates/page.html +38 -3
  69. package/plugins/back-to-top/admin/templates/back-to-top-settings.html +0 -55
  70. package/plugins/back-to-top/admin/views/back-to-top-settings.js +0 -44
  71. package/plugins/back-to-top/config.js +0 -10
  72. package/plugins/back-to-top/plugin.js +0 -24
  73. package/plugins/back-to-top/plugin.json +0 -36
  74. package/plugins/back-to-top/public/inject-body.html +0 -105
  75. package/plugins/cookie-consent/admin/templates/cookie-consent-settings.html +0 -113
  76. package/plugins/cookie-consent/admin/views/cookie-consent-settings.js +0 -73
  77. package/plugins/cookie-consent/config.js +0 -30
  78. package/plugins/cookie-consent/plugin.js +0 -24
  79. package/plugins/cookie-consent/plugin.json +0 -36
  80. package/plugins/cookie-consent/public/inject-body.html +0 -69
  81. package/plugins/custom-css/admin/templates/custom-css.html +0 -17
  82. package/plugins/custom-css/admin/views/custom-css.js +0 -35
  83. package/plugins/custom-css/config.js +0 -1
  84. package/plugins/custom-css/data/custom.css +0 -0
  85. package/plugins/custom-css/plugin.js +0 -63
  86. package/plugins/custom-css/plugin.json +0 -32
  87. package/plugins/custom-css/public/inject-head.html +0 -1
  88. package/plugins/form-builder/data/forms/contact.json +0 -52
  89. package/plugins/form-builder/data/submissions/contact.json +0 -14
package/scripts/setup.js CHANGED
@@ -11,14 +11,14 @@
11
11
  * 3. Set site title and tagline
12
12
  * 4. Pick a theme
13
13
  */
14
- import { createInterface } from 'node:readline/promises';
15
- import { randomBytes } from 'node:crypto';
16
- import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
17
- import { existsSync } from 'node:fs';
18
- import path from 'node:path';
19
- import { fileURLToPath } from 'node:url';
20
- import bcrypt from 'bcryptjs';
21
- import { v4 as uuidv4 } from 'uuid';
14
+ import {createInterface} from 'node:readline/promises';
15
+ import {randomBytes} from 'node:crypto';
16
+ import {mkdir, readdir, readFile, writeFile} from 'node:fs/promises';
17
+ import {existsSync} from 'node:fs';
18
+ import path from 'node:path';
19
+ import {fileURLToPath} from 'node:url';
20
+ import bcrypt from 'bcryptjs';
21
+ import {v4 as uuidv4} from 'uuid';
22
22
 
23
23
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
24
  const ROOT = path.resolve(__dirname, '..');
@@ -38,7 +38,10 @@ const THEMES = [
38
38
  'royal-dark', 'royal-light',
39
39
  'lemon-dark', 'lemon-light',
40
40
  'silver-dark', 'silver-light',
41
- 'grayve',
41
+ 'grayve-dark', 'grayve-light',
42
+ 'christmas-dark', 'christmas-light',
43
+ 'unicorn-dark', 'unicorn-light',
44
+ 'dreamy-dark', 'dreamy-light',
42
45
  ];
43
46
 
44
47
  // ---------------------------------------------------------------------------
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Collections API
3
+ *
4
+ * Admin endpoints (authenticated + collections role):
5
+ * GET /collections - List all collections
6
+ * POST /collections - Create collection
7
+ * GET /collections/:slug - Get schema
8
+ * PUT /collections/:slug - Update schema
9
+ * DELETE /collections/:slug - Delete collection + data
10
+ * GET /collections/:slug/entries - List entries (paginated, searchable)
11
+ * GET /collections/:slug/entries/:id - Get single entry
12
+ * POST /collections/:slug/entries - Create entry
13
+ * PUT /collections/:slug/entries/:id - Update entry
14
+ * DELETE /collections/:slug/entries/:id - Delete entry
15
+ * DELETE /collections/:slug/entries - Clear all entries
16
+ * GET /collections/:slug/export - Export (?format=json|csv)
17
+ * POST /collections/:slug/import - Import (JSON body)
18
+ *
19
+ * Public endpoints (access controlled per collection api config):
20
+ * GET /collections/:slug/public - Read entries (if api.read enabled)
21
+ * GET /collections/:slug/public/:id - Read single entry
22
+ * POST /collections/:slug/public - Create entry (if api.create enabled)
23
+ * PUT /collections/:slug/public/:id - Update entry (if api.update enabled)
24
+ * DELETE /collections/:slug/public/:id - Delete entry (if api.delete enabled)
25
+ */
26
+ import {
27
+ listCollections, getCollection, createCollection, updateCollection, deleteCollection,
28
+ listEntries, getEntry, createEntry, updateEntry, deleteEntry, clearEntries,
29
+ exportEntries, importEntries
30
+ } from '../../services/collections.js';
31
+ import { authenticate, requireRole } from '../../middleware/auth.js';
32
+ import { config } from '../../config.js';
33
+
34
+ /**
35
+ * Resolve the role level number for a named role.
36
+ *
37
+ * @param {string} roleName
38
+ * @returns {number}
39
+ */
40
+ function roleLevel(roleName) {
41
+ return config.auth.roles[roleName]?.level ?? 99;
42
+ }
43
+
44
+ /**
45
+ * Check public collection API access.
46
+ * Returns an error reply if access is denied, otherwise resolves (returns undefined).
47
+ *
48
+ * @param {object} schema
49
+ * @param {'create'|'read'|'update'|'delete'} operation
50
+ * @param {object} request - Fastify request
51
+ * @param {object} reply - Fastify reply
52
+ * @returns {Promise<object|undefined>}
53
+ */
54
+ async function checkPublicAccess(schema, operation, request, reply) {
55
+ const access = schema.api?.[operation];
56
+ if (!access?.enabled) {
57
+ return reply.status(403).send({ error: `Public ${operation} is disabled for this collection` });
58
+ }
59
+
60
+ if (access.access === 'public') return; // No auth needed
61
+
62
+ // Auth required — try to verify JWT
63
+ try {
64
+ await request.jwtVerify();
65
+ } catch {
66
+ return reply.status(401).send({ error: 'Unauthorised' });
67
+ }
68
+
69
+ const user = request.user;
70
+ const requiredLevel = roleLevel(access.access);
71
+ const userLevel = roleLevel(user?.role);
72
+
73
+ if (userLevel > requiredLevel) {
74
+ return reply.status(403).send({ error: 'Insufficient permissions' });
75
+ }
76
+ }
77
+
78
+ export async function collectionsRoutes(fastify) {
79
+ const guard = { preHandler: [authenticate, requireRole(config.auth.permissions.collections)] };
80
+
81
+ // -------------------------------------------------------------------------
82
+ // Collection CRUD (schema management)
83
+ // -------------------------------------------------------------------------
84
+
85
+ fastify.get('/collections', guard, async () => {
86
+ return listCollections();
87
+ });
88
+
89
+ fastify.post('/collections', guard, async (request, reply) => {
90
+ const { title, slug, description, fields, api } = request.body || {};
91
+ if (!title) return reply.status(400).send({ error: 'title is required' });
92
+ try {
93
+ const schema = await createCollection({ title, slug, description, fields, api });
94
+ return reply.status(201).send(schema);
95
+ } catch (err) {
96
+ return reply.status(409).send({ error: err.message });
97
+ }
98
+ });
99
+
100
+ fastify.get('/collections/:slug', guard, async (request, reply) => {
101
+ const schema = await getCollection(request.params.slug);
102
+ if (!schema) return reply.status(404).send({ error: 'Collection not found' });
103
+ return schema;
104
+ });
105
+
106
+ fastify.put('/collections/:slug', guard, async (request, reply) => {
107
+ try {
108
+ return await updateCollection(request.params.slug, request.body || {});
109
+ } catch (err) {
110
+ return reply.status(404).send({ error: err.message });
111
+ }
112
+ });
113
+
114
+ fastify.delete('/collections/:slug', guard, async (request, reply) => {
115
+ try {
116
+ await deleteCollection(request.params.slug);
117
+ return { success: true };
118
+ } catch (err) {
119
+ return reply.status(404).send({ error: err.message });
120
+ }
121
+ });
122
+
123
+ // -------------------------------------------------------------------------
124
+ // Entry CRUD
125
+ // -------------------------------------------------------------------------
126
+
127
+ fastify.get('/collections/:slug/entries', guard, async (request, reply) => {
128
+ const schema = await getCollection(request.params.slug);
129
+ if (!schema) return reply.status(404).send({ error: 'Collection not found' });
130
+ const { page, limit, sort, order, search } = request.query;
131
+ return listEntries(request.params.slug, {
132
+ page: parseInt(page, 10) || 1,
133
+ limit: parseInt(limit, 10) || 50,
134
+ sort: sort || 'createdAt',
135
+ order: order || 'desc',
136
+ search: search || undefined
137
+ });
138
+ });
139
+
140
+ fastify.get('/collections/:slug/entries/:id', guard, async (request, reply) => {
141
+ const entry = await getEntry(request.params.slug, request.params.id);
142
+ if (!entry) return reply.status(404).send({ error: 'Entry not found' });
143
+ return entry;
144
+ });
145
+
146
+ fastify.post('/collections/:slug/entries', guard, async (request, reply) => {
147
+ const user = request.user;
148
+ try {
149
+ const entry = await createEntry(request.params.slug, request.body?.data || {}, {
150
+ createdBy: user?.id || null,
151
+ source: 'admin'
152
+ });
153
+ return reply.status(201).send(entry);
154
+ } catch (err) {
155
+ return reply.status(400).send({ error: err.message });
156
+ }
157
+ });
158
+
159
+ fastify.put('/collections/:slug/entries/:id', guard, async (request, reply) => {
160
+ try {
161
+ return await updateEntry(request.params.slug, request.params.id, request.body?.data || {});
162
+ } catch (err) {
163
+ const status = err.message === 'Entry not found' ? 404 : 400;
164
+ return reply.status(status).send({ error: err.message });
165
+ }
166
+ });
167
+
168
+ fastify.delete('/collections/:slug/entries/:id', guard, async (request, reply) => {
169
+ try {
170
+ await deleteEntry(request.params.slug, request.params.id);
171
+ return { success: true };
172
+ } catch (err) {
173
+ return reply.status(404).send({ error: err.message });
174
+ }
175
+ });
176
+
177
+ // Clear all entries — DELETE /collections/:slug/entries (no :id)
178
+ fastify.delete('/collections/:slug/entries', guard, async (request, reply) => {
179
+ try {
180
+ await clearEntries(request.params.slug);
181
+ return { success: true };
182
+ } catch (err) {
183
+ return reply.status(404).send({ error: err.message });
184
+ }
185
+ });
186
+
187
+ // -------------------------------------------------------------------------
188
+ // Export / Import
189
+ // -------------------------------------------------------------------------
190
+
191
+ fastify.get('/collections/:slug/export', guard, async (request, reply) => {
192
+ const format = request.query.format === 'csv' ? 'csv' : 'json';
193
+ try {
194
+ const output = await exportEntries(request.params.slug, format);
195
+ if (format === 'csv') {
196
+ reply.header('Content-Type', 'text/csv');
197
+ reply.header('Content-Disposition', `attachment; filename="${request.params.slug}-entries.csv"`);
198
+ } else {
199
+ reply.header('Content-Type', 'application/json');
200
+ reply.header('Content-Disposition', `attachment; filename="${request.params.slug}-entries.json"`);
201
+ }
202
+ return reply.send(output);
203
+ } catch (err) {
204
+ return reply.status(404).send({ error: err.message });
205
+ }
206
+ });
207
+
208
+ fastify.post('/collections/:slug/import', guard, async (request, reply) => {
209
+ const user = request.user;
210
+ const entries = request.body?.entries;
211
+ if (!Array.isArray(entries)) return reply.status(400).send({ error: 'entries must be an array' });
212
+ try {
213
+ const result = await importEntries(request.params.slug, entries, { createdBy: user?.id || null });
214
+ return reply.status(201).send(result);
215
+ } catch (err) {
216
+ return reply.status(400).send({ error: err.message });
217
+ }
218
+ });
219
+
220
+ // -------------------------------------------------------------------------
221
+ // Public access endpoints
222
+ // -------------------------------------------------------------------------
223
+
224
+ fastify.get('/collections/:slug/public', async (request, reply) => {
225
+ const schema = await getCollection(request.params.slug);
226
+ if (!schema) return reply.status(404).send({ error: 'Collection not found' });
227
+
228
+ const denied = await checkPublicAccess(schema, 'read', request, reply);
229
+ if (denied !== undefined) return;
230
+
231
+ const { page, limit, sort, order, search } = request.query;
232
+ return listEntries(request.params.slug, {
233
+ page: parseInt(page, 10) || 1,
234
+ limit: parseInt(limit, 10) || 50,
235
+ sort: sort || 'createdAt',
236
+ order: order || 'desc',
237
+ search: search || undefined
238
+ });
239
+ });
240
+
241
+ fastify.get('/collections/:slug/public/:id', async (request, reply) => {
242
+ const schema = await getCollection(request.params.slug);
243
+ if (!schema) return reply.status(404).send({ error: 'Collection not found' });
244
+
245
+ const denied = await checkPublicAccess(schema, 'read', request, reply);
246
+ if (denied !== undefined) return;
247
+
248
+ const entry = await getEntry(request.params.slug, request.params.id);
249
+ if (!entry) return reply.status(404).send({ error: 'Entry not found' });
250
+ return entry;
251
+ });
252
+
253
+ fastify.post('/collections/:slug/public', async (request, reply) => {
254
+ const schema = await getCollection(request.params.slug);
255
+ if (!schema) return reply.status(404).send({ error: 'Collection not found' });
256
+
257
+ const denied = await checkPublicAccess(schema, 'create', request, reply);
258
+ if (denied !== undefined) return;
259
+
260
+ try {
261
+ const user = request.user;
262
+ const entry = await createEntry(request.params.slug, request.body?.data || {}, {
263
+ createdBy: user?.id || null,
264
+ source: 'api'
265
+ });
266
+ return reply.status(201).send(entry);
267
+ } catch (err) {
268
+ return reply.status(400).send({ error: err.message });
269
+ }
270
+ });
271
+
272
+ fastify.put('/collections/:slug/public/:id', async (request, reply) => {
273
+ const schema = await getCollection(request.params.slug);
274
+ if (!schema) return reply.status(404).send({ error: 'Collection not found' });
275
+
276
+ const denied = await checkPublicAccess(schema, 'update', request, reply);
277
+ if (denied !== undefined) return;
278
+
279
+ try {
280
+ return await updateEntry(request.params.slug, request.params.id, request.body?.data || {});
281
+ } catch (err) {
282
+ const status = err.message === 'Entry not found' ? 404 : 400;
283
+ return reply.status(status).send({ error: err.message });
284
+ }
285
+ });
286
+
287
+ fastify.delete('/collections/:slug/public/:id', async (request, reply) => {
288
+ const schema = await getCollection(request.params.slug);
289
+ if (!schema) return reply.status(404).send({ error: 'Collection not found' });
290
+
291
+ const denied = await checkPublicAccess(schema, 'delete', request, reply);
292
+ if (denied !== undefined) return;
293
+
294
+ try {
295
+ await deleteEntry(request.params.slug, request.params.id);
296
+ return { success: true };
297
+ } catch (err) {
298
+ return reply.status(404).send({ error: err.message });
299
+ }
300
+ });
301
+ }
@@ -1,11 +1,20 @@
1
1
  /**
2
2
  * Settings API
3
- * GET /api/settings - get site settings
4
- * PUT /api/settings - save site settings
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
5
6
  */
6
7
  import { getConfig, saveConfig } from '../../config.js';
7
8
  import { authenticate, requireRole } from '../../middleware/auth.js';
8
9
  import { config } from '../../config.js';
10
+ import nodemailer from 'nodemailer';
11
+ import fs from 'fs/promises';
12
+ import path from 'path';
13
+ import {fileURLToPath} from 'url';
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ const CUSTOM_CSS_FILE = path.resolve(__dirname, '../../../content/custom.css');
17
+ const CUSTOM_CSS_MAX = 100 * 1024; // 100 KB
9
18
 
10
19
  export async function settingsRoutes(fastify) {
11
20
  const guard = { preHandler: [authenticate, requireRole(config.auth.permissions.settings)] };
@@ -22,4 +31,59 @@ export async function settingsRoutes(fastify) {
22
31
  saveConfig('site', data);
23
32
  return { success: true };
24
33
  });
34
+
35
+ fastify.post('/settings/test-email', guard, async (request, reply) => {
36
+ const smtp = getConfig('site')?.smtp;
37
+ if (!smtp?.host) {
38
+ return reply.status(400).send({ error: 'SMTP is not configured. Save your SMTP settings first.' });
39
+ }
40
+
41
+ const transporter = nodemailer.createTransport({
42
+ host: smtp.host,
43
+ port: smtp.port || 587,
44
+ secure: smtp.secure || false,
45
+ auth: smtp.user ? { user: smtp.user, pass: smtp.pass } : undefined
46
+ });
47
+
48
+ const to = request.body?.to || smtp.fromAddress;
49
+ if (!to) {
50
+ return reply.status(400).send({ error: 'No recipient address. Provide "to" in the request body or set a From Address.' });
51
+ }
52
+
53
+ try {
54
+ await transporter.sendMail({
55
+ from: smtp.fromName ? `"${smtp.fromName}" <${smtp.fromAddress}>` : smtp.fromAddress,
56
+ to,
57
+ subject: 'Domma CMS — Test Email',
58
+ text: 'This is a test email sent from Domma CMS to verify your SMTP configuration.',
59
+ html: '<p>This is a test email sent from <strong>Domma CMS</strong> to verify your SMTP configuration.</p>'
60
+ });
61
+ return { success: true, message: `Test email sent to ${to}` };
62
+ } catch (err) {
63
+ return reply.status(500).send({ error: `Failed to send email: ${err.message}` });
64
+ }
65
+ });
66
+
67
+ // GET /api/settings/custom-css — return current CSS as JSON
68
+ fastify.get('/settings/custom-css', guard, async () => {
69
+ try {
70
+ const css = await fs.readFile(CUSTOM_CSS_FILE, 'utf8');
71
+ return { css };
72
+ } catch {
73
+ return { css: '' };
74
+ }
75
+ });
76
+
77
+ // PUT /api/settings/custom-css — save CSS to content/custom.css
78
+ fastify.put('/settings/custom-css', guard, async (request, reply) => {
79
+ const { css } = request.body || {};
80
+ if (typeof css !== 'string') {
81
+ return reply.status(400).send({ error: 'css must be a string.' });
82
+ }
83
+ if (Buffer.byteLength(css, 'utf8') > CUSTOM_CSS_MAX) {
84
+ return reply.status(400).send({ error: 'CSS exceeds the 100 KB limit.' });
85
+ }
86
+ await fs.writeFile(CUSTOM_CSS_FILE, css, 'utf8');
87
+ return { success: true };
88
+ });
25
89
  }
package/server/server.js CHANGED
@@ -75,12 +75,14 @@ await app.register(staticPlugin, {
75
75
  });
76
76
 
77
77
  // Ensure required directories exist
78
- const mediaDir = path.join(ROOT, config.content.mediaDir);
79
- const usersDir = path.join(ROOT, config.content.usersDir);
80
- const pluginsDir = path.join(ROOT, 'plugins');
81
- await fs.mkdir(mediaDir, { recursive: true });
82
- await fs.mkdir(usersDir, { recursive: true });
83
- await fs.mkdir(pluginsDir, { recursive: true });
78
+ const mediaDir = path.join(ROOT, config.content.mediaDir);
79
+ const usersDir = path.join(ROOT, config.content.usersDir);
80
+ const collectionsDir = path.join(ROOT, config.content.collectionsDir);
81
+ const pluginsDir = path.join(ROOT, 'plugins');
82
+ await fs.mkdir(mediaDir, { recursive: true });
83
+ await fs.mkdir(usersDir, { recursive: true });
84
+ await fs.mkdir(collectionsDir, { recursive: true });
85
+ await fs.mkdir(pluginsDir, { recursive: true });
84
86
 
85
87
  // Serve uploaded media files
86
88
  await app.register(staticPlugin, {
@@ -131,15 +133,17 @@ const { layoutsRoutes } = await import('./routes/api/layouts.js');
131
133
  const { navigationRoutes } = await import('./routes/api/navigation.js');
132
134
  const { mediaRoutes } = await import('./routes/api/media.js');
133
135
  const { usersRoutes } = await import('./routes/api/users.js');
134
- const { pluginsRoutes } = await import('./routes/api/plugins.js');
135
-
136
- await app.register(pagesRoutes, { prefix: '/api' });
137
- await app.register(settingsRoutes, { prefix: '/api' });
138
- await app.register(layoutsRoutes, { prefix: '/api' });
139
- await app.register(navigationRoutes, { prefix: '/api' });
140
- await app.register(mediaRoutes, { prefix: '/api' });
141
- await app.register(usersRoutes, { prefix: '/api' });
142
- await app.register(pluginsRoutes, { prefix: '/api' });
136
+ const { pluginsRoutes } = await import('./routes/api/plugins.js');
137
+ const { collectionsRoutes } = await import('./routes/api/collections.js');
138
+
139
+ await app.register(pagesRoutes, { prefix: '/api' });
140
+ await app.register(settingsRoutes, { prefix: '/api' });
141
+ await app.register(layoutsRoutes, { prefix: '/api' });
142
+ await app.register(navigationRoutes, { prefix: '/api' });
143
+ await app.register(mediaRoutes, { prefix: '/api' });
144
+ await app.register(usersRoutes, { prefix: '/api' });
145
+ await app.register(pluginsRoutes, { prefix: '/api' });
146
+ await app.register(collectionsRoutes, { prefix: '/api' });
143
147
 
144
148
  // ---------------------------------------------------------------------------
145
149
  // CMS Plugins (server-side Fastify plugins from plugins/ directory)