aegisnode 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1613 -1471
- package/package.json +1 -1
- package/scripts/smoke-test.js +186 -2
- package/src/cli/commands/createapp.js +11 -174
- package/src/cli/commands/doctor.js +98 -19
- package/src/cli/commands/fixapp.js +65 -0
- package/src/cli/commands/generateloader.js +37 -0
- package/src/cli/commands/startproject.js +1 -1
- package/src/cli/index.js +32 -1
- package/src/cli/utils/apps.js +253 -0
- package/src/cli/utils/scaffolds.js +17 -3
- package/src/runtime/kernel.js +10 -0
- package/src/runtime/upload.js +48 -0
package/README.md
CHANGED
|
@@ -43,6 +43,8 @@ It keeps the development model simple, but adds enough structure and tooling to
|
|
|
43
43
|
Core features:
|
|
44
44
|
|
|
45
45
|
- CLI generators (`startproject`, `createapp`, `runserver`)
|
|
46
|
+
- App scaffold repair command (`fix`)
|
|
47
|
+
- Startup entry generator (`generateloader`)
|
|
46
48
|
- Project health checker (`doctor`)
|
|
47
49
|
- Dependency updater (`updatedeps`)
|
|
48
50
|
- Maintenance mode with custom HTML responses
|
|
@@ -60,7 +62,7 @@ Core features:
|
|
|
60
62
|
- Root route file `routes.js` (not `routes/` folder)
|
|
61
63
|
- Automatic default confirmation page on `/` when no custom `/` route exists
|
|
62
64
|
- App folder uses `views.js` (not `controllers/` folder)
|
|
63
|
-
- `createapp` uses file modules: `views.js`, `models.js`, `validators.js`, `routes.js`, `subscribers.js`, `services.js`
|
|
65
|
+
- `createapp` uses file modules: `views.js`, `models.js`, `validators.js`, `routes.js`, `subscribers.js`, `services.js`, `utils.js`
|
|
64
66
|
- `createapp` also generates app tests in `apps/<app>/tests`
|
|
65
67
|
- EJS templates configurable in `settings.js` with Django-style base layout flow
|
|
66
68
|
- Built-in runtime helpers (`money`, `number`, `dateTime`, `timeElapsed`, `toObjectId`) + `jlive` bridge
|
|
@@ -89,15 +91,18 @@ npm --prefix blog install
|
|
|
89
91
|
aegisnode runserver --project blog
|
|
90
92
|
|
|
91
93
|
aegisnode createapp users --project blog
|
|
94
|
+
aegisnode fix --app users --project blog
|
|
92
95
|
aegisnode generate view profile --app users --project blog
|
|
93
96
|
aegisnode generate route profile --app users --project blog
|
|
97
|
+
aegisnode generateloader --project blog
|
|
94
98
|
aegisnode doctor --project blog
|
|
99
|
+
aegisnode doctor --app users --project blog
|
|
95
100
|
aegisnode updatedeps --project blog
|
|
96
101
|
```
|
|
97
102
|
|
|
98
103
|
`cd blog` is optional. You can run commands from parent folder with `--project blog`.
|
|
99
104
|
|
|
100
|
-
`createapp`, `generate`, `runserver`, `doctor`, and `updatedeps` are project-level commands.
|
|
105
|
+
`createapp`, `fix`, `generate`, `runserver`, `generateloader`, `doctor`, and `updatedeps` are project-level commands.
|
|
101
106
|
Run them from the project root; do not `cd` into `apps/<app>`.
|
|
102
107
|
Startup mode rules:
|
|
103
108
|
- Development (`env === development`): start with `aegisnode runserver` only.
|
|
@@ -105,6 +110,20 @@ Startup mode rules:
|
|
|
105
110
|
- `node app.js` and `node loader.cjs` are rejected in development mode.
|
|
106
111
|
- `aegisnode runserver` is rejected outside development mode.
|
|
107
112
|
|
|
113
|
+
### Trust Proxy
|
|
114
|
+
|
|
115
|
+
If your app runs behind Nginx, Apache, Passenger, or another reverse proxy that terminates HTTPS before the Node process, set top-level `trustProxy` in `settings.js`:
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
export default {
|
|
119
|
+
trustProxy: 1,
|
|
120
|
+
};
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
This is the AegisNode equivalent of `app.set('trust proxy', 1)` in raw Express. It makes `req.secure`, `req.protocol`, client IP detection, secure cookies, and HTTPS-aware auth logic behave correctly behind the proxy.
|
|
124
|
+
|
|
125
|
+
Prefer an exact value such as `1`, `'loopback'`, or a subnet string instead of `true`.
|
|
126
|
+
|
|
108
127
|
### Deploy On Phusion Passenger
|
|
109
128
|
|
|
110
129
|
AegisNode supports Passenger-style startup using the generated `loader.cjs`.
|
|
@@ -162,11 +181,40 @@ aegisnode doctor
|
|
|
162
181
|
|
|
163
182
|
`doctor` checks:
|
|
164
183
|
- Project structure (`settings.js`, `routes.js`, app folders)
|
|
184
|
+
- Startup entry files (`app.js`, `loader.cjs`), with production errors when `loader.cjs` is missing
|
|
165
185
|
- App declarations vs filesystem
|
|
166
186
|
- Security baseline (`appSecret`, csrf/headers/ddos toggles)
|
|
167
187
|
- Auth safety checks (JWT secret, OAuth2 `allowHttp` in production)
|
|
168
188
|
- Template directory availability
|
|
169
189
|
|
|
190
|
+
Run app-level scaffold checks for one app:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
aegisnode doctor --app users
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
App-level doctor focuses on the named app:
|
|
197
|
+
- Missing `views.js`, `models.js`, `services.js`, `validators.js`, `routes.js`, `subscribers.js`, `utils.js`
|
|
198
|
+
- Missing generated test files under `apps/<app>/tests`
|
|
199
|
+
- Missing `settings.apps` declaration for that app
|
|
200
|
+
- Missing central `routes.js` import/mount when `autoMountApps` is off
|
|
201
|
+
|
|
202
|
+
Repair a partially missing app scaffold:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
aegisnode fix --app users
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`fix` recreates missing default app files and tests without overwriting existing files. If the app is missing from `settings.apps` or central `routes.js`, it restores those registrations too.
|
|
209
|
+
|
|
210
|
+
Regenerate project startup entry files if needed:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
aegisnode generateloader
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
This restores `loader.cjs` and also recreates `app.js` if it is missing.
|
|
217
|
+
|
|
170
218
|
Update project dependencies to the current npm `latest` dist-tag:
|
|
171
219
|
|
|
172
220
|
```bash
|
|
@@ -223,7 +271,7 @@ Access environment values anywhere with `process.env`:
|
|
|
223
271
|
export default {
|
|
224
272
|
port: process.env.PORT ? Number(process.env.PORT) : 3000,
|
|
225
273
|
security: {
|
|
226
|
-
appSecret: process.env.APP_SECRET || '',
|
|
274
|
+
appSecret: process.env.APP_SECRET || '<generated-at-scaffold-time>',
|
|
227
275
|
},
|
|
228
276
|
};
|
|
229
277
|
```
|
|
@@ -240,7 +288,7 @@ export default {
|
|
|
240
288
|
port: process.env.PORT ? Number(process.env.PORT) : 3000,
|
|
241
289
|
trustProxy: false,
|
|
242
290
|
security: {
|
|
243
|
-
appSecret: process.env.APP_SECRET || '',
|
|
291
|
+
appSecret: process.env.APP_SECRET || '<generated-at-scaffold-time>',
|
|
244
292
|
},
|
|
245
293
|
logging: {
|
|
246
294
|
level: process.env.LOG_LEVEL || 'info',
|
|
@@ -265,1496 +313,1310 @@ export default {
|
|
|
265
313
|
|
|
266
314
|
Notes:
|
|
267
315
|
- Keep `AEGIS_APPS_START/END` markers; `createapp` updates this list automatically.
|
|
268
|
-
- `startproject`
|
|
316
|
+
- `startproject` writes a local `.env` with a generated `APP_SECRET` and also embeds the same generated secret in `settings.js` as a fallback.
|
|
269
317
|
- Add optional blocks manually only when needed: `https`, `templates`, `i18n`, `helpers`, `staticDir`, `websocket`, `uploads`, `mail`, `auth`, `api`, `swagger`, `loaders`, `environments`, `architecture`, `security.headers/ddos/csrf`.
|
|
270
318
|
- Any section you omit uses framework defaults from `src/runtime/config.js`.
|
|
271
319
|
|
|
272
|
-
|
|
273
|
-
## Full Settings Reference
|
|
320
|
+
## Core Concepts And App Structure
|
|
274
321
|
|
|
275
|
-
|
|
322
|
+
### App File Usage Examples
|
|
276
323
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
7. `environments.default`
|
|
285
|
-
8. `environments[env]` where `env = settings.env` (fallback `NODE_ENV`, then `development`)
|
|
324
|
+
Each generated app usually contains:
|
|
325
|
+
- `apps/<app>/views.js`
|
|
326
|
+
- `apps/<app>/models.js`
|
|
327
|
+
- `apps/<app>/services.js`
|
|
328
|
+
- `apps/<app>/utils.js`
|
|
329
|
+
- `apps/<app>/subscribers.js`
|
|
330
|
+
- `apps/<app>/routes.js`
|
|
286
331
|
|
|
287
|
-
|
|
332
|
+
Usage by file:
|
|
333
|
+
- `views.js`: HTTP handlers (`req`, `res`, `next`). Default signature can be context-first: `handler({ service, validator, services, validators, ... }, req, res, next)`.
|
|
334
|
+
Keep `views.js` thin: prefer only the view class and its imports. Avoid defining extra local helper/utility functions in the view file. Move reusable pure logic to `utils.js` and app workflows to `services.js`.
|
|
335
|
+
- `models.js`: data access layer only (SQL/NoSQL operations).
|
|
336
|
+
- `services.js`: business logic layer; orchestrates models and uses injected runtime objects when needed.
|
|
337
|
+
- `utils.js`: app-local pure utility functions. Use this for small reusable helpers that belong only to the app. Do not put DB access, request validation, or business workflows here.
|
|
338
|
+
`utils.js` is a plain module, not an injected runtime layer. If a utility needs `jlive`, `helpers`, `i18n`, or another injected runtime object, inject that object into a view/service/model first and pass it into the utility function as an argument.
|
|
339
|
+
- `subscribers.js`: event listeners (for example `app.booted`, `ws.connection`, custom events).
|
|
340
|
+
- `routes.js`: route mapping only (`route.get(...)`, `route.post(...)`, `route.use(...)`) to view handlers.
|
|
288
341
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
| `env` | `string` / `process.env.NODE_ENV || 'development'` | Active environment key for `environments` overrides. |
|
|
293
|
-
| `host` | `string` / `process.env.HOST || '0.0.0.0'` | Bind host for HTTP server. |
|
|
294
|
-
| `port` | `number` / `process.env.PORT || 3000` | Bind port for HTTP server. |
|
|
295
|
-
| `trustProxy` | `boolean \| number \| string` / `false` | Express `trust proxy` value. Set this when HTTPS is terminated by a reverse proxy/load balancer. |
|
|
296
|
-
| `https` | `object \| false` / see HTTPS table | Direct TLS server settings for Node-hosted HTTPS. |
|
|
297
|
-
| `staticDir` | `string \| null` / `null` | Static assets directory, relative to project root (if set). |
|
|
298
|
-
| `templates` | `object \| false` / see templates table | EJS template engine + layout settings. |
|
|
299
|
-
| `i18n` | `object` / see i18n table | Built-in locale detection + translator bridge (`req.aegis.t`, injected `i18n.t`). |
|
|
300
|
-
| `helpers` | `object` / see helpers table | Runtime helper defaults (for example currency/locale for `helpers.money`). |
|
|
301
|
-
| `security` | `object` / see security tables | Security headers, DDoS limiter, CSRF settings, app secret. |
|
|
302
|
-
| `logging` | `object` / `{ level: 'info' }` | Runtime logger level. |
|
|
303
|
-
| `database` | `object` / see database table | SQL or MongoDB connection settings. |
|
|
304
|
-
| `cache` | `object` / `{ enabled: true, driver: 'memory' }` | Cache backend settings. |
|
|
305
|
-
| `websocket` | `object` / `{ enabled: true, cors: { origin: false } }` | Socket.IO server options. |
|
|
306
|
-
| `uploads` | `object` / see uploads table | Built-in file upload middleware settings used by `route.upload`. |
|
|
307
|
-
| `mail` | `object` / see mail table | Nodemailer-backed mail manager available as injected `mail` and `req.aegis.mail`. |
|
|
308
|
-
| `api` | `object` / see API table | API-app middleware behavior (JSON enforcement, no-store, CSRF skip for API mounts). |
|
|
309
|
-
| `auth` | `object` / see auth tables | JWT or OAuth2 provider settings. |
|
|
310
|
-
| `swagger` | `object` / see swagger table | OpenAPI JSON + Swagger UI settings. |
|
|
311
|
-
| `architecture` | `object` / `{ strictLayers: false }` | Layering enforcement mode. |
|
|
312
|
-
| `autoMountApps` | `boolean` / `false` | Auto-mount each app route file from `settings.apps`. |
|
|
313
|
-
| `loaders` | `array` / `[]` | Startup loaders run before routes mounting. |
|
|
314
|
-
| `apps` | `array` / `[]` | Declared apps with mount points. |
|
|
315
|
-
| `environments` | `object` / `{}` | Environment-specific deep overrides. |
|
|
342
|
+
Short rule for `utils.js` vs `services.js`:
|
|
343
|
+
- Use `utils.js` for pure app-local helpers such as string formatting, slug generation, payload shaping, or small mappers.
|
|
344
|
+
- Use `services.js` for application behavior: anything that coordinates models, injected runtime objects, or feature rules.
|
|
316
345
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
346
|
+
Route modules are mapping-only (`register(route)`).
|
|
347
|
+
Framework context is injected into handlers as first argument (when handler uses 4 args): `{ service, validator, services, models, validators, auth, mail, helpers, i18n, events, ... }`.
|
|
348
|
+
`req.aegis` is also available.
|
|
349
|
+
`service`/`validator` are app-scoped conveniences. For root/non-app routes, use `services.get('<app>.<name>')` / `validators.get('<app>.<name>')`, or create an app-scoped accessor with `services.forApp('<app>')`.
|
|
320
350
|
|
|
321
|
-
|
|
351
|
+
What “app-scoped” means:
|
|
352
|
+
- In app routes (for example inside `apps/users/routes.js`), `{ service }` resolves to that app service.
|
|
353
|
+
- In root/global routes (`routes.js`), there is no single app context, so use `{ services }` and fetch with `services.forApp('<app>').get('<name>')` or `services.get('<app>.<name>')`.
|
|
322
354
|
|
|
323
|
-
|
|
355
|
+
Injected runtime dependencies:
|
|
324
356
|
|
|
325
|
-
|
|
326
|
-
| --- | --- | --- |
|
|
327
|
-
| `enabled` | `boolean` / `false` | Create an HTTPS server instead of HTTP. |
|
|
328
|
-
| `key` | `string \| Buffer` / `null` | TLS private key content. |
|
|
329
|
-
| `cert` | `string \| Buffer` / `null` | TLS certificate content. |
|
|
330
|
-
| `ca` | `string \| Buffer \| array` / `null` | Optional CA/intermediate certificate content. |
|
|
331
|
-
| `pfx` | `string \| Buffer` / `null` | PFX/PKCS#12 archive content. Use instead of `key` + `cert`. |
|
|
332
|
-
| `keyPath` | `string` / `''` | Path to TLS private key, relative to project root or absolute. |
|
|
333
|
-
| `certPath` | `string` / `''` | Path to TLS certificate, relative to project root or absolute. |
|
|
334
|
-
| `caPath` | `string \| string[]` / `null` | Optional CA/intermediate certificate path(s). |
|
|
335
|
-
| `pfxPath` | `string` / `''` | Path to PFX/PKCS#12 archive. |
|
|
336
|
-
| `passphrase` | `string` / `''` | Optional passphrase for encrypted key/PFX files. |
|
|
337
|
-
| `options` | `object` / `{}` | Extra Node `https.createServer` options (for example `minVersion`). |
|
|
357
|
+
AegisNode injects resolved runtime objects instead of asking app layers to import framework internals. `config` is the resolved runtime config from `settings.js` plus defaults and runtime overrides.
|
|
338
358
|
|
|
339
|
-
|
|
359
|
+
Available by layer:
|
|
360
|
+
- Views/handlers (`views.js` or any context-first route/controller action): `appName`, `app`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `upload`, `services`, `models`, `validators`, `service`, `model`, `validator`, `database`, `dbClient`
|
|
361
|
+
- Services (`constructor({ ... })`): `appName`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `models`, `validators`, `services`
|
|
362
|
+
- Models (`constructor({ ... })`): `appName`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `helpers`, `jlive`, `dbClient`, `database`
|
|
363
|
+
- Validators (`constructor({ ... })`): `appName`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `dbClient`, `database`
|
|
364
|
+
- Subscribers (`export default function ({ ... })`): `appName`, `rootDir`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `upload`, `services`, `models`, `validators`, `database`, `dbClient`, `app`, `server`, `templates`, `protocol`, `container`, `declaredAppNames`
|
|
365
|
+
- Controllers (`constructor({ ... })`): `appName`, `rootDir`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `upload`, `services`, `models`, `validators`, `database`, `dbClient`, `container`, `app`
|
|
366
|
+
- Loaders (`loaders` entry function): `rootDir`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `upload`, `services`, `models`, `validators`, `database`, `dbClient`, `app`, `server`, `templates`, `protocol`, `container`, `declaredAppNames`, `options`
|
|
367
|
+
- Request bridge (`req.aegis`): `config`, `env`, `i18n`, `locale`, `localeSource`, `t`, `setLocale`, `logger`, `events`, `cache`, `io`, `auth`, `mail`, `helpers`, `jlive`, `upload`, `services`, `models`, `validators`, `database`, `dbClient`, `appName`, `app`
|
|
368
|
+
- Template locals: `helpers`, `jlive`, `t`, `locale`, `i18n`, `money`, `number`, `dateTime`, `timeElapsed`, `timeDifference`, `breakStr`
|
|
369
|
+
|
|
370
|
+
Key meanings:
|
|
371
|
+
|
|
372
|
+
| Key | Description |
|
|
373
|
+
| --- | --- |
|
|
374
|
+
| `config` | Resolved runtime config from `settings.js`, framework defaults, environment overrides, and runtime overrides. |
|
|
375
|
+
| `env` | Frozen environment snapshot (`process.env` plus runtime additions such as `APP_SECRET`). |
|
|
376
|
+
| `i18n` | Translator bridge. During a request it follows the active request locale; outside a request it falls back to `defaultLocale` unless you pass `{ locale }`. |
|
|
377
|
+
| `mail` | Mail manager. Use `mail.send({ to, subject, text/html })` or `mail.sendMail(...)`. |
|
|
378
|
+
| `logger` | Runtime logger instance. |
|
|
379
|
+
| `events` | Event bus used by subscribers and app code. |
|
|
380
|
+
| `cache` | Cache backend instance (memory by default). |
|
|
381
|
+
| `io` | Socket.IO server instance when websocket support is enabled. |
|
|
382
|
+
| `auth` | Auth manager for JWT/OAuth2 flows. |
|
|
383
|
+
| `helpers` | Runtime helper functions such as `money`, `number`, `dateTime`, and `timeElapsed`. |
|
|
384
|
+
| `jlive` | jlive bridge instance. |
|
|
385
|
+
| `upload` | Upload manager used by `route.upload`. |
|
|
386
|
+
| `services` | Layer accessor used to fetch services by app/name. |
|
|
387
|
+
| `models` | Layer accessor used to fetch models by app/name. |
|
|
388
|
+
| `validators` | Layer accessor used to fetch validators by app/name. |
|
|
389
|
+
| `service` | App-scoped convenience service for the current app only. |
|
|
390
|
+
| `model` | App-scoped convenience model for the current app only. |
|
|
391
|
+
| `validator` | App-scoped convenience validator for the current app only. |
|
|
392
|
+
| `database` | Database runtime wrapper. |
|
|
393
|
+
| `dbClient` | Low-level database/query client. |
|
|
394
|
+
| `appName` | Current app name. |
|
|
395
|
+
| `app` | Current app metadata/context. |
|
|
396
|
+
| `rootDir` | Absolute project root. |
|
|
397
|
+
| `server` | HTTP/HTTPS server instance. |
|
|
398
|
+
| `templates` | Resolved template-engine configuration. |
|
|
399
|
+
| `protocol` | Server protocol (`http` or `https`). |
|
|
400
|
+
| `container` | Internal DI container. |
|
|
401
|
+
| `declaredAppNames` | Set of apps declared in config/routes. |
|
|
402
|
+
| `options` | Loader-specific options object from a `{ path, options }` loader entry. |
|
|
403
|
+
| `locale` | Active request locale. Available on `req.aegis` and template locals. |
|
|
404
|
+
| `localeSource` | Where the current locale came from (`query`, `cookie`, `header`, `manual`, or `disabled`). |
|
|
405
|
+
| `t` | Convenience translator shortcut for the current request/template scope. |
|
|
406
|
+
| `setLocale` | Request helper used to change and optionally persist the active locale. |
|
|
340
407
|
|
|
341
408
|
```js
|
|
409
|
+
// routes.js (root/global)
|
|
342
410
|
export default {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
411
|
+
register(route) {
|
|
412
|
+
route.get('/dashboard', async ({ services }, req, res, next) => {
|
|
413
|
+
try {
|
|
414
|
+
const usersService = services.forApp('users').get('users');
|
|
415
|
+
const ordersService = services.forApp('orders').get('orders');
|
|
416
|
+
res.json({
|
|
417
|
+
users: await usersService.list(),
|
|
418
|
+
orders: await ordersService.list(),
|
|
419
|
+
});
|
|
420
|
+
} catch (error) {
|
|
421
|
+
next(error);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
352
424
|
},
|
|
353
425
|
};
|
|
354
426
|
```
|
|
355
427
|
|
|
356
|
-
|
|
428
|
+
Example `views.js`:
|
|
357
429
|
|
|
358
430
|
```js
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
};
|
|
431
|
+
class UsersView {
|
|
432
|
+
static async index({ service }, req, res, next) {
|
|
433
|
+
try {
|
|
434
|
+
const data = await service.listUsers();
|
|
435
|
+
res.json({ data });
|
|
436
|
+
} catch (error) {
|
|
437
|
+
next(error);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
static async create({ service, validator }, req, res, next) {
|
|
442
|
+
try {
|
|
443
|
+
const payload = validator.create(req.body || {});
|
|
444
|
+
const created = await service.createUser(payload);
|
|
445
|
+
res.status(201).json({ data: created });
|
|
446
|
+
} catch (error) {
|
|
447
|
+
next(error);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
static async tools({ service, helpers, jlive }, req, res, next) {
|
|
452
|
+
try {
|
|
453
|
+
const stats = await service.stats();
|
|
454
|
+
res.json({
|
|
455
|
+
stats,
|
|
456
|
+
total: helpers.money(1299.5, { currency: 'USD' }),
|
|
457
|
+
elapsed: helpers.timeElapsed(Date.now() - 60_000),
|
|
458
|
+
token: jlive.generate(16),
|
|
459
|
+
});
|
|
460
|
+
} catch (error) {
|
|
461
|
+
next(error);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export default UsersView;
|
|
364
467
|
```
|
|
365
468
|
|
|
366
|
-
|
|
367
|
-
- `https` requires either `pfx`/`pfxPath` or both `key`/`keyPath` and `cert`/`certPath`.
|
|
368
|
-
- Paths resolve from project root unless absolute.
|
|
369
|
-
- `trustProxy` affects `req.secure`, `req.protocol`, secure cookies, and OAuth2 secure transport checks.
|
|
370
|
-
- Prefer `1`, a subnet, or another exact Express `trust proxy` value instead of `true` when rate limiting is enabled.
|
|
469
|
+
Example `models.js`:
|
|
371
470
|
|
|
372
|
-
|
|
471
|
+
```js
|
|
472
|
+
class UsersModel {
|
|
473
|
+
constructor({ dbClient }) {
|
|
474
|
+
this.dbClient = dbClient;
|
|
475
|
+
}
|
|
373
476
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
| `engine` | `string` / `'ejs'` | Template engine. Only `ejs` is supported. |
|
|
378
|
-
| `dir` | `string` / `'templates'` | Templates folder (absolute or relative to project root). |
|
|
379
|
-
| `base` | `string \| false \| null` / `'base'` | Default layout template (without `.ejs`). Set `false`/`null` to disable layout wrapping globally. |
|
|
380
|
-
| `appBases` | `object` / `{}` | Per-app layout override map: `{ appName: 'layout/name' }`. Set app value to `false`/`null` to disable layout for that app only. |
|
|
381
|
-
| `locals` | `object \| function` / `{}` | Global locals. If function, signature is `({ req, res, helpers, jlive, env }) => object`. |
|
|
477
|
+
async list() {
|
|
478
|
+
return [{ id: '1', name: 'Alice' }];
|
|
479
|
+
}
|
|
382
480
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
- Per-render layout override: pass `layout: 'custom-layout'` or `layout: false` in locals.
|
|
481
|
+
async create(payload) {
|
|
482
|
+
return { id: '2', ...payload };
|
|
483
|
+
}
|
|
484
|
+
}
|
|
388
485
|
|
|
389
|
-
|
|
486
|
+
export default { users: UsersModel };
|
|
487
|
+
```
|
|
390
488
|
|
|
391
|
-
|
|
392
|
-
| --- | --- | --- |
|
|
393
|
-
| `enabled` | `boolean` / `false` | Enable built-in i18n translator bridge. |
|
|
394
|
-
| `defaultLocale` | `string` / `'en'` | Default locale used when detection fails. |
|
|
395
|
-
| `fallbackLocale` | `string` / `'en'` | Fallback locale used when key is missing in active locale. |
|
|
396
|
-
| `supported` | `string[]` / `['en']` | Allowed locales. Values normalize to lowercase (for example `en-US` -> `en-us`). |
|
|
397
|
-
| `queryParam` | `string` / `'lang'` | Query parameter used for locale selection (for example `?lang=fr`). |
|
|
398
|
-
| `cookieName` | `string` / `'aegis_locale'` | Cookie used to persist locale. |
|
|
399
|
-
| `detectFromHeader` | `boolean` / `true` | Enable locale detection from `Accept-Language` header. |
|
|
400
|
-
| `detectFromCookie` | `boolean` / `true` | Enable locale detection from configured cookie. |
|
|
401
|
-
| `detectFromQuery` | `boolean` / `true` | Enable locale detection from query parameter. |
|
|
402
|
-
| `translations` | `object` / `{}` | Translation map by locale. Values can be objects or JSON file paths: `{ en: { ... }, fr: 'locales/fr.json' }`. Alias keys `locales` and `messages` are also accepted. |
|
|
403
|
-
| `translationsFile` | `string` / unset | Path to a JSON file containing all locales (example: `{ "en": {...}, "fr": {...} }`). Inline `translations` overrides file values for same locale keys. |
|
|
489
|
+
Example `services.js`:
|
|
404
490
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
- During a request, injected `i18n.t(...)` follows the active request locale. Outside a request, it falls back to `defaultLocale`.
|
|
491
|
+
```js
|
|
492
|
+
class UsersService {
|
|
493
|
+
constructor({ models, env }) {
|
|
494
|
+
this.usersModel = models.get('users');
|
|
495
|
+
this.env = env;
|
|
496
|
+
}
|
|
412
497
|
|
|
413
|
-
|
|
498
|
+
async listUsers() {
|
|
499
|
+
return this.usersModel.list();
|
|
500
|
+
}
|
|
414
501
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
| `money.currency` | `string` / `'USD'` | Default currency code for `helpers.money(amount)` when no currency option is provided. |
|
|
420
|
-
| `money.locale` | `string` / `helpers.locale` | Locale override only for `helpers.money`. |
|
|
421
|
-
| `money.currencyDisplay` | `'symbol' | 'code' | 'name' | 'narrowSymbol'` / `'symbol'` | Currency display style passed to `Intl.NumberFormat`. |
|
|
422
|
-
| `money.minimumFractionDigits` | `number` / unset | Optional minimum fraction digits for money formatting. |
|
|
423
|
-
| `money.maximumFractionDigits` | `number` / unset | Optional maximum fraction digits for money formatting. |
|
|
502
|
+
async createUser(payload) {
|
|
503
|
+
return this.usersModel.create(payload);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
424
506
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
- If `helpers.locale` is not set, AegisNode falls back to `i18n.defaultLocale` for helper locale.
|
|
428
|
-
- Legacy shorthand keys are also accepted for compatibility: `helpers.currency`, top-level `currency`, and `app.currency`.
|
|
507
|
+
export default { users: UsersService };
|
|
508
|
+
```
|
|
429
509
|
|
|
430
|
-
|
|
510
|
+
Example `subscribers.js`:
|
|
431
511
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
512
|
+
```js
|
|
513
|
+
export default function registerUsersSubscribers({ events, logger }) {
|
|
514
|
+
events.subscribe('app.booted', ({ appName }) => {
|
|
515
|
+
logger.info('[users] booted: %s', appName);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
```
|
|
438
519
|
|
|
439
|
-
|
|
520
|
+
Injected `env` is also available in:
|
|
521
|
+
- view handler context: `static index({ env }, req, res) { ... }`
|
|
522
|
+
- model constructors: `constructor({ dbClient, env }) { ... }`
|
|
523
|
+
- subscribers: `export default function ({ events, env }) { ... }`
|
|
524
|
+
- request runtime bridge: `req.aegis.env`
|
|
440
525
|
|
|
441
|
-
|
|
442
|
-
| --- | --- | --- |
|
|
443
|
-
| `enabled` | `boolean` / `true` | Enable Helmet middleware. |
|
|
444
|
-
| `csp` | `object` / see CSP table | Content Security Policy behavior. |
|
|
526
|
+
Example `routes.js`:
|
|
445
527
|
|
|
446
|
-
|
|
528
|
+
```js
|
|
529
|
+
import UsersView from './views.js';
|
|
447
530
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
531
|
+
export default {
|
|
532
|
+
appName: 'users',
|
|
533
|
+
register(route) {
|
|
534
|
+
route.get('/home', UsersView.home);
|
|
535
|
+
route.get('/', UsersView.index);
|
|
536
|
+
route.post('/', UsersView.create);
|
|
537
|
+
},
|
|
538
|
+
};
|
|
539
|
+
```
|
|
453
540
|
|
|
454
|
-
|
|
541
|
+
## Common Tasks And Feature Guides
|
|
455
542
|
|
|
456
|
-
|
|
543
|
+
### File Uploads
|
|
457
544
|
|
|
458
|
-
|
|
459
|
-
security: {
|
|
460
|
-
headers: {
|
|
461
|
-
csp: {
|
|
462
|
-
directives: {
|
|
463
|
-
scriptSrc: ["'self'", 'https://cdn.jsdelivr.net', 'https://unpkg.com'],
|
|
464
|
-
scriptSrcElem: ["'self'", 'https://cdn.jsdelivr.net', 'https://unpkg.com'],
|
|
465
|
-
styleSrc: ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net'],
|
|
466
|
-
styleSrcElem: ["'self'", 'https://cdn.jsdelivr.net'],
|
|
467
|
-
imgSrc: ["'self'", 'data:', 'https://cdn.jsdelivr.net'],
|
|
468
|
-
connectSrc: ["'self'", 'https://api.example.com', 'wss://socket.example.com'],
|
|
469
|
-
},
|
|
470
|
-
},
|
|
471
|
-
},
|
|
472
|
-
},
|
|
473
|
-
```
|
|
545
|
+
AegisNode provides built-in upload middleware on route API as `route.upload`.
|
|
474
546
|
|
|
475
|
-
|
|
476
|
-
-
|
|
477
|
-
-
|
|
478
|
-
- Prefer explicit origins instead of `*` in production.
|
|
547
|
+
Storage location:
|
|
548
|
+
- Default folder: `<project-root>/uploads`
|
|
549
|
+
- Change with `settings.uploads.dir` (relative or absolute path)
|
|
479
550
|
|
|
480
|
-
|
|
551
|
+
Recommended upload settings:
|
|
481
552
|
|
|
482
553
|
```js
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
554
|
+
uploads: {
|
|
555
|
+
enabled: true,
|
|
556
|
+
dir: 'storage/uploads',
|
|
557
|
+
createDir: true,
|
|
558
|
+
preserveExtension: true,
|
|
559
|
+
maxFileSize: '5mb',
|
|
560
|
+
maxFiles: 5,
|
|
561
|
+
maxFields: 50,
|
|
562
|
+
maxFieldSize: '1mb',
|
|
563
|
+
allowedMimeTypes: ['image/png', 'image/jpeg'],
|
|
564
|
+
allowedExtensions: ['.png', '.jpg', '.jpeg'],
|
|
565
|
+
allowApiMultipart: true,
|
|
493
566
|
},
|
|
494
567
|
```
|
|
495
568
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
| Key | Type / Default | Description |
|
|
499
|
-
| --- | --- | --- |
|
|
500
|
-
| `enabled` | `boolean` / `true` | Enable rate limiter. |
|
|
501
|
-
| `windowMs` | `number` / `60000` | Rate limit window in milliseconds. |
|
|
502
|
-
| `maxRequests` | `number` / `120` | Max requests per window per key. |
|
|
503
|
-
| `message` | `string` / `'Too many requests, please try again later.'` | JSON error message text. |
|
|
504
|
-
| `statusCode` | `number` / `429` | Response status code when limited. |
|
|
505
|
-
| `standardHeaders` | `boolean` / `true` | Emit modern rate-limit headers. |
|
|
506
|
-
| `legacyHeaders` | `boolean` / `false` | Emit legacy `X-RateLimit-*` headers. |
|
|
507
|
-
| `skipSuccessfulRequests` | `boolean` / `false` | Do not count successful responses. |
|
|
508
|
-
| `skipFailedRequests` | `boolean` / `false` | Do not count failed responses. |
|
|
509
|
-
| `trustProxy` | `boolean \| number \| string` / `false` | Legacy alias for top-level `trustProxy`. Still supported for backward compatibility. |
|
|
510
|
-
| `store` | `object \| null` / `null` | Custom rate-limit store implementation. |
|
|
511
|
-
| `skipPaths` | `string[]` / `['/health']` | Path prefixes excluded from limiter. |
|
|
512
|
-
|
|
513
|
-
#### CSRF (`security.csrf`)
|
|
514
|
-
|
|
515
|
-
| Key | Type / Default | Description |
|
|
516
|
-
| --- | --- | --- |
|
|
517
|
-
| `enabled` | `boolean` / `true` | Enable CSRF middleware. |
|
|
518
|
-
| `rejectForms` | `boolean` / `true` | Enforce CSRF on form submissions. |
|
|
519
|
-
| `rejectUnsafeMethods` | `boolean` / `true` | Enforce CSRF for unsafe methods (`POST/PUT/PATCH/DELETE`) in general. |
|
|
520
|
-
| `cookieName` | `string` / `'_aegis_csrf'` | CSRF cookie name. |
|
|
521
|
-
| `fieldName` | `string` / `'_csrf'` | Body form field name for CSRF token. |
|
|
522
|
-
| `headerName` | `string` / `'x-csrf-token'` | Header token key (normalized lowercase). |
|
|
523
|
-
| `requireSignedCookie` | `boolean` / `true` | Require signed CSRF cookie values. |
|
|
524
|
-
| `sameSite` | `'lax' \| 'strict' \| 'none' \| false` / `'lax'` | CSRF cookie same-site mode. |
|
|
525
|
-
| `secure` | `boolean \| 'auto'` / `'auto'` | Secure cookie flag (`auto` respects request security). |
|
|
526
|
-
| `httpOnly` | `boolean` / `true` | Make CSRF cookie httpOnly. |
|
|
527
|
-
| `path` | `string` / `'/'` | CSRF cookie path. |
|
|
569
|
+
Route middleware modes:
|
|
528
570
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
- CSRF is skipped for configured API app mounts when `api.disableCsrf: true`.
|
|
532
|
-
- CSRF is skipped on built-in OAuth2 server endpoints.
|
|
571
|
+
```js
|
|
572
|
+
import UsersView from './views.js';
|
|
533
573
|
|
|
534
|
-
|
|
574
|
+
export default {
|
|
575
|
+
appName: 'users',
|
|
576
|
+
register(route) {
|
|
577
|
+
// One file -> req.file
|
|
578
|
+
route.post('/avatar', route.upload.single('avatar'), UsersView.uploadAvatar);
|
|
535
579
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
| `level` | `'error' \| 'warn' \| 'info' \| 'debug' \| 'trace'` / `'info'` | Logger verbosity threshold. |
|
|
580
|
+
// Many files from one input name -> req.files (array)
|
|
581
|
+
route.post('/gallery', route.upload.array('photos', 6), UsersView.uploadGallery);
|
|
539
582
|
|
|
540
|
-
|
|
583
|
+
// Many named file inputs -> req.files.<fieldName> (array)
|
|
584
|
+
route.post(
|
|
585
|
+
'/documents',
|
|
586
|
+
route.upload.fields([
|
|
587
|
+
{ name: 'avatar', maxCount: 1 },
|
|
588
|
+
{ name: 'docs', maxCount: 3 },
|
|
589
|
+
]),
|
|
590
|
+
UsersView.uploadDocuments,
|
|
591
|
+
);
|
|
541
592
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
| `enabled` | `boolean` / `false` | Enable database bootstrap. |
|
|
545
|
-
| `dialect` | `string` / `'pg'` | SQL: `mysql`, `pg`/`postgres`/`postgresql`, `sqlite`, `mssql`, `oracle`; NoSQL: `mongo`/`mongodb`/`mongoose`. |
|
|
546
|
-
| `config` | `object` / `{}` | Connection options passed to database driver. |
|
|
547
|
-
| `options` | `object` / `{}` | Extra options (used directly for Mongo when enabled). |
|
|
548
|
-
| `uri` | `string` / unset | Legacy Mongo URI fallback (still accepted). Prefer `config.connectionString`. |
|
|
593
|
+
// Accept all file fields -> req.files (array)
|
|
594
|
+
route.post('/any-upload', route.upload.any(), UsersView.uploadAny);
|
|
549
595
|
|
|
550
|
-
|
|
551
|
-
-
|
|
552
|
-
|
|
596
|
+
// No files, parse multipart text fields only
|
|
597
|
+
route.post('/multipart-no-file', route.upload.none(), UsersView.multipartNoFile);
|
|
598
|
+
},
|
|
599
|
+
};
|
|
600
|
+
```
|
|
553
601
|
|
|
554
|
-
|
|
602
|
+
`req` payload shape:
|
|
603
|
+
- `single()`: `req.file` + `req.body`
|
|
604
|
+
- `array()`: `req.files` (array) + `req.body`
|
|
605
|
+
- `fields()`: `req.files` object (`req.files.avatar`, `req.files.docs`, ...) + `req.body`
|
|
555
606
|
|
|
556
|
-
|
|
557
|
-
| --- | --- | --- |
|
|
558
|
-
| `enabled` | `boolean` / `true` | Enable cache service. |
|
|
559
|
-
| `driver` | `string` / `'memory'` | Cache driver. Currently only `memory` is built-in. |
|
|
560
|
-
| `options` | `object` / `{}` | Reserved for future/custom drivers. |
|
|
607
|
+
Custom route with form fields + file:
|
|
561
608
|
|
|
562
|
-
|
|
609
|
+
```js
|
|
610
|
+
// apps/users/routes.js
|
|
611
|
+
import UsersView from './views.js';
|
|
563
612
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
613
|
+
export default {
|
|
614
|
+
appName: 'users',
|
|
615
|
+
register(route) {
|
|
616
|
+
route.post('/profile/update', route.upload.single('avatar'), UsersView.updateProfile);
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
```
|
|
568
620
|
|
|
569
|
-
|
|
621
|
+
```js
|
|
622
|
+
// apps/users/views.js
|
|
623
|
+
class UsersView {
|
|
624
|
+
static updateProfile(_context, req, res) {
|
|
625
|
+
const { username, bio } = req.body;
|
|
626
|
+
const avatar = req.file || null;
|
|
570
627
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
### Mail (`mail`)
|
|
586
|
-
|
|
587
|
-
| Key | Type / Default | Description |
|
|
588
|
-
| --- | --- | --- |
|
|
589
|
-
| `enabled` | `boolean` / `false` | Enable the built-in mail manager. |
|
|
590
|
-
| `defaults` | `object` / `{ from: '', replyTo: '' }` | Default message fields merged into every outgoing mail payload. |
|
|
591
|
-
| `defaults.from` | `string` / `''` | Default sender used when `mail.send(...)` omits `from`. |
|
|
592
|
-
| `defaults.replyTo` | `string` / `''` | Default `replyTo` header for outgoing mail. |
|
|
593
|
-
| `transport` | `object \| string` / `{}` | Nodemailer transport config or SMTP connection URL passed to `nodemailer.createTransport(...)`. |
|
|
594
|
-
| `transporter` | `object \| null` / `null` | Prebuilt transporter object with `sendMail()`. Useful in tests or custom integrations. |
|
|
595
|
-
| `transportFactory` | `function \| null` / `null` | Factory that returns a transporter object with `sendMail()`. |
|
|
596
|
-
| `verifyOnStartup` | `boolean` / `false` | Call `transporter.verify()` during boot when the transporter supports it. |
|
|
597
|
-
|
|
598
|
-
Mail notes:
|
|
599
|
-
- The runtime uses [Nodemailer](https://nodemailer.com/).
|
|
600
|
-
- `mail.send(payload)` and `mail.sendMail(payload)` are aliases.
|
|
601
|
-
- Each payload must include at least one of `to`, `cc`, or `bcc`.
|
|
602
|
-
- Each payload must include `from`, or configure `mail.defaults.from`.
|
|
603
|
-
- Injected `mail` is available in handlers, services, models, validators, controllers, subscribers, and loaders, plus `req.aegis.mail`.
|
|
604
|
-
|
|
605
|
-
### API (`api`)
|
|
606
|
-
|
|
607
|
-
| Key | Type / Default | Description |
|
|
608
|
-
| --- | --- | --- |
|
|
609
|
-
| `apps` | `string[]` / `[]` | App names treated as API apps. Mounts are resolved from `settings.apps`. |
|
|
610
|
-
| `disableCsrf` | `boolean` / `true` | Skip CSRF checks for API app mounts. |
|
|
611
|
-
| `requireJsonForUnsafeMethods` | `boolean` / `true` | Reject unsafe API payloads unless `Content-Type` is JSON (`415`). Multipart is allowed when `uploads.allowApiMultipart=true`. |
|
|
612
|
-
| `noStoreHeaders` | `boolean` / `true` | Set `Cache-Control: no-store` on API responses. |
|
|
613
|
-
|
|
614
|
-
API notes:
|
|
615
|
-
- `api.apps` contains app names from `settings.apps`, not URL paths.
|
|
616
|
-
- Marking an app as API does not generate REST routes or change the app file structure. It only applies API middleware to that app mount.
|
|
617
|
-
- The effective API mount comes from `settings.apps[].mount` (or `/${app}` by default). Keep that aligned with your `route.use(...)` mount when `autoMountApps` is off.
|
|
618
|
-
|
|
619
|
-
### Auth (`auth`)
|
|
620
|
-
|
|
621
|
-
| Key | Type / Default | Description |
|
|
622
|
-
| --- | --- | --- |
|
|
623
|
-
| `enabled` | `boolean` / `false` | Enable auth manager. |
|
|
624
|
-
| `provider` | `'jwt' \| 'oauth2'` / `'jwt'` | Active auth provider. |
|
|
625
|
-
| `tablePrefix` | `string` / `'aegisnode'` | Prefix for auth storage namespaces/tables; sanitized to lowercase `[a-z0-9_]`. |
|
|
626
|
-
| `storage` | `object` / see storage table | Persistence backend for auth state. |
|
|
627
|
-
| `jwt` | `object` / see jwt table | JWT configuration. |
|
|
628
|
-
| `oauth2` | `object` / see oauth2 table | OAuth2 server and token configuration. |
|
|
629
|
-
|
|
630
|
-
#### Auth Storage (`auth.storage`)
|
|
628
|
+
return res.json({
|
|
629
|
+
username,
|
|
630
|
+
bio,
|
|
631
|
+
avatar: avatar ? {
|
|
632
|
+
name: avatar.filename,
|
|
633
|
+
originalName: avatar.originalname,
|
|
634
|
+
mimeType: avatar.mimetype,
|
|
635
|
+
size: avatar.size,
|
|
636
|
+
path: avatar.path,
|
|
637
|
+
} : null,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}
|
|
631
641
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
| `driver` | `'cache' \| 'memory' \| 'file' \| 'database'` / `'cache'` | Storage backend for revocations/clients/tokens. |
|
|
635
|
-
| `filePath` | `string` / `'storage/aegisnode-auth-store.json'` | Used when `driver: 'file'`. Relative to project root if not absolute. |
|
|
636
|
-
| `tableName` | `string` / `'aegisnode_auth_store'` (prefix-based) | Used when `driver: 'database'` for SQL table or Mongo collection. |
|
|
637
|
-
| `collectionName` | `string` / alias only | Legacy alias; if set and `tableName` missing, it becomes `tableName`. |
|
|
642
|
+
export default UsersView;
|
|
643
|
+
```
|
|
638
644
|
|
|
639
|
-
|
|
645
|
+
```html
|
|
646
|
+
<form action="/users/profile/update" method="POST" enctype="multipart/form-data">
|
|
647
|
+
<%= csrfToken %>
|
|
648
|
+
<input name="username" />
|
|
649
|
+
<textarea name="bio"></textarea>
|
|
650
|
+
<input type="file" name="avatar" />
|
|
651
|
+
<button type="submit">Save</button>
|
|
652
|
+
</form>
|
|
653
|
+
```
|
|
640
654
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
| `issuer` | `string` / `appName` | JWT issuer claim. |
|
|
646
|
-
| `audience` | `string` / `appName` | JWT audience claim. |
|
|
647
|
-
| `expiresIn` | `string` / `'15m'` | Access token TTL. |
|
|
648
|
-
| `refreshExpiresIn` | `string` / `'7d'` | Refresh token TTL. |
|
|
655
|
+
Upload limits and rejections:
|
|
656
|
+
- Per-file size limit from `uploads.maxFileSize` returns `413` when exceeded.
|
|
657
|
+
- Total files limit from `uploads.maxFiles` returns `413` when exceeded.
|
|
658
|
+
- `allowedMimeTypes` / `allowedExtensions` mismatch returns `415`.
|
|
649
659
|
|
|
650
|
-
|
|
660
|
+
Important behavior:
|
|
661
|
+
- If `uploads.enabled=false`, using `route.upload.*` throws at route registration.
|
|
662
|
+
- For API mounts, multipart is allowed only when `uploads.allowApiMultipart=true`.
|
|
663
|
+
- For non-API form submissions, CSRF token is required by default.
|
|
651
664
|
|
|
652
|
-
|
|
653
|
-
| --- | --- | --- |
|
|
654
|
-
| `accessTokenTtlSeconds` | `number` / `3600` | Access token TTL in seconds. |
|
|
655
|
-
| `refreshTokenTtlSeconds` | `number` / `1209600` | Refresh token TTL in seconds. |
|
|
656
|
-
| `authorizationCodeTtlSeconds` | `number` / `600` | Authorization code TTL in seconds. |
|
|
657
|
-
| `rotateRefreshToken` | `boolean` / `true` | Rotate refresh token on refresh flow. |
|
|
658
|
-
| `requireClientSecret` | `boolean` / `true` | Require secret for confidential client flows. |
|
|
659
|
-
| `requirePkce` | `boolean` / `true` | Require PKCE for authorization_code flow. |
|
|
660
|
-
| `allowPlainPkce` | `boolean` / `false` | Allow PKCE `plain` method. |
|
|
661
|
-
| `grants` | `string[]` / `['authorization_code','refresh_token','client_credentials']` | Enabled OAuth2 grants. |
|
|
662
|
-
| `defaultScopes` | `string[]` / `[]` | Scopes assigned when none requested/provided. |
|
|
663
|
-
| `clientAuthMethod` | `'client_secret_basic' \| 'client_secret_post' \| 'none'` / `'client_secret_basic'` | Default client authentication method. |
|
|
664
|
-
| `server` | `object` / see OAuth2 server table | Built-in authorization server endpoint settings. |
|
|
665
|
+
### API Apps
|
|
665
666
|
|
|
666
|
-
|
|
667
|
+
`api` does not create a separate app type. You still build a normal AegisNode app with `routes.js`, `views.js`, `services.js`, and `validators.js`.
|
|
668
|
+
The `api` setting only changes middleware behavior for selected app mounts.
|
|
667
669
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
| `authorizePath` | `string` / `'/oauth/authorize'` | Authorization endpoint path. |
|
|
673
|
-
| `tokenPath` | `string` / `'/oauth/token'` | Token endpoint path. |
|
|
674
|
-
| `introspectionPath` | `string` / `'/oauth/introspect'` | Introspection endpoint path. |
|
|
675
|
-
| `revocationPath` | `string` / `'/oauth/revoke'` | Revocation endpoint path. |
|
|
676
|
-
| `metadataPath` | `string` / `'/.well-known/oauth-authorization-server'` | OAuth2 metadata endpoint path. |
|
|
677
|
-
| `issuer` | `string` / `server.baseUrl` fallback, then `appName` | Issuer value in OAuth2 metadata/tokens. |
|
|
678
|
-
| `baseUrl` | `string` / optional alias | Alias used as fallback for `issuer` when `issuer` is empty. |
|
|
679
|
-
| `autoApprove` | `boolean` / `true` | Auto-approve auth requests once subject is resolved. |
|
|
680
|
-
| `requireAuthenticatedUser` | `boolean` / `true` | Require authenticated user/subject for authorize endpoint. |
|
|
681
|
-
| `requireConsent` | `boolean` / `false` | Require explicit consent before issuing auth code. |
|
|
682
|
-
| `allowSubjectFromParams` | `boolean` / `false` | Allow subject from request params (`subject`/`user_id`). Keep disabled in production. |
|
|
683
|
-
| `allowHttp` | `boolean` / `false` | Allow OAuth2 endpoints on non-HTTPS requests. Keep `false` in production. |
|
|
684
|
-
| `resolveSubject` | `function \| null` / `null` | Hook to resolve subject: `({ req, params, client }) => string|null`. |
|
|
685
|
-
| `resolveConsent` | `function \| null` / `null` | Hook to resolve consent: `({ req, params, client, subject }) => boolean`. |
|
|
670
|
+
Think of it this way:
|
|
671
|
+
- `api` controls request/response behavior for an app mount.
|
|
672
|
+
- `auth` controls who can access routes and how tokens are issued/verified.
|
|
673
|
+
- You can use `api` without auth, auth without `api`, or both together.
|
|
686
674
|
|
|
687
|
-
|
|
675
|
+
Common combinations:
|
|
676
|
+
- `api` only: public or internal JSON endpoints with no token auth.
|
|
677
|
+
- `api` + JWT: first-party SPA/mobile/frontend calling your own backend.
|
|
678
|
+
- `api` + OAuth2: third-party clients, machine-to-machine access, or standards-based authorization flows.
|
|
688
679
|
|
|
689
|
-
|
|
690
|
-
| --- | --- | --- |
|
|
691
|
-
| `enabled` | `boolean` / `false` | Enable Swagger UI + OpenAPI JSON endpoints. |
|
|
692
|
-
| `docsPath` | `string` / `'/docs'` | Swagger UI route. |
|
|
693
|
-
| `jsonPath` | `string` / `'/openapi.json'` | OpenAPI JSON route. |
|
|
694
|
-
| `document` | `object \| null` / `null` | Inline OpenAPI document object. |
|
|
695
|
-
| `documentPath` | `string` / `'openapi.json'` | JSON file path used when `document` is not provided. |
|
|
696
|
-
| `explorer` | `boolean` / `true` | Enable Swagger UI explorer mode. |
|
|
680
|
+
Quick start:
|
|
697
681
|
|
|
698
|
-
|
|
682
|
+
1. Declare the app in `settings.apps` and give it a mount.
|
|
683
|
+
2. Add that app name to `api.apps`.
|
|
684
|
+
3. Mount the app at the same path in `routes.js` when `autoMountApps` is off.
|
|
685
|
+
4. Return JSON from your handlers.
|
|
686
|
+
5. Send JSON for unsafe methods unless you intentionally allow multipart uploads.
|
|
699
687
|
|
|
700
|
-
|
|
701
|
-
| --- | --- | --- |
|
|
702
|
-
| `strictLayers` | `boolean` / `false` | Enforce `route -> validator -> service -> model` restrictions. |
|
|
688
|
+
Example `settings.js`:
|
|
703
689
|
|
|
704
|
-
|
|
690
|
+
```js
|
|
691
|
+
export default {
|
|
692
|
+
apps: [
|
|
693
|
+
{ name: 'users', mount: '/users' },
|
|
694
|
+
],
|
|
695
|
+
api: {
|
|
696
|
+
apps: ['users'],
|
|
697
|
+
disableCsrf: true,
|
|
698
|
+
requireJsonForUnsafeMethods: true,
|
|
699
|
+
noStoreHeaders: true,
|
|
700
|
+
},
|
|
701
|
+
};
|
|
702
|
+
```
|
|
705
703
|
|
|
706
|
-
|
|
707
|
-
| --- | --- | --- |
|
|
708
|
-
| `autoMountApps` | `boolean` / `false` | If `true`, each `apps/<name>/routes.js` is mounted automatically from `settings.apps`. If `false`, central `routes.js` controls mounting. |
|
|
704
|
+
Example root `routes.js`:
|
|
709
705
|
|
|
710
|
-
|
|
706
|
+
```js
|
|
707
|
+
import users from './apps/users/routes.js';
|
|
711
708
|
|
|
712
|
-
|
|
709
|
+
export default {
|
|
710
|
+
register(route) {
|
|
711
|
+
route.use('/users', users); // keep this aligned with settings.apps[].mount
|
|
712
|
+
},
|
|
713
|
+
};
|
|
714
|
+
```
|
|
713
715
|
|
|
714
|
-
|
|
716
|
+
Example `apps/users/routes.js`:
|
|
715
717
|
|
|
716
718
|
```js
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
719
|
+
import UsersView from './views.js';
|
|
720
|
+
|
|
721
|
+
export default {
|
|
722
|
+
appName: 'users',
|
|
723
|
+
register(route) {
|
|
724
|
+
route.get('/', UsersView.index);
|
|
725
|
+
route.post('/', UsersView.create);
|
|
720
726
|
},
|
|
721
|
-
|
|
727
|
+
};
|
|
722
728
|
```
|
|
723
729
|
|
|
724
|
-
|
|
730
|
+
Example `apps/users/views.js`:
|
|
725
731
|
|
|
726
732
|
```js
|
|
727
|
-
|
|
728
|
-
|
|
733
|
+
class UsersView {
|
|
734
|
+
static async index({ service }, req, res, next) {
|
|
735
|
+
try {
|
|
736
|
+
const users = await service.list();
|
|
737
|
+
res.json({ data: users });
|
|
738
|
+
} catch (error) {
|
|
739
|
+
next(error);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
729
742
|
|
|
730
|
-
|
|
743
|
+
static async create({ service, validator }, req, res, next) {
|
|
744
|
+
try {
|
|
745
|
+
const payload = validator.create(req.body || {});
|
|
746
|
+
const created = await service.create(payload);
|
|
747
|
+
res.status(201).json({ data: created });
|
|
748
|
+
} catch (error) {
|
|
749
|
+
next(error);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
731
753
|
|
|
732
|
-
|
|
733
|
-
loaders: [
|
|
734
|
-
{ path: 'loaders/init-queue.js', options: { queue: 'jobs' } },
|
|
735
|
-
]
|
|
754
|
+
export default UsersView;
|
|
736
755
|
```
|
|
737
756
|
|
|
738
|
-
|
|
739
|
-
Each loader receives the shared runtime context documented in the injection matrix above, plus `options` from the loader entry.
|
|
740
|
-
|
|
741
|
-
### Apps (`apps`)
|
|
757
|
+
Example requests:
|
|
742
758
|
|
|
743
|
-
|
|
759
|
+
```bash
|
|
760
|
+
curl http://127.0.0.1:3000/users
|
|
744
761
|
|
|
745
|
-
|
|
746
|
-
|
|
762
|
+
curl -X POST http://127.0.0.1:3000/users \
|
|
763
|
+
-H "Content-Type: application/json" \
|
|
764
|
+
-d '{"name":"Alice"}'
|
|
747
765
|
```
|
|
748
766
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
{ name: 'billing', mount: '/payments' },
|
|
755
|
-
]
|
|
756
|
-
```
|
|
757
|
-
|
|
758
|
-
Rules:
|
|
759
|
-
- `name` is required in object form.
|
|
760
|
-
- `mount` defaults to `/${name}`.
|
|
761
|
-
- Mounts are normalized to a single leading slash (except root `/`).
|
|
762
|
-
- App route modules must be declared in `settings.apps` before they can load/mount.
|
|
763
|
-
|
|
764
|
-
### Environment Overrides (`environments`)
|
|
765
|
-
|
|
766
|
-
Example:
|
|
767
|
-
|
|
768
|
-
```js
|
|
769
|
-
environments: {
|
|
770
|
-
default: {
|
|
771
|
-
logging: { level: 'debug' },
|
|
772
|
-
},
|
|
773
|
-
production: {
|
|
774
|
-
logging: { level: 'warn' },
|
|
775
|
-
security: { ddos: { maxRequests: 80 } },
|
|
776
|
-
},
|
|
777
|
-
}
|
|
778
|
-
```
|
|
779
|
-
|
|
780
|
-
Behavior:
|
|
781
|
-
- Base config loads first.
|
|
782
|
-
- `environments.default` is merged next (if present).
|
|
783
|
-
- `environments[env]` is merged last.
|
|
784
|
-
- Arrays in overrides replace full arrays from base.
|
|
785
|
-
|
|
786
|
-
<!-- SETTINGS_REFERENCE_END -->
|
|
787
|
-
|
|
788
|
-
## Core Concepts And App Structure
|
|
767
|
+
What the API middleware changes:
|
|
768
|
+
- `POST`, `PUT`, `PATCH`, and `DELETE` with a request body must use `application/json` when `requireJsonForUnsafeMethods: true`.
|
|
769
|
+
- `multipart/form-data` is still allowed for API mounts when `uploads.allowApiMultipart: true`.
|
|
770
|
+
- CSRF is skipped only for configured API app mounts when `disableCsrf: true`.
|
|
771
|
+
- API responses get `Cache-Control: no-store` when `noStoreHeaders: true`.
|
|
789
772
|
|
|
790
|
-
|
|
773
|
+
What it does not change:
|
|
774
|
+
- It does not auto-generate CRUD endpoints.
|
|
775
|
+
- It does not force a separate `controllers/` or `api/` folder.
|
|
776
|
+
- It does not convert a view into JSON automatically; your handler still decides what to return.
|
|
791
777
|
|
|
792
|
-
|
|
793
|
-
- `apps/<app>/views.js`
|
|
794
|
-
- `apps/<app>/models.js`
|
|
795
|
-
- `apps/<app>/services.js`
|
|
796
|
-
- `apps/<app>/subscribers.js`
|
|
797
|
-
- `apps/<app>/routes.js`
|
|
778
|
+
### API And Auth Together
|
|
798
779
|
|
|
799
|
-
|
|
800
|
-
- `views.js`: HTTP handlers (`req`, `res`, `next`). Default signature can be context-first: `handler({ service, validator, services, validators, ... }, req, res, next)`.
|
|
801
|
-
- `models.js`: data access layer only (SQL/NoSQL operations).
|
|
802
|
-
- `services.js`: business logic layer; orchestrates models.
|
|
803
|
-
- `subscribers.js`: event listeners (for example `app.booted`, `ws.connection`, custom events).
|
|
804
|
-
- `routes.js`: route mapping only (`route.get(...)`, `route.post(...)`, `route.use(...)`) to view handlers.
|
|
780
|
+
`api` and `auth` are separate features that are often used together:
|
|
805
781
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
`req.aegis` is also available.
|
|
809
|
-
`service`/`validator` are app-scoped conveniences. For root/non-app routes, use `services.get('<app>.<name>')` / `validators.get('<app>.<name>')`, or create an app-scoped accessor with `services.forApp('<app>')`.
|
|
782
|
+
- `api` makes an app behave like an API mount: JSON body enforcement, optional CSRF skip, and `Cache-Control: no-store`.
|
|
783
|
+
- `auth` adds token issuance, token verification, route protection, client registration, and revocation/introspection behavior.
|
|
810
784
|
|
|
811
|
-
|
|
812
|
-
-
|
|
813
|
-
-
|
|
785
|
+
Examples:
|
|
786
|
+
- Public JSON API: enable `api`, leave `auth.enabled` off.
|
|
787
|
+
- Protected JSON API for your own frontend/mobile app: enable `api` and `auth.provider = 'jwt'`.
|
|
788
|
+
- Protected partner/developer API: enable `api` and `auth.provider = 'oauth2'`.
|
|
814
789
|
|
|
815
|
-
|
|
790
|
+
Rule of thumb:
|
|
791
|
+
- If you only need JSON routes, use `api`.
|
|
792
|
+
- If you need authenticated access, add `auth`.
|
|
793
|
+
- If outside clients need a standard auth protocol, choose OAuth2 instead of rolling custom JWT login flows.
|
|
816
794
|
|
|
817
|
-
|
|
795
|
+
Quick comparison:
|
|
818
796
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
- Subscribers (`export default function ({ ... })`): `appName`, `rootDir`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `upload`, `services`, `models`, `validators`, `database`, `dbClient`, `app`, `server`, `templates`, `protocol`, `container`, `declaredAppNames`
|
|
825
|
-
- Controllers (`constructor({ ... })`): `appName`, `rootDir`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `upload`, `services`, `models`, `validators`, `database`, `dbClient`, `container`, `app`
|
|
826
|
-
- Loaders (`loaders` entry function): `rootDir`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `upload`, `services`, `models`, `validators`, `database`, `dbClient`, `app`, `server`, `templates`, `protocol`, `container`, `declaredAppNames`, `options`
|
|
827
|
-
- Request bridge (`req.aegis`): `config`, `env`, `i18n`, `locale`, `localeSource`, `t`, `setLocale`, `logger`, `events`, `cache`, `io`, `auth`, `mail`, `helpers`, `jlive`, `upload`, `services`, `models`, `validators`, `database`, `dbClient`, `appName`, `app`
|
|
828
|
-
- Template locals: `helpers`, `jlive`, `t`, `locale`, `i18n`, `money`, `number`, `dateTime`, `timeElapsed`, `timeDifference`, `breakStr`
|
|
797
|
+
| Setup | Use it when | What you configure |
|
|
798
|
+
| --- | --- | --- |
|
|
799
|
+
| `api` only | You need JSON endpoints without token auth. Good for public read APIs or trusted internal services. | `api.apps = [...]` |
|
|
800
|
+
| `api` + JWT | Your own frontend/mobile app talks to your backend and you control both sides. | `api.apps = [...]`, `auth.enabled = true`, `auth.provider = 'jwt'`, plus your own login/token routes |
|
|
801
|
+
| `api` + OAuth2 | External clients, partner apps, or machine clients need standard token flows. | `api.apps = [...]`, `auth.enabled = true`, `auth.provider = 'oauth2'` |
|
|
829
802
|
|
|
830
|
-
|
|
803
|
+
### Database Config
|
|
831
804
|
|
|
832
|
-
|
|
833
|
-
| --- | --- |
|
|
834
|
-
| `config` | Resolved runtime config from `settings.js`, framework defaults, environment overrides, and runtime overrides. |
|
|
835
|
-
| `env` | Frozen environment snapshot (`process.env` plus runtime additions such as `APP_SECRET`). |
|
|
836
|
-
| `i18n` | Translator bridge. During a request it follows the active request locale; outside a request it falls back to `defaultLocale` unless you pass `{ locale }`. |
|
|
837
|
-
| `mail` | Mail manager. Use `mail.send({ to, subject, text/html })` or `mail.sendMail(...)`. |
|
|
838
|
-
| `logger` | Runtime logger instance. |
|
|
839
|
-
| `events` | Event bus used by subscribers and app code. |
|
|
840
|
-
| `cache` | Cache backend instance (memory by default). |
|
|
841
|
-
| `io` | Socket.IO server instance when websocket support is enabled. |
|
|
842
|
-
| `auth` | Auth manager for JWT/OAuth2 flows. |
|
|
843
|
-
| `helpers` | Runtime helper functions such as `money`, `number`, `dateTime`, and `timeElapsed`. |
|
|
844
|
-
| `jlive` | jlive bridge instance. |
|
|
845
|
-
| `upload` | Upload manager used by `route.upload`. |
|
|
846
|
-
| `services` | Layer accessor used to fetch services by app/name. |
|
|
847
|
-
| `models` | Layer accessor used to fetch models by app/name. |
|
|
848
|
-
| `validators` | Layer accessor used to fetch validators by app/name. |
|
|
849
|
-
| `service` | App-scoped convenience service for the current app only. |
|
|
850
|
-
| `model` | App-scoped convenience model for the current app only. |
|
|
851
|
-
| `validator` | App-scoped convenience validator for the current app only. |
|
|
852
|
-
| `database` | Database runtime wrapper. |
|
|
853
|
-
| `dbClient` | Low-level database/query client. |
|
|
854
|
-
| `appName` | Current app name. |
|
|
855
|
-
| `app` | Current app metadata/context. |
|
|
856
|
-
| `rootDir` | Absolute project root. |
|
|
857
|
-
| `server` | HTTP/HTTPS server instance. |
|
|
858
|
-
| `templates` | Resolved template-engine configuration. |
|
|
859
|
-
| `protocol` | Server protocol (`http` or `https`). |
|
|
860
|
-
| `container` | Internal DI container. |
|
|
861
|
-
| `declaredAppNames` | Set of apps declared in config/routes. |
|
|
862
|
-
| `options` | Loader-specific options object from a `{ path, options }` loader entry. |
|
|
863
|
-
| `locale` | Active request locale. Available on `req.aegis` and template locals. |
|
|
864
|
-
| `localeSource` | Where the current locale came from (`query`, `cookie`, `header`, `manual`, or `disabled`). |
|
|
865
|
-
| `t` | Convenience translator shortcut for the current request/template scope. |
|
|
866
|
-
| `setLocale` | Request helper used to change and optionally persist the active locale. |
|
|
805
|
+
Use `database.config` for every dialect (SQL and MongoDB/Mongoose):
|
|
867
806
|
|
|
868
807
|
```js
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
808
|
+
database: {
|
|
809
|
+
enabled: true,
|
|
810
|
+
dialect: 'pg', // pg | mysql | mssql | sqlite | oracle | mongo | mongodb | mongoose
|
|
811
|
+
config: {
|
|
812
|
+
// SQL example:
|
|
813
|
+
server: 'localhost',
|
|
814
|
+
port: 5432,
|
|
815
|
+
user: 'postgres',
|
|
816
|
+
password: 'postgres',
|
|
817
|
+
database: 'appdb',
|
|
818
|
+
|
|
819
|
+
// Mongo example:
|
|
820
|
+
// connectionString: 'mongodb://localhost:27017/appdb',
|
|
821
|
+
// or:
|
|
822
|
+
// server: 'localhost',
|
|
823
|
+
// port: 27017,
|
|
824
|
+
// database: 'appdb',
|
|
825
|
+
// user: 'mongo_user',
|
|
826
|
+
// password: 'mongo_pass',
|
|
884
827
|
},
|
|
885
|
-
}
|
|
828
|
+
options: {},
|
|
829
|
+
},
|
|
886
830
|
```
|
|
887
831
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
```js
|
|
891
|
-
class UsersView {
|
|
892
|
-
static async index({ service }, req, res, next) {
|
|
893
|
-
try {
|
|
894
|
-
const data = await service.listUsers();
|
|
895
|
-
res.json({ data });
|
|
896
|
-
} catch (error) {
|
|
897
|
-
next(error);
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
static async create({ service, validator }, req, res, next) {
|
|
902
|
-
try {
|
|
903
|
-
const payload = validator.create(req.body || {});
|
|
904
|
-
const created = await service.createUser(payload);
|
|
905
|
-
res.status(201).json({ data: created });
|
|
906
|
-
} catch (error) {
|
|
907
|
-
next(error);
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
static async tools({ service, helpers, jlive }, req, res, next) {
|
|
912
|
-
try {
|
|
913
|
-
const stats = await service.stats();
|
|
914
|
-
res.json({
|
|
915
|
-
stats,
|
|
916
|
-
total: helpers.money(1299.5, { currency: 'USD' }),
|
|
917
|
-
elapsed: helpers.timeElapsed(Date.now() - 60_000),
|
|
918
|
-
token: jlive.generate(16),
|
|
919
|
-
});
|
|
920
|
-
} catch (error) {
|
|
921
|
-
next(error);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
export default UsersView;
|
|
927
|
-
```
|
|
832
|
+
Legacy note:
|
|
833
|
+
- `database.uri` is still accepted for MongoDB, but `database.config.connectionString` is preferred.
|
|
928
834
|
|
|
929
|
-
|
|
835
|
+
Model usage for `mongo` / `mongodb` / `mongoose`:
|
|
836
|
+
- `dbClient` is a QueryMesh client, so you can use the same fluent API as SQL models.
|
|
930
837
|
|
|
931
838
|
```js
|
|
932
839
|
class UsersModel {
|
|
933
840
|
constructor({ dbClient }) {
|
|
934
|
-
this.
|
|
841
|
+
this.db = dbClient;
|
|
935
842
|
}
|
|
936
843
|
|
|
937
844
|
async list() {
|
|
938
|
-
return [
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
async create(payload) {
|
|
942
|
-
return { id: '2', ...payload };
|
|
845
|
+
return this.db.table('users').select(['id', 'name']).get();
|
|
943
846
|
}
|
|
944
847
|
}
|
|
945
|
-
|
|
946
|
-
export default { users: UsersModel };
|
|
947
848
|
```
|
|
948
849
|
|
|
949
|
-
|
|
850
|
+
Mongo `_id` / `ObjectId` handling:
|
|
851
|
+
- Use built-in helper `helpers.toObjectId(...)` before filtering on `_id`.
|
|
852
|
+
- Validate with `helpers.isObjectId(...)` when needed.
|
|
853
|
+
- Keep this conversion in model/service layer.
|
|
854
|
+
- If your collection stores string `_id` values (not native Mongo `ObjectId`), skip conversion.
|
|
950
855
|
|
|
951
856
|
```js
|
|
952
|
-
class
|
|
953
|
-
constructor({
|
|
954
|
-
this.
|
|
955
|
-
this.
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
async listUsers() {
|
|
959
|
-
return this.usersModel.list();
|
|
857
|
+
class UsersModel {
|
|
858
|
+
constructor({ dbClient, helpers }) {
|
|
859
|
+
this.db = dbClient;
|
|
860
|
+
this.helpers = helpers;
|
|
960
861
|
}
|
|
961
862
|
|
|
962
|
-
async
|
|
963
|
-
|
|
863
|
+
async findById(id) {
|
|
864
|
+
const _id = this.helpers.toObjectId(id);
|
|
865
|
+
if (!_id) throw new Error('Invalid Mongo ObjectId');
|
|
866
|
+
return this.db.table('users').where('_id', '=', _id).first();
|
|
964
867
|
}
|
|
965
868
|
}
|
|
966
|
-
|
|
967
|
-
export default { users: UsersService };
|
|
968
869
|
```
|
|
969
870
|
|
|
970
|
-
|
|
871
|
+
### Environment Overrides (Single settings.js)
|
|
872
|
+
|
|
873
|
+
You can keep a single `settings.js` and define per-environment overrides:
|
|
971
874
|
|
|
972
875
|
```js
|
|
973
|
-
export default function registerUsersSubscribers({ events, logger }) {
|
|
974
|
-
events.subscribe('app.booted', ({ appName }) => {
|
|
975
|
-
logger.info('[users] booted: %s', appName);
|
|
976
|
-
});
|
|
977
|
-
}
|
|
978
|
-
```
|
|
979
|
-
|
|
980
|
-
Injected `env` is also available in:
|
|
981
|
-
- view handler context: `static index({ env }, req, res) { ... }`
|
|
982
|
-
- model constructors: `constructor({ dbClient, env }) { ... }`
|
|
983
|
-
- subscribers: `export default function ({ events, env }) { ... }`
|
|
984
|
-
- request runtime bridge: `req.aegis.env`
|
|
985
|
-
|
|
986
|
-
Example `routes.js`:
|
|
987
|
-
|
|
988
|
-
```js
|
|
989
|
-
import UsersView from './views.js';
|
|
990
|
-
|
|
991
876
|
export default {
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
877
|
+
env: process.env.NODE_ENV || 'development',
|
|
878
|
+
logging: {
|
|
879
|
+
level: 'info',
|
|
880
|
+
},
|
|
881
|
+
security: {
|
|
882
|
+
ddos: {
|
|
883
|
+
maxRequests: 120,
|
|
884
|
+
},
|
|
885
|
+
},
|
|
886
|
+
environments: {
|
|
887
|
+
development: {
|
|
888
|
+
logging: { level: 'debug' },
|
|
889
|
+
},
|
|
890
|
+
production: {
|
|
891
|
+
logging: { level: 'warn' },
|
|
892
|
+
security: { ddos: { maxRequests: 80 } },
|
|
893
|
+
},
|
|
997
894
|
},
|
|
998
895
|
};
|
|
999
896
|
```
|
|
1000
897
|
|
|
1001
|
-
|
|
898
|
+
Behavior:
|
|
899
|
+
- Base config is loaded first.
|
|
900
|
+
- `environments.default` is applied (if present).
|
|
901
|
+
- `environments[env]` is applied last.
|
|
902
|
+
- `env` comes from `settings.env` (fallback: `NODE_ENV`, then `development`).
|
|
1002
903
|
|
|
1003
|
-
###
|
|
904
|
+
### Auth (JWT Or OAuth2)
|
|
1004
905
|
|
|
1005
|
-
|
|
906
|
+
`auth` is independent from `api`.
|
|
907
|
+
You can protect normal web routes, API routes, or both.
|
|
908
|
+
If your app is already listed in `api.apps`, adding `auth` simply means those JSON routes can now require tokens.
|
|
1006
909
|
|
|
1007
|
-
|
|
1008
|
-
- Default folder: `<project-root>/uploads`
|
|
1009
|
-
- Change with `settings.uploads.dir` (relative or absolute path)
|
|
910
|
+
Choose the provider based on who is calling your app:
|
|
1010
911
|
|
|
1011
|
-
|
|
912
|
+
- `provider: 'jwt'`
|
|
913
|
+
Best for first-party apps you control.
|
|
914
|
+
You create your own login/token/refresh/logout routes and call `auth.issue(...)` yourself.
|
|
915
|
+
- `provider: 'oauth2'`
|
|
916
|
+
Best when you need a standard authorization server.
|
|
917
|
+
AegisNode mounts `/oauth/*` endpoints for you and supports `authorization_code` + PKCE, `client_credentials`, and `refresh_token`.
|
|
918
|
+
|
|
919
|
+
Quick decision guide:
|
|
920
|
+
- Use JWT when your own frontend/mobile app talks only to your backend.
|
|
921
|
+
- Use OAuth2 when external clients, partner apps, or machine-to-machine integrations need standard token flows.
|
|
922
|
+
- Use `auth.middleware()` to protect routes in both modes.
|
|
923
|
+
|
|
924
|
+
Enable auth in `settings.js`:
|
|
1012
925
|
|
|
1013
926
|
```js
|
|
1014
|
-
|
|
927
|
+
auth: {
|
|
1015
928
|
enabled: true,
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
929
|
+
provider: 'jwt', // or 'oauth2'
|
|
930
|
+
tablePrefix: 'aegisnode',
|
|
931
|
+
storage: {
|
|
932
|
+
// cache | memory | file | database
|
|
933
|
+
driver: 'cache',
|
|
934
|
+
filePath: 'storage/aegisnode-auth-store.json',
|
|
935
|
+
// Used by database driver for both SQL table and Mongo collection.
|
|
936
|
+
tableName: 'aegisnode_auth_store',
|
|
937
|
+
},
|
|
938
|
+
jwt: {
|
|
939
|
+
secret: 'replace-with-strong-secret',
|
|
940
|
+
algorithm: 'HS256',
|
|
941
|
+
expiresIn: '15m',
|
|
942
|
+
refreshExpiresIn: '7d',
|
|
943
|
+
issuer: 'blog',
|
|
944
|
+
audience: 'blog',
|
|
945
|
+
},
|
|
946
|
+
oauth2: {
|
|
947
|
+
accessTokenTtlSeconds: 3600,
|
|
948
|
+
refreshTokenTtlSeconds: 1209600,
|
|
949
|
+
authorizationCodeTtlSeconds: 600,
|
|
950
|
+
rotateRefreshToken: true,
|
|
951
|
+
requireClientSecret: true,
|
|
952
|
+
requirePkce: true,
|
|
953
|
+
allowPlainPkce: false,
|
|
954
|
+
grants: ['authorization_code', 'refresh_token', 'client_credentials'],
|
|
955
|
+
defaultScopes: [],
|
|
956
|
+
clientAuthMethod: 'client_secret_basic',
|
|
957
|
+
server: {
|
|
958
|
+
enabled: true,
|
|
959
|
+
basePath: '/oauth',
|
|
960
|
+
authorizePath: '/oauth/authorize',
|
|
961
|
+
tokenPath: '/oauth/token',
|
|
962
|
+
introspectionPath: '/oauth/introspect',
|
|
963
|
+
revocationPath: '/oauth/revoke',
|
|
964
|
+
metadataPath: '/.well-known/oauth-authorization-server',
|
|
965
|
+
issuer: '',
|
|
966
|
+
autoApprove: true,
|
|
967
|
+
requireAuthenticatedUser: true,
|
|
968
|
+
requireConsent: false,
|
|
969
|
+
allowHttp: false,
|
|
970
|
+
},
|
|
971
|
+
},
|
|
1026
972
|
},
|
|
1027
973
|
```
|
|
1028
974
|
|
|
1029
|
-
|
|
975
|
+
`tablePrefix` is used for auth storage names when enabled:
|
|
976
|
+
- `${tablePrefix}_users`
|
|
977
|
+
- `${tablePrefix}_jwt_revocations`
|
|
978
|
+
- `${tablePrefix}_oauth_clients`
|
|
979
|
+
- `${tablePrefix}_oauth_authorization_codes`
|
|
980
|
+
- `${tablePrefix}_oauth_access_tokens`
|
|
981
|
+
- `${tablePrefix}_oauth_refresh_tokens`
|
|
1030
982
|
|
|
1031
|
-
|
|
1032
|
-
|
|
983
|
+
By default, these names are used as key namespaces.
|
|
984
|
+
- With `storage.driver = 'cache'` or `memory`, they are in-memory/cache key prefixes.
|
|
985
|
+
- With `storage.driver = 'database'`, they are prefixes stored inside `auth.storage.tableName` (used as SQL table name or Mongo collection name).
|
|
1033
986
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
987
|
+
Restart behavior:
|
|
988
|
+
- `auth.enabled = true`: auth manager is available in context after restart.
|
|
989
|
+
- `auth.enabled = false`: auth manager stays safe; `auth.middleware()` returns `503` instead of crashing boot.
|
|
990
|
+
- `auth.storage.driver = 'file'`: OAuth2 clients/tokens and JWT revocations persist across restarts.
|
|
991
|
+
- `auth.storage.driver = 'database'`: OAuth2 clients/tokens and JWT revocations persist in your configured `database` backend.
|
|
1039
992
|
|
|
1040
|
-
|
|
1041
|
-
route.post('/gallery', route.upload.array('photos', 6), UsersView.uploadGallery);
|
|
993
|
+
JWT usage in routes:
|
|
1042
994
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
route.upload.fields([
|
|
1047
|
-
{ name: 'avatar', maxCount: 1 },
|
|
1048
|
-
{ name: 'docs', maxCount: 3 },
|
|
1049
|
-
]),
|
|
1050
|
-
UsersView.uploadDocuments,
|
|
1051
|
-
);
|
|
995
|
+
- JWT does not create login routes for you.
|
|
996
|
+
- You define the endpoints that authenticate users and issue tokens.
|
|
997
|
+
- This is usually the simplest choice for a private API used only by your own frontend/mobile app.
|
|
1052
998
|
|
|
1053
|
-
|
|
1054
|
-
|
|
999
|
+
```js
|
|
1000
|
+
export default {
|
|
1001
|
+
register(route) {
|
|
1002
|
+
const authGuard = (req, res, next) => req.aegis.auth.middleware()(req, res, next);
|
|
1055
1003
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1004
|
+
route.get('/auth/token', (req, res) => {
|
|
1005
|
+
const token = req.aegis.auth.issue({ subject: 'u1', scope: ['read:users'] });
|
|
1006
|
+
res.json({ token });
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
route.get('/auth/me', authGuard, (req, res) => {
|
|
1010
|
+
res.json({ user: req.auth });
|
|
1011
|
+
});
|
|
1058
1012
|
},
|
|
1059
1013
|
};
|
|
1060
1014
|
```
|
|
1061
1015
|
|
|
1062
|
-
`
|
|
1063
|
-
- `single()`: `req.file` + `req.body`
|
|
1064
|
-
- `array()`: `req.files` (array) + `req.body`
|
|
1065
|
-
- `fields()`: `req.files` object (`req.files.avatar`, `req.files.docs`, ...) + `req.body`
|
|
1016
|
+
OAuth2 built-in authorization server endpoints (when `auth.provider='oauth2'` and `auth.oauth2.server.enabled=true`):
|
|
1066
1017
|
|
|
1067
|
-
|
|
1018
|
+
- `GET /oauth/authorize`
|
|
1019
|
+
- `POST /oauth/authorize`
|
|
1020
|
+
- `POST /oauth/token`
|
|
1021
|
+
- `POST /oauth/introspect`
|
|
1022
|
+
- `POST /oauth/revoke`
|
|
1023
|
+
- `GET /.well-known/oauth-authorization-server`
|
|
1024
|
+
|
|
1025
|
+
Flows supported:
|
|
1026
|
+
- `authorization_code` (with PKCE)
|
|
1027
|
+
- `client_credentials`
|
|
1028
|
+
- `refresh_token`
|
|
1029
|
+
|
|
1030
|
+
OAuth2 is the better choice when:
|
|
1031
|
+
- you need standards-based client registration and token exchange,
|
|
1032
|
+
- you need machine clients as well as browser/mobile clients,
|
|
1033
|
+
- or third parties must integrate without depending on your custom JWT login route shape.
|
|
1034
|
+
|
|
1035
|
+
#### Route Usage (JWT vs OAuth2)
|
|
1036
|
+
|
|
1037
|
+
`startproject` gives you one root route file: `routes.js`.
|
|
1038
|
+
All your custom HTTP routes are defined there (or in app routes you mount with `route.use(...)`).
|
|
1068
1039
|
|
|
1069
1040
|
```js
|
|
1070
|
-
//
|
|
1071
|
-
import
|
|
1041
|
+
// routes.js
|
|
1042
|
+
import users from './apps/users/routes.js';
|
|
1072
1043
|
|
|
1073
1044
|
export default {
|
|
1074
|
-
appName: 'users',
|
|
1075
1045
|
register(route) {
|
|
1076
|
-
route.
|
|
1046
|
+
route.use('/users', users);
|
|
1047
|
+
|
|
1048
|
+
// Your custom auth/business routes
|
|
1049
|
+
route.post('/auth/login', (req, res) => {
|
|
1050
|
+
const token = req.aegis.auth.issue({ subject: 'u1' });
|
|
1051
|
+
res.json({ token });
|
|
1052
|
+
});
|
|
1077
1053
|
},
|
|
1078
1054
|
};
|
|
1079
1055
|
```
|
|
1080
1056
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
const { username, bio } = req.body;
|
|
1086
|
-
const avatar = req.file || null;
|
|
1057
|
+
How this behaves:
|
|
1058
|
+
- `provider: 'jwt'`: Aegis does not create JWT endpoints automatically. You define login/token/refresh/logout routes yourself in `routes.js` (or mounted app routes).
|
|
1059
|
+
- `provider: 'oauth2'`: Aegis auto-mounts OAuth2 server endpoints (`/oauth/authorize`, `/oauth/token`, `/oauth/introspect`, `/oauth/revoke`, metadata). You only define your own extra routes (for example admin client setup, protected APIs, business routes).
|
|
1060
|
+
- Do not reuse built-in OAuth2 endpoint paths for your own handlers when OAuth2 server is enabled.
|
|
1087
1061
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
size: avatar.size,
|
|
1096
|
-
path: avatar.path,
|
|
1097
|
-
} : null,
|
|
1098
|
-
});
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1062
|
+
Typical setup patterns:
|
|
1063
|
+
- API + JWT:
|
|
1064
|
+
`api.apps = ['users']`, `auth.provider = 'jwt'`, custom `/auth/login`, protect `/users/*` with `req.aegis.auth.middleware()`.
|
|
1065
|
+
- API + OAuth2:
|
|
1066
|
+
`api.apps = ['users']`, `auth.provider = 'oauth2'`, use built-in `/oauth/token`, protect `/users/*` with `req.aegis.auth.middleware()`.
|
|
1067
|
+
- Web app + JWT:
|
|
1068
|
+
no `api` block required if routes are normal form/web routes, but you can still use JWT for selected endpoints.
|
|
1101
1069
|
|
|
1102
|
-
|
|
1103
|
-
```
|
|
1070
|
+
#### OAuth2 Full Usage
|
|
1104
1071
|
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1072
|
+
1. Register clients (server-side only)
|
|
1073
|
+
|
|
1074
|
+
Register clients programmatically with `auth.registerClient(...)`.
|
|
1075
|
+
Do not expose this publicly in production without admin protection.
|
|
1076
|
+
|
|
1077
|
+
```js
|
|
1078
|
+
export default {
|
|
1079
|
+
register(route) {
|
|
1080
|
+
route.post('/admin/oauth/setup-clients', (req, res) => {
|
|
1081
|
+
const webClient = req.aegis.auth.registerClient({
|
|
1082
|
+
clientId: 'web',
|
|
1083
|
+
clientSecret: 'secret',
|
|
1084
|
+
redirectUris: ['https://client.example.com/callback'],
|
|
1085
|
+
grants: ['authorization_code', 'refresh_token'],
|
|
1086
|
+
scopes: ['read:users'],
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
const machineClient = req.aegis.auth.registerClient({
|
|
1090
|
+
clientId: 'machine',
|
|
1091
|
+
clientSecret: 'machine-secret',
|
|
1092
|
+
grants: ['client_credentials'],
|
|
1093
|
+
scopes: ['read:users'],
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
res.json({ webClient, machineClient });
|
|
1097
|
+
});
|
|
1098
|
+
},
|
|
1099
|
+
};
|
|
1113
1100
|
```
|
|
1114
1101
|
|
|
1115
|
-
|
|
1116
|
-
-
|
|
1117
|
-
-
|
|
1118
|
-
- `
|
|
1102
|
+
Notes:
|
|
1103
|
+
- Secret is stored hashed (scrypt).
|
|
1104
|
+
- Returned client object does not include the secret/hash.
|
|
1105
|
+
- `authorization_code` clients must have at least one `redirectUri`.
|
|
1119
1106
|
|
|
1120
|
-
|
|
1121
|
-
- If `uploads.enabled=false`, using `route.upload.*` throws at route registration.
|
|
1122
|
-
- For API mounts, multipart is allowed only when `uploads.allowApiMultipart=true`.
|
|
1123
|
-
- For non-API form submissions, CSRF token is required by default.
|
|
1107
|
+
2. Authorization Code + PKCE flow
|
|
1124
1108
|
|
|
1125
|
-
|
|
1109
|
+
Create PKCE verifier/challenge:
|
|
1126
1110
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1111
|
+
```js
|
|
1112
|
+
import crypto from 'crypto';
|
|
1129
1113
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
-
|
|
1133
|
-
|
|
1114
|
+
function b64url(buffer) {
|
|
1115
|
+
return Buffer.from(buffer).toString('base64')
|
|
1116
|
+
.replace(/\+/g, '-')
|
|
1117
|
+
.replace(/\//g, '_')
|
|
1118
|
+
.replace(/=+$/g, '');
|
|
1119
|
+
}
|
|
1134
1120
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
- `api` + OAuth2: third-party clients, machine-to-machine access, or standards-based authorization flows.
|
|
1121
|
+
const codeVerifier = b64url(crypto.randomBytes(48));
|
|
1122
|
+
const codeChallenge = b64url(crypto.createHash('sha256').update(codeVerifier).digest());
|
|
1123
|
+
```
|
|
1139
1124
|
|
|
1140
|
-
|
|
1125
|
+
Redirect user-agent to authorize endpoint:
|
|
1141
1126
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1127
|
+
```txt
|
|
1128
|
+
GET /oauth/authorize
|
|
1129
|
+
?response_type=code
|
|
1130
|
+
&client_id=web
|
|
1131
|
+
&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback
|
|
1132
|
+
&scope=read%3Ausers
|
|
1133
|
+
&state=abc123
|
|
1134
|
+
&code_challenge=<CODE_CHALLENGE>
|
|
1135
|
+
&code_challenge_method=S256
|
|
1136
|
+
```
|
|
1147
1137
|
|
|
1148
|
-
|
|
1138
|
+
The server redirects back to:
|
|
1149
1139
|
|
|
1150
|
-
```
|
|
1151
|
-
|
|
1152
|
-
apps: [
|
|
1153
|
-
{ name: 'users', mount: '/users' },
|
|
1154
|
-
],
|
|
1155
|
-
api: {
|
|
1156
|
-
apps: ['users'],
|
|
1157
|
-
disableCsrf: true,
|
|
1158
|
-
requireJsonForUnsafeMethods: true,
|
|
1159
|
-
noStoreHeaders: true,
|
|
1160
|
-
},
|
|
1161
|
-
};
|
|
1140
|
+
```txt
|
|
1141
|
+
https://client.example.com/callback?code=<AUTH_CODE>&state=abc123
|
|
1162
1142
|
```
|
|
1163
1143
|
|
|
1164
|
-
|
|
1144
|
+
Exchange code for tokens:
|
|
1165
1145
|
|
|
1166
|
-
```
|
|
1167
|
-
|
|
1146
|
+
```bash
|
|
1147
|
+
curl -X POST http://127.0.0.1:3000/oauth/token \
|
|
1148
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
1149
|
+
-u web:secret \
|
|
1150
|
+
-d "grant_type=authorization_code" \
|
|
1151
|
+
-d "code=<AUTH_CODE>" \
|
|
1152
|
+
-d "redirect_uri=https://client.example.com/callback" \
|
|
1153
|
+
-d "code_verifier=<CODE_VERIFIER>"
|
|
1154
|
+
```
|
|
1168
1155
|
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1156
|
+
Response shape:
|
|
1157
|
+
|
|
1158
|
+
```json
|
|
1159
|
+
{
|
|
1160
|
+
"access_token": "...",
|
|
1161
|
+
"token_type": "Bearer",
|
|
1162
|
+
"expires_in": 3600,
|
|
1163
|
+
"scope": "read:users",
|
|
1164
|
+
"refresh_token": "...",
|
|
1165
|
+
"refresh_expires_in": 1209600
|
|
1166
|
+
}
|
|
1174
1167
|
```
|
|
1175
1168
|
|
|
1176
|
-
|
|
1169
|
+
3. Protect API routes with OAuth2 access tokens
|
|
1177
1170
|
|
|
1178
1171
|
```js
|
|
1179
|
-
import UsersView from './views.js';
|
|
1180
|
-
|
|
1181
1172
|
export default {
|
|
1182
|
-
appName: 'users',
|
|
1183
1173
|
register(route) {
|
|
1184
|
-
|
|
1185
|
-
route.
|
|
1174
|
+
const authGuard = (req, res, next) => req.aegis.auth.middleware()(req, res, next);
|
|
1175
|
+
route.get('/users/me', authGuard, (req, res) => {
|
|
1176
|
+
res.json({
|
|
1177
|
+
sub: req.auth.sub || null,
|
|
1178
|
+
clientId: req.auth.clientId,
|
|
1179
|
+
scope: req.auth.scope,
|
|
1180
|
+
});
|
|
1181
|
+
});
|
|
1186
1182
|
},
|
|
1187
1183
|
};
|
|
1188
1184
|
```
|
|
1189
1185
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
```js
|
|
1193
|
-
class UsersView {
|
|
1194
|
-
static async index({ service }, req, res, next) {
|
|
1195
|
-
try {
|
|
1196
|
-
const users = await service.list();
|
|
1197
|
-
res.json({ data: users });
|
|
1198
|
-
} catch (error) {
|
|
1199
|
-
next(error);
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
static async create({ service, validator }, req, res, next) {
|
|
1204
|
-
try {
|
|
1205
|
-
const payload = validator.create(req.body || {});
|
|
1206
|
-
const created = await service.create(payload);
|
|
1207
|
-
res.status(201).json({ data: created });
|
|
1208
|
-
} catch (error) {
|
|
1209
|
-
next(error);
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1186
|
+
Use token:
|
|
1213
1187
|
|
|
1214
|
-
|
|
1188
|
+
```bash
|
|
1189
|
+
curl http://127.0.0.1:3000/users/me \
|
|
1190
|
+
-H "Authorization: Bearer <ACCESS_TOKEN>"
|
|
1215
1191
|
```
|
|
1216
1192
|
|
|
1217
|
-
|
|
1193
|
+
4. Refresh token flow
|
|
1218
1194
|
|
|
1219
1195
|
```bash
|
|
1220
|
-
curl http://127.0.0.1:3000/
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
-
|
|
1224
|
-
-d
|
|
1196
|
+
curl -X POST http://127.0.0.1:3000/oauth/token \
|
|
1197
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
1198
|
+
-u web:secret \
|
|
1199
|
+
-d "grant_type=refresh_token" \
|
|
1200
|
+
-d "refresh_token=<REFRESH_TOKEN>"
|
|
1225
1201
|
```
|
|
1226
1202
|
|
|
1227
|
-
|
|
1228
|
-
- `POST`, `PUT`, `PATCH`, and `DELETE` with a request body must use `application/json` when `requireJsonForUnsafeMethods: true`.
|
|
1229
|
-
- `multipart/form-data` is still allowed for API mounts when `uploads.allowApiMultipart: true`.
|
|
1230
|
-
- CSRF is skipped only for configured API app mounts when `disableCsrf: true`.
|
|
1231
|
-
- API responses get `Cache-Control: no-store` when `noStoreHeaders: true`.
|
|
1203
|
+
5. Client Credentials flow (machine-to-machine)
|
|
1232
1204
|
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
-
|
|
1236
|
-
-
|
|
1205
|
+
```bash
|
|
1206
|
+
curl -X POST http://127.0.0.1:3000/oauth/token \
|
|
1207
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
1208
|
+
-u machine:machine-secret \
|
|
1209
|
+
-d "grant_type=client_credentials" \
|
|
1210
|
+
-d "scope=read:users"
|
|
1211
|
+
```
|
|
1237
1212
|
|
|
1238
|
-
|
|
1213
|
+
This returns access token only (no refresh token).
|
|
1239
1214
|
|
|
1240
|
-
|
|
1215
|
+
6. Introspection and revocation
|
|
1241
1216
|
|
|
1242
|
-
|
|
1243
|
-
- `auth` adds token issuance, token verification, route protection, client registration, and revocation/introspection behavior.
|
|
1217
|
+
Introspection:
|
|
1244
1218
|
|
|
1245
|
-
|
|
1246
|
-
-
|
|
1247
|
-
-
|
|
1248
|
-
-
|
|
1219
|
+
```bash
|
|
1220
|
+
curl -X POST http://127.0.0.1:3000/oauth/introspect \
|
|
1221
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
1222
|
+
-u web:secret \
|
|
1223
|
+
-d "token=<ACCESS_TOKEN>"
|
|
1224
|
+
```
|
|
1249
1225
|
|
|
1250
|
-
|
|
1251
|
-
- If you only need JSON routes, use `api`.
|
|
1252
|
-
- If you need authenticated access, add `auth`.
|
|
1253
|
-
- If outside clients need a standard auth protocol, choose OAuth2 instead of rolling custom JWT login flows.
|
|
1226
|
+
Revocation:
|
|
1254
1227
|
|
|
1255
|
-
|
|
1228
|
+
```bash
|
|
1229
|
+
curl -X POST http://127.0.0.1:3000/oauth/revoke \
|
|
1230
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
1231
|
+
-u web:secret \
|
|
1232
|
+
-d "token=<ACCESS_OR_REFRESH_TOKEN>"
|
|
1233
|
+
```
|
|
1256
1234
|
|
|
1257
|
-
|
|
1258
|
-
| --- | --- | --- |
|
|
1259
|
-
| `api` only | You need JSON endpoints without token auth. Good for public read APIs or trusted internal services. | `api.apps = [...]` |
|
|
1260
|
-
| `api` + JWT | Your own frontend/mobile app talks to your backend and you control both sides. | `api.apps = [...]`, `auth.enabled = true`, `auth.provider = 'jwt'`, plus your own login/token routes |
|
|
1261
|
-
| `api` + OAuth2 | External clients, partner apps, or machine clients need standard token flows. | `api.apps = [...]`, `auth.enabled = true`, `auth.provider = 'oauth2'` |
|
|
1235
|
+
7. Custom subject/consent resolution
|
|
1262
1236
|
|
|
1263
|
-
|
|
1237
|
+
By default, `/oauth/authorize` resolves subject from:
|
|
1238
|
+
- `req.user.id`
|
|
1239
|
+
- `req.user.sub`
|
|
1240
|
+
- `req.auth.sub`
|
|
1241
|
+
- `subject`/`user_id` query/body params
|
|
1264
1242
|
|
|
1265
|
-
|
|
1243
|
+
You can override with hooks in `settings.js`:
|
|
1266
1244
|
|
|
1267
1245
|
```js
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
// Mongo example:
|
|
1280
|
-
// connectionString: 'mongodb://localhost:27017/appdb',
|
|
1281
|
-
// or:
|
|
1282
|
-
// server: 'localhost',
|
|
1283
|
-
// port: 27017,
|
|
1284
|
-
// database: 'appdb',
|
|
1285
|
-
// user: 'mongo_user',
|
|
1286
|
-
// password: 'mongo_pass',
|
|
1246
|
+
auth: {
|
|
1247
|
+
provider: 'oauth2',
|
|
1248
|
+
oauth2: {
|
|
1249
|
+
server: {
|
|
1250
|
+
resolveSubject: ({ req }) => req.user?.id || '',
|
|
1251
|
+
resolveConsent: ({ req, client, subject }) => {
|
|
1252
|
+
// return true to approve, false to deny
|
|
1253
|
+
return true;
|
|
1254
|
+
},
|
|
1255
|
+
},
|
|
1287
1256
|
},
|
|
1288
|
-
options: {},
|
|
1289
1257
|
},
|
|
1290
1258
|
```
|
|
1291
1259
|
|
|
1292
|
-
|
|
1293
|
-
- `database.uri` is still accepted for MongoDB, but `database.config.connectionString` is preferred.
|
|
1260
|
+
8. Production checklist
|
|
1294
1261
|
|
|
1295
|
-
|
|
1296
|
-
-
|
|
1262
|
+
- Keep `auth.oauth2.server.allowHttp = false` (default).
|
|
1263
|
+
- Use HTTPS with trusted reverse proxy config.
|
|
1264
|
+
- Keep `requirePkce = true`.
|
|
1265
|
+
- Use strong client secrets and rotate regularly.
|
|
1266
|
+
- Restrict client setup endpoints to admins only.
|
|
1267
|
+
- Set explicit `defaultScopes` and per-client scopes.
|
|
1268
|
+
- Set `issuer` to your public auth server URL.
|
|
1297
1269
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
}
|
|
1270
|
+
Implementation notes:
|
|
1271
|
+
- OAuth2 server in AegisNode is framework-native (custom implementation).
|
|
1272
|
+
- CSRF checks are skipped for OAuth2 server endpoints (`/oauth/*` + metadata) by design.
|
|
1273
|
+
- This is OAuth2 (not OpenID Connect); no `id_token` endpoint/flow.
|
|
1303
1274
|
|
|
1304
|
-
|
|
1305
|
-
return this.db.table('users').select(['id', 'name']).get();
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
```
|
|
1275
|
+
### Swagger (OpenAPI UI)
|
|
1309
1276
|
|
|
1310
|
-
|
|
1311
|
-
- Use built-in helper `helpers.toObjectId(...)` before filtering on `_id`.
|
|
1312
|
-
- Validate with `helpers.isObjectId(...)` when needed.
|
|
1313
|
-
- Keep this conversion in model/service layer.
|
|
1314
|
-
- If your collection stores string `_id` values (not native Mongo `ObjectId`), skip conversion.
|
|
1277
|
+
Enable Swagger in `settings.js`:
|
|
1315
1278
|
|
|
1316
1279
|
```js
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
const _id = this.helpers.toObjectId(id);
|
|
1325
|
-
if (!_id) throw new Error('Invalid Mongo ObjectId');
|
|
1326
|
-
return this.db.table('users').where('_id', '=', _id).first();
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1280
|
+
swagger: {
|
|
1281
|
+
enabled: true,
|
|
1282
|
+
docsPath: '/docs',
|
|
1283
|
+
jsonPath: '/openapi.json',
|
|
1284
|
+
documentPath: 'openapi.json',
|
|
1285
|
+
explorer: true,
|
|
1286
|
+
},
|
|
1329
1287
|
```
|
|
1330
1288
|
|
|
1331
|
-
|
|
1289
|
+
Behavior:
|
|
1290
|
+
- UI available at `docsPath` (default `/docs`).
|
|
1291
|
+
- OpenAPI JSON available at `jsonPath` (default `/openapi.json`).
|
|
1292
|
+
- If `openapi.json` exists in project root, it is loaded.
|
|
1293
|
+
- If no file is found, AegisNode serves a default minimal OpenAPI document.
|
|
1332
1294
|
|
|
1333
|
-
|
|
1295
|
+
### Templates (EJS + base.ejs)
|
|
1296
|
+
|
|
1297
|
+
Set template config in `settings.js`:
|
|
1334
1298
|
|
|
1335
1299
|
```js
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
},
|
|
1345
|
-
},
|
|
1346
|
-
environments: {
|
|
1347
|
-
development: {
|
|
1348
|
-
logging: { level: 'debug' },
|
|
1349
|
-
},
|
|
1350
|
-
production: {
|
|
1351
|
-
logging: { level: 'warn' },
|
|
1352
|
-
security: { ddos: { maxRequests: 80 } },
|
|
1353
|
-
},
|
|
1300
|
+
templates: {
|
|
1301
|
+
enabled: true,
|
|
1302
|
+
engine: 'ejs',
|
|
1303
|
+
dir: 'templates',
|
|
1304
|
+
base: 'base',
|
|
1305
|
+
appBases: {
|
|
1306
|
+
users: 'users/base',
|
|
1307
|
+
admin: 'admin/base',
|
|
1354
1308
|
},
|
|
1355
|
-
}
|
|
1309
|
+
}
|
|
1356
1310
|
```
|
|
1357
1311
|
|
|
1358
|
-
|
|
1359
|
-
- Base config is loaded first.
|
|
1360
|
-
- `environments.default` is applied (if present).
|
|
1361
|
-
- `environments[env]` is applied last.
|
|
1362
|
-
- `env` comes from `settings.env` (fallback: `NODE_ENV`, then `development`).
|
|
1363
|
-
|
|
1364
|
-
### Auth (JWT Or OAuth2)
|
|
1365
|
-
|
|
1366
|
-
`auth` is independent from `api`.
|
|
1367
|
-
You can protect normal web routes, API routes, or both.
|
|
1368
|
-
If your app is already listed in `api.apps`, adding `auth` simply means those JSON routes can now require tokens.
|
|
1312
|
+
Then in a route handler:
|
|
1369
1313
|
|
|
1370
|
-
|
|
1314
|
+
```js
|
|
1315
|
+
route.get('/', (req, res) => {
|
|
1316
|
+
res.render('home', {
|
|
1317
|
+
title: 'Home',
|
|
1318
|
+
message: 'Welcome',
|
|
1319
|
+
});
|
|
1320
|
+
});
|
|
1321
|
+
```
|
|
1371
1322
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
You create your own login/token/refresh/logout routes and call `auth.issue(...)` yourself.
|
|
1375
|
-
- `provider: 'oauth2'`
|
|
1376
|
-
Best when you need a standard authorization server.
|
|
1377
|
-
AegisNode mounts `/oauth/*` endpoints for you and supports `authorization_code` + PKCE, `client_credentials`, and `refresh_token`.
|
|
1323
|
+
`home.ejs` is rendered first, then injected into `base.ejs`.
|
|
1324
|
+
Use `<%- content %>` (or `<%- body %>`) in your `base.ejs` to print page content.
|
|
1378
1325
|
|
|
1379
|
-
|
|
1380
|
-
- Use JWT when your own frontend/mobile app talks only to your backend.
|
|
1381
|
-
- Use OAuth2 when external clients, partner apps, or machine-to-machine integrations need standard token flows.
|
|
1382
|
-
- Use `auth.middleware()` to protect routes in both modes.
|
|
1326
|
+
### Internationalization (i18n)
|
|
1383
1327
|
|
|
1384
|
-
|
|
1328
|
+
Configure i18n in `settings.js`:
|
|
1385
1329
|
|
|
1386
1330
|
```js
|
|
1387
|
-
|
|
1331
|
+
i18n: {
|
|
1388
1332
|
enabled: true,
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
issuer: 'blog',
|
|
1404
|
-
audience: 'blog',
|
|
1405
|
-
},
|
|
1406
|
-
oauth2: {
|
|
1407
|
-
accessTokenTtlSeconds: 3600,
|
|
1408
|
-
refreshTokenTtlSeconds: 1209600,
|
|
1409
|
-
authorizationCodeTtlSeconds: 600,
|
|
1410
|
-
rotateRefreshToken: true,
|
|
1411
|
-
requireClientSecret: true,
|
|
1412
|
-
requirePkce: true,
|
|
1413
|
-
allowPlainPkce: false,
|
|
1414
|
-
grants: ['authorization_code', 'refresh_token', 'client_credentials'],
|
|
1415
|
-
defaultScopes: [],
|
|
1416
|
-
clientAuthMethod: 'client_secret_basic',
|
|
1417
|
-
server: {
|
|
1418
|
-
enabled: true,
|
|
1419
|
-
basePath: '/oauth',
|
|
1420
|
-
authorizePath: '/oauth/authorize',
|
|
1421
|
-
tokenPath: '/oauth/token',
|
|
1422
|
-
introspectionPath: '/oauth/introspect',
|
|
1423
|
-
revocationPath: '/oauth/revoke',
|
|
1424
|
-
metadataPath: '/.well-known/oauth-authorization-server',
|
|
1425
|
-
issuer: '',
|
|
1426
|
-
autoApprove: true,
|
|
1427
|
-
requireAuthenticatedUser: true,
|
|
1428
|
-
requireConsent: false,
|
|
1429
|
-
allowHttp: false,
|
|
1333
|
+
defaultLocale: 'en',
|
|
1334
|
+
fallbackLocale: 'en',
|
|
1335
|
+
supported: ['en', 'fr'],
|
|
1336
|
+
queryParam: 'lang',
|
|
1337
|
+
translations: {
|
|
1338
|
+
en: {
|
|
1339
|
+
home: {
|
|
1340
|
+
title: 'Welcome {name}',
|
|
1341
|
+
},
|
|
1342
|
+
},
|
|
1343
|
+
fr: {
|
|
1344
|
+
home: {
|
|
1345
|
+
title: 'Bienvenue {name}',
|
|
1346
|
+
},
|
|
1430
1347
|
},
|
|
1431
1348
|
},
|
|
1432
|
-
}
|
|
1349
|
+
}
|
|
1433
1350
|
```
|
|
1434
1351
|
|
|
1435
|
-
|
|
1436
|
-
- `${tablePrefix}_users`
|
|
1437
|
-
- `${tablePrefix}_jwt_revocations`
|
|
1438
|
-
- `${tablePrefix}_oauth_clients`
|
|
1439
|
-
- `${tablePrefix}_oauth_authorization_codes`
|
|
1440
|
-
- `${tablePrefix}_oauth_access_tokens`
|
|
1441
|
-
- `${tablePrefix}_oauth_refresh_tokens`
|
|
1442
|
-
|
|
1443
|
-
By default, these names are used as key namespaces.
|
|
1444
|
-
- With `storage.driver = 'cache'` or `memory`, they are in-memory/cache key prefixes.
|
|
1445
|
-
- With `storage.driver = 'database'`, they are prefixes stored inside `auth.storage.tableName` (used as SQL table name or Mongo collection name).
|
|
1446
|
-
|
|
1447
|
-
Restart behavior:
|
|
1448
|
-
- `auth.enabled = true`: auth manager is available in context after restart.
|
|
1449
|
-
- `auth.enabled = false`: auth manager stays safe; `auth.middleware()` returns `503` instead of crashing boot.
|
|
1450
|
-
- `auth.storage.driver = 'file'`: OAuth2 clients/tokens and JWT revocations persist across restarts.
|
|
1451
|
-
- `auth.storage.driver = 'database'`: OAuth2 clients/tokens and JWT revocations persist in your configured `database` backend.
|
|
1452
|
-
|
|
1453
|
-
JWT usage in routes:
|
|
1454
|
-
|
|
1455
|
-
- JWT does not create login routes for you.
|
|
1456
|
-
- You define the endpoints that authenticate users and issue tokens.
|
|
1457
|
-
- This is usually the simplest choice for a private API used only by your own frontend/mobile app.
|
|
1352
|
+
You can also load locale JSON files directly (no import needed):
|
|
1458
1353
|
|
|
1459
1354
|
```js
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
});
|
|
1468
|
-
|
|
1469
|
-
route.get('/auth/me', authGuard, (req, res) => {
|
|
1470
|
-
res.json({ user: req.auth });
|
|
1471
|
-
});
|
|
1355
|
+
i18n: {
|
|
1356
|
+
enabled: true,
|
|
1357
|
+
defaultLocale: 'en',
|
|
1358
|
+
supported: ['en', 'fr'],
|
|
1359
|
+
translations: {
|
|
1360
|
+
en: 'locales/en.json',
|
|
1361
|
+
fr: 'locales/fr.json',
|
|
1472
1362
|
},
|
|
1473
|
-
|
|
1363
|
+
// optional single-file source (inline `translations` wins per locale key):
|
|
1364
|
+
// translationsFile: 'locales/all.json',
|
|
1365
|
+
}
|
|
1474
1366
|
```
|
|
1475
1367
|
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
- `GET /oauth/authorize`
|
|
1479
|
-
- `POST /oauth/authorize`
|
|
1480
|
-
- `POST /oauth/token`
|
|
1481
|
-
- `POST /oauth/introspect`
|
|
1482
|
-
- `POST /oauth/revoke`
|
|
1483
|
-
- `GET /.well-known/oauth-authorization-server`
|
|
1484
|
-
|
|
1485
|
-
Flows supported:
|
|
1486
|
-
- `authorization_code` (with PKCE)
|
|
1487
|
-
- `client_credentials`
|
|
1488
|
-
- `refresh_token`
|
|
1489
|
-
|
|
1490
|
-
OAuth2 is the better choice when:
|
|
1491
|
-
- you need standards-based client registration and token exchange,
|
|
1492
|
-
- you need machine clients as well as browser/mobile clients,
|
|
1493
|
-
- or third parties must integrate without depending on your custom JWT login route shape.
|
|
1494
|
-
|
|
1495
|
-
#### Route Usage (JWT vs OAuth2)
|
|
1496
|
-
|
|
1497
|
-
`startproject` gives you one root route file: `routes.js`.
|
|
1498
|
-
All your custom HTTP routes are defined there (or in app routes you mount with `route.use(...)`).
|
|
1368
|
+
Route usage:
|
|
1499
1369
|
|
|
1500
1370
|
```js
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
// Your custom auth/business routes
|
|
1509
|
-
route.post('/auth/login', (req, res) => {
|
|
1510
|
-
const token = req.aegis.auth.issue({ subject: 'u1' });
|
|
1511
|
-
res.json({ token });
|
|
1512
|
-
});
|
|
1513
|
-
},
|
|
1514
|
-
};
|
|
1371
|
+
route.get('/i18n-demo', (req, res) => {
|
|
1372
|
+
// Auto-detected from query/cookie/header.
|
|
1373
|
+
res.json({
|
|
1374
|
+
locale: req.aegis.locale,
|
|
1375
|
+
title: req.aegis.t('home.title', { name: 'Jason' }),
|
|
1376
|
+
});
|
|
1377
|
+
});
|
|
1515
1378
|
```
|
|
1516
1379
|
|
|
1517
|
-
|
|
1518
|
-
- `provider: 'jwt'`: Aegis does not create JWT endpoints automatically. You define login/token/refresh/logout routes yourself in `routes.js` (or mounted app routes).
|
|
1519
|
-
- `provider: 'oauth2'`: Aegis auto-mounts OAuth2 server endpoints (`/oauth/authorize`, `/oauth/token`, `/oauth/introspect`, `/oauth/revoke`, metadata). You only define your own extra routes (for example admin client setup, protected APIs, business routes).
|
|
1520
|
-
- Do not reuse built-in OAuth2 endpoint paths for your own handlers when OAuth2 server is enabled.
|
|
1380
|
+
Choosing the API:
|
|
1521
1381
|
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
-
|
|
1526
|
-
|
|
1527
|
-
-
|
|
1528
|
-
no `api` block required if routes are normal form/web routes, but you can still use JWT for selected endpoints.
|
|
1382
|
+
- `req.aegis.t('home.title')`
|
|
1383
|
+
Shortcut for `req.aegis.i18n.t('home.title')`. Use this in routes/views when you only need a translated string.
|
|
1384
|
+
- `req.aegis.i18n`
|
|
1385
|
+
Request-scoped i18n object. Use this when you also need locale metadata or helpers such as `locale`, `localeSource`, `setLocale(...)`, `resolveLocale(...)`, or `forLocale(...)`.
|
|
1386
|
+
- Injected `i18n` in handlers/services/models/validators/controllers/subscribers/loaders
|
|
1387
|
+
Runtime-injected i18n bridge. During an HTTP request, `i18n.t(...)` resolves with the same active locale as `req.aegis.i18n.t(...)`, so the translation result is the same.
|
|
1529
1388
|
|
|
1530
|
-
|
|
1389
|
+
Important differences:
|
|
1531
1390
|
|
|
1532
|
-
|
|
1391
|
+
- `req.aegis.t` and `req.aegis.i18n.t` return the same translation for the current request.
|
|
1392
|
+
- Injected `i18n.t(...)` in a service/model/validator/subscriber is not the same object as `req.aegis.i18n`, but during a request it produces the same translation result for the same key/options.
|
|
1393
|
+
- Outside a request, injected `i18n.t(...)` falls back to `defaultLocale`.
|
|
1394
|
+
- In background jobs, loaders, or boot-time code, pass an explicit locale when needed: `i18n.t('home.title', { name: 'Jason' }, { locale: 'fr' })`.
|
|
1533
1395
|
|
|
1534
|
-
|
|
1535
|
-
Do not expose this publicly in production without admin protection.
|
|
1396
|
+
Service/model usage:
|
|
1536
1397
|
|
|
1537
1398
|
```js
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
clientId: 'web',
|
|
1543
|
-
clientSecret: 'secret',
|
|
1544
|
-
redirectUris: ['https://client.example.com/callback'],
|
|
1545
|
-
grants: ['authorization_code', 'refresh_token'],
|
|
1546
|
-
scopes: ['read:users'],
|
|
1547
|
-
});
|
|
1548
|
-
|
|
1549
|
-
const machineClient = req.aegis.auth.registerClient({
|
|
1550
|
-
clientId: 'machine',
|
|
1551
|
-
clientSecret: 'machine-secret',
|
|
1552
|
-
grants: ['client_credentials'],
|
|
1553
|
-
scopes: ['read:users'],
|
|
1554
|
-
});
|
|
1399
|
+
class UsersService {
|
|
1400
|
+
constructor({ i18n }) {
|
|
1401
|
+
this.i18n = i18n;
|
|
1402
|
+
}
|
|
1555
1403
|
|
|
1556
|
-
|
|
1557
|
-
});
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1404
|
+
greeting(name) {
|
|
1405
|
+
return this.i18n.t('home.title', { name });
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1560
1408
|
```
|
|
1561
1409
|
|
|
1562
|
-
|
|
1563
|
-
- Secret is stored hashed (scrypt).
|
|
1564
|
-
- Returned client object does not include the secret/hash.
|
|
1565
|
-
- `authorization_code` clients must have at least one `redirectUri`.
|
|
1566
|
-
|
|
1567
|
-
2. Authorization Code + PKCE flow
|
|
1568
|
-
|
|
1569
|
-
Create PKCE verifier/challenge:
|
|
1570
|
-
|
|
1571
|
-
```js
|
|
1572
|
-
import crypto from 'crypto';
|
|
1573
|
-
|
|
1574
|
-
function b64url(buffer) {
|
|
1575
|
-
return Buffer.from(buffer).toString('base64')
|
|
1576
|
-
.replace(/\+/g, '-')
|
|
1577
|
-
.replace(/\//g, '_')
|
|
1578
|
-
.replace(/=+$/g, '');
|
|
1579
|
-
}
|
|
1410
|
+
Template usage:
|
|
1580
1411
|
|
|
1581
|
-
|
|
1582
|
-
|
|
1412
|
+
```ejs
|
|
1413
|
+
<html lang="<%= locale %>">
|
|
1414
|
+
<body>
|
|
1415
|
+
<h1><%= t('home.title', { name: 'Jason' }) %></h1>
|
|
1416
|
+
</body>
|
|
1417
|
+
</html>
|
|
1583
1418
|
```
|
|
1584
1419
|
|
|
1585
|
-
|
|
1420
|
+
Manual locale switch inside a request:
|
|
1586
1421
|
|
|
1587
|
-
```
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
&scope=read%3Ausers
|
|
1593
|
-
&state=abc123
|
|
1594
|
-
&code_challenge=<CODE_CHALLENGE>
|
|
1595
|
-
&code_challenge_method=S256
|
|
1422
|
+
```js
|
|
1423
|
+
route.get('/fr', (req, res) => {
|
|
1424
|
+
req.aegis.setLocale('fr'); // persists in i18n cookie by default
|
|
1425
|
+
res.send(req.aegis.t('home.title', { name: 'Jason' }));
|
|
1426
|
+
});
|
|
1596
1427
|
```
|
|
1597
1428
|
|
|
1598
|
-
|
|
1429
|
+
Persist user-selected language as default:
|
|
1599
1430
|
|
|
1600
|
-
```
|
|
1601
|
-
|
|
1431
|
+
```js
|
|
1432
|
+
route.post('/lang', (req, res) => {
|
|
1433
|
+
const selected = String(req.body?.lang || '').trim();
|
|
1434
|
+
req.aegis.setLocale(selected); // writes i18n cookie (aegis_locale by default)
|
|
1435
|
+
res.redirect('back');
|
|
1436
|
+
});
|
|
1602
1437
|
```
|
|
1603
1438
|
|
|
1604
|
-
|
|
1439
|
+
Language picker template (keeps selected option):
|
|
1605
1440
|
|
|
1606
|
-
```
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1441
|
+
```ejs
|
|
1442
|
+
<form method="post" action="/lang">
|
|
1443
|
+
<select name="lang">
|
|
1444
|
+
<option value="en" <%= locale === 'en' ? 'selected' : '' %>>English</option>
|
|
1445
|
+
<option value="fr" <%= locale === 'fr' ? 'selected' : '' %>>Français</option>
|
|
1446
|
+
</select>
|
|
1447
|
+
<button type="submit">Change</button>
|
|
1448
|
+
</form>
|
|
1614
1449
|
```
|
|
1615
1450
|
|
|
1616
|
-
|
|
1451
|
+
Notes:
|
|
1452
|
+
- `defaultLocale` is used only when user has no saved locale.
|
|
1453
|
+
- After selection, cookie locale becomes the default for that user on next requests.
|
|
1454
|
+
- `?lang=fr` also persists automatically when `detectFromQuery` is enabled.
|
|
1455
|
+
- Templates get `t`, `locale`, and `i18n` in locals.
|
|
1617
1456
|
|
|
1618
|
-
|
|
1619
|
-
{
|
|
1620
|
-
"access_token": "...",
|
|
1621
|
-
"token_type": "Bearer",
|
|
1622
|
-
"expires_in": 3600,
|
|
1623
|
-
"scope": "read:users",
|
|
1624
|
-
"refresh_token": "...",
|
|
1625
|
-
"refresh_expires_in": 1209600
|
|
1626
|
-
}
|
|
1627
|
-
```
|
|
1457
|
+
### Mail
|
|
1628
1458
|
|
|
1629
|
-
|
|
1459
|
+
Configure mail transport in `settings.js`:
|
|
1630
1460
|
|
|
1631
1461
|
```js
|
|
1632
1462
|
export default {
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1463
|
+
mail: {
|
|
1464
|
+
enabled: true,
|
|
1465
|
+
defaults: {
|
|
1466
|
+
from: 'noreply@example.com',
|
|
1467
|
+
replyTo: 'support@example.com',
|
|
1468
|
+
},
|
|
1469
|
+
transport: {
|
|
1470
|
+
host: process.env.SMTP_HOST,
|
|
1471
|
+
port: process.env.SMTP_PORT ? Number(process.env.SMTP_PORT) : 587,
|
|
1472
|
+
secure: false,
|
|
1473
|
+
auth: {
|
|
1474
|
+
user: process.env.SMTP_USER,
|
|
1475
|
+
pass: process.env.SMTP_PASS,
|
|
1476
|
+
},
|
|
1477
|
+
},
|
|
1478
|
+
verifyOnStartup: true,
|
|
1642
1479
|
},
|
|
1643
1480
|
};
|
|
1644
1481
|
```
|
|
1645
1482
|
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
```bash
|
|
1649
|
-
curl http://127.0.0.1:3000/users/me \
|
|
1650
|
-
-H "Authorization: Bearer <ACCESS_TOKEN>"
|
|
1651
|
-
```
|
|
1483
|
+
Available mail APIs:
|
|
1652
1484
|
|
|
1653
|
-
|
|
1485
|
+
- Injected `mail` in handlers/services/models/validators/controllers/subscribers/loaders
|
|
1486
|
+
Shared runtime mail manager. Use `mail.send(...)` or `mail.sendMail(...)`.
|
|
1487
|
+
- `req.aegis.mail`
|
|
1488
|
+
Request bridge to the same mail manager used in handler context.
|
|
1654
1489
|
|
|
1655
|
-
|
|
1656
|
-
curl -X POST http://127.0.0.1:3000/oauth/token \
|
|
1657
|
-
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
1658
|
-
-u web:secret \
|
|
1659
|
-
-d "grant_type=refresh_token" \
|
|
1660
|
-
-d "refresh_token=<REFRESH_TOKEN>"
|
|
1661
|
-
```
|
|
1490
|
+
Handler usage:
|
|
1662
1491
|
|
|
1663
|
-
|
|
1492
|
+
```js
|
|
1493
|
+
route.post('/contact', async ({ mail }, req, res, next) => {
|
|
1494
|
+
try {
|
|
1495
|
+
const info = await mail.send({
|
|
1496
|
+
to: 'support@example.com',
|
|
1497
|
+
subject: 'Contact form',
|
|
1498
|
+
text: req.body?.message || '',
|
|
1499
|
+
html: `<p>${req.body?.message || ''}</p>`,
|
|
1500
|
+
});
|
|
1664
1501
|
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
-d "scope=read:users"
|
|
1502
|
+
res.status(202).json({ messageId: info.messageId });
|
|
1503
|
+
} catch (error) {
|
|
1504
|
+
next(error);
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1671
1507
|
```
|
|
1672
1508
|
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
6. Introspection and revocation
|
|
1509
|
+
Service usage:
|
|
1676
1510
|
|
|
1677
|
-
|
|
1511
|
+
```js
|
|
1512
|
+
class UsersService {
|
|
1513
|
+
constructor({ mail }) {
|
|
1514
|
+
this.mail = mail;
|
|
1515
|
+
}
|
|
1678
1516
|
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1517
|
+
async sendWelcome(user) {
|
|
1518
|
+
return this.mail.send({
|
|
1519
|
+
to: user.email,
|
|
1520
|
+
subject: 'Welcome',
|
|
1521
|
+
html: `<h1>Hello ${user.name}</h1><p>Your account is ready.</p>`,
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1684
1525
|
```
|
|
1685
1526
|
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
-u web:secret \
|
|
1692
|
-
-d "token=<ACCESS_OR_REFRESH_TOKEN>"
|
|
1693
|
-
```
|
|
1527
|
+
Notes:
|
|
1528
|
+
- `mail.send(...)` and `mail.sendMail(...)` are the same method.
|
|
1529
|
+
- Messages must include at least one of `to`, `cc`, or `bcc`.
|
|
1530
|
+
- Messages must include `from`, or configure `mail.defaults.from`.
|
|
1531
|
+
- For tests or custom providers, you can set `mail.transporter` or `mail.transportFactory` instead of `mail.transport`.
|
|
1694
1532
|
|
|
1695
|
-
|
|
1533
|
+
### Helpers And jlive
|
|
1696
1534
|
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
- `
|
|
1700
|
-
- `
|
|
1701
|
-
- `
|
|
1535
|
+
Helpers and the `jlive` bridge are available in request context (`req.aegis`) and in EJS locals.
|
|
1536
|
+
They are also available in:
|
|
1537
|
+
- Service constructors (`constructor({ helpers, jlive, env, i18n, models, ... })`)
|
|
1538
|
+
- Model constructors (`constructor({ helpers, jlive, env, i18n, dbClient, ... })`)
|
|
1539
|
+
- Subscribers context (`registerSubscribers({ helpers, jlive, env, i18n, events, ... })`)
|
|
1540
|
+
- Any view/handler via request bridge: `req.aegis.helpers`, `req.aegis.jlive`, `req.aegis.env`, `req.aegis.locale`, `req.aegis.t`, `req.aegis.i18n`
|
|
1702
1541
|
|
|
1703
|
-
|
|
1542
|
+
Set helper defaults in `settings.js` (currency/locale):
|
|
1704
1543
|
|
|
1705
1544
|
```js
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
resolveConsent: ({ req, client, subject }) => {
|
|
1712
|
-
// return true to approve, false to deny
|
|
1713
|
-
return true;
|
|
1714
|
-
},
|
|
1715
|
-
},
|
|
1545
|
+
helpers: {
|
|
1546
|
+
locale: 'fr-FR',
|
|
1547
|
+
money: {
|
|
1548
|
+
currency: 'EUR',
|
|
1549
|
+
currencyDisplay: 'code',
|
|
1716
1550
|
},
|
|
1717
1551
|
},
|
|
1718
1552
|
```
|
|
1719
1553
|
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
- Keep `auth.oauth2.server.allowHttp = false` (default).
|
|
1723
|
-
- Use HTTPS with trusted reverse proxy config.
|
|
1724
|
-
- Keep `requirePkce = true`.
|
|
1725
|
-
- Use strong client secrets and rotate regularly.
|
|
1726
|
-
- Restrict client setup endpoints to admins only.
|
|
1727
|
-
- Set explicit `defaultScopes` and per-client scopes.
|
|
1728
|
-
- Set `issuer` to your public auth server URL.
|
|
1554
|
+
Then `helpers.money(2500)` automatically uses your configured defaults unless you pass per-call overrides.
|
|
1729
1555
|
|
|
1730
|
-
|
|
1731
|
-
- OAuth2 server in AegisNode is framework-native (custom implementation).
|
|
1732
|
-
- CSRF checks are skipped for OAuth2 server endpoints (`/oauth/*` + metadata) by design.
|
|
1733
|
-
- This is OAuth2 (not OpenID Connect); no `id_token` endpoint/flow.
|
|
1556
|
+
Route usage:
|
|
1734
1557
|
|
|
1735
|
-
|
|
1558
|
+
```js
|
|
1559
|
+
export default {
|
|
1560
|
+
register(route) {
|
|
1561
|
+
route.get('/tools', (req, res) => {
|
|
1562
|
+
res.json({
|
|
1563
|
+
price: req.aegis.helpers.money(1299.5, { currency: 'USD' }),
|
|
1564
|
+
createdAgo: req.aegis.helpers.timeElapsed(Date.now() - 60_000),
|
|
1565
|
+
createdAgoShort: req.aegis.helpers.timeElapsed(Math.floor(Date.now() / 1000) - 60, true),
|
|
1566
|
+
progress: req.aegis.helpers.timeDifference(65, 0, 100),
|
|
1567
|
+
summary: req.aegis.helpers.breakStr('AegisNode framework helper utilities', 18, '...', true),
|
|
1568
|
+
objectIdValid: req.aegis.helpers.isObjectId('507f1f77bcf86cd799439011'),
|
|
1569
|
+
objectIdString: req.aegis.helpers.toObjectId('507f1f77bcf86cd799439011')?.toString() || null,
|
|
1570
|
+
secret: req.aegis.jlive.generate(32),
|
|
1571
|
+
jliveAvailable: req.aegis.jlive.available,
|
|
1572
|
+
});
|
|
1573
|
+
});
|
|
1574
|
+
},
|
|
1575
|
+
};
|
|
1576
|
+
```
|
|
1736
1577
|
|
|
1737
|
-
|
|
1578
|
+
Template usage (`res.render(...)`):
|
|
1738
1579
|
|
|
1739
|
-
```
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
},
|
|
1580
|
+
```ejs
|
|
1581
|
+
<h1><%= title %></h1>
|
|
1582
|
+
<p>Total: <%= money(1299.5, { currency: 'USD' }) %></p>
|
|
1583
|
+
<p>Updated: <%= timeElapsed(updatedAt) %></p>
|
|
1584
|
+
<p>Progress: <%= timeDifference(65, 0, 100) %>%</p>
|
|
1585
|
+
<p>Summary: <%= breakStr("AegisNode framework helper utilities", 18, "...", true) %></p>
|
|
1586
|
+
<p>With helper object: <%= helpers.number(1000000) %></p>
|
|
1747
1587
|
```
|
|
1748
1588
|
|
|
1749
|
-
|
|
1750
|
-
-
|
|
1751
|
-
-
|
|
1752
|
-
-
|
|
1753
|
-
-
|
|
1589
|
+
Available EJS locals:
|
|
1590
|
+
- `helpers`
|
|
1591
|
+
- `jlive`
|
|
1592
|
+
- `money`
|
|
1593
|
+
- `number`
|
|
1594
|
+
- `dateTime`
|
|
1595
|
+
- `timeElapsed`
|
|
1596
|
+
- `timeDifference`
|
|
1597
|
+
- `breakStr`
|
|
1598
|
+
- `isObjectId`
|
|
1599
|
+
- `toObjectId`
|
|
1600
|
+
- `locale`
|
|
1601
|
+
- `t`
|
|
1602
|
+
- `csrfToken` (raw hidden input HTML)
|
|
1603
|
+
- `csrfValue` (token string)
|
|
1754
1604
|
|
|
1755
|
-
|
|
1605
|
+
`timeElapsed` supports both styles:
|
|
1606
|
+
- `timeElapsed(value, { now, locale, numeric })` (Intl relative style)
|
|
1607
|
+
- `timeElapsed(unixTime, true)` (short legacy-style mode)
|
|
1756
1608
|
|
|
1757
|
-
|
|
1609
|
+
Mongo id helpers:
|
|
1610
|
+
- `isObjectId(value)` validates Mongo ObjectId format.
|
|
1611
|
+
- `toObjectId(value)` returns a Mongo ObjectId instance or `null` when invalid.
|
|
1612
|
+
|
|
1613
|
+
`jlive` behavior:
|
|
1614
|
+
- If `jlive` package is installed, bridge uses its methods.
|
|
1615
|
+
- If not installed, `jlive.generate()` still works (crypto fallback), while crypto methods throw `JLIVE_UNAVAILABLE`.
|
|
1616
|
+
|
|
1617
|
+
### Template Locals From Settings
|
|
1618
|
+
|
|
1619
|
+
You can inject custom functions/classes into all template renders from `settings.js`:
|
|
1758
1620
|
|
|
1759
1621
|
```js
|
|
1760
1622
|
templates: {
|
|
@@ -1762,380 +1624,658 @@ templates: {
|
|
|
1762
1624
|
engine: 'ejs',
|
|
1763
1625
|
dir: 'templates',
|
|
1764
1626
|
base: 'base',
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1627
|
+
locals: {
|
|
1628
|
+
formatCurrency: (value) => '$' + Number(value || 0).toFixed(2),
|
|
1629
|
+
ViewBag: class ViewBag {
|
|
1630
|
+
constructor(title) {
|
|
1631
|
+
this.title = title;
|
|
1632
|
+
}
|
|
1633
|
+
},
|
|
1768
1634
|
},
|
|
1769
|
-
}
|
|
1635
|
+
},
|
|
1770
1636
|
```
|
|
1771
1637
|
|
|
1772
|
-
|
|
1638
|
+
You can also pass already-defined class/function references by name (object shorthand):
|
|
1773
1639
|
|
|
1774
1640
|
```js
|
|
1775
|
-
|
|
1776
|
-
res.render('home', {
|
|
1777
|
-
title: 'Home',
|
|
1778
|
-
message: 'Welcome',
|
|
1779
|
-
});
|
|
1780
|
-
});
|
|
1781
|
-
```
|
|
1782
|
-
|
|
1783
|
-
`home.ejs` is rendered first, then injected into `base.ejs`.
|
|
1784
|
-
Use `<%- content %>` (or `<%- body %>`) in your `base.ejs` to print page content.
|
|
1785
|
-
|
|
1786
|
-
### Internationalization (i18n)
|
|
1787
|
-
|
|
1788
|
-
Configure i18n in `settings.js`:
|
|
1641
|
+
import { formatCurrency, ViewBag } from './app/template-locals.js';
|
|
1789
1642
|
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
defaultLocale: 'en',
|
|
1794
|
-
fallbackLocale: 'en',
|
|
1795
|
-
supported: ['en', 'fr'],
|
|
1796
|
-
queryParam: 'lang',
|
|
1797
|
-
translations: {
|
|
1798
|
-
en: {
|
|
1799
|
-
home: {
|
|
1800
|
-
title: 'Welcome {name}',
|
|
1801
|
-
},
|
|
1802
|
-
},
|
|
1803
|
-
fr: {
|
|
1804
|
-
home: {
|
|
1805
|
-
title: 'Bienvenue {name}',
|
|
1806
|
-
},
|
|
1807
|
-
},
|
|
1643
|
+
export default {
|
|
1644
|
+
templates: {
|
|
1645
|
+
locals: { formatCurrency, ViewBag },
|
|
1808
1646
|
},
|
|
1809
|
-
}
|
|
1647
|
+
};
|
|
1810
1648
|
```
|
|
1811
1649
|
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
i18n: {
|
|
1816
|
-
enabled: true,
|
|
1817
|
-
defaultLocale: 'en',
|
|
1818
|
-
supported: ['en', 'fr'],
|
|
1819
|
-
translations: {
|
|
1820
|
-
en: 'locales/en.json',
|
|
1821
|
-
fr: 'locales/fr.json',
|
|
1822
|
-
},
|
|
1823
|
-
// optional single-file source (inline `translations` wins per locale key):
|
|
1824
|
-
// translationsFile: 'locales/all.json',
|
|
1825
|
-
}
|
|
1826
|
-
```
|
|
1650
|
+
Note:
|
|
1651
|
+
- Use JS references/imports (as above).
|
|
1652
|
+
- String names like `{ formatCurrency: 'formatCurrency' }` are not auto-resolved.
|
|
1827
1653
|
|
|
1828
|
-
|
|
1654
|
+
Then in EJS:
|
|
1829
1655
|
|
|
1830
|
-
```
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
res.json({
|
|
1834
|
-
locale: req.aegis.locale,
|
|
1835
|
-
title: req.aegis.t('home.title', { name: 'Jason' }),
|
|
1836
|
-
});
|
|
1837
|
-
});
|
|
1656
|
+
```ejs
|
|
1657
|
+
<p><%= formatCurrency(123.45) %></p>
|
|
1658
|
+
<p><%= new ViewBag('Dashboard').title %></p>
|
|
1838
1659
|
```
|
|
1839
1660
|
|
|
1840
|
-
|
|
1661
|
+
<!-- SETTINGS_REFERENCE_START -->
|
|
1662
|
+
## Full Settings Reference
|
|
1841
1663
|
|
|
1842
|
-
|
|
1843
|
-
Shortcut for `req.aegis.i18n.t('home.title')`. Use this in routes/views when you only need a translated string.
|
|
1844
|
-
- `req.aegis.i18n`
|
|
1845
|
-
Request-scoped i18n object. Use this when you also need locale metadata or helpers such as `locale`, `localeSource`, `setLocale(...)`, `resolveLocale(...)`, or `forLocale(...)`.
|
|
1846
|
-
- Injected `i18n` in handlers/services/models/validators/controllers/subscribers/loaders
|
|
1847
|
-
Runtime-injected i18n bridge. During an HTTP request, `i18n.t(...)` resolves with the same active locale as `req.aegis.i18n.t(...)`, so the translation result is the same.
|
|
1664
|
+
All fields below are supported in `settings.js`. If you omit a field, AegisNode uses the runtime default.
|
|
1848
1665
|
|
|
1849
|
-
|
|
1666
|
+
Merge order used at startup:
|
|
1667
|
+
1. Framework defaults (`defaultConfig`)
|
|
1668
|
+
2. `settings.js`
|
|
1669
|
+
3. Legacy `settings/index.js` (if present)
|
|
1670
|
+
4. Legacy `settings/db.js` (merged into `database`)
|
|
1671
|
+
5. Legacy `settings/cache.js` (merged into `cache`)
|
|
1672
|
+
6. Legacy `settings/apps.js` (used only when `settings.js` does not define apps)
|
|
1673
|
+
7. `environments.default`
|
|
1674
|
+
8. `environments[env]` where `env = settings.env` (fallback `NODE_ENV`, then `development`)
|
|
1850
1675
|
|
|
1851
|
-
-
|
|
1852
|
-
- Injected `i18n.t(...)` in a service/model/validator/subscriber is not the same object as `req.aegis.i18n`, but during a request it produces the same translation result for the same key/options.
|
|
1853
|
-
- Outside a request, injected `i18n.t(...)` falls back to `defaultLocale`.
|
|
1854
|
-
- In background jobs, loaders, or boot-time code, pass an explicit locale when needed: `i18n.t('home.title', { name: 'Jason' }, { locale: 'fr' })`.
|
|
1676
|
+
### Top-Level
|
|
1855
1677
|
|
|
1856
|
-
|
|
1678
|
+
| Key | Type / Default | Description |
|
|
1679
|
+
| --- | --- | --- |
|
|
1680
|
+
| `appName` | `string` / folder name | Application name used in logs and defaults. |
|
|
1681
|
+
| `env` | `string` / `process.env.NODE_ENV || 'development'` | Active environment key for `environments` overrides. |
|
|
1682
|
+
| `host` | `string` / `process.env.HOST || '0.0.0.0'` | Bind host for HTTP server. |
|
|
1683
|
+
| `port` | `number` / `process.env.PORT || 3000` | Bind port for HTTP server. |
|
|
1684
|
+
| `trustProxy` | `boolean \| number \| string` / `false` | Express `trust proxy` value. Set this when HTTPS is terminated by a reverse proxy/load balancer. |
|
|
1685
|
+
| `https` | `object \| false` / see HTTPS table | Direct TLS server settings for Node-hosted HTTPS. |
|
|
1686
|
+
| `staticDir` | `string \| null` / `null` | Static assets directory, relative to project root (if set). |
|
|
1687
|
+
| `templates` | `object \| false` / see templates table | EJS template engine + layout settings. |
|
|
1688
|
+
| `i18n` | `object` / see i18n table | Built-in locale detection + translator bridge (`req.aegis.t`, injected `i18n.t`). |
|
|
1689
|
+
| `helpers` | `object` / see helpers table | Runtime helper defaults (for example currency/locale for `helpers.money`). |
|
|
1690
|
+
| `security` | `object` / see security tables | Security headers, DDoS limiter, CSRF settings, app secret. |
|
|
1691
|
+
| `logging` | `object` / `{ level: 'info' }` | Runtime logger level. |
|
|
1692
|
+
| `database` | `object` / see database table | SQL or MongoDB connection settings. |
|
|
1693
|
+
| `cache` | `object` / `{ enabled: true, driver: 'memory' }` | Cache backend settings. |
|
|
1694
|
+
| `websocket` | `object` / `{ enabled: true, cors: { origin: false } }` | Socket.IO server options. |
|
|
1695
|
+
| `uploads` | `object` / see uploads table | Built-in file upload middleware settings used by `route.upload`. |
|
|
1696
|
+
| `mail` | `object` / see mail table | Nodemailer-backed mail manager available as injected `mail` and `req.aegis.mail`. |
|
|
1697
|
+
| `api` | `object` / see API table | API-app middleware behavior (JSON enforcement, no-store, CSRF skip for API mounts). |
|
|
1698
|
+
| `auth` | `object` / see auth tables | JWT or OAuth2 provider settings. |
|
|
1699
|
+
| `swagger` | `object` / see swagger table | OpenAPI JSON + Swagger UI settings. |
|
|
1700
|
+
| `architecture` | `object` / `{ strictLayers: false }` | Layering enforcement mode. |
|
|
1701
|
+
| `autoMountApps` | `boolean` / `false` | Auto-mount each app route file from `settings.apps`. |
|
|
1702
|
+
| `loaders` | `array` / `[]` | Startup loaders run before routes mounting. |
|
|
1703
|
+
| `apps` | `array` / `[]` | Declared apps with mount points. |
|
|
1704
|
+
| `environments` | `object` / `{}` | Environment-specific deep overrides. |
|
|
1857
1705
|
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
this.i18n = i18n;
|
|
1862
|
-
}
|
|
1706
|
+
Notes:
|
|
1707
|
+
- `rootDir` is internal and set by runtime; do not manage it manually.
|
|
1708
|
+
- Arrays are replaced (not merged) during deep merge.
|
|
1863
1709
|
|
|
1864
|
-
|
|
1865
|
-
return this.i18n.t('home.title', { name });
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
```
|
|
1710
|
+
### HTTPS (`https`)
|
|
1869
1711
|
|
|
1870
|
-
|
|
1712
|
+
Use this only when Node should serve HTTPS directly. If HTTPS is handled by Passenger, Nginx, Apache, or another proxy, keep `https.enabled` off and set top-level `trustProxy` instead.
|
|
1871
1713
|
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1714
|
+
| Key | Type / Default | Description |
|
|
1715
|
+
| --- | --- | --- |
|
|
1716
|
+
| `enabled` | `boolean` / `false` | Create an HTTPS server instead of HTTP. |
|
|
1717
|
+
| `key` | `string \| Buffer` / `null` | TLS private key content. |
|
|
1718
|
+
| `cert` | `string \| Buffer` / `null` | TLS certificate content. |
|
|
1719
|
+
| `ca` | `string \| Buffer \| array` / `null` | Optional CA/intermediate certificate content. |
|
|
1720
|
+
| `pfx` | `string \| Buffer` / `null` | PFX/PKCS#12 archive content. Use instead of `key` + `cert`. |
|
|
1721
|
+
| `keyPath` | `string` / `''` | Path to TLS private key, relative to project root or absolute. |
|
|
1722
|
+
| `certPath` | `string` / `''` | Path to TLS certificate, relative to project root or absolute. |
|
|
1723
|
+
| `caPath` | `string \| string[]` / `null` | Optional CA/intermediate certificate path(s). |
|
|
1724
|
+
| `pfxPath` | `string` / `''` | Path to PFX/PKCS#12 archive. |
|
|
1725
|
+
| `passphrase` | `string` / `''` | Optional passphrase for encrypted key/PFX files. |
|
|
1726
|
+
| `options` | `object` / `{}` | Extra Node `https.createServer` options (for example `minVersion`). |
|
|
1727
|
+
|
|
1728
|
+
Direct HTTPS example:
|
|
1729
|
+
|
|
1730
|
+
```js
|
|
1731
|
+
export default {
|
|
1732
|
+
host: '0.0.0.0',
|
|
1733
|
+
port: 3443,
|
|
1734
|
+
https: {
|
|
1735
|
+
enabled: true,
|
|
1736
|
+
keyPath: 'certs/localhost-key.pem',
|
|
1737
|
+
certPath: 'certs/localhost-cert.pem',
|
|
1738
|
+
options: {
|
|
1739
|
+
minVersion: 'TLSv1.2',
|
|
1740
|
+
},
|
|
1741
|
+
},
|
|
1742
|
+
};
|
|
1878
1743
|
```
|
|
1879
1744
|
|
|
1880
|
-
|
|
1745
|
+
Reverse-proxy HTTPS example:
|
|
1881
1746
|
|
|
1882
1747
|
```js
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1748
|
+
export default {
|
|
1749
|
+
host: '127.0.0.1',
|
|
1750
|
+
port: 3000,
|
|
1751
|
+
trustProxy: 1,
|
|
1752
|
+
};
|
|
1887
1753
|
```
|
|
1888
1754
|
|
|
1889
|
-
|
|
1755
|
+
Notes:
|
|
1756
|
+
- `https` requires either `pfx`/`pfxPath` or both `key`/`keyPath` and `cert`/`certPath`.
|
|
1757
|
+
- Paths resolve from project root unless absolute.
|
|
1758
|
+
- `trustProxy` affects `req.secure`, `req.protocol`, secure cookies, and OAuth2 secure transport checks.
|
|
1759
|
+
- Prefer `1`, a subnet, or another exact Express `trust proxy` value instead of `true` when rate limiting is enabled.
|
|
1760
|
+
|
|
1761
|
+
### Templates (`templates`)
|
|
1762
|
+
|
|
1763
|
+
| Key | Type / Default | Description |
|
|
1764
|
+
| --- | --- | --- |
|
|
1765
|
+
| `enabled` | `boolean` / `true` | Enable template engine. |
|
|
1766
|
+
| `engine` | `string` / `'ejs'` | Template engine. Only `ejs` is supported. |
|
|
1767
|
+
| `dir` | `string` / `'templates'` | Templates folder (absolute or relative to project root). |
|
|
1768
|
+
| `base` | `string \| false \| null` / `'base'` | Default layout template (without `.ejs`). Set `false`/`null` to disable layout wrapping globally. |
|
|
1769
|
+
| `appBases` | `object` / `{}` | Per-app layout override map: `{ appName: 'layout/name' }`. Set app value to `false`/`null` to disable layout for that app only. |
|
|
1770
|
+
| `locals` | `object \| function` / `{}` | Global locals. If function, signature is `({ req, res, helpers, jlive, env }) => object`. |
|
|
1771
|
+
|
|
1772
|
+
Layout notes:
|
|
1773
|
+
- `res.render('view', data)` renders `view.ejs` and wraps it with `base.ejs` (or configured layout).
|
|
1774
|
+
- Per-app layout override: `templates.appBases = { users: 'users/base', admin: 'admin/base' }`.
|
|
1775
|
+
- In layout, both `<%- body %>` and `<%- content %>` are available.
|
|
1776
|
+
- Per-render layout override: pass `layout: 'custom-layout'` or `layout: false` in locals.
|
|
1777
|
+
|
|
1778
|
+
### Internationalization (`i18n`)
|
|
1779
|
+
|
|
1780
|
+
| Key | Type / Default | Description |
|
|
1781
|
+
| --- | --- | --- |
|
|
1782
|
+
| `enabled` | `boolean` / `false` | Enable built-in i18n translator bridge. |
|
|
1783
|
+
| `defaultLocale` | `string` / `'en'` | Default locale used when detection fails. |
|
|
1784
|
+
| `fallbackLocale` | `string` / `'en'` | Fallback locale used when key is missing in active locale. |
|
|
1785
|
+
| `supported` | `string[]` / `['en']` | Allowed locales. Values normalize to lowercase (for example `en-US` -> `en-us`). |
|
|
1786
|
+
| `queryParam` | `string` / `'lang'` | Query parameter used for locale selection (for example `?lang=fr`). |
|
|
1787
|
+
| `cookieName` | `string` / `'aegis_locale'` | Cookie used to persist locale. |
|
|
1788
|
+
| `detectFromHeader` | `boolean` / `true` | Enable locale detection from `Accept-Language` header. |
|
|
1789
|
+
| `detectFromCookie` | `boolean` / `true` | Enable locale detection from configured cookie. |
|
|
1790
|
+
| `detectFromQuery` | `boolean` / `true` | Enable locale detection from query parameter. |
|
|
1791
|
+
| `translations` | `object` / `{}` | Translation map by locale. Values can be objects or JSON file paths: `{ en: { ... }, fr: 'locales/fr.json' }`. Alias keys `locales` and `messages` are also accepted. |
|
|
1792
|
+
| `translationsFile` | `string` / unset | Path to a JSON file containing all locales (example: `{ "en": {...}, "fr": {...} }`). Inline `translations` overrides file values for same locale keys. |
|
|
1793
|
+
|
|
1794
|
+
i18n notes:
|
|
1795
|
+
- Detection order: query -> cookie -> `Accept-Language` -> `defaultLocale`.
|
|
1796
|
+
- Use dotted keys like `home.title`.
|
|
1797
|
+
- Placeholder interpolation supports `{name}` style tokens.
|
|
1798
|
+
- Relative JSON paths resolve from project root (`settings.js` location).
|
|
1799
|
+
- Injected `i18n` is available in handlers, services, models, validators, controllers, subscribers, and loaders. Use `i18n.t('key', vars, { locale })`.
|
|
1800
|
+
- During a request, injected `i18n.t(...)` follows the active request locale. Outside a request, it falls back to `defaultLocale`.
|
|
1801
|
+
|
|
1802
|
+
### Helpers Defaults (`helpers`)
|
|
1803
|
+
|
|
1804
|
+
| Key | Type / Default | Description |
|
|
1805
|
+
| --- | --- | --- |
|
|
1806
|
+
| `locale` | `string` / `'en-US'` | Default locale used by runtime helpers when locale is not passed explicitly. |
|
|
1807
|
+
| `money` | `object` / `{ currency: 'USD' }` | Default money formatting settings used by `helpers.money`. |
|
|
1808
|
+
| `money.currency` | `string` / `'USD'` | Default currency code for `helpers.money(amount)` when no currency option is provided. |
|
|
1809
|
+
| `money.locale` | `string` / `helpers.locale` | Locale override only for `helpers.money`. |
|
|
1810
|
+
| `money.currencyDisplay` | `'symbol' | 'code' | 'name' | 'narrowSymbol'` / `'symbol'` | Currency display style passed to `Intl.NumberFormat`. |
|
|
1811
|
+
| `money.minimumFractionDigits` | `number` / unset | Optional minimum fraction digits for money formatting. |
|
|
1812
|
+
| `money.maximumFractionDigits` | `number` / unset | Optional maximum fraction digits for money formatting. |
|
|
1813
|
+
|
|
1814
|
+
Helpers defaults notes:
|
|
1815
|
+
- Per-call options always override these defaults.
|
|
1816
|
+
- If `helpers.locale` is not set, AegisNode falls back to `i18n.defaultLocale` for helper locale.
|
|
1817
|
+
- Legacy shorthand keys are also accepted for compatibility: `helpers.currency`, top-level `currency`, and `app.currency`.
|
|
1818
|
+
|
|
1819
|
+
### Security (`security`)
|
|
1820
|
+
|
|
1821
|
+
| Key | Type / Default | Description |
|
|
1822
|
+
| --- | --- | --- |
|
|
1823
|
+
| `appSecret` | `string` / `''` | Shared secret for signing security artifacts. Use at least 16 chars. |
|
|
1824
|
+
| `headers` | `object` / see headers table | Helmet + CSP configuration. |
|
|
1825
|
+
| `ddos` | `object` / see ddos table | `express-rate-limit` based protection. |
|
|
1826
|
+
| `csrf` | `object` / see csrf table | CSRF cookie/token behavior. |
|
|
1827
|
+
|
|
1828
|
+
#### Security Headers (`security.headers`)
|
|
1829
|
+
|
|
1830
|
+
| Key | Type / Default | Description |
|
|
1831
|
+
| --- | --- | --- |
|
|
1832
|
+
| `enabled` | `boolean` / `true` | Enable Helmet middleware. |
|
|
1833
|
+
| `csp` | `object` / see CSP table | Content Security Policy behavior. |
|
|
1834
|
+
|
|
1835
|
+
#### CSP (`security.headers.csp`)
|
|
1836
|
+
|
|
1837
|
+
| Key | Type / Default | Description |
|
|
1838
|
+
| --- | --- | --- |
|
|
1839
|
+
| `enabled` | `boolean` / `true` | Enable CSP header from Helmet. |
|
|
1840
|
+
| `reportOnly` | `boolean` / `false` | Use report-only mode. |
|
|
1841
|
+
| `directives` | `object` / `{}` | Directive overrides. Set a directive to `false`/`null` to remove it. |
|
|
1842
|
+
|
|
1843
|
+
Default CSP base includes safe defaults such as `defaultSrc 'self'`, `objectSrc 'none'`, `frameAncestors 'none'`, and websocket-aware `connectSrc`.
|
|
1844
|
+
|
|
1845
|
+
Allow multiple external domains by adding them per directive (not globally):
|
|
1890
1846
|
|
|
1891
1847
|
```js
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1848
|
+
security: {
|
|
1849
|
+
headers: {
|
|
1850
|
+
csp: {
|
|
1851
|
+
directives: {
|
|
1852
|
+
scriptSrc: ["'self'", 'https://cdn.jsdelivr.net', 'https://unpkg.com'],
|
|
1853
|
+
scriptSrcElem: ["'self'", 'https://cdn.jsdelivr.net', 'https://unpkg.com'],
|
|
1854
|
+
styleSrc: ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net'],
|
|
1855
|
+
styleSrcElem: ["'self'", 'https://cdn.jsdelivr.net'],
|
|
1856
|
+
imgSrc: ["'self'", 'data:', 'https://cdn.jsdelivr.net'],
|
|
1857
|
+
connectSrc: ["'self'", 'https://api.example.com', 'wss://socket.example.com'],
|
|
1858
|
+
},
|
|
1859
|
+
},
|
|
1860
|
+
},
|
|
1861
|
+
},
|
|
1897
1862
|
```
|
|
1898
1863
|
|
|
1899
|
-
|
|
1864
|
+
Notes:
|
|
1865
|
+
- Add each origin to the exact directive needed (scripts, styles, images, API/WebSocket connections).
|
|
1866
|
+
- If browser reports a `script-src-elem` violation, whitelist the domain in `scriptSrcElem`.
|
|
1867
|
+
- Prefer explicit origins instead of `*` in production.
|
|
1900
1868
|
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1869
|
+
Google Fonts example:
|
|
1870
|
+
|
|
1871
|
+
```js
|
|
1872
|
+
security: {
|
|
1873
|
+
headers: {
|
|
1874
|
+
csp: {
|
|
1875
|
+
directives: {
|
|
1876
|
+
styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
|
|
1877
|
+
styleSrcElem: ["'self'", 'https://fonts.googleapis.com'],
|
|
1878
|
+
fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],
|
|
1879
|
+
},
|
|
1880
|
+
},
|
|
1881
|
+
},
|
|
1882
|
+
},
|
|
1909
1883
|
```
|
|
1910
1884
|
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1885
|
+
#### DDoS / Rate Limit (`security.ddos`)
|
|
1886
|
+
|
|
1887
|
+
| Key | Type / Default | Description |
|
|
1888
|
+
| --- | --- | --- |
|
|
1889
|
+
| `enabled` | `boolean` / `true` | Enable rate limiter. |
|
|
1890
|
+
| `windowMs` | `number` / `60000` | Rate limit window in milliseconds. |
|
|
1891
|
+
| `maxRequests` | `number` / `120` | Max requests per window per key. |
|
|
1892
|
+
| `message` | `string` / `'Too many requests, please try again later.'` | JSON error message text. |
|
|
1893
|
+
| `statusCode` | `number` / `429` | Response status code when limited. |
|
|
1894
|
+
| `standardHeaders` | `boolean` / `true` | Emit modern rate-limit headers. |
|
|
1895
|
+
| `legacyHeaders` | `boolean` / `false` | Emit legacy `X-RateLimit-*` headers. |
|
|
1896
|
+
| `skipSuccessfulRequests` | `boolean` / `false` | Do not count successful responses. |
|
|
1897
|
+
| `skipFailedRequests` | `boolean` / `false` | Do not count failed responses. |
|
|
1898
|
+
| `trustProxy` | `boolean \| number \| string` / `false` | Legacy alias for top-level `trustProxy`. Still supported for backward compatibility. |
|
|
1899
|
+
| `store` | `object \| null` / `null` | Custom rate-limit store implementation. |
|
|
1900
|
+
| `skipPaths` | `string[]` / `['/health']` | Path prefixes excluded from limiter. |
|
|
1901
|
+
|
|
1902
|
+
#### CSRF (`security.csrf`)
|
|
1903
|
+
|
|
1904
|
+
| Key | Type / Default | Description |
|
|
1905
|
+
| --- | --- | --- |
|
|
1906
|
+
| `enabled` | `boolean` / `true` | Enable CSRF middleware. |
|
|
1907
|
+
| `rejectForms` | `boolean` / `true` | Enforce CSRF on form submissions. |
|
|
1908
|
+
| `rejectUnsafeMethods` | `boolean` / `true` | Enforce CSRF for unsafe methods (`POST/PUT/PATCH/DELETE`) in general. |
|
|
1909
|
+
| `cookieName` | `string` / `'_aegis_csrf'` | CSRF cookie name. |
|
|
1910
|
+
| `fieldName` | `string` / `'_csrf'` | Body form field name for CSRF token. |
|
|
1911
|
+
| `headerName` | `string` / `'x-csrf-token'` | Header token key (normalized lowercase). |
|
|
1912
|
+
| `requireSignedCookie` | `boolean` / `true` | Require signed CSRF cookie values. |
|
|
1913
|
+
| `sameSite` | `'lax' \| 'strict' \| 'none' \| false` / `'lax'` | CSRF cookie same-site mode. |
|
|
1914
|
+
| `secure` | `boolean \| 'auto'` / `'auto'` | Secure cookie flag (`auto` respects request security). |
|
|
1915
|
+
| `httpOnly` | `boolean` / `true` | Make CSRF cookie httpOnly. |
|
|
1916
|
+
| `path` | `string` / `'/'` | CSRF cookie path. |
|
|
1917
|
+
|
|
1918
|
+
CSRF notes:
|
|
1919
|
+
- With `requireSignedCookie: true` (default), `security.appSecret` must be strong (minimum 16 chars) or startup fails.
|
|
1920
|
+
- CSRF is skipped for configured API app mounts when `api.disableCsrf: true`.
|
|
1921
|
+
- CSRF is skipped on built-in OAuth2 server endpoints.
|
|
1922
|
+
|
|
1923
|
+
### Logging (`logging`)
|
|
1924
|
+
|
|
1925
|
+
| Key | Type / Default | Description |
|
|
1926
|
+
| --- | --- | --- |
|
|
1927
|
+
| `level` | `'error' \| 'warn' \| 'info' \| 'debug' \| 'trace'` / `'info'` | Logger verbosity threshold. |
|
|
1928
|
+
|
|
1929
|
+
### Database (`database`)
|
|
1930
|
+
|
|
1931
|
+
| Key | Type / Default | Description |
|
|
1932
|
+
| --- | --- | --- |
|
|
1933
|
+
| `enabled` | `boolean` / `false` | Enable database bootstrap. |
|
|
1934
|
+
| `dialect` | `string` / `'pg'` | SQL: `mysql`, `pg`/`postgres`/`postgresql`, `sqlite`, `mssql`, `oracle`; NoSQL: `mongo`/`mongodb`/`mongoose`. |
|
|
1935
|
+
| `config` | `object` / `{}` | Connection options passed to database driver. |
|
|
1936
|
+
| `options` | `object` / `{}` | Extra options (used directly for Mongo when enabled). |
|
|
1937
|
+
| `uri` | `string` / unset | Legacy Mongo URI fallback (still accepted). Prefer `config.connectionString`. |
|
|
1938
|
+
|
|
1939
|
+
Mongo config shortcuts accepted in `database.config`:
|
|
1940
|
+
- `connectionString` (preferred)
|
|
1941
|
+
- or `server`/`host`, `port`, `database`/`dbName`, `user`/`username`, `password`
|
|
1942
|
+
|
|
1943
|
+
### Cache (`cache`)
|
|
1944
|
+
|
|
1945
|
+
| Key | Type / Default | Description |
|
|
1946
|
+
| --- | --- | --- |
|
|
1947
|
+
| `enabled` | `boolean` / `true` | Enable cache service. |
|
|
1948
|
+
| `driver` | `string` / `'memory'` | Cache driver. Currently only `memory` is built-in. |
|
|
1949
|
+
| `options` | `object` / `{}` | Reserved for future/custom drivers. |
|
|
1950
|
+
|
|
1951
|
+
### WebSocket (`websocket`)
|
|
1952
|
+
|
|
1953
|
+
| Key | Type / Default | Description |
|
|
1954
|
+
| --- | --- | --- |
|
|
1955
|
+
| `enabled` | `boolean` / `true` | Enable Socket.IO server. |
|
|
1956
|
+
| `cors` | `object` / `{ origin: false }` | Passed directly to Socket.IO `cors` option. |
|
|
1957
|
+
|
|
1958
|
+
### Uploads (`uploads`)
|
|
1959
|
+
|
|
1960
|
+
| Key | Type / Default | Description |
|
|
1961
|
+
| --- | --- | --- |
|
|
1962
|
+
| `enabled` | `boolean` / `true` | Enable built-in upload manager. |
|
|
1963
|
+
| `dir` | `string` / `'uploads'` | Upload destination folder (absolute or relative to project root). |
|
|
1964
|
+
| `createDir` | `boolean` / `true` | Create upload directory automatically on boot. |
|
|
1965
|
+
| `preserveExtension` | `boolean` / `true` | Preserve original file extension in generated file name. |
|
|
1966
|
+
| `maxFileSize` | `number \| string` / `5 * 1024 * 1024` | Per-file max size in bytes. String units like `'5mb'` are accepted. |
|
|
1967
|
+
| `maxFiles` | `number` / `5` | Max file count per request. |
|
|
1968
|
+
| `maxFields` | `number` / `50` | Max non-file form fields per request. |
|
|
1969
|
+
| `maxFieldSize` | `number \| string` / `1024 * 1024` | Max size per form field value. String units are accepted. |
|
|
1970
|
+
| `allowedMimeTypes` | `string[] \| string` / `[]` | Allowed MIME types. Empty means allow all. |
|
|
1971
|
+
| `allowedExtensions` | `string[] \| string` / `[]` | Allowed file extensions (`.jpg`, `.pdf`, ...). Empty means allow all. |
|
|
1972
|
+
| `allowApiMultipart` | `boolean` / `true` | Allow `multipart/form-data` on API mounts even when `api.requireJsonForUnsafeMethods` is true. |
|
|
1973
|
+
|
|
1974
|
+
### Mail (`mail`)
|
|
1975
|
+
|
|
1976
|
+
| Key | Type / Default | Description |
|
|
1977
|
+
| --- | --- | --- |
|
|
1978
|
+
| `enabled` | `boolean` / `false` | Enable the built-in mail manager. |
|
|
1979
|
+
| `defaults` | `object` / `{ from: '', replyTo: '' }` | Default message fields merged into every outgoing mail payload. |
|
|
1980
|
+
| `defaults.from` | `string` / `''` | Default sender used when `mail.send(...)` omits `from`. |
|
|
1981
|
+
| `defaults.replyTo` | `string` / `''` | Default `replyTo` header for outgoing mail. |
|
|
1982
|
+
| `transport` | `object \| string` / `{}` | Nodemailer transport config or SMTP connection URL passed to `nodemailer.createTransport(...)`. |
|
|
1983
|
+
| `transporter` | `object \| null` / `null` | Prebuilt transporter object with `sendMail()`. Useful in tests or custom integrations. |
|
|
1984
|
+
| `transportFactory` | `function \| null` / `null` | Factory that returns a transporter object with `sendMail()`. |
|
|
1985
|
+
| `verifyOnStartup` | `boolean` / `false` | Call `transporter.verify()` during boot when the transporter supports it. |
|
|
1986
|
+
|
|
1987
|
+
Mail notes:
|
|
1988
|
+
- The runtime uses [Nodemailer](https://nodemailer.com/).
|
|
1989
|
+
- `mail.send(payload)` and `mail.sendMail(payload)` are aliases.
|
|
1990
|
+
- Each payload must include at least one of `to`, `cc`, or `bcc`.
|
|
1991
|
+
- Each payload must include `from`, or configure `mail.defaults.from`.
|
|
1992
|
+
- Injected `mail` is available in handlers, services, models, validators, controllers, subscribers, and loaders, plus `req.aegis.mail`.
|
|
1993
|
+
|
|
1994
|
+
### API (`api`)
|
|
1995
|
+
|
|
1996
|
+
| Key | Type / Default | Description |
|
|
1997
|
+
| --- | --- | --- |
|
|
1998
|
+
| `apps` | `string[]` / `[]` | App names treated as API apps. Mounts are resolved from `settings.apps`. |
|
|
1999
|
+
| `disableCsrf` | `boolean` / `true` | Skip CSRF checks for API app mounts. |
|
|
2000
|
+
| `requireJsonForUnsafeMethods` | `boolean` / `true` | Reject unsafe API payloads unless `Content-Type` is JSON (`415`). Multipart is allowed when `uploads.allowApiMultipart=true`. |
|
|
2001
|
+
| `noStoreHeaders` | `boolean` / `true` | Set `Cache-Control: no-store` on API responses. |
|
|
2002
|
+
|
|
2003
|
+
API notes:
|
|
2004
|
+
- `api.apps` contains app names from `settings.apps`, not URL paths.
|
|
2005
|
+
- Marking an app as API does not generate REST routes or change the app file structure. It only applies API middleware to that app mount.
|
|
2006
|
+
- The effective API mount comes from `settings.apps[].mount` (or `/${app}` by default). Keep that aligned with your `route.use(...)` mount when `autoMountApps` is off.
|
|
2007
|
+
|
|
2008
|
+
### Auth (`auth`)
|
|
2009
|
+
|
|
2010
|
+
| Key | Type / Default | Description |
|
|
2011
|
+
| --- | --- | --- |
|
|
2012
|
+
| `enabled` | `boolean` / `false` | Enable auth manager. |
|
|
2013
|
+
| `provider` | `'jwt' \| 'oauth2'` / `'jwt'` | Active auth provider. |
|
|
2014
|
+
| `tablePrefix` | `string` / `'aegisnode'` | Prefix for auth storage namespaces/tables; sanitized to lowercase `[a-z0-9_]`. |
|
|
2015
|
+
| `storage` | `object` / see storage table | Persistence backend for auth state. |
|
|
2016
|
+
| `jwt` | `object` / see jwt table | JWT configuration. |
|
|
2017
|
+
| `oauth2` | `object` / see oauth2 table | OAuth2 server and token configuration. |
|
|
2018
|
+
|
|
2019
|
+
#### Auth Storage (`auth.storage`)
|
|
2020
|
+
|
|
2021
|
+
| Key | Type / Default | Description |
|
|
2022
|
+
| --- | --- | --- |
|
|
2023
|
+
| `driver` | `'cache' \| 'memory' \| 'file' \| 'database'` / `'cache'` | Storage backend for revocations/clients/tokens. |
|
|
2024
|
+
| `filePath` | `string` / `'storage/aegisnode-auth-store.json'` | Used when `driver: 'file'`. Relative to project root if not absolute. |
|
|
2025
|
+
| `tableName` | `string` / `'aegisnode_auth_store'` (prefix-based) | Used when `driver: 'database'` for SQL table or Mongo collection. |
|
|
2026
|
+
| `collectionName` | `string` / alias only | Legacy alias; if set and `tableName` missing, it becomes `tableName`. |
|
|
2027
|
+
|
|
2028
|
+
#### JWT (`auth.jwt`)
|
|
2029
|
+
|
|
2030
|
+
| Key | Type / Default | Description |
|
|
2031
|
+
| --- | --- | --- |
|
|
2032
|
+
| `secret` | `string` / `security.appSecret` fallback | Signing secret. Required for JWT auth. |
|
|
2033
|
+
| `algorithm` | `'HS256' \| 'HS384' \| 'HS512'` / `'HS256'` | JWT HMAC algorithm. |
|
|
2034
|
+
| `issuer` | `string` / `appName` | JWT issuer claim. |
|
|
2035
|
+
| `audience` | `string` / `appName` | JWT audience claim. |
|
|
2036
|
+
| `expiresIn` | `string` / `'15m'` | Access token TTL. |
|
|
2037
|
+
| `refreshExpiresIn` | `string` / `'7d'` | Refresh token TTL. |
|
|
2038
|
+
|
|
2039
|
+
#### OAuth2 Core (`auth.oauth2`)
|
|
2040
|
+
|
|
2041
|
+
| Key | Type / Default | Description |
|
|
2042
|
+
| --- | --- | --- |
|
|
2043
|
+
| `accessTokenTtlSeconds` | `number` / `3600` | Access token TTL in seconds. |
|
|
2044
|
+
| `refreshTokenTtlSeconds` | `number` / `1209600` | Refresh token TTL in seconds. |
|
|
2045
|
+
| `authorizationCodeTtlSeconds` | `number` / `600` | Authorization code TTL in seconds. |
|
|
2046
|
+
| `rotateRefreshToken` | `boolean` / `true` | Rotate refresh token on refresh flow. |
|
|
2047
|
+
| `requireClientSecret` | `boolean` / `true` | Require secret for confidential client flows. |
|
|
2048
|
+
| `requirePkce` | `boolean` / `true` | Require PKCE for authorization_code flow. |
|
|
2049
|
+
| `allowPlainPkce` | `boolean` / `false` | Allow PKCE `plain` method. |
|
|
2050
|
+
| `grants` | `string[]` / `['authorization_code','refresh_token','client_credentials']` | Enabled OAuth2 grants. |
|
|
2051
|
+
| `defaultScopes` | `string[]` / `[]` | Scopes assigned when none requested/provided. |
|
|
2052
|
+
| `clientAuthMethod` | `'client_secret_basic' \| 'client_secret_post' \| 'none'` / `'client_secret_basic'` | Default client authentication method. |
|
|
2053
|
+
| `server` | `object` / see OAuth2 server table | Built-in authorization server endpoint settings. |
|
|
2054
|
+
|
|
2055
|
+
#### OAuth2 Server (`auth.oauth2.server`)
|
|
2056
|
+
|
|
2057
|
+
| Key | Type / Default | Description |
|
|
2058
|
+
| --- | --- | --- |
|
|
2059
|
+
| `enabled` | `boolean` / `true` | Mount built-in OAuth2 endpoints. |
|
|
2060
|
+
| `basePath` | `string` / `'/oauth'` | Base path used to derive endpoint defaults. |
|
|
2061
|
+
| `authorizePath` | `string` / `'/oauth/authorize'` | Authorization endpoint path. |
|
|
2062
|
+
| `tokenPath` | `string` / `'/oauth/token'` | Token endpoint path. |
|
|
2063
|
+
| `introspectionPath` | `string` / `'/oauth/introspect'` | Introspection endpoint path. |
|
|
2064
|
+
| `revocationPath` | `string` / `'/oauth/revoke'` | Revocation endpoint path. |
|
|
2065
|
+
| `metadataPath` | `string` / `'/.well-known/oauth-authorization-server'` | OAuth2 metadata endpoint path. |
|
|
2066
|
+
| `issuer` | `string` / `server.baseUrl` fallback, then `appName` | Issuer value in OAuth2 metadata/tokens. |
|
|
2067
|
+
| `baseUrl` | `string` / optional alias | Alias used as fallback for `issuer` when `issuer` is empty. |
|
|
2068
|
+
| `autoApprove` | `boolean` / `true` | Auto-approve auth requests once subject is resolved. |
|
|
2069
|
+
| `requireAuthenticatedUser` | `boolean` / `true` | Require authenticated user/subject for authorize endpoint. |
|
|
2070
|
+
| `requireConsent` | `boolean` / `false` | Require explicit consent before issuing auth code. |
|
|
2071
|
+
| `allowSubjectFromParams` | `boolean` / `false` | Allow subject from request params (`subject`/`user_id`). Keep disabled in production. |
|
|
2072
|
+
| `allowHttp` | `boolean` / `false` | Allow OAuth2 endpoints on non-HTTPS requests. Keep `false` in production. |
|
|
2073
|
+
| `resolveSubject` | `function \| null` / `null` | Hook to resolve subject: `({ req, params, client }) => string|null`. |
|
|
2074
|
+
| `resolveConsent` | `function \| null` / `null` | Hook to resolve consent: `({ req, params, client, subject }) => boolean`. |
|
|
2075
|
+
|
|
2076
|
+
### Swagger (`swagger`)
|
|
2077
|
+
|
|
2078
|
+
| Key | Type / Default | Description |
|
|
2079
|
+
| --- | --- | --- |
|
|
2080
|
+
| `enabled` | `boolean` / `false` | Enable Swagger UI + OpenAPI JSON endpoints. |
|
|
2081
|
+
| `docsPath` | `string` / `'/docs'` | Swagger UI route. |
|
|
2082
|
+
| `jsonPath` | `string` / `'/openapi.json'` | OpenAPI JSON route. |
|
|
2083
|
+
| `document` | `object \| null` / `null` | Inline OpenAPI document object. |
|
|
2084
|
+
| `documentPath` | `string` / `'openapi.json'` | JSON file path used when `document` is not provided. |
|
|
2085
|
+
| `explorer` | `boolean` / `true` | Enable Swagger UI explorer mode. |
|
|
2086
|
+
|
|
2087
|
+
### Architecture (`architecture`)
|
|
2088
|
+
|
|
2089
|
+
| Key | Type / Default | Description |
|
|
2090
|
+
| --- | --- | --- |
|
|
2091
|
+
| `strictLayers` | `boolean` / `false` | Enforce `route -> validator -> service -> model` restrictions. |
|
|
1916
2092
|
|
|
1917
|
-
###
|
|
2093
|
+
### Auto Mount (`autoMountApps`)
|
|
1918
2094
|
|
|
1919
|
-
|
|
2095
|
+
| Key | Type / Default | Description |
|
|
2096
|
+
| --- | --- | --- |
|
|
2097
|
+
| `autoMountApps` | `boolean` / `false` | If `true`, each `apps/<name>/routes.js` is mounted automatically from `settings.apps`. If `false`, central `routes.js` controls mounting. |
|
|
2098
|
+
|
|
2099
|
+
### Loaders (`loaders`)
|
|
2100
|
+
|
|
2101
|
+
`loaders` accepts an array of entries run in order during startup:
|
|
2102
|
+
|
|
2103
|
+
1. Function entry:
|
|
1920
2104
|
|
|
1921
2105
|
```js
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
defaults: {
|
|
1926
|
-
from: 'noreply@example.com',
|
|
1927
|
-
replyTo: 'support@example.com',
|
|
1928
|
-
},
|
|
1929
|
-
transport: {
|
|
1930
|
-
host: process.env.SMTP_HOST,
|
|
1931
|
-
port: process.env.SMTP_PORT ? Number(process.env.SMTP_PORT) : 587,
|
|
1932
|
-
secure: false,
|
|
1933
|
-
auth: {
|
|
1934
|
-
user: process.env.SMTP_USER,
|
|
1935
|
-
pass: process.env.SMTP_PASS,
|
|
1936
|
-
},
|
|
1937
|
-
},
|
|
1938
|
-
verifyOnStartup: true,
|
|
2106
|
+
loaders: [
|
|
2107
|
+
async ({ logger, options, config }) => {
|
|
2108
|
+
logger.info('loader ran');
|
|
1939
2109
|
},
|
|
1940
|
-
|
|
2110
|
+
]
|
|
1941
2111
|
```
|
|
1942
2112
|
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
- Injected `mail` in handlers/services/models/validators/controllers/subscribers/loaders
|
|
1946
|
-
Shared runtime mail manager. Use `mail.send(...)` or `mail.sendMail(...)`.
|
|
1947
|
-
- `req.aegis.mail`
|
|
1948
|
-
Request bridge to the same mail manager used in handler context.
|
|
1949
|
-
|
|
1950
|
-
Handler usage:
|
|
2113
|
+
2. Path entry (relative or absolute):
|
|
1951
2114
|
|
|
1952
2115
|
```js
|
|
1953
|
-
|
|
1954
|
-
try {
|
|
1955
|
-
const info = await mail.send({
|
|
1956
|
-
to: 'support@example.com',
|
|
1957
|
-
subject: 'Contact form',
|
|
1958
|
-
text: req.body?.message || '',
|
|
1959
|
-
html: `<p>${req.body?.message || ''}</p>`,
|
|
1960
|
-
});
|
|
1961
|
-
|
|
1962
|
-
res.status(202).json({ messageId: info.messageId });
|
|
1963
|
-
} catch (error) {
|
|
1964
|
-
next(error);
|
|
1965
|
-
}
|
|
1966
|
-
});
|
|
2116
|
+
loaders: ['loaders/init-db.js']
|
|
1967
2117
|
```
|
|
1968
2118
|
|
|
1969
|
-
|
|
2119
|
+
3. Object entry with options:
|
|
1970
2120
|
|
|
1971
2121
|
```js
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
}
|
|
1976
|
-
|
|
1977
|
-
async sendWelcome(user) {
|
|
1978
|
-
return this.mail.send({
|
|
1979
|
-
to: user.email,
|
|
1980
|
-
subject: 'Welcome',
|
|
1981
|
-
html: `<h1>Hello ${user.name}</h1><p>Your account is ready.</p>`,
|
|
1982
|
-
});
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
2122
|
+
loaders: [
|
|
2123
|
+
{ path: 'loaders/init-queue.js', options: { queue: 'jobs' } },
|
|
2124
|
+
]
|
|
1985
2125
|
```
|
|
1986
2126
|
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
- Messages must include at least one of `to`, `cc`, or `bcc`.
|
|
1990
|
-
- Messages must include `from`, or configure `mail.defaults.from`.
|
|
1991
|
-
- For tests or custom providers, you can set `mail.transporter` or `mail.transportFactory` instead of `mail.transport`.
|
|
2127
|
+
Each loader module must export a function (default export preferred).
|
|
2128
|
+
Each loader receives the shared runtime context documented in the injection matrix above, plus `options` from the loader entry.
|
|
1992
2129
|
|
|
1993
|
-
###
|
|
2130
|
+
### Apps (`apps`)
|
|
1994
2131
|
|
|
1995
|
-
|
|
1996
|
-
They are also available in:
|
|
1997
|
-
- Service constructors (`constructor({ helpers, jlive, env, i18n, models, ... })`)
|
|
1998
|
-
- Model constructors (`constructor({ helpers, jlive, env, i18n, dbClient, ... })`)
|
|
1999
|
-
- Subscribers context (`registerSubscribers({ helpers, jlive, env, i18n, events, ... })`)
|
|
2000
|
-
- Any view/handler via request bridge: `req.aegis.helpers`, `req.aegis.jlive`, `req.aegis.env`, `req.aegis.locale`, `req.aegis.t`, `req.aegis.i18n`
|
|
2132
|
+
`apps` supports two forms:
|
|
2001
2133
|
|
|
2002
|
-
|
|
2134
|
+
```js
|
|
2135
|
+
apps: ['users', 'billing']
|
|
2136
|
+
```
|
|
2137
|
+
|
|
2138
|
+
or
|
|
2003
2139
|
|
|
2004
2140
|
```js
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
currencyDisplay: 'code',
|
|
2010
|
-
},
|
|
2011
|
-
},
|
|
2141
|
+
apps: [
|
|
2142
|
+
{ name: 'users', mount: '/users' },
|
|
2143
|
+
{ name: 'billing', mount: '/payments' },
|
|
2144
|
+
]
|
|
2012
2145
|
```
|
|
2013
2146
|
|
|
2014
|
-
|
|
2147
|
+
Rules:
|
|
2148
|
+
- `name` is required in object form.
|
|
2149
|
+
- `mount` defaults to `/${name}`.
|
|
2150
|
+
- Mounts are normalized to a single leading slash (except root `/`).
|
|
2151
|
+
- App route modules must be declared in `settings.apps` before they can load/mount.
|
|
2015
2152
|
|
|
2016
|
-
|
|
2153
|
+
### Environment Overrides (`environments`)
|
|
2154
|
+
|
|
2155
|
+
Example:
|
|
2017
2156
|
|
|
2018
2157
|
```js
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
res.json({
|
|
2023
|
-
price: req.aegis.helpers.money(1299.5, { currency: 'USD' }),
|
|
2024
|
-
createdAgo: req.aegis.helpers.timeElapsed(Date.now() - 60_000),
|
|
2025
|
-
createdAgoShort: req.aegis.helpers.timeElapsed(Math.floor(Date.now() / 1000) - 60, true),
|
|
2026
|
-
progress: req.aegis.helpers.timeDifference(65, 0, 100),
|
|
2027
|
-
summary: req.aegis.helpers.breakStr('AegisNode framework helper utilities', 18, '...', true),
|
|
2028
|
-
objectIdValid: req.aegis.helpers.isObjectId('507f1f77bcf86cd799439011'),
|
|
2029
|
-
objectIdString: req.aegis.helpers.toObjectId('507f1f77bcf86cd799439011')?.toString() || null,
|
|
2030
|
-
secret: req.aegis.jlive.generate(32),
|
|
2031
|
-
jliveAvailable: req.aegis.jlive.available,
|
|
2032
|
-
});
|
|
2033
|
-
});
|
|
2158
|
+
environments: {
|
|
2159
|
+
default: {
|
|
2160
|
+
logging: { level: 'debug' },
|
|
2034
2161
|
},
|
|
2035
|
-
|
|
2162
|
+
production: {
|
|
2163
|
+
logging: { level: 'warn' },
|
|
2164
|
+
security: { ddos: { maxRequests: 80 } },
|
|
2165
|
+
},
|
|
2166
|
+
}
|
|
2036
2167
|
```
|
|
2037
2168
|
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
<p>Updated: <%= timeElapsed(updatedAt) %></p>
|
|
2044
|
-
<p>Progress: <%= timeDifference(65, 0, 100) %>%</p>
|
|
2045
|
-
<p>Summary: <%= breakStr("AegisNode framework helper utilities", 18, "...", true) %></p>
|
|
2046
|
-
<p>With helper object: <%= helpers.number(1000000) %></p>
|
|
2047
|
-
```
|
|
2169
|
+
Behavior:
|
|
2170
|
+
- Base config loads first.
|
|
2171
|
+
- `environments.default` is merged next (if present).
|
|
2172
|
+
- `environments[env]` is merged last.
|
|
2173
|
+
- Arrays in overrides replace full arrays from base.
|
|
2048
2174
|
|
|
2049
|
-
|
|
2050
|
-
- `helpers`
|
|
2051
|
-
- `jlive`
|
|
2052
|
-
- `money`
|
|
2053
|
-
- `number`
|
|
2054
|
-
- `dateTime`
|
|
2055
|
-
- `timeElapsed`
|
|
2056
|
-
- `timeDifference`
|
|
2057
|
-
- `breakStr`
|
|
2058
|
-
- `isObjectId`
|
|
2059
|
-
- `toObjectId`
|
|
2060
|
-
- `locale`
|
|
2061
|
-
- `t`
|
|
2062
|
-
- `csrfToken` (raw hidden input HTML)
|
|
2063
|
-
- `csrfValue` (token string)
|
|
2175
|
+
<!-- SETTINGS_REFERENCE_END -->
|
|
2064
2176
|
|
|
2065
|
-
|
|
2066
|
-
- `timeElapsed(value, { now, locale, numeric })` (Intl relative style)
|
|
2067
|
-
- `timeElapsed(unixTime, true)` (short legacy-style mode)
|
|
2177
|
+
## Runtime Patterns And Advanced Topics
|
|
2068
2178
|
|
|
2069
|
-
|
|
2070
|
-
- `isObjectId(value)` validates Mongo ObjectId format.
|
|
2071
|
-
- `toObjectId(value)` returns a Mongo ObjectId instance or `null` when invalid.
|
|
2179
|
+
### Middleware
|
|
2072
2180
|
|
|
2073
|
-
|
|
2074
|
-
- If `jlive` package is installed, bridge uses its methods.
|
|
2075
|
-
- If not installed, `jlive.generate()` still works (crypto fallback), while crypto methods throw `JLIVE_UNAVAILABLE`.
|
|
2181
|
+
Route API supports Express-style middleware chains:
|
|
2076
2182
|
|
|
2077
|
-
|
|
2183
|
+
```js
|
|
2184
|
+
route.get('/secured', authGuard, UsersView.index);
|
|
2185
|
+
route.post('/users', validateBody, createUser);
|
|
2186
|
+
```
|
|
2078
2187
|
|
|
2079
|
-
You can
|
|
2188
|
+
You can also use `route.use()`:
|
|
2080
2189
|
|
|
2081
2190
|
```js
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
engine: 'ejs',
|
|
2085
|
-
dir: 'templates',
|
|
2086
|
-
base: 'base',
|
|
2087
|
-
locals: {
|
|
2088
|
-
formatCurrency: (value) => '$' + Number(value || 0).toFixed(2),
|
|
2089
|
-
ViewBag: class ViewBag {
|
|
2090
|
-
constructor(title) {
|
|
2091
|
-
this.title = title;
|
|
2092
|
-
}
|
|
2093
|
-
},
|
|
2094
|
-
},
|
|
2095
|
-
},
|
|
2191
|
+
route.use(requestLogger);
|
|
2192
|
+
route.use('/admin', adminGuard, adminRoutes);
|
|
2096
2193
|
```
|
|
2097
2194
|
|
|
2098
|
-
|
|
2195
|
+
Basic middleware example:
|
|
2099
2196
|
|
|
2100
2197
|
```js
|
|
2101
|
-
|
|
2198
|
+
function requestLogger(req, res, next) {
|
|
2199
|
+
console.log(req.method, req.originalUrl);
|
|
2200
|
+
next();
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
function requireModerator(req, res, next) {
|
|
2204
|
+
if (!req.auth?.roles?.includes('moderator')) {
|
|
2205
|
+
return res.status(403).json({ error: 'Moderator access required' });
|
|
2206
|
+
}
|
|
2207
|
+
next();
|
|
2208
|
+
}
|
|
2102
2209
|
|
|
2103
2210
|
export default {
|
|
2104
|
-
|
|
2105
|
-
|
|
2211
|
+
register(route) {
|
|
2212
|
+
route.use(requestLogger);
|
|
2213
|
+
route.get('/threads/:id/edit', requireModerator, ThreadsView.edit);
|
|
2214
|
+
route.use('/admin', requireModerator, adminRoutes);
|
|
2106
2215
|
},
|
|
2107
2216
|
};
|
|
2108
2217
|
```
|
|
2109
2218
|
|
|
2110
|
-
|
|
2111
|
-
- Use JS references/imports (as above).
|
|
2112
|
-
- String names like `{ formatCurrency: 'formatCurrency' }` are not auto-resolved.
|
|
2219
|
+
AegisNode-specific middleware with injected runtime context:
|
|
2113
2220
|
|
|
2114
|
-
|
|
2221
|
+
```js
|
|
2222
|
+
async function forumContext({ helpers, i18n, logger }, req, res, next) {
|
|
2223
|
+
logger.info(`Forum request: ${req.method} ${req.originalUrl}`);
|
|
2224
|
+
res.locals.t = i18n.t;
|
|
2225
|
+
res.locals.formatNumber = helpers.number;
|
|
2226
|
+
next();
|
|
2227
|
+
}
|
|
2115
2228
|
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2229
|
+
export default {
|
|
2230
|
+
register(route) {
|
|
2231
|
+
route.use(forumContext);
|
|
2232
|
+
route.get('/threads', ThreadsView.index);
|
|
2233
|
+
},
|
|
2234
|
+
};
|
|
2119
2235
|
```
|
|
2120
2236
|
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2237
|
+
Injected middleware context can include:
|
|
2238
|
+
- `config`
|
|
2239
|
+
- `env`
|
|
2240
|
+
- `i18n`
|
|
2241
|
+
- `mail`
|
|
2242
|
+
- `logger`
|
|
2243
|
+
- `events`
|
|
2244
|
+
- `cache`
|
|
2245
|
+
- `io`
|
|
2246
|
+
- `auth`
|
|
2247
|
+
- `helpers`
|
|
2248
|
+
- `upload`
|
|
2249
|
+
- `services`
|
|
2250
|
+
- `models`
|
|
2251
|
+
- `validators`
|
|
2252
|
+
- `service`
|
|
2253
|
+
- `model`
|
|
2254
|
+
- `validator`
|
|
2255
|
+
- `database`
|
|
2256
|
+
- `dbClient`
|
|
2124
2257
|
|
|
2125
|
-
|
|
2258
|
+
Auth middleware example:
|
|
2126
2259
|
|
|
2127
2260
|
```js
|
|
2128
|
-
|
|
2129
|
-
route
|
|
2261
|
+
export default {
|
|
2262
|
+
register(route) {
|
|
2263
|
+
const authGuard = (req, res, next) => req.aegis.auth.middleware()(req, res, next);
|
|
2264
|
+
route.get('/account', authGuard, UsersView.account);
|
|
2265
|
+
},
|
|
2266
|
+
};
|
|
2130
2267
|
```
|
|
2131
2268
|
|
|
2132
|
-
|
|
2269
|
+
Uploads also work as route middleware:
|
|
2133
2270
|
|
|
2134
2271
|
```js
|
|
2135
|
-
route.
|
|
2136
|
-
route.use('/admin', adminGuard, adminRoutes);
|
|
2272
|
+
route.post('/avatar', route.upload.single('avatar'), UsersView.uploadAvatar);
|
|
2137
2273
|
```
|
|
2138
2274
|
|
|
2275
|
+
Important note:
|
|
2276
|
+
- In AegisNode, a 4-argument function is treated as `(context, req, res, next)`.
|
|
2277
|
+
- Do not use Express error-handler shape `(err, req, res, next)` in route chains, because the first argument is reserved for injected runtime context.
|
|
2278
|
+
|
|
2139
2279
|
### Validators (Between Route And Service)
|
|
2140
2280
|
|
|
2141
2281
|
Each app can define `apps/<app>/validators.js`.
|
|
@@ -2428,7 +2568,7 @@ security: {
|
|
|
2428
2568
|
```
|
|
2429
2569
|
|
|
2430
2570
|
`security.appSecret` should be strong (at least 16 chars). It is used to sign CSRF cookies and is required when `security.csrf.requireSignedCookie` is true (default).
|
|
2431
|
-
New projects load it from `.env` through `APP_SECRET` by default.
|
|
2571
|
+
New projects load it from `.env` through `APP_SECRET` by default, and the generated `settings.js` also contains the same scaffold-time secret as a fallback literal.
|
|
2432
2572
|
If neither `APP_SECRET` nor `security.appSecret` is set, AegisNode generates a fallback secret and persists it to `.aegis/app-secret`.
|
|
2433
2573
|
If you run behind a reverse proxy, prefer top-level `trustProxy` (for example `1`) so client IP, secure-cookie detection, and HTTPS-aware auth logic are correct.
|
|
2434
2574
|
|
|
@@ -2447,6 +2587,8 @@ For forms, include token from `csrfToken`:
|
|
|
2447
2587
|
</form>
|
|
2448
2588
|
```
|
|
2449
2589
|
|
|
2590
|
+
For `multipart/form-data` routes, use `route.upload.*` so AegisNode can parse the multipart body and validate the hidden `_csrf` field before your handler runs.
|
|
2591
|
+
|
|
2450
2592
|
If you need the raw token string (for AJAX header), use `csrfValue`.
|
|
2451
2593
|
For API clients (Postman/mobile) you can either send `x-csrf-token` or set `security.csrf.rejectUnsafeMethods = false`.
|
|
2452
2594
|
|