domma-cms 0.10.0 → 0.13.0
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/CLAUDE.md +248 -159
- package/admin/css/admin.css +1 -1
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +7 -3
- package/admin/js/config/sidebar-config.js +1 -1
- package/admin/js/http-interceptor.js +1 -0
- package/admin/js/lib/safe-html.js +1 -0
- package/admin/js/templates/documentation.html +611 -2
- package/admin/js/templates/layouts.html +5 -4
- package/admin/js/templates/notifications.html +14 -0
- package/admin/js/templates/plugin-marketplace.html +16 -0
- package/admin/js/templates/plugins.html +17 -5
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/layouts.js +1 -16
- package/admin/js/views/notifications.js +1 -0
- package/admin/js/views/plugin-marketplace.js +1 -0
- package/admin/js/views/plugins.js +16 -16
- package/config/navigation.json +5 -72
- package/config/plugins.json +10 -14
- package/config/presets.json +50 -13
- package/config/site.json +11 -63
- package/package.json +2 -1
- package/plugins/_template/admin/templates/index.html +17 -0
- package/plugins/_template/admin/views/index.js +19 -0
- package/plugins/_template/config.js +8 -0
- package/plugins/_template/plugin.js +23 -0
- package/plugins/_template/plugin.json +34 -0
- package/plugins/analytics/plugin.json +41 -31
- package/plugins/blog/admin/templates/blog.html +22 -0
- package/plugins/blog/admin/templates/categories.html +7 -0
- package/plugins/blog/admin/templates/comments.html +11 -0
- package/plugins/blog/admin/templates/post-editor.html +97 -0
- package/plugins/blog/admin/templates/settings.html +11 -0
- package/plugins/blog/admin/views/blog.js +183 -0
- package/plugins/blog/admin/views/categories.js +235 -0
- package/plugins/blog/admin/views/comments.js +187 -0
- package/plugins/blog/admin/views/post-editor.js +291 -0
- package/plugins/blog/admin/views/settings.js +100 -0
- package/plugins/blog/collections/categories/schema.json +12 -0
- package/plugins/blog/collections/comments/schema.json +16 -0
- package/plugins/blog/collections/posts/schema.json +19 -0
- package/plugins/blog/config.js +8 -0
- package/plugins/blog/plugin.js +352 -0
- package/plugins/blog/plugin.json +96 -0
- package/plugins/blog/roles/blog-author.json +10 -0
- package/plugins/blog/roles/blog-editor.json +12 -0
- package/plugins/blog/templates/author.html +9 -0
- package/plugins/blog/templates/category.html +9 -0
- package/plugins/blog/templates/index.html +9 -0
- package/plugins/blog/templates/post.html +17 -0
- package/plugins/blog/templates/tag.html +9 -0
- package/plugins/contacts/collections/user-contact-groups/schema.json +1 -1
- package/plugins/contacts/collections/user-contacts/schema.json +1 -1
- package/plugins/contacts/plugin.js +4 -10
- package/plugins/contacts/plugin.json +13 -3
- package/plugins/notes/collections/user-notes/schema.json +1 -1
- package/plugins/notes/plugin.js +3 -9
- package/plugins/notes/plugin.json +13 -3
- package/plugins/site-search/plugin.json +5 -2
- package/plugins/theme-switcher/plugin.json +1 -1
- package/plugins/todo/collections/todos/schema.json +1 -1
- package/plugins/todo/plugin.js +3 -9
- package/plugins/todo/plugin.json +13 -3
- package/public/css/site.css +1 -1
- package/scripts/build.js +48 -0
- package/scripts/create-plugin.js +113 -0
- package/scripts/fresh.js +6 -7
- package/scripts/gen-instance-secret.js +46 -0
- package/scripts/reset.js +3 -3
- package/scripts/setup.js +31 -13
- package/server/middleware/auth.js +48 -0
- package/server/middleware/managerAuth.js +36 -0
- package/server/routes/api/actions.js +1 -1
- package/server/routes/api/auth.js +4 -3
- package/server/routes/api/layouts.js +173 -49
- package/server/routes/api/notifications.js +155 -0
- package/server/routes/api/plugin-marketplace.js +75 -0
- package/server/routes/api/users.js +1 -1
- package/server/routes/api/views.js +1 -1
- package/server/routes/public.js +4 -9
- package/server/server.js +32 -3
- package/server/services/actions.js +1 -1
- package/server/services/managerClient.js +182 -0
- package/server/services/markdown.js +52 -14
- package/server/services/permissionRegistry.js +245 -173
- package/server/services/pluginInstaller.js +301 -0
- package/server/services/plugins.js +117 -10
- package/server/services/presetCollections.js +66 -251
- package/server/services/renderer.js +99 -0
- package/server/services/roles.js +191 -39
- package/server/services/users.js +1 -1
- package/server/services/views.js +1 -1
- package/server/templates/page.html +2 -2
- package/plugins/docs/admin/templates/docs.html +0 -69
- package/plugins/docs/admin/views/docs.js +0 -276
- package/plugins/docs/config.js +0 -8
- package/plugins/docs/data/documents/57e003f0-68f2-47dc-9c36-ed4b10ed3deb.json +0 -11
- package/plugins/docs/data/folders.json +0 -9
- package/plugins/docs/data/templates.json +0 -1
- package/plugins/docs/data/versions/57e003f0-68f2-47dc-9c36-ed4b10ed3deb/1.json +0 -5
- package/plugins/docs/plugin.js +0 -375
- package/plugins/docs/plugin.json +0 -23
- package/plugins/form-builder/data/forms/contacts.json +0 -66
- package/plugins/form-builder/data/forms/enquiries.json +0 -103
- package/plugins/form-builder/data/forms/feedback.json +0 -131
- package/plugins/form-builder/data/forms/notes.json +0 -79
- package/plugins/form-builder/data/forms/to-do.json +0 -100
- package/plugins/form-builder/data/submissions/contacts.json +0 -1
- package/plugins/form-builder/data/submissions/enquiries.json +0 -1
- package/plugins/form-builder/data/submissions/feedback.json +0 -1
- package/plugins/form-builder/data/submissions/notes.json +0 -1
- package/plugins/form-builder/data/submissions/to-do.json +0 -1
- package/plugins/garage/admin/templates/garage.html +0 -111
- package/plugins/garage/admin/views/garage.js +0 -622
- package/plugins/garage/collections/garage-vehicles/schema.json +0 -101
- package/plugins/garage/config.js +0 -18
- package/plugins/garage/data/vehicles.json +0 -70
- package/plugins/garage/plugin.js +0 -398
- package/plugins/garage/plugin.json +0 -33
- package/scripts/seed.js +0 -1996
- package/server/services/userTypes.js +0 -227
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import defaultConfig from './config.js';
|
|
2
|
+
import {createEntry, deleteEntry, getEntry, listEntries, updateEntry} from '../../server/services/collections.js';
|
|
3
|
+
import {getConfig, saveConfig} from '../../server/config.js';
|
|
4
|
+
|
|
5
|
+
const POSTS_SLUG = 'blog-posts';
|
|
6
|
+
const CATEGORIES_SLUG = 'blog-categories';
|
|
7
|
+
const COMMENTS_SLUG = 'blog-comments';
|
|
8
|
+
|
|
9
|
+
const PUBLISH_ROLES = ['blog-editor', 'admin', 'super-admin'];
|
|
10
|
+
|
|
11
|
+
function canPublish(user) {
|
|
12
|
+
return PUBLISH_ROLES.includes(user?.role);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isEditorOrAdmin(user) {
|
|
16
|
+
return canPublish(user);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Derive a URL slug from a title string. */
|
|
20
|
+
function slugify(str) {
|
|
21
|
+
return String(str)
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
24
|
+
.replace(/^-+|-+$/g, '');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toPost(entry) {
|
|
28
|
+
return {
|
|
29
|
+
id: entry.id,
|
|
30
|
+
...entry.data,
|
|
31
|
+
createdAt: entry.meta.createdAt,
|
|
32
|
+
updatedAt: entry.meta.updatedAt
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function toCategory(entry) {
|
|
37
|
+
return {
|
|
38
|
+
id: entry.id,
|
|
39
|
+
...entry.data,
|
|
40
|
+
createdAt: entry.meta.createdAt,
|
|
41
|
+
updatedAt: entry.meta.updatedAt
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toComment(entry) {
|
|
46
|
+
return {
|
|
47
|
+
id: entry.id,
|
|
48
|
+
...entry.data,
|
|
49
|
+
createdAt: entry.meta.createdAt,
|
|
50
|
+
updatedAt: entry.meta.updatedAt
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default async function blogPlugin(fastify, options) {
|
|
55
|
+
const { authenticate, requireAdmin } = options.auth;
|
|
56
|
+
const settings = { ...defaultConfig, ...(options.settings || {}) };
|
|
57
|
+
|
|
58
|
+
// -------------------------------------------------------------------------
|
|
59
|
+
// Posts
|
|
60
|
+
// -------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
/** GET /api/plugins/blog/posts */
|
|
63
|
+
fastify.get('/posts', { preHandler: [authenticate] }, async (request, reply) => {
|
|
64
|
+
const user = request.user;
|
|
65
|
+
const { entries } = await listEntries(POSTS_SLUG, { limit: 100000, sort: 'createdAt', order: 'desc' });
|
|
66
|
+
let posts = entries.map(toPost);
|
|
67
|
+
|
|
68
|
+
if (!isEditorOrAdmin(user) && user?.role === 'blog-author') {
|
|
69
|
+
const uid = user?.id ?? user?.sub ?? null;
|
|
70
|
+
posts = posts.filter(p => p.authorId === uid);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return reply.send(posts);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/** POST /api/plugins/blog/posts */
|
|
77
|
+
fastify.post('/posts', { preHandler: [authenticate] }, async (request, reply) => {
|
|
78
|
+
const user = request.user;
|
|
79
|
+
const uid = user?.id ?? user?.sub ?? null;
|
|
80
|
+
const body = request.body ?? {};
|
|
81
|
+
|
|
82
|
+
const {
|
|
83
|
+
title, content, excerpt, featuredImage,
|
|
84
|
+
categories, tags, status, slug
|
|
85
|
+
} = body;
|
|
86
|
+
|
|
87
|
+
const derivedSlug = slug
|
|
88
|
+
? String(slug).trim()
|
|
89
|
+
: slugify(title ?? '');
|
|
90
|
+
|
|
91
|
+
const entry = await createEntry(POSTS_SLUG, {
|
|
92
|
+
title: typeof title === 'string' ? title.trim() : '',
|
|
93
|
+
slug: derivedSlug,
|
|
94
|
+
content: typeof content === 'string' ? content : '',
|
|
95
|
+
excerpt: typeof excerpt === 'string' ? excerpt.trim() : '',
|
|
96
|
+
featuredImage: featuredImage ?? null,
|
|
97
|
+
categories: Array.isArray(categories) ? categories : [],
|
|
98
|
+
tags: Array.isArray(tags) ? tags : [],
|
|
99
|
+
status: status ?? 'draft',
|
|
100
|
+
authorId: uid
|
|
101
|
+
}, { createdBy: uid, source: 'admin' });
|
|
102
|
+
|
|
103
|
+
return reply.code(201).send(toPost(entry));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/** GET /api/plugins/blog/posts/:id */
|
|
107
|
+
fastify.get('/posts/:id', { preHandler: [authenticate] }, async (request, reply) => {
|
|
108
|
+
const entry = await getEntry(POSTS_SLUG, request.params.id);
|
|
109
|
+
if (!entry) return reply.code(404).send({ error: 'Post not found' });
|
|
110
|
+
return reply.send(toPost(entry));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
/** PUT /api/plugins/blog/posts/:id */
|
|
114
|
+
fastify.put('/posts/:id', { preHandler: [authenticate] }, async (request, reply) => {
|
|
115
|
+
const user = request.user;
|
|
116
|
+
const uid = user?.id ?? user?.sub ?? null;
|
|
117
|
+
const { id } = request.params;
|
|
118
|
+
|
|
119
|
+
const entry = await getEntry(POSTS_SLUG, id);
|
|
120
|
+
if (!entry) return reply.code(404).send({ error: 'Post not found' });
|
|
121
|
+
|
|
122
|
+
if (!isEditorOrAdmin(user) && entry.data.authorId !== uid) {
|
|
123
|
+
return reply.code(403).send({ error: 'Forbidden' });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const updates = request.body ?? {};
|
|
127
|
+
const merged = { ...entry.data, ...updates };
|
|
128
|
+
|
|
129
|
+
const updated = await updateEntry(POSTS_SLUG, id, merged);
|
|
130
|
+
return reply.send(toPost(updated));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/** DELETE /api/plugins/blog/posts/:id */
|
|
134
|
+
fastify.delete('/posts/:id', { preHandler: [authenticate] }, async (request, reply) => {
|
|
135
|
+
const user = request.user;
|
|
136
|
+
if (!isEditorOrAdmin(user)) {
|
|
137
|
+
return reply.code(403).send({ error: 'Forbidden' });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const entry = await getEntry(POSTS_SLUG, request.params.id);
|
|
141
|
+
if (!entry) return reply.code(404).send({ error: 'Post not found' });
|
|
142
|
+
|
|
143
|
+
await deleteEntry(POSTS_SLUG, request.params.id);
|
|
144
|
+
return reply.send({ ok: true });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
/** POST /api/plugins/blog/posts/:id/publish */
|
|
148
|
+
fastify.post('/posts/:id/publish', { preHandler: [authenticate] }, async (request, reply) => {
|
|
149
|
+
const user = request.user;
|
|
150
|
+
if (!canPublish(user)) {
|
|
151
|
+
return reply.code(403).send({ error: 'Forbidden' });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const entry = await getEntry(POSTS_SLUG, request.params.id);
|
|
155
|
+
if (!entry) return reply.code(404).send({ error: 'Post not found' });
|
|
156
|
+
|
|
157
|
+
const updated = await updateEntry(POSTS_SLUG, request.params.id, {
|
|
158
|
+
...entry.data,
|
|
159
|
+
status: 'published',
|
|
160
|
+
publishedAt: new Date().toISOString()
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return reply.send(toPost(updated));
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
/** POST /api/plugins/blog/posts/:id/schedule */
|
|
167
|
+
fastify.post('/posts/:id/schedule', { preHandler: [authenticate] }, async (request, reply) => {
|
|
168
|
+
const user = request.user;
|
|
169
|
+
if (!canPublish(user)) {
|
|
170
|
+
return reply.code(403).send({ error: 'Forbidden' });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const { publishedAt } = request.body ?? {};
|
|
174
|
+
if (!publishedAt) {
|
|
175
|
+
return reply.code(400).send({ error: 'publishedAt is required' });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const entry = await getEntry(POSTS_SLUG, request.params.id);
|
|
179
|
+
if (!entry) return reply.code(404).send({ error: 'Post not found' });
|
|
180
|
+
|
|
181
|
+
const updated = await updateEntry(POSTS_SLUG, request.params.id, {
|
|
182
|
+
...entry.data,
|
|
183
|
+
status: 'scheduled',
|
|
184
|
+
publishedAt: String(publishedAt)
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return reply.send(toPost(updated));
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// -------------------------------------------------------------------------
|
|
191
|
+
// Categories
|
|
192
|
+
// -------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
/** GET /api/plugins/blog/categories */
|
|
195
|
+
fastify.get('/categories', { preHandler: [authenticate] }, async (request, reply) => {
|
|
196
|
+
const { entries } = await listEntries(CATEGORIES_SLUG, { limit: 10000, sort: 'createdAt', order: 'asc' });
|
|
197
|
+
return reply.send(entries.map(toCategory));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
/** POST /api/plugins/blog/categories */
|
|
201
|
+
fastify.post('/categories', { preHandler: [authenticate] }, async (request, reply) => {
|
|
202
|
+
if (!isEditorOrAdmin(request.user)) {
|
|
203
|
+
return reply.code(403).send({ error: 'Forbidden' });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const { name, slug, description } = request.body ?? {};
|
|
207
|
+
if (!name || typeof name !== 'string' || !name.trim()) {
|
|
208
|
+
return reply.code(400).send({ error: 'name is required' });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const derivedSlug = slug ? String(slug).trim() : slugify(name);
|
|
212
|
+
|
|
213
|
+
const entry = await createEntry(CATEGORIES_SLUG, {
|
|
214
|
+
name: name.trim(),
|
|
215
|
+
slug: derivedSlug,
|
|
216
|
+
description: typeof description === 'string' ? description.trim() : ''
|
|
217
|
+
}, { source: 'admin' });
|
|
218
|
+
|
|
219
|
+
return reply.code(201).send(toCategory(entry));
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
/** PUT /api/plugins/blog/categories/:id */
|
|
223
|
+
fastify.put('/categories/:id', { preHandler: [authenticate] }, async (request, reply) => {
|
|
224
|
+
if (!isEditorOrAdmin(request.user)) {
|
|
225
|
+
return reply.code(403).send({ error: 'Forbidden' });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const entry = await getEntry(CATEGORIES_SLUG, request.params.id);
|
|
229
|
+
if (!entry) return reply.code(404).send({ error: 'Category not found' });
|
|
230
|
+
|
|
231
|
+
const updates = request.body ?? {};
|
|
232
|
+
const merged = { ...entry.data, ...updates };
|
|
233
|
+
|
|
234
|
+
const updated = await updateEntry(CATEGORIES_SLUG, request.params.id, merged);
|
|
235
|
+
return reply.send(toCategory(updated));
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
/** DELETE /api/plugins/blog/categories/:id */
|
|
239
|
+
fastify.delete('/categories/:id', { preHandler: [authenticate] }, async (request, reply) => {
|
|
240
|
+
if (!isEditorOrAdmin(request.user)) {
|
|
241
|
+
return reply.code(403).send({ error: 'Forbidden' });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const entry = await getEntry(CATEGORIES_SLUG, request.params.id);
|
|
245
|
+
if (!entry) return reply.code(404).send({ error: 'Category not found' });
|
|
246
|
+
|
|
247
|
+
await deleteEntry(CATEGORIES_SLUG, request.params.id);
|
|
248
|
+
return reply.send({ ok: true });
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// -------------------------------------------------------------------------
|
|
252
|
+
// Comments
|
|
253
|
+
// -------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
/** GET /api/plugins/blog/comments */
|
|
256
|
+
fastify.get('/comments', { preHandler: [authenticate] }, async (request, reply) => {
|
|
257
|
+
const { status: statusFilter, postId } = request.query;
|
|
258
|
+
const { entries } = await listEntries(COMMENTS_SLUG, { limit: 100000, sort: 'createdAt', order: 'desc' });
|
|
259
|
+
let comments = entries.map(toComment);
|
|
260
|
+
|
|
261
|
+
if (statusFilter) {
|
|
262
|
+
comments = comments.filter(c => c.status === statusFilter);
|
|
263
|
+
}
|
|
264
|
+
if (postId) {
|
|
265
|
+
comments = comments.filter(c => c.postId === postId);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return reply.send(comments);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
/** POST /api/plugins/blog/comments — public, no authenticate */
|
|
272
|
+
fastify.post('/comments', async (request, reply) => {
|
|
273
|
+
const body = request.body ?? {};
|
|
274
|
+
|
|
275
|
+
// Honeypot: non-empty `website` field = bot, silently discard
|
|
276
|
+
if (body.website) {
|
|
277
|
+
return reply.code(201).send({ ok: true });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const { postId, authorName, authorEmail, authorUrl, body: commentBody } = body;
|
|
281
|
+
|
|
282
|
+
if (!postId) return reply.code(400).send({ error: 'postId is required' });
|
|
283
|
+
if (!authorName) return reply.code(400).send({ error: 'authorName is required' });
|
|
284
|
+
if (!authorEmail) return reply.code(400).send({ error: 'authorEmail is required' });
|
|
285
|
+
if (!commentBody) return reply.code(400).send({ error: 'body is required' });
|
|
286
|
+
|
|
287
|
+
const commentStatus = settings.commentModeration !== false ? 'pending' : 'approved';
|
|
288
|
+
|
|
289
|
+
const entry = await createEntry(COMMENTS_SLUG, {
|
|
290
|
+
postId: String(postId),
|
|
291
|
+
authorName: String(authorName).trim(),
|
|
292
|
+
authorEmail: String(authorEmail).trim(),
|
|
293
|
+
authorUrl: authorUrl ? String(authorUrl).trim() : '',
|
|
294
|
+
body: String(commentBody),
|
|
295
|
+
status: commentStatus,
|
|
296
|
+
createdAt: new Date().toISOString()
|
|
297
|
+
}, { source: 'public' });
|
|
298
|
+
|
|
299
|
+
return reply.code(201).send(toComment(entry));
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
/** PUT /api/plugins/blog/comments/:id */
|
|
303
|
+
fastify.put('/comments/:id', { preHandler: [authenticate] }, async (request, reply) => {
|
|
304
|
+
const entry = await getEntry(COMMENTS_SLUG, request.params.id);
|
|
305
|
+
if (!entry) return reply.code(404).send({ error: 'Comment not found' });
|
|
306
|
+
|
|
307
|
+
const { status } = request.body ?? {};
|
|
308
|
+
const VALID_STATUSES = ['pending', 'approved', 'rejected', 'spam'];
|
|
309
|
+
|
|
310
|
+
const merged = { ...entry.data };
|
|
311
|
+
if (status !== undefined) {
|
|
312
|
+
if (!VALID_STATUSES.includes(status)) {
|
|
313
|
+
return reply.code(400).send({ error: `status must be one of: ${VALID_STATUSES.join(', ')}` });
|
|
314
|
+
}
|
|
315
|
+
merged.status = status;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const updated = await updateEntry(COMMENTS_SLUG, request.params.id, merged);
|
|
319
|
+
return reply.send(toComment(updated));
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
/** DELETE /api/plugins/blog/comments/:id */
|
|
323
|
+
fastify.delete('/comments/:id', { preHandler: [authenticate] }, async (request, reply) => {
|
|
324
|
+
const entry = await getEntry(COMMENTS_SLUG, request.params.id);
|
|
325
|
+
if (!entry) return reply.code(404).send({ error: 'Comment not found' });
|
|
326
|
+
|
|
327
|
+
await deleteEntry(COMMENTS_SLUG, request.params.id);
|
|
328
|
+
return reply.send({ ok: true });
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// -------------------------------------------------------------------------
|
|
332
|
+
// Settings
|
|
333
|
+
// -------------------------------------------------------------------------
|
|
334
|
+
|
|
335
|
+
/** GET /api/plugins/blog/settings */
|
|
336
|
+
fastify.get('/settings', { preHandler: [authenticate] }, async (request, reply) => {
|
|
337
|
+
const cfg = getConfig();
|
|
338
|
+
const overrides = cfg.plugins?.blog?.settings ?? {};
|
|
339
|
+
return reply.send({ ...defaultConfig, ...overrides });
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
/** PUT /api/plugins/blog/settings */
|
|
343
|
+
fastify.put('/settings', { preHandler: [requireAdmin] }, async (request, reply) => {
|
|
344
|
+
const body = request.body ?? {};
|
|
345
|
+
const cfg = getConfig();
|
|
346
|
+
cfg.plugins ??= {};
|
|
347
|
+
cfg.plugins.blog ??= {};
|
|
348
|
+
cfg.plugins.blog.settings = body;
|
|
349
|
+
await saveConfig(cfg);
|
|
350
|
+
return reply.send({ ok: true });
|
|
351
|
+
});
|
|
352
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "blog",
|
|
3
|
+
"displayName": "Blog",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Public-facing blog with posts, categories, and comments.",
|
|
6
|
+
"author": "Domma CMS",
|
|
7
|
+
"date": "2026-04-07",
|
|
8
|
+
"icon": "file-text",
|
|
9
|
+
"admin": {
|
|
10
|
+
"sidebar": [
|
|
11
|
+
{
|
|
12
|
+
"id": "blog",
|
|
13
|
+
"text": "Blog",
|
|
14
|
+
"icon": "file-text",
|
|
15
|
+
"url": "#/plugins/blog",
|
|
16
|
+
"section": "#/plugins/blog",
|
|
17
|
+
"children": [
|
|
18
|
+
{
|
|
19
|
+
"id": "blog-posts",
|
|
20
|
+
"text": "Posts",
|
|
21
|
+
"url": "#/plugins/blog/posts"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "blog-categories",
|
|
25
|
+
"text": "Categories",
|
|
26
|
+
"url": "#/plugins/blog/categories"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "blog-comments",
|
|
30
|
+
"text": "Comments",
|
|
31
|
+
"url": "#/plugins/blog/comments"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "blog-settings",
|
|
35
|
+
"text": "Settings",
|
|
36
|
+
"url": "#/plugins/blog/settings"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"routes": [
|
|
42
|
+
{
|
|
43
|
+
"path": "/plugins/blog/posts",
|
|
44
|
+
"view": "plugin-blog-posts",
|
|
45
|
+
"title": "Blog Posts - Domma CMS"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"path": "/plugins/blog/posts/new",
|
|
49
|
+
"view": "plugin-blog-post-editor",
|
|
50
|
+
"title": "New Post - Domma CMS"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"path": "/plugins/blog/posts/:id",
|
|
54
|
+
"view": "plugin-blog-post-editor",
|
|
55
|
+
"title": "Edit Post - Domma CMS"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"path": "/plugins/blog/categories",
|
|
59
|
+
"view": "plugin-blog-categories",
|
|
60
|
+
"title": "Blog Categories - Domma CMS"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"path": "/plugins/blog/comments",
|
|
64
|
+
"view": "plugin-blog-comments",
|
|
65
|
+
"title": "Blog Comments - Domma CMS"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"path": "/plugins/blog/settings",
|
|
69
|
+
"view": "plugin-blog-settings",
|
|
70
|
+
"title": "Blog Settings - Domma CMS"
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
"views": {
|
|
74
|
+
"plugin-blog-posts": {
|
|
75
|
+
"entry": "blog/admin/views/blog.js?v=73a4a936",
|
|
76
|
+
"exportName": "blogView"
|
|
77
|
+
},
|
|
78
|
+
"plugin-blog-post-editor": {
|
|
79
|
+
"entry": "blog/admin/views/post-editor.js?v=3288949c",
|
|
80
|
+
"exportName": "postEditorView"
|
|
81
|
+
},
|
|
82
|
+
"plugin-blog-categories": {
|
|
83
|
+
"entry": "blog/admin/views/categories.js?v=489c8dfe",
|
|
84
|
+
"exportName": "categoriesView"
|
|
85
|
+
},
|
|
86
|
+
"plugin-blog-comments": {
|
|
87
|
+
"entry": "blog/admin/views/comments.js?v=47c59ee8",
|
|
88
|
+
"exportName": "commentsView"
|
|
89
|
+
},
|
|
90
|
+
"plugin-blog-settings": {
|
|
91
|
+
"entry": "blog/admin/views/settings.js?v=3fab4235",
|
|
92
|
+
"exportName": "settingsView"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "blog-editor",
|
|
3
|
+
"label": "Blog Editor",
|
|
4
|
+
"level": 2,
|
|
5
|
+
"permissions": ["blog-posts", "blog-categories", "blog-comments"],
|
|
6
|
+
"badgeClass": "badge-success",
|
|
7
|
+
"resources": [
|
|
8
|
+
{ "key": "blog-posts", "label": "Blog Posts", "actions": ["read", "create", "update", "delete", "publish"] },
|
|
9
|
+
{ "key": "blog-categories", "label": "Blog Categories", "actions": ["read", "create", "update", "delete"] },
|
|
10
|
+
{ "key": "blog-comments", "label": "Blog Comments", "actions": ["read", "update", "delete"] }
|
|
11
|
+
]
|
|
12
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<main class="container" style="padding: 2rem 0;">
|
|
2
|
+
<article>
|
|
3
|
+
<header class="mb-4">
|
|
4
|
+
<h1>{{title}}</h1>
|
|
5
|
+
<p class="text-muted">by {{authorName}} • <time>{{publishedAt}}</time></p>
|
|
6
|
+
<div class="mb-2">{{categories}}{{tags}}</div>
|
|
7
|
+
</header>
|
|
8
|
+
<div class="post-content mb-5">
|
|
9
|
+
{{content}}
|
|
10
|
+
</div>
|
|
11
|
+
</article>
|
|
12
|
+
|
|
13
|
+
<section id="comments" class="mt-5">
|
|
14
|
+
{{comments}}
|
|
15
|
+
{{commentForm}}
|
|
16
|
+
</section>
|
|
17
|
+
</main>
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import defaultConfig from './config.js';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
deleteEntry,
|
|
7
|
-
getEntry
|
|
8
|
-
} from '../../server/services/collections.js';
|
|
9
|
-
|
|
10
|
-
const CONTACTS_SLUG = 'user-contacts';
|
|
11
|
-
const GROUPS_SLUG = 'user-contact-groups';
|
|
2
|
+
import {createEntry, deleteEntry, getEntry, listEntries, updateEntry} from '../../server/services/collections.js';
|
|
3
|
+
|
|
4
|
+
const CONTACTS_SLUG = 'contacts-contacts';
|
|
5
|
+
const GROUPS_SLUG = 'contacts-groups';
|
|
12
6
|
|
|
13
7
|
/** Map a collection entry to the shape the admin view expects. */
|
|
14
8
|
function toContact(entry) {
|
|
@@ -8,14 +8,24 @@
|
|
|
8
8
|
"icon": "users",
|
|
9
9
|
"admin": {
|
|
10
10
|
"sidebar": [
|
|
11
|
-
{
|
|
11
|
+
{
|
|
12
|
+
"id": "contacts",
|
|
13
|
+
"text": "Contacts",
|
|
14
|
+
"icon": "users",
|
|
15
|
+
"url": "#/plugins/contacts",
|
|
16
|
+
"section": "#/plugins/contacts"
|
|
17
|
+
}
|
|
12
18
|
],
|
|
13
19
|
"routes": [
|
|
14
|
-
{
|
|
20
|
+
{
|
|
21
|
+
"path": "/plugins/contacts",
|
|
22
|
+
"view": "plugin-contacts",
|
|
23
|
+
"title": "Contacts - Domma CMS"
|
|
24
|
+
}
|
|
15
25
|
],
|
|
16
26
|
"views": {
|
|
17
27
|
"plugin-contacts": {
|
|
18
|
-
"entry": "contacts/admin/views/contacts.js",
|
|
28
|
+
"entry": "contacts/admin/views/contacts.js?v=4d7ec7f7",
|
|
19
29
|
"exportName": "contactsView"
|
|
20
30
|
}
|
|
21
31
|
}
|
package/plugins/notes/plugin.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import defaultConfig from './config.js';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
updateEntry,
|
|
6
|
-
deleteEntry,
|
|
7
|
-
getEntry
|
|
8
|
-
} from '../../server/services/collections.js';
|
|
9
|
-
|
|
10
|
-
const SLUG = 'user-notes';
|
|
2
|
+
import {createEntry, deleteEntry, getEntry, listEntries, updateEntry} from '../../server/services/collections.js';
|
|
3
|
+
|
|
4
|
+
const SLUG = 'notes-notes';
|
|
11
5
|
|
|
12
6
|
/** Flatten a collection entry into the shape the admin view expects. */
|
|
13
7
|
function toNote(entry) {
|
|
@@ -8,14 +8,24 @@
|
|
|
8
8
|
"icon": "file-text",
|
|
9
9
|
"admin": {
|
|
10
10
|
"sidebar": [
|
|
11
|
-
{
|
|
11
|
+
{
|
|
12
|
+
"id": "notes",
|
|
13
|
+
"text": "Notes",
|
|
14
|
+
"icon": "file-text",
|
|
15
|
+
"url": "#/plugins/notes",
|
|
16
|
+
"section": "#/plugins/notes"
|
|
17
|
+
}
|
|
12
18
|
],
|
|
13
19
|
"routes": [
|
|
14
|
-
{
|
|
20
|
+
{
|
|
21
|
+
"path": "/plugins/notes",
|
|
22
|
+
"view": "plugin-notes",
|
|
23
|
+
"title": "Notes - Domma CMS"
|
|
24
|
+
}
|
|
15
25
|
],
|
|
16
26
|
"views": {
|
|
17
27
|
"plugin-notes": {
|
|
18
|
-
"entry": "notes/admin/views/notes.js?v=
|
|
28
|
+
"entry": "notes/admin/views/notes.js?v=43194b9f",
|
|
19
29
|
"exportName": "notesView"
|
|
20
30
|
}
|
|
21
31
|
}
|
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
"text": "Site Search",
|
|
14
14
|
"icon": "search",
|
|
15
15
|
"url": "#/plugins/site-search",
|
|
16
|
-
"section": "#/plugins/site-search"
|
|
16
|
+
"section": "#/plugins/site-search",
|
|
17
|
+
"permissions": [
|
|
18
|
+
"settings"
|
|
19
|
+
]
|
|
17
20
|
}
|
|
18
21
|
],
|
|
19
22
|
"routes": [
|
|
@@ -25,7 +28,7 @@
|
|
|
25
28
|
],
|
|
26
29
|
"views": {
|
|
27
30
|
"plugin-site-search": {
|
|
28
|
-
"entry": "site-search/admin/views/site-search.js?v=
|
|
31
|
+
"entry": "site-search/admin/views/site-search.js?v=ff425f47",
|
|
29
32
|
"exportName": "siteSearchView"
|
|
30
33
|
}
|
|
31
34
|
}
|