mastercontroller 1.2.12 → 1.2.13
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/CSPConfig.js +319 -0
- package/EventHandlerValidator.js +464 -0
- package/MasterAction.js +296 -72
- package/MasterBackendErrorHandler.js +769 -0
- package/MasterBenchmark.js +89 -0
- package/MasterBuildOptimizer.js +376 -0
- package/MasterBundleAnalyzer.js +108 -0
- package/MasterCache.js +400 -0
- package/MasterControl.js +76 -6
- package/MasterErrorHandler.js +487 -0
- package/MasterErrorLogger.js +360 -0
- package/MasterErrorMiddleware.js +407 -0
- package/MasterHtml.js +101 -14
- package/MasterMemoryMonitor.js +188 -0
- package/MasterProfiler.js +409 -0
- package/MasterRouter.js +273 -66
- package/MasterSanitizer.js +429 -0
- package/MasterTemplate.js +96 -3
- package/MasterValidator.js +546 -0
- package/README.md +0 -44
- package/SecurityMiddleware.js +486 -0
- package/SessionSecurity.js +416 -0
- package/package.json +3 -3
- package/ssr/ErrorBoundary.js +353 -0
- package/ssr/HTMLUtils.js +15 -0
- package/ssr/HydrationMismatch.js +265 -0
- package/ssr/PerformanceMonitor.js +233 -0
- package/ssr/SSRErrorHandler.js +273 -0
- package/ssr/hydration-client.js +93 -0
- package/ssr/runtime-ssr.cjs +553 -0
- package/ssr/ssr-shims.js +73 -0
- package/examples/FileServingExample.js +0 -88
package/MasterAction.js
CHANGED
|
@@ -5,15 +5,43 @@ var master = require('./MasterControl');
|
|
|
5
5
|
var fileserver = require('fs');
|
|
6
6
|
var toolClass = require('./MasterTools');
|
|
7
7
|
var tempClass = require('./MasterTemplate');
|
|
8
|
+
// Templating helpers
|
|
8
9
|
var temp = new tempClass();
|
|
9
10
|
var tools = new toolClass();
|
|
10
11
|
|
|
12
|
+
// Node utils
|
|
13
|
+
var path = require('path');
|
|
14
|
+
|
|
15
|
+
// Vanilla Web Components SSR runtime (LinkeDOM) - executes connectedCallback() and upgrades
|
|
16
|
+
const compileWebComponentsHTML = require('./ssr/runtime-ssr.cjs');
|
|
17
|
+
|
|
18
|
+
// Enhanced error handling
|
|
19
|
+
const { handleTemplateError, sendErrorResponse } = require('./MasterBackendErrorHandler');
|
|
20
|
+
const { safeReadFile } = require('./MasterErrorMiddleware');
|
|
21
|
+
const { logger } = require('./MasterErrorLogger');
|
|
22
|
+
|
|
23
|
+
// Security - CSRF, validation, sanitization
|
|
24
|
+
const { generateCSRFToken, validateCSRFToken } = require('./SecurityMiddleware');
|
|
25
|
+
const { validator, validateRequestBody, sanitizeObject } = require('./MasterValidator');
|
|
26
|
+
const { sanitizeUserHTML, escapeHTML } = require('./MasterSanitizer');
|
|
27
|
+
|
|
11
28
|
class MasterAction{
|
|
12
29
|
|
|
13
30
|
getView(location, data){
|
|
14
31
|
var actionUrl = master.root + location;
|
|
15
|
-
|
|
16
|
-
|
|
32
|
+
const fileResult = safeReadFile(fileserver, actionUrl);
|
|
33
|
+
|
|
34
|
+
if (!fileResult.success) {
|
|
35
|
+
const error = handleTemplateError(fileResult.error.originalError, actionUrl, data);
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
return temp.htmlBuilder(fileResult.content, data);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
const mcError = handleTemplateError(error, actionUrl, data);
|
|
43
|
+
throw mcError;
|
|
44
|
+
}
|
|
17
45
|
}
|
|
18
46
|
|
|
19
47
|
|
|
@@ -110,6 +138,16 @@ class MasterAction{
|
|
|
110
138
|
this.params = tools.combineObjects(data, this.params);
|
|
111
139
|
var func = master.viewList;
|
|
112
140
|
this.params = tools.combineObjects(this.params, func);
|
|
141
|
+
// Prefer page.js module if present (no legacy .html file)
|
|
142
|
+
try {
|
|
143
|
+
const controller = this.__currentRoute.toController;
|
|
144
|
+
const action = this.__currentRoute.toAction;
|
|
145
|
+
const pageModuleAbs = path.join(master.root, 'app/views', controller, action, 'page.js');
|
|
146
|
+
if (fileserver.existsSync(pageModuleAbs)) {
|
|
147
|
+
if (this._renderPageModule(controller, action, data)) { return; }
|
|
148
|
+
}
|
|
149
|
+
} catch (_) {}
|
|
150
|
+
|
|
113
151
|
var actionUrl = (location === undefined) ? this.__currentRoute.root + "/app/views/" + this.__currentRoute.toController + "/" + this.__currentRoute.toAction + ".html" : master.root + location;
|
|
114
152
|
var actionView = fileserver.readFileSync(actionUrl, 'utf8');
|
|
115
153
|
if(master.overwrite.isTemplate){
|
|
@@ -119,8 +157,21 @@ class MasterAction{
|
|
|
119
157
|
masterView = temp.htmlBuilder(actionView, data);
|
|
120
158
|
}
|
|
121
159
|
if (!this.__requestObject.response._headerSent) {
|
|
122
|
-
|
|
123
|
-
|
|
160
|
+
const send = (htmlOut) => {
|
|
161
|
+
try {
|
|
162
|
+
this.__requestObject.response.writeHead(200, {'Content-Type': 'text/html'});
|
|
163
|
+
this.__requestObject.response.end(htmlOut);
|
|
164
|
+
} catch (e) {
|
|
165
|
+
// Fallback in case of double send
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
try {
|
|
169
|
+
Promise.resolve(compileWebComponentsHTML(masterView))
|
|
170
|
+
.then(send)
|
|
171
|
+
.catch(() => send(masterView));
|
|
172
|
+
} catch (_) {
|
|
173
|
+
send(masterView);
|
|
174
|
+
}
|
|
124
175
|
}
|
|
125
176
|
}
|
|
126
177
|
|
|
@@ -133,6 +184,18 @@ class MasterAction{
|
|
|
133
184
|
}
|
|
134
185
|
}
|
|
135
186
|
|
|
187
|
+
returnReact(data, location){
|
|
188
|
+
|
|
189
|
+
var masterView = null;
|
|
190
|
+
data = data === undefined ? {} : data;
|
|
191
|
+
this.params = this.params === undefined ? {} : this.params;
|
|
192
|
+
this.params = tools.combineObjects(data, this.params);
|
|
193
|
+
var func = master.viewList;
|
|
194
|
+
this.params = tools.combineObjects(this.params, func);
|
|
195
|
+
var html = master.reactView.compile(this.__currentRoute.toController, this.__currentRoute.toAction, this.__currentRoute.root);
|
|
196
|
+
|
|
197
|
+
}
|
|
198
|
+
|
|
136
199
|
returnView(data, location){
|
|
137
200
|
|
|
138
201
|
var masterView = null;
|
|
@@ -141,6 +204,16 @@ class MasterAction{
|
|
|
141
204
|
this.params = tools.combineObjects(data, this.params);
|
|
142
205
|
var func = master.viewList;
|
|
143
206
|
this.params = tools.combineObjects(this.params, func);
|
|
207
|
+
// Prefer page.js module if present (no legacy .html file)
|
|
208
|
+
try {
|
|
209
|
+
const controller = this.__currentRoute.toController;
|
|
210
|
+
const action = this.__currentRoute.toAction;
|
|
211
|
+
const pageModuleAbs = path.join(master.root, 'app/views', controller, action, 'page.js');
|
|
212
|
+
if (fileserver.existsSync(pageModuleAbs)) {
|
|
213
|
+
if (this._renderPageModule(controller, action, data)) { return; }
|
|
214
|
+
}
|
|
215
|
+
} catch (_) {}
|
|
216
|
+
|
|
144
217
|
var viewUrl = (location === undefined || location === "" || location === null) ? this.__currentRoute.root + "/app/views/" + this.__currentRoute.toController + "/" + this.__currentRoute.toAction + ".html" : master.root + location;
|
|
145
218
|
var viewFile = fileserver.readFileSync(viewUrl,'utf8');
|
|
146
219
|
var masterFile = fileserver.readFileSync(this.__currentRoute.root + "/app/views/layouts/master.html", 'utf8');
|
|
@@ -154,8 +227,21 @@ class MasterAction{
|
|
|
154
227
|
}
|
|
155
228
|
|
|
156
229
|
if (!this.__response._headerSent) {
|
|
157
|
-
|
|
158
|
-
|
|
230
|
+
const send = (htmlOut) => {
|
|
231
|
+
try {
|
|
232
|
+
this.__response.writeHead(200, {'Content-Type': 'text/html'});
|
|
233
|
+
this.__response.end(htmlOut);
|
|
234
|
+
} catch (e) {
|
|
235
|
+
// Fallback in case of double send
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
try {
|
|
239
|
+
Promise.resolve(compileWebComponentsHTML(masterView))
|
|
240
|
+
.then(send)
|
|
241
|
+
.catch(() => send(masterView));
|
|
242
|
+
} catch (_) {
|
|
243
|
+
send(masterView);
|
|
244
|
+
}
|
|
159
245
|
}
|
|
160
246
|
|
|
161
247
|
}
|
|
@@ -180,7 +266,7 @@ class MasterAction{
|
|
|
180
266
|
return true;
|
|
181
267
|
}
|
|
182
268
|
|
|
183
|
-
|
|
269
|
+
// Enhanced returnJson that checks readiness first
|
|
184
270
|
safeReturnJson(data){
|
|
185
271
|
if (this.waitUntilReady()) {
|
|
186
272
|
this.returnJson(data);
|
|
@@ -190,75 +276,213 @@ class MasterAction{
|
|
|
190
276
|
return false;
|
|
191
277
|
}
|
|
192
278
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
options = options || {};
|
|
201
|
-
|
|
202
|
-
// Default options
|
|
203
|
-
var disposition = options.disposition || 'attachment';
|
|
204
|
-
var filename = options.filename || filePath.split('/').pop();
|
|
205
|
-
var contentType = options.contentType;
|
|
206
|
-
|
|
207
|
-
// Auto-detect content type if not provided
|
|
208
|
-
if (!contentType) {
|
|
209
|
-
var ext = filePath.split('.').pop().toLowerCase();
|
|
210
|
-
var mimeTypes = {
|
|
211
|
-
'pdf': 'application/pdf',
|
|
212
|
-
'jpg': 'image/jpeg',
|
|
213
|
-
'jpeg': 'image/jpeg',
|
|
214
|
-
'png': 'image/png',
|
|
215
|
-
'gif': 'image/gif',
|
|
216
|
-
'svg': 'image/svg+xml',
|
|
217
|
-
'zip': 'application/zip',
|
|
218
|
-
'csv': 'text/csv',
|
|
219
|
-
'txt': 'text/plain',
|
|
220
|
-
'xml': 'application/xml',
|
|
221
|
-
'json': 'application/json',
|
|
222
|
-
'mp4': 'video/mp4',
|
|
223
|
-
'mp3': 'audio/mpeg',
|
|
224
|
-
'wav': 'audio/wav',
|
|
225
|
-
'doc': 'application/msword',
|
|
226
|
-
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
227
|
-
'xls': 'application/vnd.ms-excel',
|
|
228
|
-
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
229
|
-
'ppt': 'application/vnd.ms-powerpoint',
|
|
230
|
-
'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
|
231
|
-
};
|
|
232
|
-
contentType = mimeTypes[ext] || 'application/octet-stream';
|
|
233
|
-
}
|
|
279
|
+
// Render using a page.js Web Component module when present
|
|
280
|
+
_renderPageModule(controller, action, data) {
|
|
281
|
+
try {
|
|
282
|
+
const pageModuleAbs = path.join(master.root, 'app/views', controller, action, 'page.js');
|
|
283
|
+
const layoutModuleAbs = path.join(master.root, 'app/views', 'layouts', 'master.js');
|
|
284
|
+
const stylesPath = '/app/assets/stylesheets/output.css';
|
|
285
|
+
const pageTag = `home-${action}-page`;
|
|
234
286
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
287
|
+
const htmlDoc =
|
|
288
|
+
`<!DOCTYPE html>
|
|
289
|
+
<html lang="en">
|
|
290
|
+
<head>
|
|
291
|
+
<meta charset="utf-8"/>
|
|
292
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
293
|
+
<title>${controller}/${action}</title>
|
|
294
|
+
<link rel="stylesheet" href="${stylesPath}"/>
|
|
295
|
+
</head>
|
|
296
|
+
<body class="geist-variable antialiased">
|
|
297
|
+
<root-layout>
|
|
298
|
+
<${pageTag}></${pageTag}>
|
|
299
|
+
</root-layout>
|
|
300
|
+
<script type="module" src="/app/views/layouts/master.js"></script>
|
|
301
|
+
<script type="module" src="/app/views/${controller}/${action}/page.js"></script>
|
|
302
|
+
</body>
|
|
303
|
+
</html>`;
|
|
245
304
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
305
|
+
const send = (htmlOut) => {
|
|
306
|
+
try {
|
|
307
|
+
const res = this.__response || (this.__requestObject && this.__requestObject.response);
|
|
308
|
+
if (res && !res._headerSent) {
|
|
309
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
310
|
+
res.end(htmlOut);
|
|
311
|
+
}
|
|
312
|
+
} catch (_) {}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
Promise
|
|
316
|
+
.resolve(require('./ssr/runtime-ssr.cjs')(htmlDoc, [layoutModuleAbs, pageModuleAbs]))
|
|
317
|
+
.then(send)
|
|
318
|
+
.catch(() => send(htmlDoc));
|
|
319
|
+
} catch (e) {
|
|
320
|
+
// Fallback to legacy view if something goes wrong
|
|
321
|
+
console.warn('[SSR] _renderPageModule failed:', e && e.message);
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Delegate to standard Enhance-based SSR only
|
|
328
|
+
returnWebComponent(data) {
|
|
329
|
+
this.returnView(data);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ==================== Security Methods ====================
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Generate CSRF token for forms
|
|
336
|
+
* Usage: const token = this.generateCSRFToken();
|
|
337
|
+
*/
|
|
338
|
+
generateCSRFToken() {
|
|
339
|
+
const sessionId = this.__requestObject && this.__requestObject.session ? this.__requestObject.session.id : null;
|
|
340
|
+
return generateCSRFToken(sessionId);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Validate CSRF token from request
|
|
345
|
+
* Usage: if (!this.validateCSRF()) { return this.returnError(403, 'Invalid CSRF token'); }
|
|
346
|
+
*/
|
|
347
|
+
validateCSRF(token = null) {
|
|
348
|
+
// Get token from parameter, header, or body
|
|
349
|
+
const csrfToken = token ||
|
|
350
|
+
this.__requestObject.headers['x-csrf-token'] ||
|
|
351
|
+
(this.__requestObject.body && this.__requestObject.body._csrf) ||
|
|
352
|
+
this.params._csrf;
|
|
353
|
+
|
|
354
|
+
if (!csrfToken) {
|
|
355
|
+
logger.warn({
|
|
356
|
+
code: 'MC_SECURITY_CSRF_MISSING',
|
|
357
|
+
message: 'CSRF token missing in request',
|
|
358
|
+
path: this.__requestObject.pathName
|
|
359
|
+
});
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const validation = validateCSRFToken(csrfToken);
|
|
260
364
|
|
|
365
|
+
if (!validation.valid) {
|
|
366
|
+
logger.warn({
|
|
367
|
+
code: 'MC_SECURITY_CSRF_INVALID',
|
|
368
|
+
message: 'CSRF token validation failed',
|
|
369
|
+
path: this.__requestObject.pathName,
|
|
370
|
+
reason: validation.reason
|
|
371
|
+
});
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Validate request body against schema
|
|
380
|
+
* Usage: const result = this.validateRequest({ email: { type: 'email' }, age: { type: 'integer', min: 18 } });
|
|
381
|
+
*/
|
|
382
|
+
validateRequest(schema = {}) {
|
|
383
|
+
const body = this.__requestObject.body || this.params || {};
|
|
384
|
+
const result = validateRequestBody(body, schema);
|
|
385
|
+
|
|
386
|
+
if (!result.valid) {
|
|
387
|
+
logger.warn({
|
|
388
|
+
code: 'MC_VALIDATION_REQUEST_FAILED',
|
|
389
|
+
message: 'Request validation failed',
|
|
390
|
+
path: this.__requestObject.pathName,
|
|
391
|
+
errors: result.errors
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return result;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Sanitize user input (HTML)
|
|
400
|
+
* Usage: const clean = this.sanitizeInput(userInput);
|
|
401
|
+
*/
|
|
402
|
+
sanitizeInput(input) {
|
|
403
|
+
if (typeof input === 'string') {
|
|
404
|
+
return sanitizeUserHTML(input);
|
|
405
|
+
} else if (typeof input === 'object' && input !== null) {
|
|
406
|
+
return sanitizeObject(input);
|
|
407
|
+
}
|
|
408
|
+
return input;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Escape HTML for display
|
|
413
|
+
* Usage: const safe = this.escapeHTML(userContent);
|
|
414
|
+
*/
|
|
415
|
+
escapeHTML(text) {
|
|
416
|
+
return escapeHTML(text);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Validate single field
|
|
421
|
+
* Usage: const result = this.validate(email, { type: 'email' });
|
|
422
|
+
*/
|
|
423
|
+
validate(value, rules = {}) {
|
|
424
|
+
switch (rules.type) {
|
|
425
|
+
case 'string':
|
|
426
|
+
return validator.validateString(value, rules);
|
|
427
|
+
case 'integer':
|
|
428
|
+
return validator.validateInteger(value, rules);
|
|
429
|
+
case 'email':
|
|
430
|
+
return validator.validateEmail(value, rules);
|
|
431
|
+
case 'url':
|
|
432
|
+
return validator.validateURL(value, rules);
|
|
433
|
+
case 'uuid':
|
|
434
|
+
return validator.validateUUID(value, rules);
|
|
435
|
+
default:
|
|
436
|
+
return { valid: true, value };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Check if request is secure (HTTPS)
|
|
442
|
+
*/
|
|
443
|
+
isSecure() {
|
|
444
|
+
const req = this.__requestObject.request || this.__requestObject;
|
|
445
|
+
return req.connection.encrypted || req.headers['x-forwarded-proto'] === 'https';
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Require HTTPS for this action
|
|
450
|
+
* Usage: if (!this.requireHTTPS()) return;
|
|
451
|
+
*/
|
|
452
|
+
requireHTTPS() {
|
|
453
|
+
if (!this.isSecure()) {
|
|
454
|
+
logger.warn({
|
|
455
|
+
code: 'MC_SECURITY_HTTPS_REQUIRED',
|
|
456
|
+
message: 'HTTPS required but request is HTTP',
|
|
457
|
+
path: this.__requestObject.pathName
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
const httpsUrl = `https://${this.__requestObject.request.headers.host}${this.__requestObject.pathName}`;
|
|
461
|
+
this.redirectTo(httpsUrl);
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Return error response with proper status
|
|
469
|
+
* Usage: this.returnError(400, 'Invalid input');
|
|
470
|
+
*/
|
|
471
|
+
returnError(statusCode, message, details = {}) {
|
|
472
|
+
const res = this.__response || (this.__requestObject && this.__requestObject.response);
|
|
473
|
+
|
|
474
|
+
if (res && !res._headerSent) {
|
|
475
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
476
|
+
res.end(JSON.stringify({
|
|
477
|
+
error: true,
|
|
478
|
+
statusCode,
|
|
479
|
+
message,
|
|
480
|
+
...details
|
|
481
|
+
}));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
261
484
|
|
|
262
485
|
}
|
|
263
486
|
|
|
487
|
+
|
|
264
488
|
master.extendController(MasterAction);
|