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/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
- var actionView = fileserver.readFileSync(actionUrl, 'utf8');
16
- return temp.htmlBuilder(actionView, data);
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
- this.__requestObject.response.writeHead(200, {'Content-Type': 'text/html'});
123
- this.__requestObject.response.end(masterView);
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
- this.__response.writeHead(200, {'Content-Type': 'text/html'});
158
- this.__response.end(masterView);
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
- // Enhanced returnJson that checks readiness first
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
- // Serves binary files with proper headers and MIME types
194
- // options: {
195
- // contentType: string (auto-detected if not provided),
196
- // disposition: 'inline' | 'attachment' (default: 'attachment'),
197
- // filename: string (defaults to original filename)
198
- // }
199
- returnFile(filePath, options){
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
- try {
236
- // Read file as binary
237
- var fileBuffer = fileserver.readFileSync(filePath);
238
-
239
- // Build headers
240
- var headers = {
241
- 'Content-Type': contentType,
242
- 'Content-Length': fileBuffer.length,
243
- 'Content-Disposition': disposition + '; filename="' + filename + '"'
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
- // Send response
247
- if (!this.__response._headerSent) {
248
- this.__response.writeHead(200, headers);
249
- this.__response.end(fileBuffer);
250
- }
251
- } catch(error) {
252
- // Handle file not found or read errors
253
- if (!this.__response._headerSent) {
254
- this.__response.writeHead(404, {'Content-Type': 'text/plain'});
255
- this.__response.end('File not found: ' + filePath);
256
- }
257
- console.error('Error serving file:', error);
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);