domma-cms 0.85.0 → 0.86.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/admin/js/lib/sidebar-renderer.js +3 -3
- package/config/menus/admin-sidebar.json +21 -8
- package/config/plugins.json +0 -18
- package/package.json +1 -1
- package/plugins/_lib/mail/diagnostics.js +3 -2
- package/plugins/blog/CLAUDE.md +33 -0
- package/plugins/blog/lib/access.js +64 -0
- package/plugins/blog/lib/notices.js +38 -0
- package/plugins/blog/plugin.js +84 -26
- package/plugins/blog/plugin.json +173 -20
- package/plugins/blog/roles/blog-author.json +15 -2
- package/plugins/blog/tests/access.test.js +32 -0
- package/plugins/blog/tests/comment-notices.test.js +140 -0
- package/plugins/blog/tests/notices.test.js +20 -0
- package/plugins/blog/tests/public.test.js +3 -1
- package/plugins/free-tier.lock.json +15 -10
- package/plugins/mail-reader/plugin.js +12 -1
- package/plugins/mail-reader/plugin.json +23 -4
- package/server/routes/api/notifications.js +4 -3
- package/server/services/notify.js +4 -2
- package/server/services/pluginScope.js +85 -0
- package/server/services/plugins.js +7 -2
package/plugins/blog/plugin.js
CHANGED
|
@@ -8,15 +8,25 @@ import {catalogue, seedBlocks} from './lib/render.js';
|
|
|
8
8
|
import {liveBase, liveSettings, loadEntries as loadAll, renderPostPage} from './lib/page.js';
|
|
9
9
|
import {addNavLink, moveNavLink, removeNavLink} from './lib/nav-link.js';
|
|
10
10
|
import {SAMPLE_CATEGORIES, samplePosts} from './lib/samples.js';
|
|
11
|
+
import {commentKey, commentNotice} from './lib/notices.js';
|
|
12
|
+
import {authorPatch, permissionChecker} from './lib/access.js';
|
|
13
|
+
import {getPermissionsFor, invalidate as invalidateRoles} from '../../server/services/roles.js';
|
|
11
14
|
|
|
12
15
|
const POSTS_SLUG = 'blog-posts';
|
|
13
16
|
const CATEGORIES_SLUG = 'blog-categories';
|
|
14
17
|
const COMMENTS_SLUG = 'blog-comments';
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
// Who may do what is a PERMISSION (lib/access.js), set when the plugin
|
|
20
|
+
// registers - never a role name. An editor is whoever holds blog-posts.publish.
|
|
21
|
+
let can = () => false;
|
|
22
|
+
|
|
23
|
+
/** Point the checks at a permission lookup - the plugin does, on start; tests do, with their own. */
|
|
24
|
+
export function usePermissionLookup(permissionsFor) {
|
|
25
|
+
can = permissionChecker(permissionsFor);
|
|
26
|
+
}
|
|
17
27
|
|
|
18
28
|
function canPublish(user) {
|
|
19
|
-
return
|
|
29
|
+
return can(user, 'blog-posts.publish');
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
function isEditorOrAdmin(user) {
|
|
@@ -242,8 +252,41 @@ function registerEmbedShortcodes(registerShortcode, baseOf) {
|
|
|
242
252
|
|
|
243
253
|
export default async function blogPlugin(fastify, options) {
|
|
244
254
|
const { authenticate, requireAdmin } = options.auth;
|
|
255
|
+
|
|
256
|
+
// Permissions (tests pass their own lookup). An editor held as an additional
|
|
257
|
+
// role counts, and a custom role given blog-posts.publish under Roles is one.
|
|
258
|
+
usePermissionLookup(options.getPermissionsFor || getPermissionsFor);
|
|
259
|
+
const need = (key) => async (request, reply) => {
|
|
260
|
+
if (!can(request.user, key)) return reply.code(403).send({ error: 'Forbidden' });
|
|
261
|
+
};
|
|
262
|
+
const uidOf = (user) => user?.id ?? user?.sub ?? null;
|
|
263
|
+
// A Blog Author made before this release holds a bare 'blog-posts', which a
|
|
264
|
+
// permission check reads as every action - publish and delete too. Once only.
|
|
265
|
+
try {
|
|
266
|
+
let patched = false;
|
|
267
|
+
for (const e of (await listEntries('roles', { limit: 500 })).entries || []) {
|
|
268
|
+
const patch = authorPatch(e.data);
|
|
269
|
+
if (!patch) continue;
|
|
270
|
+
await updateEntry('roles', e.id, { ...e.data, ...patch });
|
|
271
|
+
patched = true;
|
|
272
|
+
}
|
|
273
|
+
if (patched) { await invalidateRoles(); fastify.log.info('[blog] Blog Author: posts read/create/update and categories read (no publish or delete)'); }
|
|
274
|
+
} catch (err) {
|
|
275
|
+
fastify.log.warn(`[blog] could not update the Blog Author role: ${err.message}`);
|
|
276
|
+
}
|
|
245
277
|
const settings = { ...defaultConfig, ...(options.settings || {}) };
|
|
246
278
|
|
|
279
|
+
// Notifications (core 0.80+; feature-detected): a comment held for approval
|
|
280
|
+
// tells the site's staff (the source's audience); it goes when moderated.
|
|
281
|
+
const notify = (p) => { try { options.hooks?.notify?.(p); } catch { /* a notice never breaks a request */ } };
|
|
282
|
+
const resolveNotice = (key) => { try { options.hooks?.resolveNotification?.(key, 'comments'); } catch { /* ditto */ } };
|
|
283
|
+
if (typeof options.hooks?.registerNotificationSource === 'function') {
|
|
284
|
+
try {
|
|
285
|
+
options.hooks.registerNotificationSource({id: 'comments', label: 'Comments awaiting moderation', severity: 'info',
|
|
286
|
+
description: 'A reader left a comment that is waiting for approval. It goes once the comment is approved, rejected or deleted.'});
|
|
287
|
+
} catch { /* older engine */ }
|
|
288
|
+
}
|
|
289
|
+
|
|
247
290
|
if (options.hooks?.registerShortcode) {
|
|
248
291
|
registerEmbedShortcodes(options.hooks.registerShortcode, () => liveBase(settings));
|
|
249
292
|
}
|
|
@@ -262,7 +305,7 @@ export default async function blogPlugin(fastify, options) {
|
|
|
262
305
|
// -------------------------------------------------------------------------
|
|
263
306
|
|
|
264
307
|
/** GET /api/plugins/blog/layouts - every post layout and listing display on offer */
|
|
265
|
-
fastify.get('/layouts', { preHandler: [authenticate] }, async () => {
|
|
308
|
+
fastify.get('/layouts', { preHandler: [authenticate, need('blog-posts.read')] }, async () => {
|
|
266
309
|
const s = liveSettings(settings);
|
|
267
310
|
return { ...(await catalogue()), postLayout: s.postLayout, listDisplay: s.listDisplay };
|
|
268
311
|
});
|
|
@@ -272,7 +315,7 @@ export default async function blogPlugin(fastify, options) {
|
|
|
272
315
|
* the editor's UNSAVED fields and in any layout, for the preview panel.
|
|
273
316
|
* The whole page, chrome and all, so what the author sees is what readers get.
|
|
274
317
|
*/
|
|
275
|
-
fastify.post('/preview', { preHandler: [authenticate] }, async (request, reply) => {
|
|
318
|
+
fastify.post('/preview', { preHandler: [authenticate, need('blog-posts.read')] }, async (request, reply) => {
|
|
276
319
|
const body = request.body ?? {};
|
|
277
320
|
const user = request.user;
|
|
278
321
|
let stored = null;
|
|
@@ -368,13 +411,16 @@ export default async function blogPlugin(fastify, options) {
|
|
|
368
411
|
// -------------------------------------------------------------------------
|
|
369
412
|
|
|
370
413
|
/** GET /api/plugins/blog/posts */
|
|
371
|
-
fastify.get('/posts', { preHandler: [authenticate] }, async (request, reply) => {
|
|
414
|
+
fastify.get('/posts', { preHandler: [authenticate, need('blog-posts.read')] }, async (request, reply) => {
|
|
372
415
|
const user = request.user;
|
|
373
416
|
const { entries } = await listEntries(POSTS_SLUG, { limit: 100000, sort: 'createdAt', order: 'desc' });
|
|
374
417
|
let posts = entries.map(toPost);
|
|
375
418
|
|
|
376
|
-
|
|
377
|
-
|
|
419
|
+
// A writer who is not an editor sees their own posts only - drafts are
|
|
420
|
+
// nobody else's business. (Until 1.8 only the role named blog-author
|
|
421
|
+
// was filtered: every other signed-in account saw every draft.)
|
|
422
|
+
if (!isEditorOrAdmin(user)) {
|
|
423
|
+
const uid = uidOf(user);
|
|
378
424
|
posts = posts.filter(p => p.authorId === uid);
|
|
379
425
|
}
|
|
380
426
|
|
|
@@ -382,7 +428,7 @@ export default async function blogPlugin(fastify, options) {
|
|
|
382
428
|
});
|
|
383
429
|
|
|
384
430
|
/** POST /api/plugins/blog/posts */
|
|
385
|
-
fastify.post('/posts', { preHandler: [authenticate] }, async (request, reply) => {
|
|
431
|
+
fastify.post('/posts', { preHandler: [authenticate, need('blog-posts.create')] }, async (request, reply) => {
|
|
386
432
|
const user = request.user;
|
|
387
433
|
const uid = user?.id ?? user?.sub ?? null;
|
|
388
434
|
const body = request.body ?? {};
|
|
@@ -419,14 +465,16 @@ export default async function blogPlugin(fastify, options) {
|
|
|
419
465
|
});
|
|
420
466
|
|
|
421
467
|
/** GET /api/plugins/blog/posts/:id */
|
|
422
|
-
fastify.get('/posts/:id', { preHandler: [authenticate] }, async (request, reply) => {
|
|
468
|
+
fastify.get('/posts/:id', { preHandler: [authenticate, need('blog-posts.read')] }, async (request, reply) => {
|
|
423
469
|
const entry = await getEntry(POSTS_SLUG, request.params.id);
|
|
424
|
-
if (!entry
|
|
470
|
+
if (!entry || (!isEditorOrAdmin(request.user) && entry.data.authorId !== uidOf(request.user))) {
|
|
471
|
+
return reply.code(404).send({ error: 'Post not found' });
|
|
472
|
+
}
|
|
425
473
|
return reply.send(toPost(entry));
|
|
426
474
|
});
|
|
427
475
|
|
|
428
476
|
/** PUT /api/plugins/blog/posts/:id */
|
|
429
|
-
fastify.put('/posts/:id', { preHandler: [authenticate] }, async (request, reply) => {
|
|
477
|
+
fastify.put('/posts/:id', { preHandler: [authenticate, need('blog-posts.update')] }, async (request, reply) => {
|
|
430
478
|
const user = request.user;
|
|
431
479
|
const uid = user?.id ?? user?.sub ?? null;
|
|
432
480
|
const { id } = request.params;
|
|
@@ -445,7 +493,7 @@ export default async function blogPlugin(fastify, options) {
|
|
|
445
493
|
});
|
|
446
494
|
|
|
447
495
|
/** DELETE /api/plugins/blog/posts/:id */
|
|
448
|
-
fastify.delete('/posts/:id', { preHandler: [authenticate] }, async (request, reply) => {
|
|
496
|
+
fastify.delete('/posts/:id', { preHandler: [authenticate, need('blog-posts.delete')] }, async (request, reply) => {
|
|
449
497
|
const user = request.user;
|
|
450
498
|
if (!isEditorOrAdmin(user)) {
|
|
451
499
|
return reply.code(403).send({ error: 'Forbidden' });
|
|
@@ -459,7 +507,7 @@ export default async function blogPlugin(fastify, options) {
|
|
|
459
507
|
});
|
|
460
508
|
|
|
461
509
|
/** POST /api/plugins/blog/posts/:id/publish */
|
|
462
|
-
fastify.post('/posts/:id/publish', { preHandler: [authenticate] }, async (request, reply) => {
|
|
510
|
+
fastify.post('/posts/:id/publish', { preHandler: [authenticate, need('blog-posts.publish')] }, async (request, reply) => {
|
|
463
511
|
const user = request.user;
|
|
464
512
|
if (!canPublish(user)) {
|
|
465
513
|
return reply.code(403).send({ error: 'Forbidden' });
|
|
@@ -478,7 +526,7 @@ export default async function blogPlugin(fastify, options) {
|
|
|
478
526
|
});
|
|
479
527
|
|
|
480
528
|
/** POST /api/plugins/blog/posts/:id/schedule */
|
|
481
|
-
fastify.post('/posts/:id/schedule', { preHandler: [authenticate] }, async (request, reply) => {
|
|
529
|
+
fastify.post('/posts/:id/schedule', { preHandler: [authenticate, need('blog-posts.publish')] }, async (request, reply) => {
|
|
482
530
|
const user = request.user;
|
|
483
531
|
if (!canPublish(user)) {
|
|
484
532
|
return reply.code(403).send({ error: 'Forbidden' });
|
|
@@ -506,13 +554,13 @@ export default async function blogPlugin(fastify, options) {
|
|
|
506
554
|
// -------------------------------------------------------------------------
|
|
507
555
|
|
|
508
556
|
/** GET /api/plugins/blog/categories */
|
|
509
|
-
fastify.get('/categories', { preHandler: [authenticate] }, async (request, reply) => {
|
|
557
|
+
fastify.get('/categories', { preHandler: [authenticate, need('blog-categories.read')] }, async (request, reply) => {
|
|
510
558
|
const { entries } = await listEntries(CATEGORIES_SLUG, { limit: 10000, sort: 'createdAt', order: 'asc' });
|
|
511
559
|
return reply.send(entries.map(toCategory));
|
|
512
560
|
});
|
|
513
561
|
|
|
514
562
|
/** POST /api/plugins/blog/categories */
|
|
515
|
-
fastify.post('/categories', { preHandler: [authenticate] }, async (request, reply) => {
|
|
563
|
+
fastify.post('/categories', { preHandler: [authenticate, need('blog-categories.create')] }, async (request, reply) => {
|
|
516
564
|
if (!isEditorOrAdmin(request.user)) {
|
|
517
565
|
return reply.code(403).send({ error: 'Forbidden' });
|
|
518
566
|
}
|
|
@@ -534,7 +582,7 @@ export default async function blogPlugin(fastify, options) {
|
|
|
534
582
|
});
|
|
535
583
|
|
|
536
584
|
/** PUT /api/plugins/blog/categories/:id */
|
|
537
|
-
fastify.put('/categories/:id', { preHandler: [authenticate] }, async (request, reply) => {
|
|
585
|
+
fastify.put('/categories/:id', { preHandler: [authenticate, need('blog-categories.update')] }, async (request, reply) => {
|
|
538
586
|
if (!isEditorOrAdmin(request.user)) {
|
|
539
587
|
return reply.code(403).send({ error: 'Forbidden' });
|
|
540
588
|
}
|
|
@@ -542,15 +590,19 @@ export default async function blogPlugin(fastify, options) {
|
|
|
542
590
|
const entry = await getEntry(CATEGORIES_SLUG, request.params.id);
|
|
543
591
|
if (!entry) return reply.code(404).send({ error: 'Category not found' });
|
|
544
592
|
|
|
545
|
-
|
|
546
|
-
const
|
|
593
|
+
// Its own fields only - the body used to be merged in whole.
|
|
594
|
+
const b = request.body ?? {};
|
|
595
|
+
const merged = { ...entry.data };
|
|
596
|
+
if (typeof b.name === 'string' && b.name.trim()) merged.name = b.name.trim();
|
|
597
|
+
if (typeof b.slug === 'string' && b.slug.trim()) merged.slug = slugify(b.slug);
|
|
598
|
+
if (typeof b.description === 'string') merged.description = b.description.trim();
|
|
547
599
|
|
|
548
600
|
const updated = await updateEntry(CATEGORIES_SLUG, request.params.id, merged);
|
|
549
601
|
return reply.send(toCategory(updated));
|
|
550
602
|
});
|
|
551
603
|
|
|
552
604
|
/** DELETE /api/plugins/blog/categories/:id */
|
|
553
|
-
fastify.delete('/categories/:id', { preHandler: [authenticate] }, async (request, reply) => {
|
|
605
|
+
fastify.delete('/categories/:id', { preHandler: [authenticate, need('blog-categories.delete')] }, async (request, reply) => {
|
|
554
606
|
if (!isEditorOrAdmin(request.user)) {
|
|
555
607
|
return reply.code(403).send({ error: 'Forbidden' });
|
|
556
608
|
}
|
|
@@ -567,7 +619,7 @@ export default async function blogPlugin(fastify, options) {
|
|
|
567
619
|
// -------------------------------------------------------------------------
|
|
568
620
|
|
|
569
621
|
/** GET /api/plugins/blog/comments */
|
|
570
|
-
fastify.get('/comments', { preHandler: [authenticate] }, async (request, reply) => {
|
|
622
|
+
fastify.get('/comments', { preHandler: [authenticate, need('blog-comments.read')] }, async (request, reply) => {
|
|
571
623
|
const { status: statusFilter, postId } = request.query;
|
|
572
624
|
const { entries } = await listEntries(COMMENTS_SLUG, { limit: 100000, sort: 'createdAt', order: 'desc' });
|
|
573
625
|
let comments = entries.map(toComment);
|
|
@@ -583,7 +635,7 @@ export default async function blogPlugin(fastify, options) {
|
|
|
583
635
|
});
|
|
584
636
|
|
|
585
637
|
/** GET /api/plugins/blog/comments/pending-count - the sidebar badge on Blog: comments waiting for approval */
|
|
586
|
-
fastify.get('/comments/pending-count', { preHandler: [authenticate] }, async () => {
|
|
638
|
+
fastify.get('/comments/pending-count', { preHandler: [authenticate, need('blog-comments.read')] }, async () => {
|
|
587
639
|
const [comments, posts] = await Promise.all([loadAll(COMMENTS_SLUG), loadAll(POSTS_SLUG)]);
|
|
588
640
|
const pending = comments.filter(c => c.status === 'pending')
|
|
589
641
|
.sort((a, b) => String(b.createdAt ?? '').localeCompare(String(a.createdAt ?? '')));
|
|
@@ -649,11 +701,15 @@ export default async function blogPlugin(fastify, options) {
|
|
|
649
701
|
createdAt: new Date().toISOString()
|
|
650
702
|
}, { source: 'public' });
|
|
651
703
|
|
|
652
|
-
|
|
704
|
+
const made = toComment(entry);
|
|
705
|
+
const post = await getEntry(POSTS_SLUG, String(postId)).catch(() => null);
|
|
706
|
+
const notice = commentNotice(made, post?.data?.title, '#/plugins/blog/comments');
|
|
707
|
+
if (notice) notify(notice);
|
|
708
|
+
return reply.code(201).send(made);
|
|
653
709
|
});
|
|
654
710
|
|
|
655
711
|
/** PUT /api/plugins/blog/comments/:id */
|
|
656
|
-
fastify.put('/comments/:id', { preHandler: [authenticate] }, async (request, reply) => {
|
|
712
|
+
fastify.put('/comments/:id', { preHandler: [authenticate, need('blog-comments.update')] }, async (request, reply) => {
|
|
657
713
|
const entry = await getEntry(COMMENTS_SLUG, request.params.id);
|
|
658
714
|
if (!entry) return reply.code(404).send({ error: 'Comment not found' });
|
|
659
715
|
|
|
@@ -669,15 +725,17 @@ export default async function blogPlugin(fastify, options) {
|
|
|
669
725
|
}
|
|
670
726
|
|
|
671
727
|
const updated = await updateEntry(COMMENTS_SLUG, request.params.id, merged);
|
|
728
|
+
if (merged.status !== 'pending') resolveNotice(commentKey(request.params.id));
|
|
672
729
|
return reply.send(toComment(updated));
|
|
673
730
|
});
|
|
674
731
|
|
|
675
732
|
/** DELETE /api/plugins/blog/comments/:id */
|
|
676
|
-
fastify.delete('/comments/:id', { preHandler: [authenticate] }, async (request, reply) => {
|
|
733
|
+
fastify.delete('/comments/:id', { preHandler: [authenticate, need('blog-comments.delete')] }, async (request, reply) => {
|
|
677
734
|
const entry = await getEntry(COMMENTS_SLUG, request.params.id);
|
|
678
735
|
if (!entry) return reply.code(404).send({ error: 'Comment not found' });
|
|
679
736
|
|
|
680
737
|
await deleteEntry(COMMENTS_SLUG, request.params.id);
|
|
738
|
+
resolveNotice(commentKey(request.params.id));
|
|
681
739
|
return reply.send({ ok: true });
|
|
682
740
|
});
|
|
683
741
|
|
|
@@ -693,7 +751,7 @@ export default async function blogPlugin(fastify, options) {
|
|
|
693
751
|
// whole config object as the name.
|
|
694
752
|
|
|
695
753
|
/** GET /api/plugins/blog/settings */
|
|
696
|
-
fastify.get('/settings', { preHandler: [authenticate] }, async (request, reply) => {
|
|
754
|
+
fastify.get('/settings', { preHandler: [authenticate, need('blog-posts.read')] }, async (request, reply) => {
|
|
697
755
|
const stored = getPluginStates()?.blog?.settings ?? {};
|
|
698
756
|
return reply.send({ ...defaultConfig, ...DISPLAY_DEFAULTS, ...stored });
|
|
699
757
|
});
|
package/plugins/blog/plugin.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blog",
|
|
3
3
|
"displayName": "Blog",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.8.0",
|
|
5
5
|
"tier": "free",
|
|
6
6
|
"description": "Public-facing blog with posts, categories and comments - layouts and listings built from editable blocks.",
|
|
7
7
|
"author": "Domma CMS",
|
|
8
|
-
"date": "2026-09-
|
|
8
|
+
"date": "2026-09-26",
|
|
9
9
|
"icon": "file-text",
|
|
10
10
|
"admin": {
|
|
11
|
-
"css": [
|
|
11
|
+
"css": [
|
|
12
|
+
"admin/css/index.css"
|
|
13
|
+
],
|
|
12
14
|
"sidebar": [
|
|
13
15
|
{
|
|
14
16
|
"id": "blog",
|
|
@@ -18,27 +20,178 @@
|
|
|
18
20
|
"section": "#/plugins/blog",
|
|
19
21
|
"countUrl": "/api/plugins/blog/comments/pending-count",
|
|
20
22
|
"children": [
|
|
21
|
-
{
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
{
|
|
24
|
+
"id": "blog-posts",
|
|
25
|
+
"text": "Posts",
|
|
26
|
+
"url": "#/plugins/blog/posts"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "blog-categories",
|
|
30
|
+
"text": "Categories",
|
|
31
|
+
"url": "#/plugins/blog/categories"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "blog-comments",
|
|
35
|
+
"text": "Comments",
|
|
36
|
+
"url": "#/plugins/blog/comments"
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"permission": "blog-posts"
|
|
25
40
|
}
|
|
26
41
|
],
|
|
27
42
|
"routes": [
|
|
28
|
-
{
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
{
|
|
34
|
-
|
|
43
|
+
{
|
|
44
|
+
"path": "/plugins/blog",
|
|
45
|
+
"view": "plugin-blog-posts",
|
|
46
|
+
"title": "Blog - Domma CMS"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"path": "/plugins/blog/posts",
|
|
50
|
+
"view": "plugin-blog-posts",
|
|
51
|
+
"title": "Blog Posts - Domma CMS"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"path": "/plugins/blog/posts/new",
|
|
55
|
+
"view": "plugin-blog-post-editor",
|
|
56
|
+
"title": "New Post - Domma CMS"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"path": "/plugins/blog/posts/:id",
|
|
60
|
+
"view": "plugin-blog-post-editor",
|
|
61
|
+
"title": "Edit Post - Domma CMS"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"path": "/plugins/blog/categories",
|
|
65
|
+
"view": "plugin-blog-categories",
|
|
66
|
+
"title": "Blog Categories - Domma CMS"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"path": "/plugins/blog/comments",
|
|
70
|
+
"view": "plugin-blog-comments",
|
|
71
|
+
"title": "Blog Comments - Domma CMS"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"path": "/plugins/blog/settings",
|
|
75
|
+
"view": "plugin-blog-settings",
|
|
76
|
+
"title": "Blog Settings - Domma CMS"
|
|
77
|
+
}
|
|
35
78
|
],
|
|
36
79
|
"views": {
|
|
37
|
-
"plugin-blog-posts":
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"plugin-blog-
|
|
80
|
+
"plugin-blog-posts": {
|
|
81
|
+
"entry": "blog/admin/views/blog.js?v=9",
|
|
82
|
+
"exportName": "blogView"
|
|
83
|
+
},
|
|
84
|
+
"plugin-blog-post-editor": {
|
|
85
|
+
"entry": "blog/admin/views/post-editor.js?v=9",
|
|
86
|
+
"exportName": "postEditorView"
|
|
87
|
+
},
|
|
88
|
+
"plugin-blog-categories": {
|
|
89
|
+
"entry": "blog/admin/views/categories.js?v=9",
|
|
90
|
+
"exportName": "categoriesView"
|
|
91
|
+
},
|
|
92
|
+
"plugin-blog-comments": {
|
|
93
|
+
"entry": "blog/admin/views/comments.js?v=9",
|
|
94
|
+
"exportName": "commentsView"
|
|
95
|
+
},
|
|
96
|
+
"plugin-blog-settings": {
|
|
97
|
+
"entry": "blog/admin/views/settings.js?v=9",
|
|
98
|
+
"exportName": "settingsView"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"permissions": [
|
|
103
|
+
{
|
|
104
|
+
"key": "blog-posts",
|
|
105
|
+
"label": "Blog posts",
|
|
106
|
+
"group": "Blog",
|
|
107
|
+
"description": "Write, edit and publish blog posts.",
|
|
108
|
+
"actions": [
|
|
109
|
+
{
|
|
110
|
+
"key": "read",
|
|
111
|
+
"label": "View",
|
|
112
|
+
"description": "See the posts screens (a writer sees their own posts)"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"key": "create",
|
|
116
|
+
"label": "Write",
|
|
117
|
+
"description": "Write new posts"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"key": "update",
|
|
121
|
+
"label": "Edit",
|
|
122
|
+
"description": "Edit posts (a writer: their own)"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"key": "delete",
|
|
126
|
+
"label": "Delete",
|
|
127
|
+
"description": "Delete any post"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"key": "publish",
|
|
131
|
+
"label": "Publish",
|
|
132
|
+
"description": "Publish and schedule; see and edit everyone's posts"
|
|
133
|
+
}
|
|
134
|
+
],
|
|
135
|
+
"grant": [
|
|
136
|
+
"admin"
|
|
137
|
+
]
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"key": "blog-categories",
|
|
141
|
+
"label": "Blog categories",
|
|
142
|
+
"group": "Blog",
|
|
143
|
+
"description": "The blog's categories.",
|
|
144
|
+
"actions": [
|
|
145
|
+
{
|
|
146
|
+
"key": "read",
|
|
147
|
+
"label": "View",
|
|
148
|
+
"description": "See and pick categories"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"key": "create",
|
|
152
|
+
"label": "Create",
|
|
153
|
+
"description": "Add categories"
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"key": "update",
|
|
157
|
+
"label": "Edit",
|
|
158
|
+
"description": "Rename categories"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"key": "delete",
|
|
162
|
+
"label": "Delete",
|
|
163
|
+
"description": "Remove categories"
|
|
164
|
+
}
|
|
165
|
+
],
|
|
166
|
+
"grant": [
|
|
167
|
+
"admin"
|
|
168
|
+
]
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"key": "blog-comments",
|
|
172
|
+
"label": "Blog comments",
|
|
173
|
+
"group": "Blog",
|
|
174
|
+
"description": "Readers' comments and moderation.",
|
|
175
|
+
"actions": [
|
|
176
|
+
{
|
|
177
|
+
"key": "read",
|
|
178
|
+
"label": "View",
|
|
179
|
+
"description": "See comments, with the commenter's email"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"key": "update",
|
|
183
|
+
"label": "Moderate",
|
|
184
|
+
"description": "Approve, reject, mark as spam"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"key": "delete",
|
|
188
|
+
"label": "Delete",
|
|
189
|
+
"description": "Delete comments"
|
|
190
|
+
}
|
|
191
|
+
],
|
|
192
|
+
"grant": [
|
|
193
|
+
"admin"
|
|
194
|
+
]
|
|
42
195
|
}
|
|
43
|
-
|
|
196
|
+
]
|
|
44
197
|
}
|
|
@@ -2,9 +2,22 @@
|
|
|
2
2
|
"name": "blog-author",
|
|
3
3
|
"label": "Blog Author",
|
|
4
4
|
"level": 2,
|
|
5
|
-
"permissions": [
|
|
5
|
+
"permissions": [
|
|
6
|
+
"blog-posts.read",
|
|
7
|
+
"blog-posts.create",
|
|
8
|
+
"blog-posts.update",
|
|
9
|
+
"blog-categories.read"
|
|
10
|
+
],
|
|
6
11
|
"badgeClass": "badge-info",
|
|
7
12
|
"resources": [
|
|
8
|
-
{
|
|
13
|
+
{
|
|
14
|
+
"key": "blog-posts",
|
|
15
|
+
"label": "Blog Posts",
|
|
16
|
+
"actions": [
|
|
17
|
+
"read",
|
|
18
|
+
"create",
|
|
19
|
+
"update"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
9
22
|
]
|
|
10
23
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Blog access by permission (lib/access.js). */
|
|
2
|
+
import {test} from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import {AUTHOR_MARKER, AUTHOR_PERMISSIONS, authorPatch, permissionChecker, rolesOf} from '../lib/access.js';
|
|
5
|
+
|
|
6
|
+
const HOLDERS = {
|
|
7
|
+
'blog-posts.read': ['super-admin', 'admin', 'blog-editor', 'blog-author'],
|
|
8
|
+
'blog-posts.create': ['super-admin', 'admin', 'blog-editor', 'blog-author'],
|
|
9
|
+
'blog-posts.publish': ['super-admin', 'admin', 'blog-editor'],
|
|
10
|
+
'blog-comments.read': ['super-admin', 'admin', 'blog-editor']
|
|
11
|
+
};
|
|
12
|
+
const can = permissionChecker((k) => HOLDERS[k] || []);
|
|
13
|
+
|
|
14
|
+
test('by permission, counting additional roles - never by role name alone', () => {
|
|
15
|
+
assert.equal(can({role: 'user', additionalRoles: ['blog-editor']}, 'blog-posts.publish'), true, 'an editor held as an additional role');
|
|
16
|
+
assert.equal(can({role: 'blog-author'}, 'blog-posts.publish'), false);
|
|
17
|
+
assert.equal(can({role: 'blog-author'}, 'blog-posts.create'), true);
|
|
18
|
+
assert.equal(can({role: 'user'}, 'blog-posts.read'), false, 'a plain account reads no drafts');
|
|
19
|
+
assert.equal(can({role: 'user'}, 'blog-comments.read'), false, 'nor commenters');
|
|
20
|
+
assert.equal(can(null, 'blog-posts.read'), false);
|
|
21
|
+
assert.deepEqual(rolesOf({role: 'a', additionalRoles: ['b', 'a']}), ['a', 'b']);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('authorPatch makes a bare blog-posts explicit, once, and leaves edited roles alone', () => {
|
|
25
|
+
const shipped = {name: 'blog-author', plugin: 'blog', permissions: ['blog-posts']};
|
|
26
|
+
assert.deepEqual(authorPatch(shipped), {grantedBy: [AUTHOR_MARKER], permissions: [...AUTHOR_PERMISSIONS]});
|
|
27
|
+
assert.equal(authorPatch({...shipped, grantedBy: [AUTHOR_MARKER]}), null, 'once');
|
|
28
|
+
assert.deepEqual(authorPatch({...shipped, permissions: ['blog-posts.read']}), {grantedBy: [AUTHOR_MARKER]}, 'an admin\'s own list is kept');
|
|
29
|
+
assert.equal(authorPatch({...shipped, plugin: 'other'}), null);
|
|
30
|
+
assert.equal(authorPatch({name: 'blog-editor', plugin: 'blog', permissions: ['blog-posts']}), null);
|
|
31
|
+
assert.ok(!AUTHOR_PERMISSIONS.some(p => /publish|delete/.test(p)));
|
|
32
|
+
});
|