domma-cms 0.10.0 → 0.12.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/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/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
|
@@ -265,7 +265,7 @@ export async function createAction(data, userId = null) {
|
|
|
265
265
|
},
|
|
266
266
|
steps,
|
|
267
267
|
access: {
|
|
268
|
-
roles: access?.roles || ['admin'],
|
|
268
|
+
roles: access?.roles || ['admin', 'super-admin'],
|
|
269
269
|
rowLevel: access?.rowLevel || null
|
|
270
270
|
},
|
|
271
271
|
meta: { createdAt: now, updatedAt: now, createdBy: userId }
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for outbound calls from domma-cms → domma-cms-manager.
|
|
3
|
+
* Reads MANAGER_URL and INSTANCE_SECRET from env at call time.
|
|
4
|
+
* All functions return null/[] gracefully if MANAGER_URL is unset.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {createHmac} from 'node:crypto';
|
|
8
|
+
import {readFile} from 'node:fs/promises';
|
|
9
|
+
import {fileURLToPath} from 'node:url';
|
|
10
|
+
import {dirname, join} from 'node:path';
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Read the CMS version from the root package.json.
|
|
16
|
+
* @returns {Promise<string>} The version string, or '0.0.0' on error.
|
|
17
|
+
*/
|
|
18
|
+
async function getCmsVersion() {
|
|
19
|
+
try {
|
|
20
|
+
const pkgPath = join(__dirname, '../../package.json');
|
|
21
|
+
const raw = await readFile(pkgPath, 'utf-8');
|
|
22
|
+
return JSON.parse(raw).version ?? '0.0.0';
|
|
23
|
+
} catch {
|
|
24
|
+
return '0.0.0';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Register this CMS instance with domma-cms-manager.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} fingerprint - Unique instance fingerprint.
|
|
32
|
+
* @returns {Promise<{licenseToken: string, instanceId: string}|null>}
|
|
33
|
+
*/
|
|
34
|
+
export async function registerInstance(fingerprint) {
|
|
35
|
+
const MANAGER_URL = process.env.MANAGER_URL;
|
|
36
|
+
const INSTANCE_SECRET = process.env.INSTANCE_SECRET;
|
|
37
|
+
|
|
38
|
+
if (!MANAGER_URL) return null;
|
|
39
|
+
if (!INSTANCE_SECRET) {
|
|
40
|
+
console.warn('[managerClient] INSTANCE_SECRET not set — skipping registerInstance');
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const cmsVersion = await getCmsVersion();
|
|
46
|
+
const hostname = process.env.HOSTNAME ?? 'unknown';
|
|
47
|
+
|
|
48
|
+
const token = createHmac('sha256', INSTANCE_SECRET)
|
|
49
|
+
.update(fingerprint)
|
|
50
|
+
.digest('hex');
|
|
51
|
+
|
|
52
|
+
const res = await fetch(`${MANAGER_URL}/api/instances/register`, {
|
|
53
|
+
signal: AbortSignal.timeout(10_000),
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: {
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
'X-Instance-Token': token,
|
|
58
|
+
},
|
|
59
|
+
body: JSON.stringify({ fingerprint, cmsVersion, hostname }),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
console.warn(`[managerClient] registerInstance failed: ${res.status} ${res.statusText}`);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return await res.json();
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.warn('[managerClient] registerInstance error:', err.message);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Fetch the plugin catalogue from domma-cms-manager.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} licenseToken - License token obtained from registerInstance.
|
|
78
|
+
* @returns {Promise<Array<{slug: string, version: string, displayName: string, description: string, price: number, entitled: boolean}>>}
|
|
79
|
+
*/
|
|
80
|
+
export async function fetchCatalogue(licenseToken) {
|
|
81
|
+
const MANAGER_URL = process.env.MANAGER_URL;
|
|
82
|
+
|
|
83
|
+
if (!MANAGER_URL) return [];
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const res = await fetch(`${MANAGER_URL}/api/plugins/catalogue`, {
|
|
87
|
+
signal: AbortSignal.timeout(10_000),
|
|
88
|
+
headers: {
|
|
89
|
+
Authorization: `Bearer ${licenseToken}`,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
console.warn(`[managerClient] fetchCatalogue failed: ${res.status} ${res.statusText}`);
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return await res.json();
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.warn('[managerClient] fetchCatalogue error:', err.message);
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Fetch a plugin bundle (tarball + signature) from domma-cms-manager.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} slug - Plugin slug.
|
|
109
|
+
* @param {string} version - Plugin version.
|
|
110
|
+
* @param {string} licenseToken - License token obtained from registerInstance.
|
|
111
|
+
* @returns {Promise<{tarball: Buffer, signature: string, publicKeyId: string}|null>}
|
|
112
|
+
*/
|
|
113
|
+
export async function fetchPluginBundle(slug, version, licenseToken) {
|
|
114
|
+
const MANAGER_URL = process.env.MANAGER_URL;
|
|
115
|
+
|
|
116
|
+
if (!MANAGER_URL) return null;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const res = await fetch(`${MANAGER_URL}/api/plugins/install/${slug}/${version}`, {
|
|
120
|
+
signal: AbortSignal.timeout(10_000),
|
|
121
|
+
headers: {
|
|
122
|
+
Authorization: `Bearer ${licenseToken}`,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (!res.ok) {
|
|
127
|
+
console.warn(`[managerClient] fetchPluginBundle failed: ${res.status} ${res.statusText}`);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const json = await res.json();
|
|
132
|
+
|
|
133
|
+
if (typeof json.tarball !== 'string' || json.tarball.length > 100 * 1024 * 1024) {
|
|
134
|
+
console.warn('[managerClient] fetchPluginBundle: tarball too large or invalid, refusing decode');
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
tarball: Buffer.from(json.tarball, 'base64'),
|
|
140
|
+
signature: json.signature,
|
|
141
|
+
publicKeyId: json.publicKeyId,
|
|
142
|
+
};
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.warn('[managerClient] fetchPluginBundle error:', err.message);
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Send a heartbeat to domma-cms-manager with the list of installed plugins.
|
|
151
|
+
*
|
|
152
|
+
* @param {string} licenseToken - License token obtained from registerInstance.
|
|
153
|
+
* @param {string[]} installedSlugs - Array of installed plugin slugs.
|
|
154
|
+
* @returns {Promise<boolean>} True on success, false on error.
|
|
155
|
+
*/
|
|
156
|
+
export async function heartbeat(licenseToken, installedSlugs) {
|
|
157
|
+
const MANAGER_URL = process.env.MANAGER_URL;
|
|
158
|
+
|
|
159
|
+
if (!MANAGER_URL) return false;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const res = await fetch(`${MANAGER_URL}/api/instances/heartbeat`, {
|
|
163
|
+
signal: AbortSignal.timeout(10_000),
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: {
|
|
166
|
+
'Content-Type': 'application/json',
|
|
167
|
+
Authorization: `Bearer ${licenseToken}`,
|
|
168
|
+
},
|
|
169
|
+
body: JSON.stringify({ installedSlugs, versions: {} }),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (!res.ok) {
|
|
173
|
+
console.warn(`[managerClient] heartbeat failed: ${res.status} ${res.statusText}`);
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return true;
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.warn('[managerClient] heartbeat error:', err.message);
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -1,173 +1,245 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Permission Registry
|
|
3
|
-
* Single source of truth for all resource/action definitions in the CMS.
|
|
4
|
-
* Adding a new resource or custom action is a one-file change here.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
{key: '
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
{key: '
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
{key: '
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
{key: '
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
{key: '
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
{key: '
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
{key: '
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
{key: '
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
{key: '
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
{key: '
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
{key: '
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Permission Registry
|
|
3
|
+
* Single source of truth for all resource/action definitions in the CMS.
|
|
4
|
+
* Adding a new resource or custom action is a one-file change here.
|
|
5
|
+
*
|
|
6
|
+
* Plugin-contributed resources live in _pluginContributed and are merged at
|
|
7
|
+
* call time via getEffectiveRegistry(). The base REGISTRY is never mutated.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** @type {Array<{key:string,label:string,description:string,icon:string,group:string,plugin?:string,actions:{key:string,label:string,description:string}[]}>} */
|
|
11
|
+
export const REGISTRY = [
|
|
12
|
+
{
|
|
13
|
+
key: 'pages',
|
|
14
|
+
label: 'Pages',
|
|
15
|
+
description: 'Manage site pages and their content.',
|
|
16
|
+
icon: 'file-text',
|
|
17
|
+
group: 'Content',
|
|
18
|
+
actions: [
|
|
19
|
+
{key: 'read', label: 'View', description: 'View pages and their content'},
|
|
20
|
+
{key: 'create', label: 'Create', description: 'Create new pages'},
|
|
21
|
+
{key: 'update', label: 'Edit', description: 'Edit existing pages'},
|
|
22
|
+
{key: 'delete', label: 'Delete', description: 'Delete pages'}
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
key: 'media',
|
|
27
|
+
label: 'Media',
|
|
28
|
+
description: 'Upload and manage media files.',
|
|
29
|
+
icon: 'image',
|
|
30
|
+
group: 'Content',
|
|
31
|
+
actions: [
|
|
32
|
+
{key: 'read', label: 'View', description: 'View media library'},
|
|
33
|
+
{key: 'create', label: 'Upload', description: 'Upload new files'},
|
|
34
|
+
{key: 'update', label: 'Edit', description: 'Rename or update file metadata'},
|
|
35
|
+
{key: 'delete', label: 'Delete', description: 'Delete media files'}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: 'blocks',
|
|
40
|
+
label: 'Blocks',
|
|
41
|
+
description: 'Manage reusable content blocks.',
|
|
42
|
+
icon: 'box',
|
|
43
|
+
group: 'Content',
|
|
44
|
+
actions: [
|
|
45
|
+
{key: 'read', label: 'View', description: 'View content blocks'},
|
|
46
|
+
{key: 'create', label: 'Create', description: 'Create new blocks'},
|
|
47
|
+
{key: 'update', label: 'Edit', description: 'Edit existing blocks'},
|
|
48
|
+
{key: 'delete', label: 'Delete', description: 'Delete blocks'}
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: 'navigation',
|
|
53
|
+
label: 'Navigation',
|
|
54
|
+
description: 'Configure site navigation menus.',
|
|
55
|
+
icon: 'menu',
|
|
56
|
+
group: 'Structure',
|
|
57
|
+
actions: [
|
|
58
|
+
{key: 'read', label: 'View', description: 'View navigation menus'},
|
|
59
|
+
{key: 'create', label: 'Create', description: 'Create new menu items'},
|
|
60
|
+
{key: 'update', label: 'Edit', description: 'Edit navigation structure'},
|
|
61
|
+
{key: 'delete', label: 'Delete', description: 'Remove menu items'}
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: 'layouts',
|
|
66
|
+
label: 'Layouts',
|
|
67
|
+
description: 'Manage page layouts and templates.',
|
|
68
|
+
icon: 'layout',
|
|
69
|
+
group: 'Structure',
|
|
70
|
+
actions: [
|
|
71
|
+
{key: 'read', label: 'View', description: 'View layouts'},
|
|
72
|
+
{key: 'create', label: 'Create', description: 'Create new layouts'},
|
|
73
|
+
{key: 'update', label: 'Edit', description: 'Edit layouts'},
|
|
74
|
+
{key: 'delete', label: 'Delete', description: 'Delete layouts'}
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
key: 'collections',
|
|
79
|
+
label: 'Collections',
|
|
80
|
+
description: 'Manage data collections and entries.',
|
|
81
|
+
icon: 'database',
|
|
82
|
+
group: 'Data',
|
|
83
|
+
actions: [
|
|
84
|
+
{key: 'read', label: 'View', description: 'View collection entries'},
|
|
85
|
+
{key: 'create', label: 'Create', description: 'Create new entries'},
|
|
86
|
+
{key: 'update', label: 'Edit', description: 'Edit existing entries'},
|
|
87
|
+
{key: 'delete', label: 'Delete', description: 'Delete entries'}
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
key: 'views',
|
|
92
|
+
label: 'Views',
|
|
93
|
+
description: 'Create and manage data views.',
|
|
94
|
+
icon: 'eye',
|
|
95
|
+
group: 'Data',
|
|
96
|
+
actions: [
|
|
97
|
+
{key: 'read', label: 'View', description: 'View data views'},
|
|
98
|
+
{key: 'create', label: 'Create', description: 'Create new views'},
|
|
99
|
+
{key: 'update', label: 'Edit', description: 'Edit views'},
|
|
100
|
+
{key: 'delete', label: 'Delete', description: 'Delete views'}
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
key: 'actions',
|
|
105
|
+
label: 'Actions',
|
|
106
|
+
description: 'Configure automated actions.',
|
|
107
|
+
icon: 'zap',
|
|
108
|
+
group: 'Data',
|
|
109
|
+
actions: [
|
|
110
|
+
{key: 'read', label: 'View', description: 'View automated actions'},
|
|
111
|
+
{key: 'create', label: 'Create', description: 'Create new actions'},
|
|
112
|
+
{key: 'update', label: 'Edit', description: 'Edit actions'},
|
|
113
|
+
{key: 'delete', label: 'Delete', description: 'Delete actions'}
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
key: 'users',
|
|
118
|
+
label: 'Users',
|
|
119
|
+
description: 'Manage user accounts.',
|
|
120
|
+
icon: 'users',
|
|
121
|
+
group: 'Configuration',
|
|
122
|
+
actions: [
|
|
123
|
+
{key: 'read', label: 'View', description: 'View user accounts'},
|
|
124
|
+
{key: 'create', label: 'Create', description: 'Create new users'},
|
|
125
|
+
{key: 'update', label: 'Edit', description: 'Edit user accounts'},
|
|
126
|
+
{key: 'delete', label: 'Delete', description: 'Delete users'}
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
key: 'settings',
|
|
131
|
+
label: 'Settings',
|
|
132
|
+
description: 'Configure site settings.',
|
|
133
|
+
icon: 'settings',
|
|
134
|
+
group: 'Configuration',
|
|
135
|
+
actions: [
|
|
136
|
+
{key: 'read', label: 'View', description: 'View site settings'},
|
|
137
|
+
{key: 'create', label: 'Create', description: 'Add new settings'},
|
|
138
|
+
{key: 'update', label: 'Edit', description: 'Modify settings'},
|
|
139
|
+
{key: 'delete', label: 'Delete', description: 'Remove settings'}
|
|
140
|
+
]
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
key: 'plugins',
|
|
144
|
+
label: 'Plugins',
|
|
145
|
+
description: 'Manage CMS plugins.',
|
|
146
|
+
icon: 'package',
|
|
147
|
+
group: 'Configuration',
|
|
148
|
+
actions: [
|
|
149
|
+
{key: 'read', label: 'View', description: 'View installed plugins'},
|
|
150
|
+
{key: 'create', label: 'Install', description: 'Install new plugins'},
|
|
151
|
+
{key: 'update', label: 'Configure', description: 'Configure plugin settings'},
|
|
152
|
+
{key: 'delete', label: 'Remove', description: 'Remove plugins'}
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
key: 'notifications',
|
|
157
|
+
label: 'Notifications',
|
|
158
|
+
description: 'View and manage system notifications.',
|
|
159
|
+
icon: 'bell',
|
|
160
|
+
group: 'Configuration',
|
|
161
|
+
actions: [
|
|
162
|
+
{key: 'read', label: 'View', description: 'View notifications'},
|
|
163
|
+
{key: 'update', label: 'Dismiss', description: 'Mark notifications as read'},
|
|
164
|
+
{key: 'delete', label: 'Clear', description: 'Delete notifications'}
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Plugin-contributed resources registered at runtime via registerPluginResource().
|
|
171
|
+
* Cleared on teardown via unregisterPluginResourcesByPlugin().
|
|
172
|
+
*
|
|
173
|
+
* @type {Array<{key:string,label:string,description:string,icon:string,group:string,plugin:string,actions:{key:string,label:string,description:string}[]}>}
|
|
174
|
+
*/
|
|
175
|
+
const _pluginContributed = [];
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Register a resource contributed by a plugin.
|
|
179
|
+
* Idempotent — if the key already exists in the effective registry it is skipped.
|
|
180
|
+
*
|
|
181
|
+
* @param {{key:string,label:string,description?:string,icon?:string,group?:string,actions?:{key:string,label:string,description?:string}[]}} resource
|
|
182
|
+
* @param {string} plugin - Plugin name that owns this resource
|
|
183
|
+
*/
|
|
184
|
+
export function registerPluginResource(resource, plugin) {
|
|
185
|
+
const effective = getEffectiveRegistry();
|
|
186
|
+
if (effective.some(r => r.key === resource.key)) return;
|
|
187
|
+
|
|
188
|
+
_pluginContributed.push({
|
|
189
|
+
key: resource.key,
|
|
190
|
+
label: resource.label,
|
|
191
|
+
description: resource.description || `${resource.label} (${plugin} plugin)`,
|
|
192
|
+
icon: resource.icon || 'box',
|
|
193
|
+
group: resource.group || 'Plugins',
|
|
194
|
+
plugin,
|
|
195
|
+
actions: resource.actions?.map(a => ({
|
|
196
|
+
key: a.key,
|
|
197
|
+
label: a.label,
|
|
198
|
+
description: a.description || a.label
|
|
199
|
+
})) ?? ACTIONS.map(key => ({key, label: key, description: key}))
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Remove all resources registered by a given plugin.
|
|
205
|
+
*
|
|
206
|
+
* @param {string} plugin - Plugin name
|
|
207
|
+
*/
|
|
208
|
+
export function unregisterPluginResourcesByPlugin(plugin) {
|
|
209
|
+
let i = _pluginContributed.length;
|
|
210
|
+
while (i--) {
|
|
211
|
+
if (_pluginContributed[i].plugin === plugin) {
|
|
212
|
+
_pluginContributed.splice(i, 1);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Return the combined registry: base resources + all plugin-contributed resources.
|
|
219
|
+
*
|
|
220
|
+
* @returns {typeof REGISTRY}
|
|
221
|
+
*/
|
|
222
|
+
export function getEffectiveRegistry() {
|
|
223
|
+
return [...REGISTRY, ..._pluginContributed];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Derived array of base resource key strings. */
|
|
227
|
+
export const RESOURCES = REGISTRY.map(r => r.key);
|
|
228
|
+
|
|
229
|
+
/** Default CRUD action keys. */
|
|
230
|
+
export const ACTIONS = ['read', 'create', 'update', 'delete'];
|
|
231
|
+
|
|
232
|
+
/** Display order for permission groups. */
|
|
233
|
+
export const GROUP_ORDER = ['Content', 'Structure', 'Data', 'Configuration', 'Plugins'];
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Return the action keys defined for a given resource (base or plugin-contributed).
|
|
237
|
+
* Falls back to the default CRUD set if the resource is not found.
|
|
238
|
+
*
|
|
239
|
+
* @param {string} resourceKey
|
|
240
|
+
* @returns {string[]}
|
|
241
|
+
*/
|
|
242
|
+
export function getActionsForResource(resourceKey) {
|
|
243
|
+
const resource = getEffectiveRegistry().find(r => r.key === resourceKey);
|
|
244
|
+
return resource ? resource.actions.map(a => a.key) : [...ACTIONS];
|
|
245
|
+
}
|