domma-cms 0.9.10 → 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/card-builder.js +2 -2
- package/admin/js/lib/markdown-toolbar.js +5 -5
- package/admin/js/lib/safe-html.js +1 -0
- package/admin/js/lib/shortcode-modal.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/page-editor.js +37 -33
- 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/public/js/site.js +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 +76 -9
- 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,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Marketplace API
|
|
3
|
+
* GET /api/plugins/marketplace — fetch catalogue from manager (requires MANAGER_URL)
|
|
4
|
+
* POST /api/plugins/marketplace/install — download + install a plugin bundle
|
|
5
|
+
* DELETE /api/plugins/marketplace/:slug — uninstall an installed plugin
|
|
6
|
+
*
|
|
7
|
+
* All routes require authentication + admin role.
|
|
8
|
+
*/
|
|
9
|
+
import {authenticate, requireAdmin} from '../../middleware/auth.js';
|
|
10
|
+
import {fetchCatalogue, fetchPluginBundle} from '../../services/managerClient.js';
|
|
11
|
+
import {installPlugin, uninstallPlugin} from '../../services/pluginInstaller.js';
|
|
12
|
+
|
|
13
|
+
export async function pluginMarketplaceRoutes(fastify) {
|
|
14
|
+
const adminOnly = { preHandler: [authenticate, requireAdmin] };
|
|
15
|
+
|
|
16
|
+
// GET /plugins/marketplace
|
|
17
|
+
fastify.get('/plugins/marketplace', adminOnly, async (_request, reply) => {
|
|
18
|
+
if (!process.env.MANAGER_URL) {
|
|
19
|
+
return reply.send({ available: false, catalogue: [] });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const licenceToken = process.env.LICENCE_TOKEN;
|
|
23
|
+
const catalogue = await fetchCatalogue(licenceToken);
|
|
24
|
+
return reply.send({ available: true, catalogue });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// POST /plugins/marketplace/install
|
|
28
|
+
fastify.post('/plugins/marketplace/install', adminOnly, async (request, reply) => {
|
|
29
|
+
const { slug, version } = request.body || {};
|
|
30
|
+
|
|
31
|
+
if (!slug || !/^[a-z0-9][a-z0-9-_]{0,63}$/.test(slug)) {
|
|
32
|
+
return reply.code(400).send({ error: 'Invalid plugin slug.' });
|
|
33
|
+
}
|
|
34
|
+
if (!version || !/^\d+\.\d+\.\d+$/.test(version)) {
|
|
35
|
+
return reply.code(400).send({ error: 'Invalid version format. Expected semver (e.g. 1.0.0).' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const licenceToken = process.env.LICENCE_TOKEN;
|
|
39
|
+
const bundle = await fetchPluginBundle(slug, version, licenceToken);
|
|
40
|
+
|
|
41
|
+
if (!bundle) {
|
|
42
|
+
return reply.code(503).send({ error: 'Manager unavailable' });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await installPlugin(slug, bundle);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
fastify.log.error({ err }, '[marketplace] install failed');
|
|
49
|
+
// Known safe error messages from pluginInstaller (for admin clarity)
|
|
50
|
+
const safeMessages = ['Plugin already installed', 'Plugin signature invalid', 'Unknown public key', 'Invalid plugin slug'];
|
|
51
|
+
const safe = safeMessages.some(m => err.message.startsWith(m));
|
|
52
|
+
return reply.code(400).send({ error: safe ? err.message : 'Installation failed.' });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return reply.send({ success: true });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// DELETE /plugins/marketplace/:slug
|
|
59
|
+
fastify.delete('/plugins/marketplace/:slug', adminOnly, async (request, reply) => {
|
|
60
|
+
const { slug } = request.params;
|
|
61
|
+
|
|
62
|
+
if (!slug || !/^[a-z0-9][a-z0-9-_]{0,63}$/.test(slug)) {
|
|
63
|
+
return reply.code(400).send({ error: 'Invalid plugin slug.' });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await uninstallPlugin(slug);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
fastify.log.error({ err }, '[marketplace] uninstall failed');
|
|
70
|
+
return reply.code(400).send({ error: 'Uninstall failed.' });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return reply.send({ success: true });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -54,7 +54,7 @@ export async function usersRoutes(fastify) {
|
|
|
54
54
|
return reply.status(400).send({ error: 'Password must be at least 8 characters' });
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
const targetRole = role || '
|
|
57
|
+
const targetRole = role || 'user';
|
|
58
58
|
if (!canManageUser(request.user.role, targetRole)) {
|
|
59
59
|
return reply.code(403).send({ error: 'You cannot create a user with that role' });
|
|
60
60
|
}
|
|
@@ -125,7 +125,7 @@ export async function viewsRoutes(fastify) {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
const user = request.user;
|
|
128
|
-
const allowedRoles = view.access?.roles || ['admin'];
|
|
128
|
+
const allowedRoles = view.access?.roles || ['admin', 'super-admin'];
|
|
129
129
|
const userLevel = getRoleLevel(user?.role);
|
|
130
130
|
const minAllowed = Math.min(...allowedRoles.map(r => getRoleLevel(r)));
|
|
131
131
|
|
package/server/routes/public.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import {getPage} from '../services/content.js';
|
|
8
8
|
import {renderPage} from '../services/renderer.js';
|
|
9
|
-
import {
|
|
9
|
+
import {checkVisibility} from '../middleware/auth.js';
|
|
10
10
|
import {hooks} from '../services/hooks.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -69,15 +69,10 @@ export async function publicRoutes(fastify) {
|
|
|
69
69
|
let userRole = null;
|
|
70
70
|
try {
|
|
71
71
|
const decoded = await request.jwtVerify();
|
|
72
|
-
userRole = decoded.role;
|
|
73
|
-
} catch { /* no token — treat as unauthenticated */
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const userLevel = getRoleLevel(userRole);
|
|
77
|
-
const visibilityLevel = getRoleLevel(page.visibility);
|
|
78
|
-
const requiredLevel = visibilityLevel === Infinity ? 0 : visibilityLevel;
|
|
72
|
+
if (decoded.type === 'access') userRole = decoded.role;
|
|
73
|
+
} catch { /* no token — treat as unauthenticated */ }
|
|
79
74
|
|
|
80
|
-
if (
|
|
75
|
+
if (!checkVisibility(userRole, page.visibility)) {
|
|
81
76
|
reply.status(403);
|
|
82
77
|
return reply.type('text/html').send(accessDeniedHtml(urlPath));
|
|
83
78
|
}
|
package/server/server.js
CHANGED
|
@@ -17,7 +17,7 @@ import fs from 'fs/promises';
|
|
|
17
17
|
import {fileURLToPath} from 'url';
|
|
18
18
|
import {createRequire} from 'module';
|
|
19
19
|
import {config, getConfig} from './config.js';
|
|
20
|
-
import {registerPlugins} from './services/plugins.js';
|
|
20
|
+
import {getLoadedPlugins, getPluginSettings, registerPlugins} from './services/plugins.js';
|
|
21
21
|
import {load as loadRoles, seed as seedRoles} from './services/roles.js';
|
|
22
22
|
import {ensureAllProfiles, seed as seedUserProfiles} from './services/userProfiles.js';
|
|
23
23
|
import {seedAll as seedPresetCollections} from './services/presetCollections.js';
|
|
@@ -44,6 +44,13 @@ if (!JWT_SECRET || JWT_SECRET === 'CHANGE_ME' || JWT_SECRET.length < 32) {
|
|
|
44
44
|
process.exit(1);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// MANAGER_SECRET is optional — only needed when domma-cms-manager pushes notifications.
|
|
48
|
+
// Warn if set but insecure; silently accept if absent (manager push is disabled).
|
|
49
|
+
const MANAGER_SECRET = process.env.MANAGER_SECRET;
|
|
50
|
+
if (MANAGER_SECRET && MANAGER_SECRET.length < 32) {
|
|
51
|
+
console.warn(' WARNING: MANAGER_SECRET is set but too short (minimum 32 characters). Manager notifications disabled until fixed.');
|
|
52
|
+
}
|
|
53
|
+
|
|
47
54
|
const app = Fastify({
|
|
48
55
|
logger: {level: process.env.NODE_ENV === 'development' ? 'info' : 'warn'},
|
|
49
56
|
// When running behind a reverse proxy (e.g. domma-cms-manager), trust the
|
|
@@ -227,7 +234,8 @@ const { layoutsRoutes } = await import('./routes/api/layouts.js');
|
|
|
227
234
|
const { navigationRoutes } = await import('./routes/api/navigation.js');
|
|
228
235
|
const { mediaRoutes } = await import('./routes/api/media.js');
|
|
229
236
|
const { usersRoutes } = await import('./routes/api/users.js');
|
|
230
|
-
const { pluginsRoutes }
|
|
237
|
+
const { pluginsRoutes } = await import('./routes/api/plugins.js');
|
|
238
|
+
const { pluginMarketplaceRoutes } = await import('./routes/api/plugin-marketplace.js');
|
|
231
239
|
const { collectionsRoutes } = await import('./routes/api/collections.js');
|
|
232
240
|
const { formsRoutes } = await import('./routes/api/forms.js');
|
|
233
241
|
const { viewsRoutes } = await import('./routes/api/views.js');
|
|
@@ -242,7 +250,8 @@ await app.register(layoutsRoutes, { prefix: '/api' });
|
|
|
242
250
|
await app.register(navigationRoutes, { prefix: '/api' });
|
|
243
251
|
await app.register(mediaRoutes, { prefix: '/api' });
|
|
244
252
|
await app.register(usersRoutes, { prefix: '/api' });
|
|
245
|
-
await app.register(pluginsRoutes,
|
|
253
|
+
await app.register(pluginsRoutes, { prefix: '/api' });
|
|
254
|
+
await app.register(pluginMarketplaceRoutes, { prefix: '/api' });
|
|
246
255
|
await app.register(collectionsRoutes, { prefix: '/api' });
|
|
247
256
|
await app.register(formsRoutes, { prefix: '/api' });
|
|
248
257
|
await app.register(viewsRoutes, { prefix: '/api' });
|
|
@@ -257,6 +266,26 @@ await app.register(effectsRoutes, {prefix: '/api'});
|
|
|
257
266
|
|
|
258
267
|
await registerPlugins(app);
|
|
259
268
|
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// Public Plugin Routes (root-level, before catch-all)
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
const { authenticate: _authenticate, requireAdmin: _requireAdmin, requireVisibility: _requireVisibility } = await import('./middleware/auth.js');
|
|
274
|
+
for (const [name, plugin] of Object.entries(getLoadedPlugins())) {
|
|
275
|
+
if (!plugin.enabled || !plugin.publicEntry) continue;
|
|
276
|
+
try {
|
|
277
|
+
const { default: publicPlugin } = await import(plugin.publicEntry);
|
|
278
|
+
const settings = await getPluginSettings(name);
|
|
279
|
+
await app.register(publicPlugin, {
|
|
280
|
+
settings,
|
|
281
|
+
auth: { authenticate: _authenticate, requireAdmin: _requireAdmin, requireVisibility: _requireVisibility },
|
|
282
|
+
config: getConfig()
|
|
283
|
+
});
|
|
284
|
+
} catch (err) {
|
|
285
|
+
app.log.error(`[plugins] Failed to register public plugin ${name}: ${err.message}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
260
289
|
// ---------------------------------------------------------------------------
|
|
261
290
|
// Public Site (catch-all — must be last)
|
|
262
291
|
// ---------------------------------------------------------------------------
|
|
@@ -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
|
+
}
|
|
@@ -20,7 +20,7 @@ const BUILTIN_SHORTCODES = new Set([
|
|
|
20
20
|
'accordion', 'item', 'carousel', 'slide', 'countdown', 'timeline',
|
|
21
21
|
'event', 'spacer', 'center', 'icon', 'form', 'hero', 'table', 'badge',
|
|
22
22
|
'text', 'button', 'link', 'cta', 'grid', 'row', 'col', 'card',
|
|
23
|
-
'slideover', 'counter', 'celebrate', 'firework', 'fireworks', 'scribe',
|
|
23
|
+
'banner', 'slideover', 'counter', 'celebrate', 'firework', 'fireworks', 'scribe',
|
|
24
24
|
'reveal', 'breathe', 'pulse', 'shake', 'scramble', 'ripple', 'twinkle',
|
|
25
25
|
'animate', 'ambient', 'list-group',
|
|
26
26
|
]);
|
|
@@ -1473,7 +1473,10 @@ function processTabsBlocks(markdown) {
|
|
|
1473
1473
|
const prefix = `dm-tab-${counter}`;
|
|
1474
1474
|
const attrs = parseShortcodeAttrs(attrStr);
|
|
1475
1475
|
const pillsClass = attrs.style === 'pills' ? ' tabs-pills' : '';
|
|
1476
|
+
const borderedClass = attrs.style === 'bordered' ? ' tabs--bordered' : '';
|
|
1477
|
+
const fadeClass = 'fade' in attrs ? ' tabs--fade' : '';
|
|
1476
1478
|
const centeredClass = attrs.align === 'center' ? ' tabs-centered' : '';
|
|
1479
|
+
const rightClass = attrs.align === 'right' ? ' tabs--right' : '';
|
|
1477
1480
|
const idAttr = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
|
|
1478
1481
|
|
|
1479
1482
|
// Parse inner [tab title="..."] items
|
|
@@ -1489,20 +1492,22 @@ function processTabsBlocks(markdown) {
|
|
|
1489
1492
|
// that would confuse subsequent scrubCodeRegions calls.
|
|
1490
1493
|
const restoredBody = restore(tabBody.trim());
|
|
1491
1494
|
const bodyHtml = marked.parse(processCardBlocks(processGridBlocks(restoredBody)));
|
|
1492
|
-
|
|
1495
|
+
const icon = tabAttrs.icon ? tabAttrs.icon.trim() : '';
|
|
1496
|
+
items.push({title, icon, paneId, bodyHtml, first: tabIdx === 1});
|
|
1493
1497
|
});
|
|
1494
1498
|
|
|
1495
1499
|
if (!items.length) return '';
|
|
1496
1500
|
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1501
|
+
const navItems = items.map(t => {
|
|
1502
|
+
const iconHtml = t.icon ? `<span data-icon="${escapeAttr(t.icon)}"></span>` : '';
|
|
1503
|
+
return `<button class="tab-item${t.first ? ' active' : ''}">${iconHtml}${escapeAttr(t.title)}</button>`;
|
|
1504
|
+
}).join('\n ');
|
|
1500
1505
|
const panes = items.map(t =>
|
|
1501
1506
|
`<div class="tab-panel${t.first ? ' active' : ''}">${t.bodyHtml}</div>`
|
|
1502
1507
|
).join('\n ');
|
|
1503
1508
|
|
|
1504
1509
|
return (
|
|
1505
|
-
`<div class="tabs${pillsClass}${centeredClass}"${idAttr}>\n` +
|
|
1510
|
+
`<div class="tabs${pillsClass}${borderedClass}${fadeClass}${centeredClass}${rightClass}"${idAttr}>\n` +
|
|
1506
1511
|
` <div class="tab-list">\n ${navItems}\n </div>\n` +
|
|
1507
1512
|
` <div class="tab-content">\n ${panes}\n </div>\n` +
|
|
1508
1513
|
`</div>\n`
|
|
@@ -2279,6 +2284,8 @@ function processHeroBlocks(markdown) {
|
|
|
2279
2284
|
const cls = attrs.class || '';
|
|
2280
2285
|
const id = attrs.id || '';
|
|
2281
2286
|
const bg = attrs.bg || '';
|
|
2287
|
+
const heroColor = attrs.color ? String(attrs.color) : '';
|
|
2288
|
+
const minHeight = attrs['min-height'] ? String(attrs['min-height']) : '';
|
|
2282
2289
|
|
|
2283
2290
|
const twinkle = 'twinkle' in attrs;
|
|
2284
2291
|
const twinkleCount = attrs['twinkle-count'] || '';
|
|
@@ -2299,7 +2306,11 @@ function processHeroBlocks(markdown) {
|
|
|
2299
2306
|
|
|
2300
2307
|
const styleParts = [];
|
|
2301
2308
|
if (image) styleParts.push(`background-image:url('${escapeAttr(image)}')`);
|
|
2302
|
-
|
|
2309
|
+
const resolvedBg = heroColor || bg;
|
|
2310
|
+
const safeBg = resolvedBg && /^[a-zA-Z0-9#(),.\s%/-]+$/.test(resolvedBg) ? resolvedBg : '';
|
|
2311
|
+
if (safeBg) styleParts.push(`background-color:${safeBg}`);
|
|
2312
|
+
const safeMinHeight = minHeight && /^[0-9]+(?:px|em|rem|vh|vw|%)$/.test(minHeight) ? minHeight : '';
|
|
2313
|
+
if (safeMinHeight) styleParts.push(`min-height:${safeMinHeight}`);
|
|
2303
2314
|
const style = styleParts.length ? ` style="${styleParts.join(';')}"` : '';
|
|
2304
2315
|
const idAttr = id ? ` id="${escapeAttr(id)}"` : '';
|
|
2305
2316
|
|
|
@@ -2337,6 +2348,61 @@ function processHeroBlocks(markdown) {
|
|
|
2337
2348
|
* @param {string} markdown
|
|
2338
2349
|
* @returns {string}
|
|
2339
2350
|
*/
|
|
2351
|
+
/**
|
|
2352
|
+
* Pre-process [banner] shortcodes before running through marked.
|
|
2353
|
+
*
|
|
2354
|
+
* Syntax:
|
|
2355
|
+
* [banner type="info" title="Note" icon="info" dismissible]
|
|
2356
|
+
* Body content (Markdown parsed).
|
|
2357
|
+
* [/banner]
|
|
2358
|
+
*
|
|
2359
|
+
* Supported attributes:
|
|
2360
|
+
* type - info (default) / success / warning / danger / neutral
|
|
2361
|
+
* title - optional heading text
|
|
2362
|
+
* icon - Domma icon name (renders <span data-icon="...">)
|
|
2363
|
+
* dismissible - bare flag; adds dismiss button
|
|
2364
|
+
* class - extra CSS class on the wrapper
|
|
2365
|
+
*
|
|
2366
|
+
* @param {string} markdown
|
|
2367
|
+
* @returns {string}
|
|
2368
|
+
*/
|
|
2369
|
+
function processBannerBlocks(markdown) {
|
|
2370
|
+
return markdown.replace(
|
|
2371
|
+
/\[banner([^\]]*)\]([\s\S]*?)\[\/banner\]/gi,
|
|
2372
|
+
(_, attrStr, body) => {
|
|
2373
|
+
const attrs = parseShortcodeAttrs(attrStr);
|
|
2374
|
+
const VALID_BANNER_TYPES = new Set(['info', 'success', 'warning', 'danger', 'neutral']);
|
|
2375
|
+
const type = VALID_BANNER_TYPES.has(attrs.type) ? attrs.type : 'info';
|
|
2376
|
+
const title = attrs.title || '';
|
|
2377
|
+
const icon = attrs.icon || '';
|
|
2378
|
+
const dismissible = 'dismissible' in attrs;
|
|
2379
|
+
const extraClass = attrs.class || '';
|
|
2380
|
+
|
|
2381
|
+
const classes = [`dm-banner`, `dm-banner--${type}`];
|
|
2382
|
+
if (extraClass) classes.push(extraClass);
|
|
2383
|
+
|
|
2384
|
+
const iconHtml = icon
|
|
2385
|
+
? `<span class="dm-banner__icon" data-icon="${escapeAttr(icon)}"></span>\n `
|
|
2386
|
+
: '';
|
|
2387
|
+
const titleHtml = title
|
|
2388
|
+
? `<strong class="dm-banner__title">${escapeAttr(title)}</strong>\n `
|
|
2389
|
+
: '';
|
|
2390
|
+
const bodyHtml = marked.parse(body.trim());
|
|
2391
|
+
const dismissHtml = dismissible
|
|
2392
|
+
? `\n <button class="dm-banner__dismiss" aria-label="Dismiss">×</button>`
|
|
2393
|
+
: '';
|
|
2394
|
+
|
|
2395
|
+
return (
|
|
2396
|
+
`<div class="${classes.join(' ')}">\n` +
|
|
2397
|
+
` ${iconHtml}<div class="dm-banner__body">\n` +
|
|
2398
|
+
` ${titleHtml}${bodyHtml}` +
|
|
2399
|
+
` </div>${dismissHtml}\n` +
|
|
2400
|
+
`</div>\n`
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2340
2406
|
function processSlideoverBlocks(markdown) {
|
|
2341
2407
|
const {scrubbed, restore} = scrubCodeRegions(markdown);
|
|
2342
2408
|
let counter = 0;
|
|
@@ -2478,7 +2544,8 @@ export async function parseMarkdown(raw) {
|
|
|
2478
2544
|
const withCta = processCtaBlocks(withLink);
|
|
2479
2545
|
const withGrid = processGridBlocks(withCta);
|
|
2480
2546
|
const withCard = processCardBlocks(withGrid);
|
|
2481
|
-
|
|
2547
|
+
const withBanner = processBannerBlocks(withCard);
|
|
2548
|
+
const withSlideover = processSlideoverBlocks(withBanner);
|
|
2482
2549
|
const rendered = marked.parse(withSlideover);
|
|
2483
2550
|
|
|
2484
2551
|
const sanitized = sanitizeHtml(rendered, {
|
|
@@ -2501,7 +2568,7 @@ export async function parseMarkdown(raw) {
|
|
|
2501
2568
|
select: ['name', 'required', 'disabled', 'multiple'],
|
|
2502
2569
|
option: ['value', 'selected', 'disabled'],
|
|
2503
2570
|
optgroup: ['label', 'disabled'],
|
|
2504
|
-
|
|
2571
|
+
button: ['type', 'disabled', 'aria-label', 'data-action', 'data-entry', 'data-confirm'],
|
|
2505
2572
|
label: ['for'],
|
|
2506
2573
|
fieldset: ['disabled'],
|
|
2507
2574
|
...extensions.attributes
|