domma-cms 0.9.1 → 0.9.6
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 +13 -10
- package/admin/js/views/view-editor.js +1 -1
- package/config/plugins.json +25 -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 +108 -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 +123 -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/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
|
+
}
|
|
@@ -37,7 +37,6 @@ function slugify(str) {
|
|
|
37
37
|
async function readSchema(slug) {
|
|
38
38
|
const raw = await fs.readFile(schemaPath(slug), 'utf8');
|
|
39
39
|
const schema = JSON.parse(raw);
|
|
40
|
-
// Normalise legacy 'preset' flag — both mean "bundled with fresh installs"
|
|
41
40
|
if (schema.preset && !schema.bundled) {
|
|
42
41
|
schema.bundled = true;
|
|
43
42
|
delete schema.preset;
|
|
@@ -139,11 +138,11 @@ export async function createCollection({title, slug, description = '', fields =
|
|
|
139
138
|
slug: finalSlug,
|
|
140
139
|
title: title.trim(),
|
|
141
140
|
description: description.trim(),
|
|
142
|
-
...(bundled ? {bundled: true} : {}),
|
|
143
|
-
...(plugin ? {plugin} : {}),
|
|
144
141
|
fields,
|
|
145
142
|
api: { ...defaultApiAccess(), ...api },
|
|
146
143
|
storage: storage || {adapter: 'file'},
|
|
144
|
+
...(bundled ? {bundled: true} : {}),
|
|
145
|
+
...(plugin ? {plugin} : {}),
|
|
147
146
|
createdAt: now,
|
|
148
147
|
updatedAt: now
|
|
149
148
|
};
|
|
@@ -165,20 +164,28 @@ export async function updateCollection(slug, updates) {
|
|
|
165
164
|
const schema = await getCollection(slug);
|
|
166
165
|
if (!schema) throw new Error(`Collection "${slug}" not found`);
|
|
167
166
|
|
|
168
|
-
const {
|
|
167
|
+
const {slug: _ignore, createdAt, plugin: _stripPlugin, ...rest} = updates;
|
|
169
168
|
const updated = {
|
|
170
169
|
...schema,
|
|
171
170
|
...rest,
|
|
172
|
-
// bundled is user-editable — set from update, omit if falsy
|
|
173
|
-
...(updates.bundled ? {bundled: true} : {}),
|
|
174
|
-
// plugin is ownership metadata — never overwrite from updates
|
|
175
|
-
...(schema.plugin ? {plugin: schema.plugin} : {}),
|
|
176
171
|
slug,
|
|
177
172
|
createdAt: schema.createdAt,
|
|
178
173
|
updatedAt: new Date().toISOString()
|
|
179
174
|
};
|
|
180
|
-
|
|
181
|
-
|
|
175
|
+
|
|
176
|
+
// Preserve the original plugin field (never overwrite)
|
|
177
|
+
if (schema.plugin) {
|
|
178
|
+
updated.plugin = schema.plugin;
|
|
179
|
+
} else {
|
|
180
|
+
delete updated.plugin;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Allow bundled to be set or cleared
|
|
184
|
+
if (updates.bundled) {
|
|
185
|
+
updated.bundled = true;
|
|
186
|
+
} else {
|
|
187
|
+
delete updated.bundled;
|
|
188
|
+
}
|
|
182
189
|
|
|
183
190
|
await writeSchema(updated);
|
|
184
191
|
|