domma-cms 0.9.0 → 0.9.5
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/templates/block-editor.html +163 -163
- package/admin/js/templates/form-editor.html +245 -245
- package/admin/js/views/action-editor.js +1 -1
- package/admin/js/views/block-editor.js +8 -8
- package/admin/js/views/collection-editor.js +4 -4
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/forms.js +1 -1
- package/admin/js/views/navigation.js +14 -14
- package/admin/js/views/page-editor.js +35 -35
- package/admin/js/views/pages.js +5 -5
- package/admin/js/views/plugins.js +19 -10
- package/admin/js/views/view-editor.js +1 -1
- package/config/plugins.json +35 -0
- package/package.json +1 -1
- package/plugins/docs/data/documents/57e003f0-68f2-47dc-9c36-ed4b10ed3deb.json +4 -4
- package/plugins/docs/data/folders.json +3 -3
- package/plugins/docs/data/versions/57e003f0-68f2-47dc-9c36-ed4b10ed3deb/1.json +5 -0
- package/plugins/garage/admin/templates/garage.html +30 -0
- package/plugins/garage/admin/views/garage.js +62 -1
- package/plugins/garage/plugin.json +1 -1
- package/plugins/notes/admin/templates/notes.html +2 -11
- package/plugins/notes/admin/views/notes.js +107 -129
- package/plugins/notes/collections/user-notes/schema.json +2 -1
- package/plugins/notes/plugin.json +1 -1
- package/plugins/site-search/admin/templates/site-search.html +174 -46
- package/plugins/site-search/admin/views/site-search.js +72 -1
- package/plugins/site-search/config.js +6 -1
- package/plugins/site-search/plugin.json +1 -1
- package/plugins/site-search/public/inject-head.html +1 -1
- package/plugins/site-search/public/search.css +1 -1
- package/plugins/site-search/public/search.js +1 -1
- package/plugins/todo/admin/templates/todo.html +2 -8
- package/plugins/todo/admin/views/todo.js +122 -106
- package/plugins/todo/collections/todos/schema.json +2 -1
- package/plugins/todo/plugin.json +1 -1
- package/server/routes/api/media.js +127 -118
- package/server/routes/api/plugins.js +15 -4
- package/server/server.js +288 -285
- package/server/services/blocks.js +6 -3
- package/server/services/collections.js +17 -10
- package/server/services/plugins.js +77 -67
- package/server/services/renderer.js +3 -3
- package/plugins/docs/data/documents/452f49b7-9c93-4a67-874d-27f882891ad2.json +0 -11
package/server/server.js
CHANGED
|
@@ -1,285 +1,288 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Domma CMS - Fastify Server
|
|
3
|
-
* Serves the admin SPA, public site, REST API, and static assets.
|
|
4
|
-
* Domma dist files are served directly from node_modules/domma-js/public/dist.
|
|
5
|
-
* Updated: blobs + z-index fixes.
|
|
6
|
-
*/
|
|
7
|
-
import 'dotenv/config';
|
|
8
|
-
import Fastify from 'fastify';
|
|
9
|
-
import jwt from '@fastify/jwt';
|
|
10
|
-
import staticPlugin from '@fastify/static';
|
|
11
|
-
import cors from '@fastify/cors';
|
|
12
|
-
import multipart from '@fastify/multipart';
|
|
13
|
-
import rateLimit from '@fastify/rate-limit';
|
|
14
|
-
import helmet from '@fastify/helmet';
|
|
15
|
-
import path from 'path';
|
|
16
|
-
import fs from 'fs/promises';
|
|
17
|
-
import {fileURLToPath} from 'url';
|
|
18
|
-
import {createRequire} from 'module';
|
|
19
|
-
import {config, getConfig} from './config.js';
|
|
20
|
-
import {registerPlugins} from './services/plugins.js';
|
|
21
|
-
import {load as loadRoles, seed as seedRoles} from './services/roles.js';
|
|
22
|
-
import {ensureAllProfiles, seed as seedUserProfiles} from './services/userProfiles.js';
|
|
23
|
-
import {seedAll as seedPresetCollections} from './services/presetCollections.js';
|
|
24
|
-
import {seedDefaultBlocks} from './services/blocks.js';
|
|
25
|
-
|
|
26
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
|
-
const ROOT = path.resolve(__dirname, '..');
|
|
28
|
-
|
|
29
|
-
// Resolve domma-js package location via require resolution
|
|
30
|
-
const require = createRequire(import.meta.url);
|
|
31
|
-
const dommaPackageDir = path.dirname(require.resolve('domma-js/package.json'));
|
|
32
|
-
const DOMMA_DIST = path.join(dommaPackageDir, 'public', 'dist');
|
|
33
|
-
|
|
34
|
-
const { server: serverConfig, auth: authConfig } = config;
|
|
35
|
-
|
|
36
|
-
// Validate JWT_SECRET before starting — prevents silent auth failures
|
|
37
|
-
const JWT_SECRET = process.env.JWT_SECRET;
|
|
38
|
-
if (!JWT_SECRET || JWT_SECRET === 'CHANGE_ME' || JWT_SECRET.length < 32) {
|
|
39
|
-
console.error('');
|
|
40
|
-
console.error(' ERROR: JWT_SECRET is not set or is insecure.');
|
|
41
|
-
console.error(' Run `npm run setup` or set a secure JWT_SECRET in .env');
|
|
42
|
-
console.error(' (minimum 32 characters, not "CHANGE_ME")');
|
|
43
|
-
console.error('');
|
|
44
|
-
process.exit(1);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const app = Fastify({
|
|
48
|
-
logger: {level: process.env.NODE_ENV === 'development' ? 'info' : 'warn'},
|
|
49
|
-
// When running behind a reverse proxy (e.g. domma-cms-manager), trust the
|
|
50
|
-
// X-Forwarded-For header so @fastify/rate-limit keys on the real client IP
|
|
51
|
-
// rather than the proxy's loopback address.
|
|
52
|
-
trustProxy: !!process.env.TRUST_PROXY,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// Register process error handlers immediately so startup errors are captured
|
|
56
|
-
process.on('uncaughtException', (err) => {
|
|
57
|
-
app.log.error({ err }, 'Uncaught exception');
|
|
58
|
-
process.exit(1);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
process.on('unhandledRejection', (reason) => {
|
|
62
|
-
app.log.error({ reason }, 'Unhandled rejection');
|
|
63
|
-
process.exit(1);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
// Core Plugins
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
|
|
70
|
-
await app.register(helmet, {
|
|
71
|
-
contentSecurityPolicy: {
|
|
72
|
-
directives: {
|
|
73
|
-
defaultSrc: ["'self'"],
|
|
74
|
-
scriptSrc: ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net'],
|
|
75
|
-
styleSrc: ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net', 'https://fonts.googleapis.com'],
|
|
76
|
-
imgSrc: ["'self'", 'data:', 'blob:'],
|
|
77
|
-
fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],
|
|
78
|
-
connectSrc: ["'self'", 'https://cdn.jsdelivr.net'],
|
|
79
|
-
upgradeInsecureRequests: null, // prevent browsers forcing HTTPS on HTTP deployments
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
|
-
crossOriginEmbedderPolicy: false, // allow embedding images/resources
|
|
83
|
-
hsts: false, // disable HSTS — server runs HTTP only; HSTS would force browser to https
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
await app.register(jwt, { secret: process.env.JWT_SECRET });
|
|
87
|
-
await app.register(cors, serverConfig.cors);
|
|
88
|
-
await app.register(multipart, { limits: { fileSize: serverConfig.uploads.maxFileSize } });
|
|
89
|
-
await app.register(rateLimit, {
|
|
90
|
-
global: true, // apply default limit to all routes; stricter per-route limits override this
|
|
91
|
-
max: 500,
|
|
92
|
-
timeWindow: '1 minute',
|
|
93
|
-
// Loopback is always exempt — local admin use should never be rate-limited.
|
|
94
|
-
// When behind a reverse proxy (TRUST_PROXY), the proxy's loopback is also exempt.
|
|
95
|
-
allowList: ['127.0.0.1', '::1', '::ffff:127.0.0.1'],
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
// Static Assets
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
|
-
|
|
102
|
-
// Serve Domma dist files from npm package (shared by admin + public)
|
|
103
|
-
await app.register(staticPlugin, {
|
|
104
|
-
root: DOMMA_DIST,
|
|
105
|
-
prefix: '/dist/domma/',
|
|
106
|
-
decorateReply: false
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
// Serve public site assets (CSS, JS, images, etc.)
|
|
110
|
-
await app.register(staticPlugin, {
|
|
111
|
-
root: path.join(ROOT, 'public'),
|
|
112
|
-
prefix: '/public/',
|
|
113
|
-
decorateReply: false
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// Serve admin panel assets — no-cache so JS/CSS changes are always picked up
|
|
117
|
-
await app.register(staticPlugin, {
|
|
118
|
-
root: path.join(ROOT, 'admin'),
|
|
119
|
-
prefix: '/admin/',
|
|
120
|
-
decorateReply: false,
|
|
121
|
-
setHeaders: (res) => {
|
|
122
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// Ensure required directories exist
|
|
127
|
-
const mediaDir = path.join(ROOT, config.content.mediaDir);
|
|
128
|
-
const usersDir = path.join(ROOT, config.content.usersDir);
|
|
129
|
-
const collectionsDir = path.join(ROOT, config.content.collectionsDir);
|
|
130
|
-
const pluginsDir = path.join(ROOT, 'plugins');
|
|
131
|
-
const blocksDir = path.join(ROOT, 'content', 'blocks');
|
|
132
|
-
await fs.mkdir(mediaDir, { recursive: true });
|
|
133
|
-
await fs.mkdir(usersDir, { recursive: true });
|
|
134
|
-
await fs.mkdir(collectionsDir, { recursive: true });
|
|
135
|
-
await fs.mkdir(pluginsDir, { recursive: true });
|
|
136
|
-
await fs.mkdir(blocksDir, {recursive: true});
|
|
137
|
-
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
139
|
-
// Pro feature — optional MongoDB connections
|
|
140
|
-
// ---------------------------------------------------------------------------
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
const connections = getConfig('connections');
|
|
144
|
-
if (connections && Object.keys(connections).length > 0) {
|
|
145
|
-
const { initialise, shutdown } = await import('./services/connectionManager.js');
|
|
146
|
-
await initialise(connections);
|
|
147
|
-
app.addHook('onClose', shutdown);
|
|
148
|
-
}
|
|
149
|
-
} catch {
|
|
150
|
-
// No connections.json or empty — pure file-based mode (free version).
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ---------------------------------------------------------------------------
|
|
154
|
-
// Roles — seed preset collection + load into cache
|
|
155
|
-
// ---------------------------------------------------------------------------
|
|
156
|
-
|
|
157
|
-
await seedRoles();
|
|
158
|
-
await loadRoles();
|
|
159
|
-
await seedUserProfiles();
|
|
160
|
-
await ensureAllProfiles();
|
|
161
|
-
await seedPresetCollections();
|
|
162
|
-
await seedDefaultBlocks();
|
|
163
|
-
|
|
164
|
-
// Serve uploaded media files
|
|
165
|
-
await app.register(staticPlugin, {
|
|
166
|
-
root: mediaDir,
|
|
167
|
-
prefix: '/media/',
|
|
168
|
-
decorateReply: false
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const {
|
|
225
|
-
const {
|
|
226
|
-
const {
|
|
227
|
-
const {
|
|
228
|
-
const {
|
|
229
|
-
const {
|
|
230
|
-
const {
|
|
231
|
-
const {
|
|
232
|
-
const {
|
|
233
|
-
const {
|
|
234
|
-
const {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
await app.register(
|
|
240
|
-
await app.register(
|
|
241
|
-
await app.register(
|
|
242
|
-
await app.register(
|
|
243
|
-
await app.register(
|
|
244
|
-
await app.register(
|
|
245
|
-
await app.register(
|
|
246
|
-
await app.register(
|
|
247
|
-
await app.register(
|
|
248
|
-
await app.register(
|
|
249
|
-
await app.register(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
process.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Domma CMS - Fastify Server
|
|
3
|
+
* Serves the admin SPA, public site, REST API, and static assets.
|
|
4
|
+
* Domma dist files are served directly from node_modules/domma-js/public/dist.
|
|
5
|
+
* Updated: blobs + z-index fixes.
|
|
6
|
+
*/
|
|
7
|
+
import 'dotenv/config';
|
|
8
|
+
import Fastify from 'fastify';
|
|
9
|
+
import jwt from '@fastify/jwt';
|
|
10
|
+
import staticPlugin from '@fastify/static';
|
|
11
|
+
import cors from '@fastify/cors';
|
|
12
|
+
import multipart from '@fastify/multipart';
|
|
13
|
+
import rateLimit from '@fastify/rate-limit';
|
|
14
|
+
import helmet from '@fastify/helmet';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import fs from 'fs/promises';
|
|
17
|
+
import {fileURLToPath} from 'url';
|
|
18
|
+
import {createRequire} from 'module';
|
|
19
|
+
import {config, getConfig} from './config.js';
|
|
20
|
+
import {registerPlugins} from './services/plugins.js';
|
|
21
|
+
import {load as loadRoles, seed as seedRoles} from './services/roles.js';
|
|
22
|
+
import {ensureAllProfiles, seed as seedUserProfiles} from './services/userProfiles.js';
|
|
23
|
+
import {seedAll as seedPresetCollections} from './services/presetCollections.js';
|
|
24
|
+
import {seedDefaultBlocks} from './services/blocks.js';
|
|
25
|
+
|
|
26
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
28
|
+
|
|
29
|
+
// Resolve domma-js package location via require resolution
|
|
30
|
+
const require = createRequire(import.meta.url);
|
|
31
|
+
const dommaPackageDir = path.dirname(require.resolve('domma-js/package.json'));
|
|
32
|
+
const DOMMA_DIST = path.join(dommaPackageDir, 'public', 'dist');
|
|
33
|
+
|
|
34
|
+
const { server: serverConfig, auth: authConfig } = config;
|
|
35
|
+
|
|
36
|
+
// Validate JWT_SECRET before starting — prevents silent auth failures
|
|
37
|
+
const JWT_SECRET = process.env.JWT_SECRET;
|
|
38
|
+
if (!JWT_SECRET || JWT_SECRET === 'CHANGE_ME' || JWT_SECRET.length < 32) {
|
|
39
|
+
console.error('');
|
|
40
|
+
console.error(' ERROR: JWT_SECRET is not set or is insecure.');
|
|
41
|
+
console.error(' Run `npm run setup` or set a secure JWT_SECRET in .env');
|
|
42
|
+
console.error(' (minimum 32 characters, not "CHANGE_ME")');
|
|
43
|
+
console.error('');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const app = Fastify({
|
|
48
|
+
logger: {level: process.env.NODE_ENV === 'development' ? 'info' : 'warn'},
|
|
49
|
+
// When running behind a reverse proxy (e.g. domma-cms-manager), trust the
|
|
50
|
+
// X-Forwarded-For header so @fastify/rate-limit keys on the real client IP
|
|
51
|
+
// rather than the proxy's loopback address.
|
|
52
|
+
trustProxy: !!process.env.TRUST_PROXY,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Register process error handlers immediately so startup errors are captured
|
|
56
|
+
process.on('uncaughtException', (err) => {
|
|
57
|
+
app.log.error({ err }, 'Uncaught exception');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
process.on('unhandledRejection', (reason) => {
|
|
62
|
+
app.log.error({ reason }, 'Unhandled rejection');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Core Plugins
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
await app.register(helmet, {
|
|
71
|
+
contentSecurityPolicy: {
|
|
72
|
+
directives: {
|
|
73
|
+
defaultSrc: ["'self'"],
|
|
74
|
+
scriptSrc: ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net'],
|
|
75
|
+
styleSrc: ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net', 'https://fonts.googleapis.com'],
|
|
76
|
+
imgSrc: ["'self'", 'data:', 'blob:'],
|
|
77
|
+
fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],
|
|
78
|
+
connectSrc: ["'self'", 'https://cdn.jsdelivr.net'],
|
|
79
|
+
upgradeInsecureRequests: null, // prevent browsers forcing HTTPS on HTTP deployments
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
crossOriginEmbedderPolicy: false, // allow embedding images/resources
|
|
83
|
+
hsts: false, // disable HSTS — server runs HTTP only; HSTS would force browser to https
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await app.register(jwt, { secret: process.env.JWT_SECRET });
|
|
87
|
+
await app.register(cors, serverConfig.cors);
|
|
88
|
+
await app.register(multipart, { limits: { fileSize: serverConfig.uploads.maxFileSize } });
|
|
89
|
+
await app.register(rateLimit, {
|
|
90
|
+
global: true, // apply default limit to all routes; stricter per-route limits override this
|
|
91
|
+
max: 500,
|
|
92
|
+
timeWindow: '1 minute',
|
|
93
|
+
// Loopback is always exempt — local admin use should never be rate-limited.
|
|
94
|
+
// When behind a reverse proxy (TRUST_PROXY), the proxy's loopback is also exempt.
|
|
95
|
+
allowList: ['127.0.0.1', '::1', '::ffff:127.0.0.1'],
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Static Assets
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
// Serve Domma dist files from npm package (shared by admin + public)
|
|
103
|
+
await app.register(staticPlugin, {
|
|
104
|
+
root: DOMMA_DIST,
|
|
105
|
+
prefix: '/dist/domma/',
|
|
106
|
+
decorateReply: false
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Serve public site assets (CSS, JS, images, etc.)
|
|
110
|
+
await app.register(staticPlugin, {
|
|
111
|
+
root: path.join(ROOT, 'public'),
|
|
112
|
+
prefix: '/public/',
|
|
113
|
+
decorateReply: false
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Serve admin panel assets — no-cache so JS/CSS changes are always picked up
|
|
117
|
+
await app.register(staticPlugin, {
|
|
118
|
+
root: path.join(ROOT, 'admin'),
|
|
119
|
+
prefix: '/admin/',
|
|
120
|
+
decorateReply: false,
|
|
121
|
+
setHeaders: (res) => {
|
|
122
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Ensure required directories exist
|
|
127
|
+
const mediaDir = path.join(ROOT, config.content.mediaDir);
|
|
128
|
+
const usersDir = path.join(ROOT, config.content.usersDir);
|
|
129
|
+
const collectionsDir = path.join(ROOT, config.content.collectionsDir);
|
|
130
|
+
const pluginsDir = path.join(ROOT, 'plugins');
|
|
131
|
+
const blocksDir = path.join(ROOT, 'content', 'blocks');
|
|
132
|
+
await fs.mkdir(mediaDir, { recursive: true });
|
|
133
|
+
await fs.mkdir(usersDir, { recursive: true });
|
|
134
|
+
await fs.mkdir(collectionsDir, { recursive: true });
|
|
135
|
+
await fs.mkdir(pluginsDir, { recursive: true });
|
|
136
|
+
await fs.mkdir(blocksDir, {recursive: true});
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Pro feature — optional MongoDB connections
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const connections = getConfig('connections');
|
|
144
|
+
if (connections && Object.keys(connections).length > 0) {
|
|
145
|
+
const { initialise, shutdown } = await import('./services/connectionManager.js');
|
|
146
|
+
await initialise(connections);
|
|
147
|
+
app.addHook('onClose', shutdown);
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
// No connections.json or empty — pure file-based mode (free version).
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Roles — seed preset collection + load into cache
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
await seedRoles();
|
|
158
|
+
await loadRoles();
|
|
159
|
+
await seedUserProfiles();
|
|
160
|
+
await ensureAllProfiles();
|
|
161
|
+
await seedPresetCollections();
|
|
162
|
+
await seedDefaultBlocks();
|
|
163
|
+
|
|
164
|
+
// Serve uploaded media files — nosniff prevents browsers rendering spoofed content types
|
|
165
|
+
await app.register(staticPlugin, {
|
|
166
|
+
root: mediaDir,
|
|
167
|
+
prefix: '/media/',
|
|
168
|
+
decorateReply: false,
|
|
169
|
+
setHeaders: (res) => {
|
|
170
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Serve plugin admin/ and public/ subdirs only — block plugin.js, config.js, data/, etc.
|
|
175
|
+
await app.register(async function pluginStaticScope(instance) {
|
|
176
|
+
instance.addHook('onRequest', (request, reply, done) => {
|
|
177
|
+
const relative = request.url.replace(/^\/plugins\//, '').split('?')[0];
|
|
178
|
+
const segments = relative.split('/');
|
|
179
|
+
|
|
180
|
+
if (relative.includes('..')) {
|
|
181
|
+
reply.code(403).send({error: 'Forbidden'});
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// Must be: {pluginName}/{admin|public}/{file...}
|
|
185
|
+
if (segments.length < 3 || !['admin', 'public'].includes(segments[1])) {
|
|
186
|
+
reply.code(404).send({error: 'Not found'});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
done();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
await instance.register(staticPlugin, {
|
|
193
|
+
root: pluginsDir,
|
|
194
|
+
prefix: '/plugins/',
|
|
195
|
+
decorateReply: false
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Global error handler — prevents stack trace leaks in production
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
app.setErrorHandler((error, request, reply) => {
|
|
204
|
+
app.log.error({ err: error, url: request.url }, 'Request error');
|
|
205
|
+
const statusCode = error.statusCode || 500;
|
|
206
|
+
// Don't leak stack traces in production
|
|
207
|
+
const message = process.env.NODE_ENV === 'production' && statusCode === 500
|
|
208
|
+
? 'Internal server error'
|
|
209
|
+
: error.message;
|
|
210
|
+
return reply.code(statusCode).send({ error: message });
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Auth API Routes (no authentication required on these endpoints themselves)
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
const { authRoutes } = await import('./routes/api/auth.js');
|
|
218
|
+
await app.register(authRoutes, { prefix: '/api' });
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Protected API Routes
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
const { pagesRoutes } = await import('./routes/api/pages.js');
|
|
225
|
+
const { settingsRoutes } = await import('./routes/api/settings.js');
|
|
226
|
+
const { layoutsRoutes } = await import('./routes/api/layouts.js');
|
|
227
|
+
const { navigationRoutes } = await import('./routes/api/navigation.js');
|
|
228
|
+
const { mediaRoutes } = await import('./routes/api/media.js');
|
|
229
|
+
const { usersRoutes } = await import('./routes/api/users.js');
|
|
230
|
+
const { pluginsRoutes } = await import('./routes/api/plugins.js');
|
|
231
|
+
const { collectionsRoutes } = await import('./routes/api/collections.js');
|
|
232
|
+
const { formsRoutes } = await import('./routes/api/forms.js');
|
|
233
|
+
const { viewsRoutes } = await import('./routes/api/views.js');
|
|
234
|
+
const { actionsRoutes } = await import('./routes/api/actions.js');
|
|
235
|
+
const {blocksRoutes} = await import('./routes/api/blocks.js');
|
|
236
|
+
const {versionsRoutes} = await import('./routes/api/versions.js');
|
|
237
|
+
const {effectsRoutes} = await import('./routes/api/effects.js');
|
|
238
|
+
|
|
239
|
+
await app.register(pagesRoutes, { prefix: '/api' });
|
|
240
|
+
await app.register(settingsRoutes, { prefix: '/api' });
|
|
241
|
+
await app.register(layoutsRoutes, { prefix: '/api' });
|
|
242
|
+
await app.register(navigationRoutes, { prefix: '/api' });
|
|
243
|
+
await app.register(mediaRoutes, { prefix: '/api' });
|
|
244
|
+
await app.register(usersRoutes, { prefix: '/api' });
|
|
245
|
+
await app.register(pluginsRoutes, { prefix: '/api' });
|
|
246
|
+
await app.register(collectionsRoutes, { prefix: '/api' });
|
|
247
|
+
await app.register(formsRoutes, { prefix: '/api' });
|
|
248
|
+
await app.register(viewsRoutes, { prefix: '/api' });
|
|
249
|
+
await app.register(actionsRoutes, { prefix: '/api' });
|
|
250
|
+
await app.register(blocksRoutes, {prefix: '/api'});
|
|
251
|
+
await app.register(versionsRoutes, {prefix: '/api'});
|
|
252
|
+
await app.register(effectsRoutes, {prefix: '/api'});
|
|
253
|
+
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// CMS Plugins (server-side Fastify plugins from plugins/ directory)
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
await registerPlugins(app);
|
|
259
|
+
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// Public Site (catch-all — must be last)
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
const { publicRoutes } = await import('./routes/public.js');
|
|
265
|
+
await app.register(publicRoutes);
|
|
266
|
+
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Graceful shutdown and unhandled error guards
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
const shutdown = async (signal) => {
|
|
272
|
+
app.log.info(`Received ${signal}, shutting down gracefully...`);
|
|
273
|
+
await app.close();
|
|
274
|
+
process.exit(0);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
278
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
await app.listen({ port: serverConfig.port, host: serverConfig.host });
|
|
282
|
+
console.log(`Domma CMS running at http://localhost:${serverConfig.port}`);
|
|
283
|
+
console.log(` Admin: http://localhost:${serverConfig.port}/admin/`);
|
|
284
|
+
console.log(` Public: http://localhost:${serverConfig.port}/`);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
app.log.error(err);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
@@ -184,7 +184,8 @@ export async function listBlocks() {
|
|
|
184
184
|
try {
|
|
185
185
|
const meta = JSON.parse(await fs.readFile(path.join(BLOCKS_DIR, `${name}.meta.json`), 'utf8'));
|
|
186
186
|
bundled = !!meta.bundled;
|
|
187
|
-
} catch { /* no meta file */
|
|
187
|
+
} catch { /* no meta file */
|
|
188
|
+
}
|
|
188
189
|
blocks.push({
|
|
189
190
|
name,
|
|
190
191
|
size: fileStat?.size ?? 0,
|
|
@@ -210,7 +211,8 @@ export async function getBlock(name) {
|
|
|
210
211
|
try {
|
|
211
212
|
const meta = JSON.parse(await fs.readFile(path.join(BLOCKS_DIR, `${name}.meta.json`), 'utf8'));
|
|
212
213
|
bundled = !!meta.bundled;
|
|
213
|
-
} catch { /* no meta file */
|
|
214
|
+
} catch { /* no meta file */
|
|
215
|
+
}
|
|
214
216
|
return {name, content, bundled};
|
|
215
217
|
} catch (err) {
|
|
216
218
|
if (err.code === 'ENOENT') {
|
|
@@ -238,7 +240,8 @@ export async function saveBlock(name, content, {bundled} = {}) {
|
|
|
238
240
|
if (bundled) {
|
|
239
241
|
await fs.writeFile(metaPath, JSON.stringify({bundled: true}, null, 2) + '\n', 'utf8');
|
|
240
242
|
} else {
|
|
241
|
-
await fs.unlink(metaPath).catch(() => {
|
|
243
|
+
await fs.unlink(metaPath).catch(() => {
|
|
244
|
+
});
|
|
242
245
|
}
|
|
243
246
|
return {success: true, name};
|
|
244
247
|
}
|