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