mastercontroller 1.2.14 → 1.3.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/MasterControl.js +148 -101
- package/MasterCors.js +29 -0
- package/MasterPipeline.js +344 -0
- package/MasterRouter.js +44 -22
- package/MasterSession.js +19 -0
- package/MasterTimeout.js +332 -0
- package/README.md +1496 -36
- package/docs/timeout-and-error-handling.md +712 -0
- package/error/MasterErrorRenderer.js +529 -0
- package/package.json +5 -5
- package/security/SecurityMiddleware.js +73 -1
package/README.md
CHANGED
|
@@ -1,58 +1,1518 @@
|
|
|
1
|
-
|
|
1
|
+
# MasterController Framework
|
|
2
2
|
|
|
3
|
-
MasterController is a lightweight MVC-style server framework for Node.js with routing, controllers, views, dependency injection, CORS, sessions, sockets, and more.
|
|
3
|
+
MasterController is a lightweight MVC-style server framework for Node.js with middleware pipeline, routing, controllers, views, dependency injection, CORS, sessions, sockets, and more.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
## Table of Contents
|
|
6
|
+
- [Installation](#installation)
|
|
7
|
+
- [Quickstart](#quickstart)
|
|
8
|
+
- [Middleware Pipeline](#middleware-pipeline)
|
|
9
|
+
- [Routing](#routing)
|
|
10
|
+
- [Controllers](#controllers)
|
|
11
|
+
- [Views and Templates](#views-and-templates)
|
|
12
|
+
- [View Pattern Hooks](#view-pattern-hooks)
|
|
13
|
+
- [Dependency Injection](#dependency-injection)
|
|
14
|
+
- [CORS](#cors)
|
|
15
|
+
- [Sessions](#sessions)
|
|
16
|
+
- [Security](#security)
|
|
17
|
+
- [Components](#components)
|
|
18
|
+
- [Timeout System](#timeout-system)
|
|
19
|
+
- [Error Handling](#error-handling)
|
|
20
|
+
- [HTTPS Setup](#https-setup)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
7
27
|
npm install mastercontroller
|
|
8
28
|
```
|
|
9
29
|
|
|
10
|
-
|
|
11
|
-
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Quickstart
|
|
33
|
+
|
|
34
|
+
```javascript
|
|
12
35
|
// server.js
|
|
13
|
-
const master = require('
|
|
36
|
+
const master = require('mastercontroller');
|
|
37
|
+
|
|
38
|
+
master.root = __dirname;
|
|
39
|
+
master.environmentType = 'development'; // or process.env.NODE_ENV
|
|
14
40
|
|
|
15
|
-
|
|
16
|
-
|
|
41
|
+
const server = master.setupServer('http'); // or 'https'
|
|
42
|
+
|
|
43
|
+
// Load configuration (registers middleware, routes, DI services)
|
|
44
|
+
require('./config/initializers/config');
|
|
17
45
|
|
|
18
|
-
const server = master.setupServer('http'); // or 'https'
|
|
19
46
|
master.start(server);
|
|
20
|
-
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
// config/initializers/config.js
|
|
51
|
+
const master = require('mastercontroller');
|
|
52
|
+
const cors = require('./cors.json');
|
|
53
|
+
|
|
54
|
+
// Initialize CORS (auto-registers with pipeline)
|
|
55
|
+
master.cors.init(cors);
|
|
56
|
+
|
|
57
|
+
// Initialize sessions (auto-registers with pipeline)
|
|
58
|
+
master.sessions.init();
|
|
59
|
+
|
|
60
|
+
// Auto-discover custom middleware from middleware/ folder
|
|
61
|
+
master.pipeline.discoverMiddleware('middleware');
|
|
62
|
+
|
|
63
|
+
// Configure server settings
|
|
64
|
+
master.serverSettings({
|
|
65
|
+
httpPort: 3000,
|
|
66
|
+
hostname: '127.0.0.1',
|
|
67
|
+
requestTimeout: 60000
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Register routes
|
|
71
|
+
master.startMVC('config');
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Middleware Pipeline
|
|
77
|
+
|
|
78
|
+
MasterController uses an ASP.NET Core-style middleware pipeline for request processing.
|
|
79
|
+
|
|
80
|
+
### Core Methods
|
|
81
|
+
|
|
82
|
+
#### `master.pipeline.use(middleware)`
|
|
83
|
+
Add pass-through middleware that calls `next()` to continue the chain.
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
master.pipeline.use(async (ctx, next) => {
|
|
87
|
+
// Before request
|
|
88
|
+
console.log(`→ ${ctx.type.toUpperCase()} ${ctx.request.url}`);
|
|
89
|
+
|
|
90
|
+
await next(); // Continue to next middleware
|
|
91
|
+
|
|
92
|
+
// After response
|
|
93
|
+
console.log(`← ${ctx.response.statusCode}`);
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### `master.pipeline.run(middleware)`
|
|
98
|
+
Add terminal middleware that ends the pipeline (does not call `next()`).
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
master.pipeline.run(async (ctx) => {
|
|
102
|
+
ctx.response.statusCode = 200;
|
|
103
|
+
ctx.response.end('Hello World');
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### `master.pipeline.map(path, configure)`
|
|
108
|
+
Conditionally execute middleware only for matching paths.
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
// Apply authentication only to /api/* routes
|
|
112
|
+
master.pipeline.map('/api/*', (api) => {
|
|
113
|
+
api.use(async (ctx, next) => {
|
|
114
|
+
const token = ctx.request.headers['authorization'];
|
|
115
|
+
if (!token) {
|
|
116
|
+
ctx.response.statusCode = 401;
|
|
117
|
+
ctx.response.end('Unauthorized');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
ctx.state.user = await validateToken(token);
|
|
121
|
+
await next();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Apply rate limiting to API
|
|
125
|
+
api.use(rateLimitMiddleware);
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### `master.pipeline.useError(errorHandler)`
|
|
130
|
+
Add error handling middleware.
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
master.pipeline.useError(async (error, ctx, next) => {
|
|
134
|
+
console.error('Error:', error);
|
|
135
|
+
|
|
136
|
+
if (!ctx.response.headersSent) {
|
|
137
|
+
ctx.response.statusCode = 500;
|
|
138
|
+
ctx.response.end('Internal Server Error');
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### `master.pipeline.discoverMiddleware(options)`
|
|
144
|
+
Auto-discover and load middleware from folders.
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
// Single folder
|
|
148
|
+
master.pipeline.discoverMiddleware('middleware');
|
|
149
|
+
|
|
150
|
+
// Multiple folders
|
|
151
|
+
master.pipeline.discoverMiddleware({
|
|
152
|
+
folders: ['middleware', 'app/middleware']
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Context Object
|
|
157
|
+
|
|
158
|
+
Middleware receives a context object:
|
|
159
|
+
|
|
160
|
+
```javascript
|
|
161
|
+
{
|
|
162
|
+
request: req, // Node.js request object
|
|
163
|
+
response: res, // Node.js response object
|
|
164
|
+
requrl: parsedUrl, // Parsed URL with query
|
|
165
|
+
pathName: 'api/users', // Normalized path (lowercase)
|
|
166
|
+
type: 'get', // HTTP method (lowercase)
|
|
167
|
+
params: { // Route parameters + query + form data
|
|
168
|
+
query: {}, // Query string parameters
|
|
169
|
+
formData: {}, // POST body data
|
|
170
|
+
periodId: '123' // Route parameters (e.g., /period/:periodId)
|
|
171
|
+
},
|
|
172
|
+
state: {}, // Custom state to share between middleware
|
|
173
|
+
master: master, // Framework instance
|
|
174
|
+
isStatic: false // Is this a static file request?
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Custom Middleware Files
|
|
179
|
+
|
|
180
|
+
Create middleware files that are auto-discovered:
|
|
181
|
+
|
|
182
|
+
**Simple function export:**
|
|
183
|
+
```javascript
|
|
184
|
+
// middleware/01-logger.js
|
|
185
|
+
module.exports = async (ctx, next) => {
|
|
186
|
+
const start = Date.now();
|
|
187
|
+
await next();
|
|
188
|
+
const duration = Date.now() - start;
|
|
189
|
+
console.log(`${ctx.type.toUpperCase()} ${ctx.request.url} - ${duration}ms`);
|
|
190
|
+
};
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Object with register() method:**
|
|
194
|
+
```javascript
|
|
195
|
+
// middleware/02-auth.js
|
|
196
|
+
module.exports = {
|
|
197
|
+
register: (master) => {
|
|
198
|
+
master.pipeline.map('/admin/*', (admin) => {
|
|
199
|
+
admin.use(async (ctx, next) => {
|
|
200
|
+
if (!ctx.state.user?.isAdmin) {
|
|
201
|
+
ctx.response.statusCode = 403;
|
|
202
|
+
ctx.response.end('Forbidden');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
await next();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Files are loaded alphabetically (use `01-`, `02-` prefixes for ordering).
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Routing
|
|
217
|
+
|
|
218
|
+
### Setup Routes
|
|
219
|
+
|
|
220
|
+
Create `config/routes.js`:
|
|
221
|
+
|
|
222
|
+
```javascript
|
|
223
|
+
var master = require('mastercontroller');
|
|
224
|
+
var router = master.router.start();
|
|
225
|
+
|
|
226
|
+
// Basic route
|
|
227
|
+
router.route('/users', 'users#index', 'get');
|
|
228
|
+
|
|
229
|
+
// Route with parameters (preserves casing!)
|
|
230
|
+
router.route('/period/:periodId/items/:itemId', 'period#show', 'get');
|
|
231
|
+
|
|
232
|
+
// RESTful routes (generates 7 routes automatically)
|
|
233
|
+
router.resources('posts');
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### API
|
|
237
|
+
|
|
238
|
+
#### `router.route(path, toPath, method, constraint)`
|
|
239
|
+
Register a single route.
|
|
240
|
+
|
|
241
|
+
- `path`: URL path (can include `:paramName`)
|
|
242
|
+
- `toPath`: Controller#action (e.g., `'users#index'`)
|
|
243
|
+
- `method`: HTTP method (`'get'`, `'post'`, `'put'`, `'delete'`, `'patch'`)
|
|
244
|
+
- `constraint`: Optional constraint function
|
|
245
|
+
|
|
246
|
+
**Parameter casing is preserved:**
|
|
247
|
+
```javascript
|
|
248
|
+
router.route('/period/:periodId', 'period#show', 'get');
|
|
249
|
+
// In controller: obj.params.periodId (not periodid)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### `router.resources(routeName)`
|
|
253
|
+
Generate RESTful routes for a resource:
|
|
254
|
+
|
|
255
|
+
```javascript
|
|
256
|
+
router.resources('posts');
|
|
257
|
+
|
|
258
|
+
// Generates:
|
|
259
|
+
// GET /posts -> posts#index
|
|
260
|
+
// GET /posts/new -> posts#new
|
|
261
|
+
// POST /posts -> posts#create
|
|
262
|
+
// GET /posts/:id -> posts#show
|
|
263
|
+
// GET /posts/:id/edit -> posts#edit
|
|
264
|
+
// PUT /posts/:id -> posts#update
|
|
265
|
+
// DELETE /posts/:id -> posts#destroy
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
#### Route Constraints
|
|
269
|
+
|
|
270
|
+
Add custom logic to routes with constraints:
|
|
271
|
+
|
|
272
|
+
```javascript
|
|
273
|
+
router.route('/admin', 'admin#index', 'get', function(requestObject) {
|
|
274
|
+
// Check authentication
|
|
275
|
+
if (!isAuthenticated(requestObject)) {
|
|
276
|
+
requestObject.response.statusCode = 401;
|
|
277
|
+
requestObject.response.end('Unauthorized');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Continue to controller
|
|
282
|
+
this.next();
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Controllers
|
|
289
|
+
|
|
290
|
+
### Creating Controllers
|
|
291
|
+
|
|
292
|
+
Create controllers in `app/controllers/`:
|
|
293
|
+
|
|
294
|
+
```javascript
|
|
295
|
+
// app/controllers/usersController.js
|
|
296
|
+
class UsersController {
|
|
297
|
+
constructor(requestObject) {
|
|
298
|
+
// Called for every request
|
|
299
|
+
this.requestObject = requestObject;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Actions
|
|
303
|
+
index(obj) {
|
|
304
|
+
// obj = requestObject
|
|
305
|
+
this.render('index', {
|
|
306
|
+
users: ['Alice', 'Bob', 'Charlie']
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
show(obj) {
|
|
311
|
+
const userId = obj.params.id;
|
|
312
|
+
this.render('show', { userId });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
create(obj) {
|
|
316
|
+
const userData = obj.params.formData;
|
|
317
|
+
// Save user...
|
|
318
|
+
this.redirect('/users');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
module.exports = UsersController;
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Controller API
|
|
326
|
+
|
|
327
|
+
#### `this.render(view, data)`
|
|
328
|
+
Render a view with data.
|
|
329
|
+
|
|
330
|
+
```javascript
|
|
331
|
+
this.render('index', {
|
|
332
|
+
title: 'Users',
|
|
333
|
+
users: userList
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Views are located at: `app/views/<controller>/<view>.html`
|
|
338
|
+
|
|
339
|
+
#### `this.redirect(path)`
|
|
340
|
+
Redirect to another path.
|
|
341
|
+
|
|
342
|
+
```javascript
|
|
343
|
+
this.redirect('/users');
|
|
344
|
+
this.redirect('/users/123');
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
#### `this.renderComponent(componentName, viewName, data)`
|
|
348
|
+
Render a view from a component.
|
|
349
|
+
|
|
350
|
+
```javascript
|
|
351
|
+
this.renderComponent('mail', 'inbox', { emails });
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
#### `this.json(data)`
|
|
355
|
+
Send JSON response.
|
|
356
|
+
|
|
357
|
+
```javascript
|
|
358
|
+
this.json({
|
|
359
|
+
success: true,
|
|
360
|
+
users: userList
|
|
361
|
+
});
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
#### Access Request Data
|
|
365
|
+
|
|
366
|
+
```javascript
|
|
367
|
+
class UsersController {
|
|
368
|
+
show(obj) {
|
|
369
|
+
// Route parameters
|
|
370
|
+
const userId = obj.params.id;
|
|
371
|
+
const periodId = obj.params.periodId; // Casing preserved!
|
|
372
|
+
|
|
373
|
+
// Query string
|
|
374
|
+
const search = obj.params.query.search;
|
|
375
|
+
|
|
376
|
+
// Form data
|
|
377
|
+
const email = obj.params.formData.email;
|
|
378
|
+
|
|
379
|
+
// Files (multipart/form-data)
|
|
380
|
+
const avatar = obj.params.formData.files.avatar;
|
|
381
|
+
|
|
382
|
+
// Request method
|
|
383
|
+
const method = obj.type; // 'get', 'post', etc.
|
|
384
|
+
|
|
385
|
+
// Full request/response
|
|
386
|
+
const req = obj.request;
|
|
387
|
+
const res = obj.response;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Before/After Action Filters
|
|
393
|
+
|
|
394
|
+
Execute code before or after specific actions:
|
|
395
|
+
|
|
396
|
+
```javascript
|
|
397
|
+
class UsersController {
|
|
398
|
+
constructor(requestObject) {
|
|
399
|
+
// Run before 'edit' and 'update' actions
|
|
400
|
+
this.beforeAction(['edit', 'update'], function(obj) {
|
|
401
|
+
if (!isAuthenticated(obj)) {
|
|
402
|
+
obj.response.statusCode = 401;
|
|
403
|
+
obj.response.end('Unauthorized');
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Continue to action
|
|
408
|
+
this.next();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Run after 'create' and 'update' actions
|
|
412
|
+
this.afterAction(['create', 'update'], function(obj) {
|
|
413
|
+
console.log('User saved');
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
edit(obj) {
|
|
418
|
+
// beforeAction runs first
|
|
419
|
+
this.render('edit');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
update(obj) {
|
|
423
|
+
// beforeAction runs first
|
|
424
|
+
// ... update user ...
|
|
425
|
+
// afterAction runs after
|
|
426
|
+
this.redirect('/users');
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
**Methods:**
|
|
432
|
+
- `this.beforeAction(actionList, callback)` - Run before specific actions
|
|
433
|
+
- `this.afterAction(actionList, callback)` - Run after specific actions
|
|
434
|
+
- `this.next()` - Continue from beforeAction to action
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## Views and Templates
|
|
439
|
+
|
|
440
|
+
### View Structure
|
|
441
|
+
|
|
442
|
+
```
|
|
443
|
+
app/
|
|
444
|
+
views/
|
|
445
|
+
layouts/
|
|
446
|
+
master.html # Main layout
|
|
447
|
+
users/
|
|
448
|
+
index.html # Users index view
|
|
449
|
+
show.html # Users show view
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Layout (master.html)
|
|
453
|
+
|
|
454
|
+
```html
|
|
455
|
+
<!DOCTYPE html>
|
|
456
|
+
<html>
|
|
457
|
+
<head>
|
|
458
|
+
<title>{{title}}</title>
|
|
459
|
+
</head>
|
|
460
|
+
<body>
|
|
461
|
+
<header>
|
|
462
|
+
<h1>My App</h1>
|
|
463
|
+
</header>
|
|
464
|
+
|
|
465
|
+
<main>
|
|
466
|
+
{{body}} <!-- View content inserted here -->
|
|
467
|
+
</main>
|
|
468
|
+
|
|
469
|
+
<footer>
|
|
470
|
+
© 2025
|
|
471
|
+
</footer>
|
|
472
|
+
</body>
|
|
473
|
+
</html>
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### View (users/index.html)
|
|
477
|
+
|
|
478
|
+
```html
|
|
479
|
+
<h2>{{title}}</h2>
|
|
480
|
+
|
|
481
|
+
<ul>
|
|
482
|
+
{{#each users}}
|
|
483
|
+
<li>{{this}}</li>
|
|
484
|
+
{{/each}}
|
|
485
|
+
</ul>
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Template Syntax
|
|
489
|
+
|
|
490
|
+
MasterController uses Handlebars-style templates:
|
|
491
|
+
|
|
492
|
+
```html
|
|
493
|
+
<!-- Variables -->
|
|
494
|
+
{{name}}
|
|
495
|
+
{{user.email}}
|
|
496
|
+
|
|
497
|
+
<!-- HTML escaping (automatic) -->
|
|
498
|
+
{{description}}
|
|
499
|
+
|
|
500
|
+
<!-- Conditionals -->
|
|
501
|
+
{{#if isAdmin}}
|
|
502
|
+
<a href="/admin">Admin Panel</a>
|
|
503
|
+
{{/if}}
|
|
504
|
+
|
|
505
|
+
{{#unless isGuest}}
|
|
506
|
+
<p>Welcome back!</p>
|
|
507
|
+
{{/unless}}
|
|
508
|
+
|
|
509
|
+
<!-- Loops -->
|
|
510
|
+
{{#each items}}
|
|
511
|
+
<div>{{this.name}}</div>
|
|
512
|
+
{{/each}}
|
|
513
|
+
|
|
514
|
+
<!-- Partials -->
|
|
515
|
+
{{> header}}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
## View Pattern Hooks
|
|
521
|
+
|
|
522
|
+
Extend views with custom methods using the **view pattern hook system**.
|
|
523
|
+
|
|
524
|
+
### `master.extendView(name, ViewClass)`
|
|
525
|
+
|
|
526
|
+
Add custom methods that are available in all views via `this` keyword.
|
|
527
|
+
|
|
528
|
+
```javascript
|
|
529
|
+
// Create a view helper class
|
|
530
|
+
class MyViewHelpers {
|
|
531
|
+
// Format currency
|
|
532
|
+
currency(amount) {
|
|
533
|
+
return `$${amount.toFixed(2)}`;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Format date
|
|
537
|
+
formatDate(date) {
|
|
538
|
+
return new Date(date).toLocaleDateString();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Truncate text
|
|
542
|
+
truncate(text, length) {
|
|
543
|
+
if (text.length <= length) return text;
|
|
544
|
+
return text.substring(0, length) + '...';
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Check if user has permission
|
|
548
|
+
can(permission) {
|
|
549
|
+
// Access request context if needed
|
|
550
|
+
return this.__requestObject.user?.permissions.includes(permission);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Register the helpers
|
|
555
|
+
master.extendView('helpers', MyViewHelpers);
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
**Use in views:**
|
|
559
|
+
|
|
560
|
+
```html
|
|
561
|
+
<p>Price: {{helpers.currency(product.price)}}</p>
|
|
562
|
+
<p>Posted: {{helpers.formatDate(post.createdAt)}}</p>
|
|
563
|
+
<p>{{helpers.truncate(post.body, 100)}}</p>
|
|
564
|
+
|
|
565
|
+
{{#if helpers.can('edit')}}
|
|
566
|
+
<button>Edit</button>
|
|
567
|
+
{{/if}}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### Built-in View Context
|
|
571
|
+
|
|
572
|
+
View methods have access to:
|
|
573
|
+
- `this.__requestObject` - Full request object
|
|
574
|
+
- `this.__response` - Response object
|
|
575
|
+
- `this.__request` - Request object
|
|
576
|
+
- `this.__namespace` - Controller namespace
|
|
577
|
+
- All methods from registered view extensions
|
|
578
|
+
|
|
579
|
+
**Example: Access request data in view helpers**
|
|
580
|
+
|
|
581
|
+
```javascript
|
|
582
|
+
class AuthHelpers {
|
|
583
|
+
currentUser() {
|
|
584
|
+
return this.__requestObject.session?.user;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
isAuthenticated() {
|
|
588
|
+
return !!this.currentUser();
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
csrf() {
|
|
592
|
+
// Generate CSRF token
|
|
593
|
+
return this.__requestObject.csrfToken;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
master.extendView('auth', AuthHelpers);
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
```html
|
|
601
|
+
<!-- In views -->
|
|
602
|
+
{{#if auth.isAuthenticated}}
|
|
603
|
+
<p>Welcome, {{auth.currentUser.name}}!</p>
|
|
604
|
+
{{else}}
|
|
605
|
+
<a href="/login">Login</a>
|
|
606
|
+
{{/if}}
|
|
607
|
+
|
|
608
|
+
<form method="post">
|
|
609
|
+
<input type="hidden" name="_csrf" value="{{auth.csrf}}">
|
|
610
|
+
<!-- form fields -->
|
|
611
|
+
</form>
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
## Dependency Injection
|
|
617
|
+
|
|
618
|
+
MasterController provides three DI lifetimes:
|
|
619
|
+
|
|
620
|
+
### `master.addSingleton(name, Class)`
|
|
621
|
+
One instance for the entire application lifetime.
|
|
622
|
+
|
|
623
|
+
```javascript
|
|
624
|
+
class DatabaseConnection {
|
|
625
|
+
constructor() {
|
|
626
|
+
this.connection = createDbConnection();
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
query(sql) {
|
|
630
|
+
return this.connection.query(sql);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
master.addSingleton('db', DatabaseConnection);
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
**Usage in controllers:**
|
|
638
|
+
```javascript
|
|
639
|
+
class UsersController {
|
|
640
|
+
index(obj) {
|
|
641
|
+
const users = this.db.query('SELECT * FROM users');
|
|
642
|
+
this.render('index', { users });
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### `master.addScoped(name, Class)`
|
|
648
|
+
One instance per request (scoped to request lifetime).
|
|
649
|
+
|
|
650
|
+
```javascript
|
|
651
|
+
class RequestLogger {
|
|
652
|
+
constructor() {
|
|
653
|
+
this.logs = [];
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
log(message) {
|
|
657
|
+
this.logs.push({ message, timestamp: Date.now() });
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
flush() {
|
|
661
|
+
console.log('Request logs:', this.logs);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
master.addScoped('logger', RequestLogger);
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
**Usage:**
|
|
669
|
+
```javascript
|
|
670
|
+
class UsersController {
|
|
671
|
+
index(obj) {
|
|
672
|
+
this.logger.log('Fetching users');
|
|
673
|
+
const users = getUsers();
|
|
674
|
+
this.logger.log('Users fetched');
|
|
675
|
+
this.logger.flush();
|
|
676
|
+
this.render('index', { users });
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
### `master.addTransient(name, Class)`
|
|
682
|
+
New instance every time it's accessed.
|
|
683
|
+
|
|
684
|
+
```javascript
|
|
685
|
+
class EmailService {
|
|
686
|
+
constructor() {
|
|
687
|
+
this.id = Math.random();
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
send(to, subject, body) {
|
|
691
|
+
console.log(`Sending email from instance ${this.id}`);
|
|
692
|
+
// Send email...
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
master.addTransient('email', EmailService);
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
**Usage:**
|
|
700
|
+
```javascript
|
|
701
|
+
class UsersController {
|
|
702
|
+
create(obj) {
|
|
703
|
+
// New instance each access
|
|
704
|
+
this.email.send(obj.params.formData.email, 'Welcome!', 'Thanks for joining');
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
### Accessing Services
|
|
710
|
+
|
|
711
|
+
Services are automatically available on `this` in controllers:
|
|
712
|
+
|
|
713
|
+
```javascript
|
|
714
|
+
class UsersController {
|
|
715
|
+
index(obj) {
|
|
716
|
+
// Access singleton
|
|
717
|
+
const users = this.db.query('SELECT * FROM users');
|
|
718
|
+
|
|
719
|
+
// Access scoped
|
|
720
|
+
this.logger.log('Query executed');
|
|
721
|
+
|
|
722
|
+
// Access transient
|
|
723
|
+
this.email.send(user.email, 'Subject', 'Body');
|
|
724
|
+
|
|
725
|
+
this.render('index', { users });
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
---
|
|
731
|
+
|
|
732
|
+
## CORS
|
|
21
733
|
|
|
22
|
-
|
|
23
|
-
|
|
734
|
+
### `master.cors.init(options)`
|
|
735
|
+
|
|
736
|
+
Initialize CORS (auto-registers with middleware pipeline).
|
|
737
|
+
|
|
738
|
+
```javascript
|
|
739
|
+
master.cors.init({
|
|
740
|
+
origin: true, // Reflect request origin, or '*', or ['https://example.com']
|
|
741
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
742
|
+
allowedHeaders: true, // Reflect requested headers, or specify array
|
|
743
|
+
exposeHeaders: ['X-Total-Count'],
|
|
744
|
+
credentials: true,
|
|
745
|
+
maxAge: 86400
|
|
746
|
+
});
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
**Options:**
|
|
750
|
+
|
|
751
|
+
- `origin`:
|
|
752
|
+
- `true` - Reflect request origin (or `*` if no credentials)
|
|
753
|
+
- `false` - Remove CORS headers
|
|
754
|
+
- `'*'` - Allow all origins
|
|
755
|
+
- `'https://example.com'` - Specific origin
|
|
756
|
+
- `['https://example.com', 'https://app.com']` - Array of origins
|
|
757
|
+
- `function(origin, req)` - Custom function returning `true`, `false`, or origin string
|
|
758
|
+
|
|
759
|
+
- `methods`: Array of allowed HTTP methods
|
|
760
|
+
- `allowedHeaders`: `true` (all), `false` (none), array, or string
|
|
761
|
+
- `exposeHeaders`: Array of headers to expose to browser
|
|
762
|
+
- `credentials`: `true` to allow credentials (cookies, auth headers)
|
|
763
|
+
- `maxAge`: Preflight cache duration in seconds
|
|
764
|
+
|
|
765
|
+
**CORS automatically:**
|
|
766
|
+
- Handles preflight OPTIONS requests
|
|
767
|
+
- Sets appropriate headers
|
|
768
|
+
- Varies by Origin for security
|
|
769
|
+
|
|
770
|
+
### Advanced CORS
|
|
771
|
+
|
|
772
|
+
```javascript
|
|
773
|
+
// Function-based origin validation
|
|
774
|
+
master.cors.init({
|
|
775
|
+
origin: (origin, req) => {
|
|
776
|
+
// Custom validation logic
|
|
777
|
+
if (req.headers['x-api-key'] === 'secret') {
|
|
778
|
+
return true; // Reflect origin
|
|
779
|
+
}
|
|
780
|
+
if (origin === 'https://trusted.com') {
|
|
781
|
+
return origin;
|
|
782
|
+
}
|
|
783
|
+
return false; // Deny
|
|
784
|
+
},
|
|
785
|
+
credentials: true
|
|
786
|
+
});
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
---
|
|
790
|
+
|
|
791
|
+
## Sessions
|
|
792
|
+
|
|
793
|
+
### `master.sessions.init(options)`
|
|
794
|
+
|
|
795
|
+
Initialize sessions (auto-registers with middleware pipeline).
|
|
796
|
+
|
|
797
|
+
```javascript
|
|
798
|
+
master.sessions.init({
|
|
799
|
+
secret: 'your-secret-key',
|
|
800
|
+
maxAge: 900000, // 15 minutes
|
|
801
|
+
httpOnly: true,
|
|
802
|
+
secure: true, // HTTPS only
|
|
803
|
+
sameSite: true,
|
|
804
|
+
path: '/'
|
|
805
|
+
});
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
### Session API
|
|
809
|
+
|
|
810
|
+
#### `master.sessions.set(name, data, response, secret, options)`
|
|
811
|
+
Create a session.
|
|
812
|
+
|
|
813
|
+
```javascript
|
|
814
|
+
class AuthController {
|
|
815
|
+
login(obj) {
|
|
816
|
+
const user = authenticateUser(obj.params.formData);
|
|
817
|
+
|
|
818
|
+
master.sessions.set('user', user, obj.response);
|
|
819
|
+
|
|
820
|
+
this.redirect('/dashboard');
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
#### `master.sessions.get(name, request, secret)`
|
|
826
|
+
Retrieve session data.
|
|
827
|
+
|
|
828
|
+
```javascript
|
|
829
|
+
class DashboardController {
|
|
830
|
+
index(obj) {
|
|
831
|
+
const user = master.sessions.get('user', obj.request);
|
|
832
|
+
|
|
833
|
+
if (!user) {
|
|
834
|
+
this.redirect('/login');
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
this.render('dashboard', { user });
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
#### `master.sessions.delete(name, response)`
|
|
844
|
+
Delete a session.
|
|
845
|
+
|
|
846
|
+
```javascript
|
|
847
|
+
class AuthController {
|
|
848
|
+
logout(obj) {
|
|
849
|
+
master.sessions.delete('user', obj.response);
|
|
850
|
+
this.redirect('/');
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
#### `master.sessions.reset()`
|
|
856
|
+
Clear all sessions (useful for testing).
|
|
857
|
+
|
|
858
|
+
```javascript
|
|
859
|
+
master.sessions.reset();
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
### Cookie Methods
|
|
863
|
+
|
|
864
|
+
Direct cookie access:
|
|
865
|
+
|
|
866
|
+
#### `master.sessions.setCookie(name, value, response, options)`
|
|
867
|
+
```javascript
|
|
868
|
+
master.sessions.setCookie('theme', 'dark', obj.response);
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
#### `master.sessions.getCookie(name, request, secret)`
|
|
872
|
+
```javascript
|
|
873
|
+
const theme = master.sessions.getCookie('theme', obj.request);
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
#### `master.sessions.deleteCookie(name, response, options)`
|
|
877
|
+
```javascript
|
|
878
|
+
master.sessions.deleteCookie('theme', obj.response);
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
---
|
|
882
|
+
|
|
883
|
+
## Security
|
|
884
|
+
|
|
885
|
+
MasterController includes built-in security middleware.
|
|
886
|
+
|
|
887
|
+
### Security Headers
|
|
888
|
+
|
|
889
|
+
```javascript
|
|
890
|
+
const { pipelineSecurityHeaders } = require('./security/SecurityMiddleware');
|
|
891
|
+
|
|
892
|
+
master.pipeline.use(pipelineSecurityHeaders());
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
**Applied headers:**
|
|
896
|
+
- `X-XSS-Protection: 1; mode=block`
|
|
897
|
+
- `X-Frame-Options: SAMEORIGIN`
|
|
898
|
+
- `X-Content-Type-Options: nosniff`
|
|
899
|
+
- `X-DNS-Prefetch-Control: off`
|
|
900
|
+
- `Permissions-Policy: geolocation=(), microphone=(), camera=()`
|
|
901
|
+
- `Referrer-Policy: strict-origin-when-cross-origin`
|
|
902
|
+
- `Strict-Transport-Security` (HTTPS production only)
|
|
903
|
+
|
|
904
|
+
### Rate Limiting
|
|
905
|
+
|
|
906
|
+
```javascript
|
|
907
|
+
const { pipelineRateLimit } = require('./security/SecurityMiddleware');
|
|
908
|
+
|
|
909
|
+
master.pipeline.use(pipelineRateLimit({
|
|
910
|
+
rateLimitWindow: 60000, // 1 minute
|
|
911
|
+
rateLimitMax: 100 // 100 requests per window
|
|
912
|
+
}));
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
**Rate limit headers:**
|
|
916
|
+
- `X-RateLimit-Limit` - Maximum requests allowed
|
|
917
|
+
- `X-RateLimit-Remaining` - Requests remaining in window
|
|
918
|
+
- `X-RateLimit-Reset` - When the limit resets
|
|
919
|
+
- `Retry-After` - Seconds until retry (when blocked)
|
|
920
|
+
|
|
921
|
+
### CSRF Protection
|
|
922
|
+
|
|
923
|
+
```javascript
|
|
924
|
+
const { pipelineCsrf, generateCSRFToken } = require('./security/SecurityMiddleware');
|
|
925
|
+
|
|
926
|
+
// Apply to all routes
|
|
927
|
+
master.pipeline.use(pipelineCsrf());
|
|
928
|
+
|
|
929
|
+
// Or only to specific routes
|
|
930
|
+
master.pipeline.map('/admin/*', (admin) => {
|
|
931
|
+
admin.use(pipelineCsrf());
|
|
932
|
+
});
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
**Generate token:**
|
|
936
|
+
```javascript
|
|
937
|
+
const token = generateCSRFToken(sessionId);
|
|
938
|
+
|
|
939
|
+
// In controller
|
|
940
|
+
class FormController {
|
|
941
|
+
show(obj) {
|
|
942
|
+
const csrfToken = generateCSRFToken();
|
|
943
|
+
this.render('form', { csrfToken });
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
**In forms:**
|
|
949
|
+
```html
|
|
950
|
+
<form method="post">
|
|
951
|
+
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
|
952
|
+
<!-- or -->
|
|
953
|
+
<!-- Send as header: x-csrf-token -->
|
|
954
|
+
<!-- or -->
|
|
955
|
+
<!-- Send as query: ?_csrf=token -->
|
|
956
|
+
</form>
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
### Input Validation
|
|
960
|
+
|
|
961
|
+
```javascript
|
|
962
|
+
const { validator } = require('./security/MasterValidator');
|
|
963
|
+
|
|
964
|
+
class UsersController {
|
|
965
|
+
create(obj) {
|
|
966
|
+
const email = obj.params.formData.email;
|
|
967
|
+
|
|
968
|
+
// Validate email
|
|
969
|
+
const emailCheck = validator.isEmail(email);
|
|
970
|
+
if (!emailCheck.valid) {
|
|
971
|
+
this.json({ error: emailCheck.error });
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Continue with valid data
|
|
976
|
+
// ...
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
**Available validators:**
|
|
982
|
+
- `validator.isEmail(email)`
|
|
983
|
+
- `validator.isURL(url)`
|
|
984
|
+
- `validator.isAlphanumeric(str)`
|
|
985
|
+
- `validator.isLength(str, min, max)`
|
|
986
|
+
- `detectPathTraversal(path)` - Detect `../` attacks
|
|
987
|
+
- `detectSQLInjection(input)` - Detect SQL injection
|
|
988
|
+
- `detectCommandInjection(input)` - Detect command injection
|
|
989
|
+
|
|
990
|
+
---
|
|
991
|
+
|
|
992
|
+
## Components
|
|
993
|
+
|
|
994
|
+
Components are self-contained modules with their own routes, controllers, and views.
|
|
995
|
+
|
|
996
|
+
### Structure
|
|
24
997
|
|
|
25
|
-
|
|
26
|
-
|
|
998
|
+
```
|
|
999
|
+
components/
|
|
1000
|
+
user/
|
|
1001
|
+
config/
|
|
1002
|
+
initializers/
|
|
1003
|
+
config.js
|
|
1004
|
+
routes.js
|
|
1005
|
+
app/
|
|
1006
|
+
controllers/
|
|
1007
|
+
authController.js
|
|
1008
|
+
views/
|
|
1009
|
+
auth/
|
|
1010
|
+
login.html
|
|
1011
|
+
models/
|
|
1012
|
+
userContext.js
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
### Register Component
|
|
1016
|
+
|
|
1017
|
+
```javascript
|
|
1018
|
+
// In config/initializers/config.js
|
|
1019
|
+
master.component('components', 'user');
|
|
1020
|
+
master.component('components', 'mail');
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
### Absolute Path Components
|
|
1024
|
+
|
|
1025
|
+
```javascript
|
|
1026
|
+
// Load component from absolute path
|
|
1027
|
+
master.component('/var/www/shared-components', 'analytics');
|
|
27
1028
|
```
|
|
28
1029
|
|
|
29
|
-
|
|
30
|
-
Create `app/config/routes.js` and define routes with `master.router.start()` API.
|
|
1030
|
+
Components are isolated and can be reused across projects.
|
|
31
1031
|
|
|
32
|
-
|
|
33
|
-
|
|
1032
|
+
---
|
|
1033
|
+
|
|
1034
|
+
## Timeout System
|
|
1035
|
+
|
|
1036
|
+
MasterController v2.0 includes a professional timeout system with per-request tracking (Rails/Django style).
|
|
1037
|
+
|
|
1038
|
+
### Configuration
|
|
1039
|
+
|
|
1040
|
+
```javascript
|
|
1041
|
+
// config/initializers/config.js
|
|
1042
|
+
master.timeout.init({
|
|
1043
|
+
globalTimeout: 120000, // 120 seconds (2 minutes) default
|
|
1044
|
+
enabled: true,
|
|
1045
|
+
onTimeout: (ctx, timeoutInfo) => {
|
|
1046
|
+
// Optional custom timeout handler
|
|
1047
|
+
console.log(`Request timeout: ${timeoutInfo.path}`);
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
// Register timeout middleware
|
|
1052
|
+
master.pipeline.use(master.timeout.middleware());
|
|
1053
|
+
```
|
|
34
1054
|
|
|
35
|
-
###
|
|
36
|
-
|
|
1055
|
+
### Route-Specific Timeouts
|
|
1056
|
+
|
|
1057
|
+
```javascript
|
|
1058
|
+
// Short timeout for API endpoints
|
|
1059
|
+
master.timeout.setRouteTimeout('/api/*', 30000); // 30 seconds
|
|
1060
|
+
|
|
1061
|
+
// Long timeout for reports
|
|
1062
|
+
master.timeout.setRouteTimeout('/admin/reports', 300000); // 5 minutes
|
|
1063
|
+
|
|
1064
|
+
// Very long timeout for batch operations
|
|
1065
|
+
master.timeout.setRouteTimeout('/batch/process', 600000); // 10 minutes
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
### Timeout Statistics
|
|
1069
|
+
|
|
1070
|
+
```javascript
|
|
1071
|
+
const stats = master.timeout.getStats();
|
|
1072
|
+
|
|
1073
|
+
console.log(stats);
|
|
1074
|
+
// {
|
|
1075
|
+
// enabled: true,
|
|
1076
|
+
// globalTimeout: 120000,
|
|
1077
|
+
// routeTimeouts: [
|
|
1078
|
+
// { pattern: '/api/*', timeout: 30000 }
|
|
1079
|
+
// ],
|
|
1080
|
+
// activeRequests: 5,
|
|
1081
|
+
// requests: [
|
|
1082
|
+
// {
|
|
1083
|
+
// requestId: 'req_1234567890_abc123',
|
|
1084
|
+
// path: 'api/users',
|
|
1085
|
+
// method: 'get',
|
|
1086
|
+
// timeout: 30000,
|
|
1087
|
+
// elapsed: 15000,
|
|
1088
|
+
// remaining: 15000
|
|
1089
|
+
// }
|
|
1090
|
+
// ]
|
|
1091
|
+
// }
|
|
1092
|
+
```
|
|
1093
|
+
|
|
1094
|
+
### Disable/Enable Timeouts
|
|
1095
|
+
|
|
1096
|
+
```javascript
|
|
1097
|
+
// Disable for debugging
|
|
1098
|
+
master.timeout.disable();
|
|
1099
|
+
|
|
1100
|
+
// Re-enable
|
|
1101
|
+
master.timeout.enable();
|
|
1102
|
+
```
|
|
37
1103
|
|
|
38
|
-
|
|
39
|
-
`MasterCors` configures CORS headers. Preflight `OPTIONS` requests are short-circuited with 204.
|
|
1104
|
+
---
|
|
40
1105
|
|
|
41
|
-
|
|
42
|
-
Use `setupServer('https', credentials)` or configure via environment TLS; see docs in `docs/` for multiple setups.
|
|
1106
|
+
## Error Handling
|
|
43
1107
|
|
|
44
|
-
|
|
45
|
-
- `docs/server-setup-http.md`
|
|
46
|
-
- `docs/server-setup-https-credentials.md`
|
|
47
|
-
- `docs/server-setup-https-env-tls-sni.md`
|
|
48
|
-
- `docs/server-setup-hostname-binding.md`
|
|
49
|
-
- `docs/server-setup-nginx-reverse-proxy.md`
|
|
50
|
-
- `docs/environment-tls-reference.md`
|
|
1108
|
+
MasterController v2.0 includes a professional error template system inspired by Rails and Django.
|
|
51
1109
|
|
|
52
|
-
###
|
|
53
|
-
|
|
54
|
-
|
|
1110
|
+
### Error Renderer Configuration
|
|
1111
|
+
|
|
1112
|
+
```javascript
|
|
1113
|
+
// config/initializers/config.js
|
|
1114
|
+
master.errorRenderer.init({
|
|
1115
|
+
templateDir: 'public/errors', // Error templates directory
|
|
1116
|
+
environment: master.environmentType,
|
|
1117
|
+
showStackTrace: master.environmentType === 'development' // Dev only
|
|
1118
|
+
});
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
### Using Error Renderer
|
|
1122
|
+
|
|
1123
|
+
```javascript
|
|
1124
|
+
// In middleware
|
|
1125
|
+
master.pipeline.use(async (ctx, next) => {
|
|
1126
|
+
if (!isAuthenticated(ctx)) {
|
|
1127
|
+
master.errorRenderer.send(ctx, 401, {
|
|
1128
|
+
message: 'Please log in to access this resource',
|
|
1129
|
+
suggestions: [
|
|
1130
|
+
'Sign in with your credentials',
|
|
1131
|
+
'Request a password reset if forgotten'
|
|
1132
|
+
]
|
|
1133
|
+
});
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
await next();
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
// In controllers
|
|
1140
|
+
class UsersController {
|
|
1141
|
+
async show(obj) {
|
|
1142
|
+
const userId = obj.params.userId;
|
|
1143
|
+
const user = await this.db.query('SELECT * FROM users WHERE id = ?', [userId]);
|
|
1144
|
+
|
|
1145
|
+
if (!user) {
|
|
1146
|
+
master.errorRenderer.send(obj, 404, {
|
|
1147
|
+
message: `User #${userId} not found`,
|
|
1148
|
+
suggestions: [
|
|
1149
|
+
'Check the user ID',
|
|
1150
|
+
'Browse all users',
|
|
1151
|
+
'Search for the user by name'
|
|
1152
|
+
]
|
|
1153
|
+
});
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
this.render('show', { user });
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
### Error Templates
|
|
1163
|
+
|
|
1164
|
+
Create templates in `public/errors/`:
|
|
1165
|
+
|
|
1166
|
+
```
|
|
1167
|
+
public/errors/
|
|
1168
|
+
├── 400.html # Bad Request
|
|
1169
|
+
├── 401.html # Unauthorized
|
|
1170
|
+
├── 403.html # Forbidden
|
|
1171
|
+
├── 404.html # Not Found
|
|
1172
|
+
├── 405.html # Method Not Allowed
|
|
1173
|
+
├── 422.html # Unprocessable Entity
|
|
1174
|
+
├── 429.html # Too Many Requests
|
|
1175
|
+
├── 500.html # Internal Server Error
|
|
1176
|
+
├── 502.html # Bad Gateway
|
|
1177
|
+
├── 503.html # Service Unavailable
|
|
1178
|
+
└── 504.html # Gateway Timeout
|
|
1179
|
+
```
|
|
1180
|
+
|
|
1181
|
+
**Template Variables:**
|
|
1182
|
+
|
|
1183
|
+
```html
|
|
1184
|
+
<!DOCTYPE html>
|
|
1185
|
+
<html>
|
|
1186
|
+
<head>
|
|
1187
|
+
<title>{{title}} ({{statusCode}})</title>
|
|
1188
|
+
</head>
|
|
1189
|
+
<body>
|
|
1190
|
+
<h1>{{statusCode}} - {{title}}</h1>
|
|
1191
|
+
<p>{{message}}</p>
|
|
1192
|
+
|
|
1193
|
+
<!-- Conditionals (dev only) -->
|
|
1194
|
+
{{#if showStackTrace}}
|
|
1195
|
+
<pre>{{stack}}</pre>
|
|
1196
|
+
{{/if}}
|
|
1197
|
+
|
|
1198
|
+
<!-- Loops -->
|
|
1199
|
+
{{#each suggestions}}
|
|
1200
|
+
<li>{{this}}</li>
|
|
1201
|
+
{{/each}}
|
|
1202
|
+
</body>
|
|
1203
|
+
</html>
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1206
|
+
**Available Variables:**
|
|
1207
|
+
- `{{statusCode}}` - HTTP status code (404, 500, etc.)
|
|
1208
|
+
- `{{title}}` - Error title
|
|
1209
|
+
- `{{message}}` - Error message
|
|
1210
|
+
- `{{code}}` - Error code
|
|
1211
|
+
- `{{stack}}` - Stack trace (development only)
|
|
1212
|
+
- `{{suggestions}}` - Array of suggestions
|
|
1213
|
+
- `{{environment}}` - Current environment
|
|
1214
|
+
|
|
1215
|
+
### Custom Error Handlers
|
|
1216
|
+
|
|
1217
|
+
```javascript
|
|
1218
|
+
// Register custom handler for specific status code
|
|
1219
|
+
master.errorRenderer.registerHandler(503, (ctx, errorData) => {
|
|
1220
|
+
return `
|
|
1221
|
+
<!DOCTYPE html>
|
|
1222
|
+
<html>
|
|
1223
|
+
<body>
|
|
1224
|
+
<h1>Maintenance Mode</h1>
|
|
1225
|
+
<p>We'll be back soon! Expected completion: 2:00 PM EST</p>
|
|
1226
|
+
</body>
|
|
1227
|
+
</html>
|
|
1228
|
+
`;
|
|
1229
|
+
});
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
### Content Negotiation
|
|
1233
|
+
|
|
1234
|
+
The error renderer automatically detects API requests and returns JSON:
|
|
1235
|
+
|
|
1236
|
+
```javascript
|
|
1237
|
+
// Browser request → HTML
|
|
1238
|
+
GET /users/999
|
|
1239
|
+
Accept: text/html
|
|
1240
|
+
→ Returns beautiful HTML error page
|
|
1241
|
+
|
|
1242
|
+
// API request → JSON
|
|
1243
|
+
GET /api/users/999
|
|
1244
|
+
Accept: application/json
|
|
1245
|
+
→ Returns JSON error response
|
|
1246
|
+
{
|
|
1247
|
+
"error": "Page Not Found",
|
|
1248
|
+
"statusCode": 404,
|
|
1249
|
+
"code": "MC_HTTP_ERROR",
|
|
1250
|
+
"message": "The user you're looking for doesn't exist."
|
|
1251
|
+
}
|
|
1252
|
+
```
|
|
1253
|
+
|
|
1254
|
+
### Global Error Handler (Pipeline)
|
|
1255
|
+
|
|
1256
|
+
```javascript
|
|
1257
|
+
master.pipeline.useError(async (error, ctx, next) => {
|
|
1258
|
+
console.error('Pipeline error:', error);
|
|
1259
|
+
|
|
1260
|
+
// Use error renderer for HTTP errors
|
|
1261
|
+
master.errorRenderer.send(ctx, 500, {
|
|
1262
|
+
message: error.message,
|
|
1263
|
+
code: error.code,
|
|
1264
|
+
stack: error.stack
|
|
1265
|
+
});
|
|
1266
|
+
});
|
|
1267
|
+
```
|
|
1268
|
+
|
|
1269
|
+
### Controller Error Handling
|
|
1270
|
+
|
|
1271
|
+
```javascript
|
|
1272
|
+
class UsersController {
|
|
1273
|
+
async index(obj) {
|
|
1274
|
+
try {
|
|
1275
|
+
const users = await this.db.query('SELECT * FROM users');
|
|
1276
|
+
this.render('index', { users });
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
console.error('Database error:', error);
|
|
1279
|
+
|
|
1280
|
+
master.errorRenderer.send(obj, 500, {
|
|
1281
|
+
message: 'Failed to load users',
|
|
1282
|
+
code: 'DB_ERROR',
|
|
1283
|
+
stack: error.stack
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
```
|
|
1289
|
+
|
|
1290
|
+
### Logging
|
|
1291
|
+
|
|
1292
|
+
```javascript
|
|
1293
|
+
const { logger } = require('./error/MasterErrorLogger');
|
|
1294
|
+
|
|
1295
|
+
// In controllers or middleware
|
|
1296
|
+
logger.info({
|
|
1297
|
+
code: 'USER_LOGIN',
|
|
1298
|
+
message: 'User logged in',
|
|
1299
|
+
userId: user.id
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
logger.warn({
|
|
1303
|
+
code: 'INVALID_INPUT',
|
|
1304
|
+
message: 'Invalid email format',
|
|
1305
|
+
email: input
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
logger.error({
|
|
1309
|
+
code: 'DB_ERROR',
|
|
1310
|
+
message: 'Database query failed',
|
|
1311
|
+
error: error.message,
|
|
1312
|
+
stack: error.stack
|
|
1313
|
+
});
|
|
1314
|
+
```
|
|
1315
|
+
|
|
1316
|
+
---
|
|
1317
|
+
|
|
1318
|
+
## HTTPS Setup
|
|
1319
|
+
|
|
1320
|
+
### Basic HTTPS
|
|
1321
|
+
|
|
1322
|
+
```javascript
|
|
1323
|
+
const fs = require('fs');
|
|
1324
|
+
|
|
1325
|
+
const credentials = {
|
|
1326
|
+
key: fs.readFileSync('path/to/key.pem'),
|
|
1327
|
+
cert: fs.readFileSync('path/to/cert.pem')
|
|
1328
|
+
};
|
|
1329
|
+
|
|
1330
|
+
const server = master.setupServer('https', credentials);
|
|
1331
|
+
```
|
|
1332
|
+
|
|
1333
|
+
### Environment-based TLS
|
|
1334
|
+
|
|
1335
|
+
Configure TLS in `config/environments/env.production.json`:
|
|
1336
|
+
|
|
1337
|
+
```json
|
|
1338
|
+
{
|
|
1339
|
+
"server": {
|
|
1340
|
+
"httpPort": 443,
|
|
1341
|
+
"tls": {
|
|
1342
|
+
"default": {
|
|
1343
|
+
"keyPath": "/path/to/default.key",
|
|
1344
|
+
"certPath": "/path/to/default.crt"
|
|
1345
|
+
},
|
|
1346
|
+
"sni": {
|
|
1347
|
+
"example.com": {
|
|
1348
|
+
"keyPath": "/path/to/example.key",
|
|
1349
|
+
"certPath": "/path/to/example.crt"
|
|
1350
|
+
},
|
|
1351
|
+
"app.example.com": {
|
|
1352
|
+
"keyPath": "/path/to/app.key",
|
|
1353
|
+
"certPath": "/path/to/app.crt"
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
```
|
|
1360
|
+
|
|
1361
|
+
```javascript
|
|
1362
|
+
const server = master.setupServer('https');
|
|
1363
|
+
master.serverSettings(master.env.server);
|
|
1364
|
+
```
|
|
1365
|
+
|
|
1366
|
+
### HTTP to HTTPS Redirect
|
|
1367
|
+
|
|
1368
|
+
```javascript
|
|
1369
|
+
// Start HTTPS server on 443
|
|
1370
|
+
const httpsServer = master.setupServer('https');
|
|
1371
|
+
master.start(httpsServer);
|
|
1372
|
+
master.serverSettings({ httpPort: 443 });
|
|
1373
|
+
|
|
1374
|
+
// Start redirect server on 80
|
|
1375
|
+
const redirectServer = master.startHttpToHttpsRedirect(80);
|
|
1376
|
+
```
|
|
1377
|
+
|
|
1378
|
+
### HSTS (HTTP Strict Transport Security)
|
|
1379
|
+
|
|
1380
|
+
```javascript
|
|
1381
|
+
master.enableHSTS(); // In production HTTPS
|
|
1382
|
+
```
|
|
1383
|
+
|
|
1384
|
+
---
|
|
1385
|
+
|
|
1386
|
+
## API Reference
|
|
1387
|
+
|
|
1388
|
+
### Master Instance
|
|
1389
|
+
|
|
1390
|
+
- `master.root` - Project root directory
|
|
1391
|
+
- `master.environmentType` - Environment ('development', 'production', etc.)
|
|
1392
|
+
- `master.env` - Environment config from `config/environments/env.<env>.json`
|
|
1393
|
+
- `master.serverProtocol` - 'http' or 'https'
|
|
1394
|
+
|
|
1395
|
+
### Setup Methods
|
|
1396
|
+
|
|
1397
|
+
- `master.setupServer(type, credentials)` - Create HTTP or HTTPS server
|
|
1398
|
+
- `master.start(server)` - Start the server
|
|
1399
|
+
- `master.serverSettings(options)` - Configure server (port, host, timeout)
|
|
1400
|
+
- `master.startMVC(folder)` - Load routes from folder
|
|
1401
|
+
- `master.component(folder, name)` - Load component
|
|
1402
|
+
- `master.enableHSTS()` - Enable HSTS for HTTPS
|
|
1403
|
+
- `master.startHttpToHttpsRedirect(port, host)` - Create redirect server
|
|
1404
|
+
|
|
1405
|
+
### Middleware Pipeline
|
|
1406
|
+
|
|
1407
|
+
- `master.pipeline.use(middleware)` - Add middleware
|
|
1408
|
+
- `master.pipeline.run(middleware)` - Add terminal middleware
|
|
1409
|
+
- `master.pipeline.map(path, configure)` - Conditional middleware
|
|
1410
|
+
- `master.pipeline.useError(handler)` - Add error handler
|
|
1411
|
+
- `master.pipeline.discoverMiddleware(options)` - Auto-discover middleware
|
|
1412
|
+
- `master.pipeline.execute(context)` - Execute pipeline (internal)
|
|
1413
|
+
- `master.pipeline.clear()` - Clear all middleware (testing)
|
|
1414
|
+
- `master.pipeline.inspect()` - Inspect middleware stack (debugging)
|
|
1415
|
+
|
|
1416
|
+
### Dependency Injection
|
|
1417
|
+
|
|
1418
|
+
- `master.addSingleton(name, Class)` - Register singleton service
|
|
1419
|
+
- `master.addScoped(name, Class)` - Register scoped service (per request)
|
|
1420
|
+
- `master.addTransient(name, Class)` - Register transient service (per access)
|
|
1421
|
+
|
|
1422
|
+
### Extensions
|
|
1423
|
+
|
|
1424
|
+
- `master.extend(name, Class)` - Extend master with new functionality
|
|
1425
|
+
- `master.extendController(Class)` - Extend all controllers
|
|
1426
|
+
- `master.extendView(name, Class)` - Extend all views
|
|
1427
|
+
|
|
1428
|
+
### Router
|
|
1429
|
+
|
|
1430
|
+
- `master.router.start()` - Get router API
|
|
1431
|
+
- `router.route(path, toPath, method, constraint)` - Register route
|
|
1432
|
+
- `router.resources(name)` - Register RESTful routes
|
|
1433
|
+
- `master.router.setup(options)` - Setup route namespace (internal)
|
|
1434
|
+
- `master.router.load(requestObject)` - Load and match routes (internal)
|
|
1435
|
+
- `master.router.currentRoute` - Current route info
|
|
1436
|
+
- `master.router.findMimeType(ext)` - Get MIME type for extension
|
|
1437
|
+
- `master.router.addMimeList(mimes)` - Add MIME type mappings
|
|
1438
|
+
|
|
1439
|
+
### CORS
|
|
1440
|
+
|
|
1441
|
+
- `master.cors.init(options)` - Initialize CORS
|
|
1442
|
+
- `master.cors.load(params)` - Apply CORS headers (internal)
|
|
1443
|
+
- `master.cors.middleware()` - Get pipeline middleware
|
|
1444
|
+
|
|
1445
|
+
### Sessions
|
|
1446
|
+
|
|
1447
|
+
- `master.sessions.init(options)` - Initialize sessions
|
|
1448
|
+
- `master.sessions.set(name, data, response, secret, options)` - Create session
|
|
1449
|
+
- `master.sessions.get(name, request, secret)` - Get session data
|
|
1450
|
+
- `master.sessions.delete(name, response)` - Delete session
|
|
1451
|
+
- `master.sessions.reset()` - Clear all sessions
|
|
1452
|
+
- `master.sessions.setCookie(name, value, response, options)` - Set cookie
|
|
1453
|
+
- `master.sessions.getCookie(name, request, secret)` - Get cookie
|
|
1454
|
+
- `master.sessions.deleteCookie(name, response, options)` - Delete cookie
|
|
1455
|
+
- `master.sessions.createSessionID()` - Generate random session ID
|
|
1456
|
+
- `master.sessions.middleware()` - Get pipeline middleware
|
|
1457
|
+
|
|
1458
|
+
### Request
|
|
1459
|
+
|
|
1460
|
+
- `master.request.getRequestParam(request, response)` - Parse request body (internal)
|
|
1461
|
+
|
|
1462
|
+
### HTML/Template
|
|
1463
|
+
|
|
1464
|
+
- `master.html.init(path)` - Set views path
|
|
1465
|
+
- `master.template.init(layout)` - Set layout template
|
|
1466
|
+
|
|
1467
|
+
### Tools
|
|
1468
|
+
|
|
1469
|
+
- `master.tools.encrypt(data, secret)` - Encrypt data
|
|
1470
|
+
- `master.tools.decrypt(data, secret)` - Decrypt data
|
|
1471
|
+
- `master.tools.combineObjects(target, source)` - Merge objects
|
|
1472
|
+
- `master.tools.makeWordId(length)` - Generate random ID
|
|
1473
|
+
|
|
1474
|
+
---
|
|
1475
|
+
|
|
1476
|
+
## Production Tips
|
|
1477
|
+
|
|
1478
|
+
1. **Use a reverse proxy** (nginx, Apache) for TLS termination
|
|
1479
|
+
2. **Run Node.js on a high port** (3000, 8080) behind the proxy
|
|
1480
|
+
3. **Enable HSTS** for HTTPS: `master.enableHSTS()`
|
|
1481
|
+
4. **Use environment variables** for secrets and config
|
|
1482
|
+
5. **Enable rate limiting** for public APIs
|
|
1483
|
+
6. **Enable CSRF protection** for forms
|
|
1484
|
+
7. **Use security headers** middleware
|
|
1485
|
+
8. **Monitor logs** with `logger` module
|
|
1486
|
+
9. **Use process manager** (PM2, systemd) for restarts
|
|
1487
|
+
10. **Keep dependencies updated**
|
|
1488
|
+
|
|
1489
|
+
---
|
|
1490
|
+
|
|
1491
|
+
## Documentation
|
|
1492
|
+
|
|
1493
|
+
Detailed guides:
|
|
1494
|
+
|
|
1495
|
+
- [HTTP Server Setup](docs/server-setup-http.md)
|
|
1496
|
+
- [HTTPS with Credentials](docs/server-setup-https-credentials.md)
|
|
1497
|
+
- [HTTPS with Environment TLS & SNI](docs/server-setup-https-env-tls-sni.md)
|
|
1498
|
+
- [Hostname Binding](docs/server-setup-hostname-binding.md)
|
|
1499
|
+
- [Nginx Reverse Proxy](docs/server-setup-nginx-reverse-proxy.md)
|
|
1500
|
+
- [Environment TLS Reference](docs/environment-tls-reference.md)
|
|
1501
|
+
|
|
1502
|
+
---
|
|
1503
|
+
|
|
1504
|
+
## License
|
|
55
1505
|
|
|
56
|
-
### License
|
|
57
1506
|
MIT
|
|
58
1507
|
|
|
1508
|
+
---
|
|
1509
|
+
|
|
1510
|
+
## Contributing
|
|
1511
|
+
|
|
1512
|
+
Contributions are welcome! Please open an issue or submit a pull request.
|
|
1513
|
+
|
|
1514
|
+
---
|
|
1515
|
+
|
|
1516
|
+
## Support
|
|
1517
|
+
|
|
1518
|
+
For issues and questions, please visit: [GitHub Issues](https://github.com/alexanderrich/MasterController/issues)
|