domma-cms 0.25.15 → 0.30.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/app.js +2 -2
- package/admin/js/lib/sidebar-renderer.js +4 -4
- package/admin/js/templates/api-reference.html +68 -1233
- package/admin/js/templates/docs/api-actions.html +146 -0
- package/admin/js/templates/docs/api-authentication.html +202 -0
- package/admin/js/templates/docs/api-collections.html +347 -0
- package/admin/js/templates/docs/api-layouts.html +123 -0
- package/admin/js/templates/docs/api-media.html +158 -0
- package/admin/js/templates/docs/api-navigation.html +99 -0
- package/admin/js/templates/docs/api-pages.html +214 -0
- package/admin/js/templates/docs/api-plugins.html +136 -0
- package/admin/js/templates/docs/api-settings.html +148 -0
- package/admin/js/templates/docs/api-users.html +189 -0
- package/admin/js/templates/docs/api-views.html +142 -0
- package/admin/js/templates/docs/components-howto.html +175 -0
- package/admin/js/templates/docs/components-reference.html +259 -0
- package/admin/js/templates/docs/components-rules.html +167 -0
- package/admin/js/templates/docs/components-walkthrough.html +167 -0
- package/admin/js/templates/docs/tutorial-crud.html +344 -0
- package/admin/js/templates/docs/tutorial-forms.html +93 -0
- package/admin/js/templates/docs/tutorial-plugin.html +234 -0
- package/admin/js/templates/docs/usage-actions.html +122 -0
- package/admin/js/templates/docs/usage-cta-shortcode.html +124 -0
- package/admin/js/templates/docs/usage-dconfig.html +148 -0
- package/admin/js/templates/docs/usage-media.html +25 -0
- package/admin/js/templates/docs/usage-navigation.html +31 -0
- package/admin/js/templates/docs/usage-pages.html +68 -0
- package/admin/js/templates/docs/usage-plugins.html +36 -0
- package/admin/js/templates/docs/usage-shortcodes.html +810 -0
- package/admin/js/templates/docs/usage-site-settings.html +52 -0
- package/admin/js/templates/docs/usage-users-roles.html +84 -0
- package/admin/js/templates/docs/usage-views.html +105 -0
- package/admin/js/templates/documentation.html +60 -1522
- package/admin/js/templates/effects.html +752 -752
- package/admin/js/templates/forms.html +17 -17
- package/admin/js/templates/my-profile.html +17 -17
- package/admin/js/templates/project-settings.html +7 -0
- package/admin/js/templates/role-editor.html +70 -70
- package/admin/js/templates/roles.html +10 -10
- package/admin/js/templates/tutorials.html +46 -659
- package/admin/js/views/api-reference.js +1 -1
- package/admin/js/views/doc-pages.js +1 -0
- package/admin/js/views/documentation.js +1 -1
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/project-settings.js +1 -1
- package/admin/js/views/projects.js +3 -7
- package/admin/js/views/tutorials.js +1 -1
- package/bin/lib/config-merge.js +44 -44
- package/config/connections.json.bak +9 -0
- package/config/menus/admin-sidebar.json +76 -6
- package/package.json +1 -1
- package/public/js/site.js +1 -1
- package/scripts/migrate-docs.js +280 -0
- package/server/middleware/auth.js +253 -253
- package/server/routes/api/auth.js +309 -309
- package/server/routes/api/collections.js +10 -1
- package/server/routes/api/navigation.js +42 -42
- package/server/routes/api/projects.js +9 -1
- package/server/routes/api/settings.js +141 -141
- package/server/routes/public.js +9 -0
- package/server/server.js +6 -2
- package/server/services/email.js +167 -167
- package/server/services/presetCollections.js +1 -0
- package/server/services/projects.js +72 -3
- package/server/services/sidebar-migration.js +145 -3
- package/server/services/userProfiles.js +199 -199
- package/server/services/users.js +302 -302
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Navigation API
|
|
3
|
-
* GET /api/navigation - get navigation config
|
|
4
|
-
* PUT /api/navigation - save navigation config
|
|
5
|
-
*/
|
|
6
|
-
import {getConfig, saveConfig} from '../../config.js';
|
|
7
|
-
import {authenticate, requirePermission} from '../../middleware/auth.js';
|
|
8
|
-
import * as cache from '../../services/cache/index.js';
|
|
9
|
-
|
|
10
|
-
export async function navigationRoutes(fastify) {
|
|
11
|
-
const canRead = {preHandler: [authenticate, requirePermission('navigation', 'read')]};
|
|
12
|
-
const canUpdate = {preHandler: [authenticate, requirePermission('navigation', 'update')]};
|
|
13
|
-
|
|
14
|
-
fastify.get('/navigation', canRead, async () => {
|
|
15
|
-
return getConfig('navigation');
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
fastify.put('/navigation', canUpdate, async (request, reply) => {
|
|
19
|
-
const data = request.body;
|
|
20
|
-
if (!data || typeof data !== 'object') {
|
|
21
|
-
return reply.status(400).send({ error: 'Invalid navigation data' });
|
|
22
|
-
}
|
|
23
|
-
if (!Array.isArray(data.items) && !Array.isArray(data)) {
|
|
24
|
-
return reply.status(400).send({ error: 'Navigation must be an array of items' });
|
|
25
|
-
}
|
|
26
|
-
// Normalise child key: Domma navbar expects `items`, not `children`
|
|
27
|
-
if (Array.isArray(data.items)) {
|
|
28
|
-
data.items = data.items.map(item => {
|
|
29
|
-
const children = item.items || item.children;
|
|
30
|
-
if (children?.length) {
|
|
31
|
-
const { children: _c, ...rest } = item;
|
|
32
|
-
return { ...rest, items: children };
|
|
33
|
-
}
|
|
34
|
-
const { children: _c, ...rest } = item;
|
|
35
|
-
return rest;
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
saveConfig('navigation', data);
|
|
39
|
-
await cache.invalidateTags(['nav']);
|
|
40
|
-
return { success: true };
|
|
41
|
-
});
|
|
42
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Navigation API
|
|
3
|
+
* GET /api/navigation - get navigation config
|
|
4
|
+
* PUT /api/navigation - save navigation config
|
|
5
|
+
*/
|
|
6
|
+
import {getConfig, saveConfig} from '../../config.js';
|
|
7
|
+
import {authenticate, requirePermission} from '../../middleware/auth.js';
|
|
8
|
+
import * as cache from '../../services/cache/index.js';
|
|
9
|
+
|
|
10
|
+
export async function navigationRoutes(fastify) {
|
|
11
|
+
const canRead = {preHandler: [authenticate, requirePermission('navigation', 'read')]};
|
|
12
|
+
const canUpdate = {preHandler: [authenticate, requirePermission('navigation', 'update')]};
|
|
13
|
+
|
|
14
|
+
fastify.get('/navigation', canRead, async () => {
|
|
15
|
+
return getConfig('navigation');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
fastify.put('/navigation', canUpdate, async (request, reply) => {
|
|
19
|
+
const data = request.body;
|
|
20
|
+
if (!data || typeof data !== 'object') {
|
|
21
|
+
return reply.status(400).send({ error: 'Invalid navigation data' });
|
|
22
|
+
}
|
|
23
|
+
if (!Array.isArray(data.items) && !Array.isArray(data)) {
|
|
24
|
+
return reply.status(400).send({ error: 'Navigation must be an array of items' });
|
|
25
|
+
}
|
|
26
|
+
// Normalise child key: Domma navbar expects `items`, not `children`
|
|
27
|
+
if (Array.isArray(data.items)) {
|
|
28
|
+
data.items = data.items.map(item => {
|
|
29
|
+
const children = item.items || item.children;
|
|
30
|
+
if (children?.length) {
|
|
31
|
+
const { children: _c, ...rest } = item;
|
|
32
|
+
return { ...rest, items: children };
|
|
33
|
+
}
|
|
34
|
+
const { children: _c, ...rest } = item;
|
|
35
|
+
return rest;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
saveConfig('navigation', data);
|
|
39
|
+
await cache.invalidateTags(['nav']);
|
|
40
|
+
return { success: true };
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -80,6 +80,10 @@ export async function projectsRoutes(fastify, opts = {}) {
|
|
|
80
80
|
if (request.params.slug === CORE_PROJECT_SLUG) {
|
|
81
81
|
return reply.status(403).send({error: 'Cannot delete the core project'});
|
|
82
82
|
}
|
|
83
|
+
const target = await getProject(request.params.slug);
|
|
84
|
+
if (target?.protected === true) {
|
|
85
|
+
return reply.status(403).send({error: 'This project is protected and cannot be deleted'});
|
|
86
|
+
}
|
|
83
87
|
const grouped = await getArtefactsForProject(request.params.slug);
|
|
84
88
|
const total = Object.values(grouped).reduce((sum, arr) => sum + arr.length, 0);
|
|
85
89
|
if (total > 0) {
|
|
@@ -105,7 +109,11 @@ export async function projectsRoutes(fastify, opts = {}) {
|
|
|
105
109
|
if (request.params.slug === CORE_PROJECT_SLUG) {
|
|
106
110
|
return reply.status(403).send({error: 'Cannot untag the core project — artefacts would immediately resolve back to it'});
|
|
107
111
|
}
|
|
108
|
-
|
|
112
|
+
const target = await getProject(request.params.slug);
|
|
113
|
+
if (target?.protected === true) {
|
|
114
|
+
return reply.status(403).send({error: 'This project is protected and cannot be untagged'});
|
|
115
|
+
}
|
|
116
|
+
if (!target) {
|
|
109
117
|
return reply.status(404).send({error: 'Project not found'});
|
|
110
118
|
}
|
|
111
119
|
const counts = await untagAllForProject(request.params.slug);
|
|
@@ -1,141 +1,141 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Settings API
|
|
3
|
-
* GET /api/settings - get site settings
|
|
4
|
-
* PUT /api/settings - save site settings
|
|
5
|
-
* POST /api/settings/test-email - send a test email using stored SMTP config
|
|
6
|
-
*/
|
|
7
|
-
import {getConfig, saveConfig} from '../../config.js';
|
|
8
|
-
import {authenticate, requirePermission} from '../../middleware/auth.js';
|
|
9
|
-
import nodemailer from 'nodemailer';
|
|
10
|
-
import fs from 'fs/promises';
|
|
11
|
-
import path from 'path';
|
|
12
|
-
import {fileURLToPath} from 'url';
|
|
13
|
-
import * as cache from '../../services/cache/index.js';
|
|
14
|
-
|
|
15
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
-
const CUSTOM_CSS_FILE = path.resolve(__dirname, '../../../content/custom.css');
|
|
17
|
-
const CONNECTIONS_FILE = path.resolve(__dirname, '../../../config/connections.json');
|
|
18
|
-
const CUSTOM_CSS_MAX = 100 * 1024; // 100 KB
|
|
19
|
-
|
|
20
|
-
export async function settingsRoutes(fastify) {
|
|
21
|
-
const canRead = {preHandler: [authenticate, requirePermission('settings', 'read')]};
|
|
22
|
-
const canUpdate = {preHandler: [authenticate, requirePermission('settings', 'update')]};
|
|
23
|
-
|
|
24
|
-
fastify.get('/settings', canRead, async (request, reply) => {
|
|
25
|
-
const config = getConfig('site');
|
|
26
|
-
const safeConfig = { ...config };
|
|
27
|
-
if (safeConfig.smtp) {
|
|
28
|
-
safeConfig.smtp = { ...safeConfig.smtp, pass: '' };
|
|
29
|
-
}
|
|
30
|
-
return reply.send(safeConfig);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
fastify.put('/settings', canUpdate, async (request, reply) => {
|
|
34
|
-
const data = request.body;
|
|
35
|
-
if (!data || typeof data !== 'object') {
|
|
36
|
-
return reply.status(400).send({ error: 'Invalid settings data' });
|
|
37
|
-
}
|
|
38
|
-
const ALLOWED_KEYS = new Set([
|
|
39
|
-
'title', 'tagline', 'description', 'logo', 'favicon',
|
|
40
|
-
'theme', 'adminTheme', 'fontFamily', 'fontSize',
|
|
41
|
-
'smtp', 'footer', 'analytics', 'social', 'locale',
|
|
42
|
-
'layoutOptions', 'seo', 'backToTop', 'cookieConsent', 'breadcrumbs', 'autoTheme',
|
|
43
|
-
'adminBrand'
|
|
44
|
-
]);
|
|
45
|
-
const unknownKeys = Object.keys(data).filter(k => !ALLOWED_KEYS.has(k));
|
|
46
|
-
if (unknownKeys.length > 0) {
|
|
47
|
-
return reply.status(400).send({ error: `Unknown settings keys: ${unknownKeys.join(', ')}` });
|
|
48
|
-
}
|
|
49
|
-
// Merge incoming data with existing config so partial updates (e.g. just adminBrand)
|
|
50
|
-
// don't wipe unrelated keys. Nested objects get a shallow merge so sub-fields are
|
|
51
|
-
// preserved even when only some sub-fields are sent.
|
|
52
|
-
const existing = getConfig('site');
|
|
53
|
-
const merged = {...existing};
|
|
54
|
-
for (const [k, v] of Object.entries(data)) {
|
|
55
|
-
const isPlainObject = v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
56
|
-
const existingIsPlainObject = existing[k] !== null && typeof existing[k] === 'object' && !Array.isArray(existing[k]);
|
|
57
|
-
if (isPlainObject && existingIsPlainObject) {
|
|
58
|
-
merged[k] = {...existing[k], ...v};
|
|
59
|
-
} else {
|
|
60
|
-
merged[k] = v;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
// Never erase smtp.pass with the empty string that the GET endpoint injects for safety
|
|
64
|
-
if (merged.smtp && merged.smtp.pass === '' && existing.smtp?.pass) {
|
|
65
|
-
merged.smtp = {...merged.smtp, pass: existing.smtp.pass};
|
|
66
|
-
}
|
|
67
|
-
saveConfig('site', merged);
|
|
68
|
-
await cache.invalidateTags(['site']);
|
|
69
|
-
return { success: true };
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
fastify.post('/settings/test-email', canRead, async (request, reply) => {
|
|
73
|
-
const smtp = getConfig('site')?.smtp;
|
|
74
|
-
if (!smtp?.host) {
|
|
75
|
-
return reply.status(400).send({ error: 'SMTP is not configured. Save your SMTP settings first.' });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const transporter = nodemailer.createTransport({
|
|
79
|
-
host: smtp.host,
|
|
80
|
-
port: smtp.port || 587,
|
|
81
|
-
secure: smtp.secure || false,
|
|
82
|
-
auth: smtp.user ? { user: smtp.user, pass: smtp.pass } : undefined
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const to = request.body?.to || smtp.fromAddress;
|
|
86
|
-
if (!to) {
|
|
87
|
-
return reply.status(400).send({ error: 'No recipient address. Provide "to" in the request body or set a From Address.' });
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
await transporter.sendMail({
|
|
92
|
-
from: smtp.fromName ? `"${smtp.fromName}" <${smtp.fromAddress}>` : smtp.fromAddress,
|
|
93
|
-
to,
|
|
94
|
-
subject: 'Domma CMS — Test Email',
|
|
95
|
-
text: 'This is a test email sent from Domma CMS to verify your SMTP configuration.',
|
|
96
|
-
html: '<p>This is a test email sent from <strong>Domma CMS</strong> to verify your SMTP configuration.</p>'
|
|
97
|
-
});
|
|
98
|
-
return { success: true, message: `Test email sent to ${to}` };
|
|
99
|
-
} catch (err) {
|
|
100
|
-
return reply.status(500).send({ error: `Failed to send email: ${err.message}` });
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// GET /api/settings/db-status — returns whether MongoDB connections are configured
|
|
105
|
-
fastify.get('/settings/db-status', canRead, async () => {
|
|
106
|
-
try {
|
|
107
|
-
const raw = await fs.readFile(CONNECTIONS_FILE, 'utf8');
|
|
108
|
-
const connections = JSON.parse(raw);
|
|
109
|
-
const names = Object.keys(connections).filter(k =>
|
|
110
|
-
connections[k]?.type === 'mongodb' && connections[k]?.uri
|
|
111
|
-
);
|
|
112
|
-
return {configured: names.length > 0, connections: names};
|
|
113
|
-
} catch {
|
|
114
|
-
return {configured: false, connections: []};
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// GET /api/settings/custom-css — return current CSS as JSON
|
|
119
|
-
fastify.get('/settings/custom-css', canUpdate, async () => {
|
|
120
|
-
try {
|
|
121
|
-
const css = await fs.readFile(CUSTOM_CSS_FILE, 'utf8');
|
|
122
|
-
return { css };
|
|
123
|
-
} catch {
|
|
124
|
-
return { css: '' };
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// PUT /api/settings/custom-css — save CSS to content/custom.css
|
|
129
|
-
fastify.put('/settings/custom-css', canUpdate, async (request, reply) => {
|
|
130
|
-
const { css } = request.body || {};
|
|
131
|
-
if (typeof css !== 'string') {
|
|
132
|
-
return reply.status(400).send({ error: 'css must be a string.' });
|
|
133
|
-
}
|
|
134
|
-
if (Buffer.byteLength(css, 'utf8') > CUSTOM_CSS_MAX) {
|
|
135
|
-
return reply.status(400).send({ error: 'CSS exceeds the 100 KB limit.' });
|
|
136
|
-
}
|
|
137
|
-
await fs.writeFile(CUSTOM_CSS_FILE, css, 'utf8');
|
|
138
|
-
await cache.invalidateTags(['site']);
|
|
139
|
-
return { success: true };
|
|
140
|
-
});
|
|
141
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Settings API
|
|
3
|
+
* GET /api/settings - get site settings
|
|
4
|
+
* PUT /api/settings - save site settings
|
|
5
|
+
* POST /api/settings/test-email - send a test email using stored SMTP config
|
|
6
|
+
*/
|
|
7
|
+
import {getConfig, saveConfig} from '../../config.js';
|
|
8
|
+
import {authenticate, requirePermission} from '../../middleware/auth.js';
|
|
9
|
+
import nodemailer from 'nodemailer';
|
|
10
|
+
import fs from 'fs/promises';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import {fileURLToPath} from 'url';
|
|
13
|
+
import * as cache from '../../services/cache/index.js';
|
|
14
|
+
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const CUSTOM_CSS_FILE = path.resolve(__dirname, '../../../content/custom.css');
|
|
17
|
+
const CONNECTIONS_FILE = path.resolve(__dirname, '../../../config/connections.json');
|
|
18
|
+
const CUSTOM_CSS_MAX = 100 * 1024; // 100 KB
|
|
19
|
+
|
|
20
|
+
export async function settingsRoutes(fastify) {
|
|
21
|
+
const canRead = {preHandler: [authenticate, requirePermission('settings', 'read')]};
|
|
22
|
+
const canUpdate = {preHandler: [authenticate, requirePermission('settings', 'update')]};
|
|
23
|
+
|
|
24
|
+
fastify.get('/settings', canRead, async (request, reply) => {
|
|
25
|
+
const config = getConfig('site');
|
|
26
|
+
const safeConfig = { ...config };
|
|
27
|
+
if (safeConfig.smtp) {
|
|
28
|
+
safeConfig.smtp = { ...safeConfig.smtp, pass: '' };
|
|
29
|
+
}
|
|
30
|
+
return reply.send(safeConfig);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
fastify.put('/settings', canUpdate, async (request, reply) => {
|
|
34
|
+
const data = request.body;
|
|
35
|
+
if (!data || typeof data !== 'object') {
|
|
36
|
+
return reply.status(400).send({ error: 'Invalid settings data' });
|
|
37
|
+
}
|
|
38
|
+
const ALLOWED_KEYS = new Set([
|
|
39
|
+
'title', 'tagline', 'description', 'logo', 'favicon',
|
|
40
|
+
'theme', 'adminTheme', 'fontFamily', 'fontSize',
|
|
41
|
+
'smtp', 'footer', 'analytics', 'social', 'locale',
|
|
42
|
+
'layoutOptions', 'seo', 'backToTop', 'cookieConsent', 'breadcrumbs', 'autoTheme',
|
|
43
|
+
'adminBrand'
|
|
44
|
+
]);
|
|
45
|
+
const unknownKeys = Object.keys(data).filter(k => !ALLOWED_KEYS.has(k));
|
|
46
|
+
if (unknownKeys.length > 0) {
|
|
47
|
+
return reply.status(400).send({ error: `Unknown settings keys: ${unknownKeys.join(', ')}` });
|
|
48
|
+
}
|
|
49
|
+
// Merge incoming data with existing config so partial updates (e.g. just adminBrand)
|
|
50
|
+
// don't wipe unrelated keys. Nested objects get a shallow merge so sub-fields are
|
|
51
|
+
// preserved even when only some sub-fields are sent.
|
|
52
|
+
const existing = getConfig('site');
|
|
53
|
+
const merged = {...existing};
|
|
54
|
+
for (const [k, v] of Object.entries(data)) {
|
|
55
|
+
const isPlainObject = v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
56
|
+
const existingIsPlainObject = existing[k] !== null && typeof existing[k] === 'object' && !Array.isArray(existing[k]);
|
|
57
|
+
if (isPlainObject && existingIsPlainObject) {
|
|
58
|
+
merged[k] = {...existing[k], ...v};
|
|
59
|
+
} else {
|
|
60
|
+
merged[k] = v;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Never erase smtp.pass with the empty string that the GET endpoint injects for safety
|
|
64
|
+
if (merged.smtp && merged.smtp.pass === '' && existing.smtp?.pass) {
|
|
65
|
+
merged.smtp = {...merged.smtp, pass: existing.smtp.pass};
|
|
66
|
+
}
|
|
67
|
+
saveConfig('site', merged);
|
|
68
|
+
await cache.invalidateTags(['site']);
|
|
69
|
+
return { success: true };
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
fastify.post('/settings/test-email', canRead, async (request, reply) => {
|
|
73
|
+
const smtp = getConfig('site')?.smtp;
|
|
74
|
+
if (!smtp?.host) {
|
|
75
|
+
return reply.status(400).send({ error: 'SMTP is not configured. Save your SMTP settings first.' });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const transporter = nodemailer.createTransport({
|
|
79
|
+
host: smtp.host,
|
|
80
|
+
port: smtp.port || 587,
|
|
81
|
+
secure: smtp.secure || false,
|
|
82
|
+
auth: smtp.user ? { user: smtp.user, pass: smtp.pass } : undefined
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const to = request.body?.to || smtp.fromAddress;
|
|
86
|
+
if (!to) {
|
|
87
|
+
return reply.status(400).send({ error: 'No recipient address. Provide "to" in the request body or set a From Address.' });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await transporter.sendMail({
|
|
92
|
+
from: smtp.fromName ? `"${smtp.fromName}" <${smtp.fromAddress}>` : smtp.fromAddress,
|
|
93
|
+
to,
|
|
94
|
+
subject: 'Domma CMS — Test Email',
|
|
95
|
+
text: 'This is a test email sent from Domma CMS to verify your SMTP configuration.',
|
|
96
|
+
html: '<p>This is a test email sent from <strong>Domma CMS</strong> to verify your SMTP configuration.</p>'
|
|
97
|
+
});
|
|
98
|
+
return { success: true, message: `Test email sent to ${to}` };
|
|
99
|
+
} catch (err) {
|
|
100
|
+
return reply.status(500).send({ error: `Failed to send email: ${err.message}` });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// GET /api/settings/db-status — returns whether MongoDB connections are configured
|
|
105
|
+
fastify.get('/settings/db-status', canRead, async () => {
|
|
106
|
+
try {
|
|
107
|
+
const raw = await fs.readFile(CONNECTIONS_FILE, 'utf8');
|
|
108
|
+
const connections = JSON.parse(raw);
|
|
109
|
+
const names = Object.keys(connections).filter(k =>
|
|
110
|
+
connections[k]?.type === 'mongodb' && connections[k]?.uri
|
|
111
|
+
);
|
|
112
|
+
return {configured: names.length > 0, connections: names};
|
|
113
|
+
} catch {
|
|
114
|
+
return {configured: false, connections: []};
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// GET /api/settings/custom-css — return current CSS as JSON
|
|
119
|
+
fastify.get('/settings/custom-css', canUpdate, async () => {
|
|
120
|
+
try {
|
|
121
|
+
const css = await fs.readFile(CUSTOM_CSS_FILE, 'utf8');
|
|
122
|
+
return { css };
|
|
123
|
+
} catch {
|
|
124
|
+
return { css: '' };
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// PUT /api/settings/custom-css — save CSS to content/custom.css
|
|
129
|
+
fastify.put('/settings/custom-css', canUpdate, async (request, reply) => {
|
|
130
|
+
const { css } = request.body || {};
|
|
131
|
+
if (typeof css !== 'string') {
|
|
132
|
+
return reply.status(400).send({ error: 'css must be a string.' });
|
|
133
|
+
}
|
|
134
|
+
if (Buffer.byteLength(css, 'utf8') > CUSTOM_CSS_MAX) {
|
|
135
|
+
return reply.status(400).send({ error: 'CSS exceeds the 100 KB limit.' });
|
|
136
|
+
}
|
|
137
|
+
await fs.writeFile(CUSTOM_CSS_FILE, css, 'utf8');
|
|
138
|
+
await cache.invalidateTags(['site']);
|
|
139
|
+
return { success: true };
|
|
140
|
+
});
|
|
141
|
+
}
|
package/server/routes/public.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* The admin panel is excluded (handled by static serving).
|
|
6
6
|
*/
|
|
7
7
|
import {getPage} from '../services/content.js';
|
|
8
|
+
import {getProjectForPage, isProjectEnabled} from '../services/projects.js';
|
|
8
9
|
import {renderPage} from '../services/renderer.js';
|
|
9
10
|
import {buildRobotsTxt, generate as generateSitemap} from '../services/sitemap.js';
|
|
10
11
|
import {checkVisibility} from '../middleware/auth.js';
|
|
@@ -108,6 +109,14 @@ export async function publicRoutes(fastify) {
|
|
|
108
109
|
return reply.type('text/html').send(await render404(urlPath));
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
// Hard kill-switch: a disabled project takes its pages off the public
|
|
113
|
+
// site entirely — 404 (no oracle), regardless of page visibility.
|
|
114
|
+
const pageProject = await getProjectForPage(page.urlPath || urlPath, page.project);
|
|
115
|
+
if (!(await isProjectEnabled(pageProject))) {
|
|
116
|
+
reply.status(404);
|
|
117
|
+
return reply.type('text/html').send(await render404(urlPath));
|
|
118
|
+
}
|
|
119
|
+
|
|
111
120
|
// Enforce page visibility — role only resolved for gated pages,
|
|
112
121
|
// so public pages share a single cache entry keyed `roleanon`.
|
|
113
122
|
//
|
package/server/server.js
CHANGED
|
@@ -22,7 +22,7 @@ import {getLoadedPlugins, getPluginSettings, registerPlugins} from './services/p
|
|
|
22
22
|
import {load as loadRoles, seed as seedRoles} from './services/roles.js';
|
|
23
23
|
import {ensureAllProfiles, seed as seedUserProfiles} from './services/userProfiles.js';
|
|
24
24
|
import {seedAll as seedPresetCollections} from './services/presetCollections.js';
|
|
25
|
-
import {seedCoreProject} from './services/projects.js';
|
|
25
|
+
import {seedCoreProject, seedDocsProject} from './services/projects.js';
|
|
26
26
|
import {seedDefaultBlocks} from './services/blocks.js';
|
|
27
27
|
import {seedDefaultComponents} from './services/components.js';
|
|
28
28
|
import {refreshComponentTagAllowlist} from './services/markdown.js';
|
|
@@ -186,6 +186,7 @@ await seedUserProfiles();
|
|
|
186
186
|
await ensureAllProfiles();
|
|
187
187
|
await seedPresetCollections();
|
|
188
188
|
await seedCoreProject();
|
|
189
|
+
await seedDocsProject(); // built-in Domma Docs project — seeded disabled by default
|
|
189
190
|
await seedDefaultBlocks();
|
|
190
191
|
await seedDefaultComponents();
|
|
191
192
|
await refreshComponentTagAllowlist();
|
|
@@ -202,7 +203,7 @@ try {
|
|
|
202
203
|
}
|
|
203
204
|
|
|
204
205
|
try {
|
|
205
|
-
const {runMigration: runSidebarMigration, ensureSidebarItem} = await import('./services/sidebar-migration.js');
|
|
206
|
+
const {runMigration: runSidebarMigration, ensureSidebarItem, ensureDocumentationSubmenus} = await import('./services/sidebar-migration.js');
|
|
206
207
|
await runSidebarMigration();
|
|
207
208
|
// Surface admin pages added after first boot on existing installs.
|
|
208
209
|
await ensureSidebarItem({
|
|
@@ -213,6 +214,8 @@ try {
|
|
|
213
214
|
groupText: 'Data',
|
|
214
215
|
item: {text: 'API Builder', url: '#/api-endpoints', icon: 'code', permission: 'api-endpoints'}
|
|
215
216
|
});
|
|
217
|
+
// Restructure Documentation: Tutorials → submenu, add Components reference set.
|
|
218
|
+
await ensureDocumentationSubmenus();
|
|
216
219
|
} catch (err) {
|
|
217
220
|
app.log.warn(`[admin-sidebar] Migration skipped: ${err.message}`);
|
|
218
221
|
}
|
|
@@ -393,3 +396,4 @@ try {
|
|
|
393
396
|
process.exit(1);
|
|
394
397
|
}
|
|
395
398
|
// scaffold-restart 1779615684
|
|
399
|
+
|