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.
- package/README.md +2 -3
- package/admin/css/admin.css +78 -1
- package/admin/js/api.js +32 -0
- package/admin/js/app.js +24 -7
- package/admin/js/config/sidebar-config.js +8 -0
- package/admin/js/templates/collection-editor.html +80 -0
- package/admin/js/templates/collection-entries.html +36 -0
- package/admin/js/templates/collections.html +12 -0
- package/admin/js/templates/documentation.html +136 -0
- package/admin/js/templates/navigation.html +26 -4
- package/admin/js/templates/page-editor.html +91 -85
- package/admin/js/templates/settings.html +433 -172
- package/admin/js/views/collection-editor.js +487 -0
- package/admin/js/views/collection-entries.js +484 -0
- package/admin/js/views/collections.js +153 -0
- package/admin/js/views/dashboard.js +14 -6
- package/admin/js/views/index.js +9 -3
- package/admin/js/views/login.js +3 -2
- package/admin/js/views/navigation.js +77 -11
- package/admin/js/views/page-editor.js +207 -25
- package/admin/js/views/pages.js +14 -6
- package/admin/js/views/settings.js +137 -2
- package/admin/js/views/users.js +10 -7
- package/bin/cli.js +53 -17
- package/config/auth.json +2 -1
- package/config/content.json +1 -0
- package/config/navigation.json +14 -4
- package/config/plugins.json +0 -18
- package/config/presets.json +4 -8
- package/config/site.json +44 -3
- package/package.json +6 -2
- package/plugins/domma-effects/admin/templates/domma-effects.html +92 -3
- package/plugins/domma-effects/plugin.js +125 -0
- package/plugins/domma-effects/public/inject-body.html +19 -0
- package/plugins/example-analytics/admin/views/analytics.js +2 -2
- package/plugins/example-analytics/plugin.json +8 -0
- package/plugins/example-analytics/stats.json +15 -1
- package/plugins/form-builder/admin/templates/form-editor.html +19 -6
- package/plugins/form-builder/admin/views/form-editor.js +634 -9
- package/plugins/form-builder/admin/views/form-submissions.js +4 -4
- package/plugins/form-builder/admin/views/forms-list.js +5 -5
- package/plugins/form-builder/data/forms/consent.json +104 -0
- package/plugins/form-builder/data/forms/contacts.json +66 -0
- package/plugins/form-builder/data/submissions/consent.json +13 -0
- package/plugins/form-builder/data/submissions/contacts.json +26 -0
- package/plugins/form-builder/plugin.js +62 -11
- package/plugins/form-builder/plugin.json +12 -16
- package/plugins/form-builder/public/form-logic-engine.js +568 -0
- package/plugins/form-builder/public/inject-body.html +88 -6
- package/plugins/form-builder/public/inject-head.html +16 -0
- package/plugins/form-builder/public/package.json +1 -0
- package/public/css/site.css +113 -0
- package/public/js/btt.js +90 -0
- package/public/js/cookie-consent.js +61 -0
- package/public/js/site.js +129 -34
- package/scripts/build.js +129 -0
- package/scripts/seed.js +517 -7
- package/scripts/setup.js +12 -9
- package/server/routes/api/collections.js +301 -0
- package/server/routes/api/settings.js +66 -2
- package/server/server.js +19 -15
- package/server/services/collections.js +430 -0
- package/server/services/content.js +11 -2
- package/server/services/hooks.js +109 -0
- package/server/services/markdown.js +500 -149
- package/server/services/plugins.js +6 -1
- package/server/services/renderer.js +73 -7
- package/server/templates/page.html +38 -3
- package/plugins/back-to-top/admin/templates/back-to-top-settings.html +0 -55
- package/plugins/back-to-top/admin/views/back-to-top-settings.js +0 -44
- package/plugins/back-to-top/config.js +0 -10
- package/plugins/back-to-top/plugin.js +0 -24
- package/plugins/back-to-top/plugin.json +0 -36
- package/plugins/back-to-top/public/inject-body.html +0 -105
- package/plugins/cookie-consent/admin/templates/cookie-consent-settings.html +0 -113
- package/plugins/cookie-consent/admin/views/cookie-consent-settings.js +0 -73
- package/plugins/cookie-consent/config.js +0 -30
- package/plugins/cookie-consent/plugin.js +0 -24
- package/plugins/cookie-consent/plugin.json +0 -36
- package/plugins/cookie-consent/public/inject-body.html +0 -69
- package/plugins/custom-css/admin/templates/custom-css.html +0 -17
- package/plugins/custom-css/admin/views/custom-css.js +0 -35
- package/plugins/custom-css/config.js +0 -1
- package/plugins/custom-css/data/custom.css +0 -0
- package/plugins/custom-css/plugin.js +0 -63
- package/plugins/custom-css/plugin.json +0 -32
- package/plugins/custom-css/public/inject-head.html +0 -1
- package/plugins/form-builder/data/forms/contact.json +0 -52
- 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 {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import path
|
|
19
|
-
import {
|
|
20
|
-
import bcrypt
|
|
21
|
-
import {
|
|
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
|
-
|
|
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
|
|
4
|
-
* PUT
|
|
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
|
|
79
|
-
const usersDir
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
await fs.mkdir(
|
|
83
|
-
await fs.mkdir(
|
|
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 }
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
await app.register(
|
|
138
|
-
await app.register(
|
|
139
|
-
await app.register(
|
|
140
|
-
await app.register(
|
|
141
|
-
await app.register(
|
|
142
|
-
await app.register(
|
|
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)
|