aegisnode 0.0.1
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/LICENSE +21 -0
- package/README.md +2461 -0
- package/bin/aegisnode.js +9 -0
- package/package.json +56 -0
- package/scripts/smoke-test.js +1831 -0
- package/src/cli/commands/createapp.js +191 -0
- package/src/cli/commands/doctor.js +199 -0
- package/src/cli/commands/generate.js +266 -0
- package/src/cli/commands/runserver.js +17 -0
- package/src/cli/commands/startproject.js +72 -0
- package/src/cli/commands/updatedeps.js +355 -0
- package/src/cli/index.js +151 -0
- package/src/cli/utils/fs.js +53 -0
- package/src/cli/utils/project.js +67 -0
- package/src/cli/utils/scaffolds.js +596 -0
- package/src/index.js +20 -0
- package/src/runtime/auth.js +2291 -0
- package/src/runtime/cache.js +37 -0
- package/src/runtime/config.js +482 -0
- package/src/runtime/container.js +43 -0
- package/src/runtime/database.js +195 -0
- package/src/runtime/events.js +33 -0
- package/src/runtime/helpers.js +575 -0
- package/src/runtime/kernel.js +3713 -0
- package/src/runtime/loaders.js +46 -0
- package/src/runtime/logger.js +56 -0
- package/src/runtime/mail.js +225 -0
- package/src/runtime/upload.js +272 -0
- package/src/runtime/views/default-install.ejs +183 -0
- package/src/runtime/views/default-maintenance.ejs +148 -0
package/README.md
ADDED
|
@@ -0,0 +1,2461 @@
|
|
|
1
|
+
# AegisNode
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
AegisNode is a modular, view-first Node.js framework for building web apps, JSON APIs, and hybrid projects without spending the first part of the project wiring the same infrastructure again and again.
|
|
6
|
+
It gives you a structured project layout, runtime injection, CLI scaffolding, and production-ready defaults while still keeping the Node.js and Express ecosystem familiar.
|
|
7
|
+
|
|
8
|
+
AegisNode is designed for developers who want more structure than raw Express, but do not want a framework that hides the Node.js runtime behind too many abstractions.
|
|
9
|
+
It keeps the request/response model familiar while organizing the codebase around clear app boundaries, runtime-injected dependencies, and reusable layers such as views, services, models, validators, and subscribers.
|
|
10
|
+
|
|
11
|
+
It works well for projects that mix server-rendered pages and JSON endpoints, for teams that want a consistent project shape from the start, and for codebases that need built-in support for common backend concerns like auth, uploads, i18n, mail, maintenance mode, and environment-driven configuration.
|
|
12
|
+
The goal is to reduce setup time, remove repetitive infrastructure work, and give the project a cleaner long-term structure without making day-to-day development feel heavy.
|
|
13
|
+
|
|
14
|
+
## Documentation Guide
|
|
15
|
+
|
|
16
|
+
If you are new to AegisNode, read this README in this order:
|
|
17
|
+
- Quick Start: create a project, install dependencies, run the server, and understand startup mode rules.
|
|
18
|
+
- Core Concepts And App Structure: understand apps, layers, injected runtime context, and project flow.
|
|
19
|
+
- Common Tasks And Feature Guides: uploads, API apps, auth, templates, i18n, mail, and related features.
|
|
20
|
+
- Full Settings Reference: use this as the complete config manual once you already know what feature you need.
|
|
21
|
+
- Runtime Patterns And Advanced Topics: validators, strict layers, subscribers, and security details.
|
|
22
|
+
|
|
23
|
+
Standalone HTML handbook:
|
|
24
|
+
- Open `docs/index.html` in a browser for a sidebar-based documentation view.
|
|
25
|
+
|
|
26
|
+
## How AegisNode Helps
|
|
27
|
+
|
|
28
|
+
AegisNode helps by standardizing the parts that usually consume time early in a project:
|
|
29
|
+
- project scaffolding and app generation,
|
|
30
|
+
- route and layer organization,
|
|
31
|
+
- dependency injection and shared runtime context,
|
|
32
|
+
- config loading and environment overrides,
|
|
33
|
+
- auth, upload, mail, websocket, and i18n integration,
|
|
34
|
+
- operational helpers such as health checks, maintenance mode, and project diagnostics.
|
|
35
|
+
|
|
36
|
+
This means you spend less time writing framework glue and more time writing business features.
|
|
37
|
+
|
|
38
|
+
## Why Use AegisNode
|
|
39
|
+
|
|
40
|
+
Choose AegisNode if you want a project starter that remains readable as it grows.
|
|
41
|
+
It keeps the development model simple, but adds enough structure and tooling to make larger codebases easier to navigate, extend, and maintain.
|
|
42
|
+
|
|
43
|
+
Core features:
|
|
44
|
+
|
|
45
|
+
- CLI generators (`startproject`, `createapp`, `runserver`)
|
|
46
|
+
- Project health checker (`doctor`)
|
|
47
|
+
- Dependency updater (`updatedeps`)
|
|
48
|
+
- Maintenance mode with custom HTML responses
|
|
49
|
+
- Generators for app artifacts (`generate view|model|validator|dto|service|subscriber|route`)
|
|
50
|
+
- DI container
|
|
51
|
+
- Event system with subscribers
|
|
52
|
+
- Modular app structure
|
|
53
|
+
- SQL/NoSQL bootstrap via QueryMesh/Mongoose
|
|
54
|
+
- WebSocket bootstrap using Socket.IO
|
|
55
|
+
- Built-in file uploads with size/type limits (`route.upload`)
|
|
56
|
+
- Built-in mail transport wrapper (`mail.send`, `req.aegis.mail.send`)
|
|
57
|
+
- Centralized config and loaders
|
|
58
|
+
- Security headers via Helmet (configurable CSP) + CSRF protection for form submissions
|
|
59
|
+
- Built-in rate limiting for basic DDoS resistance
|
|
60
|
+
- Root route file `routes.js` (not `routes/` folder)
|
|
61
|
+
- Automatic default confirmation page on `/` when no custom `/` route exists
|
|
62
|
+
- App folder uses `views.js` (not `controllers/` folder)
|
|
63
|
+
- `createapp` uses file modules: `views.js`, `models.js`, `validators.js`, `routes.js`, `subscribers.js`, `services.js`
|
|
64
|
+
- `createapp` also generates app tests in `apps/<app>/tests`
|
|
65
|
+
- EJS templates configurable in `settings.js` with Django-style base layout flow
|
|
66
|
+
- Built-in runtime helpers (`money`, `number`, `dateTime`, `timeElapsed`, `toObjectId`) + `jlive` bridge
|
|
67
|
+
|
|
68
|
+
`startproject` creates `app.js`, `loader.cjs`, `.env`, `settings.js`, and `routes.js` without creating any default app.
|
|
69
|
+
It does not create `public/` or `logs/`; create your own folders and set them in `settings.js`.
|
|
70
|
+
|
|
71
|
+
Environment files are loaded automatically before `settings.js` is imported.
|
|
72
|
+
Supported files:
|
|
73
|
+
- `.env`
|
|
74
|
+
- `.env.local`
|
|
75
|
+
- `.env.<NODE_ENV>`
|
|
76
|
+
- `.env.<NODE_ENV>.local`
|
|
77
|
+
|
|
78
|
+
Shell or hosting-panel environment variables win over values from `.env` files.
|
|
79
|
+
|
|
80
|
+
## Quick Start
|
|
81
|
+
|
|
82
|
+
### CLI
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npm install -g aegisnode
|
|
86
|
+
|
|
87
|
+
aegisnode startproject blog
|
|
88
|
+
npm --prefix blog install
|
|
89
|
+
aegisnode runserver --project blog
|
|
90
|
+
|
|
91
|
+
aegisnode createapp users --project blog
|
|
92
|
+
aegisnode generate view profile --app users --project blog
|
|
93
|
+
aegisnode generate route profile --app users --project blog
|
|
94
|
+
aegisnode doctor --project blog
|
|
95
|
+
aegisnode updatedeps --project blog
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`cd blog` is optional. You can run commands from parent folder with `--project blog`.
|
|
99
|
+
|
|
100
|
+
`createapp`, `generate`, `runserver`, `doctor`, and `updatedeps` are project-level commands.
|
|
101
|
+
Run them from the project root; do not `cd` into `apps/<app>`.
|
|
102
|
+
Startup mode rules:
|
|
103
|
+
- Development (`env === development`): start with `aegisnode runserver` only.
|
|
104
|
+
- Non-development (`env !== development`): start with `node loader.cjs` (or your process manager/host pointing to `loader.cjs`).
|
|
105
|
+
- `node app.js` and `node loader.cjs` are rejected in development mode.
|
|
106
|
+
- `aegisnode runserver` is rejected outside development mode.
|
|
107
|
+
|
|
108
|
+
### Deploy On Phusion Passenger
|
|
109
|
+
|
|
110
|
+
AegisNode supports Passenger-style startup using the generated `loader.cjs`.
|
|
111
|
+
|
|
112
|
+
Passenger setup (Apache/Nginx/Plesk/cPanel/etc.):
|
|
113
|
+
1. Set **Application Root** to your project folder.
|
|
114
|
+
2. Set **Startup File** to `loader.cjs`.
|
|
115
|
+
3. Install dependencies in project root (for example: `npm install --omit=dev`).
|
|
116
|
+
4. Set environment variables (at minimum `NODE_ENV=production`; keep `PORT` managed by Passenger).
|
|
117
|
+
5. Restart the Node app from your hosting panel/service.
|
|
118
|
+
|
|
119
|
+
Plesk note: these map to **Application Root** and **Application Startup File** fields.
|
|
120
|
+
|
|
121
|
+
HTTPS note:
|
|
122
|
+
- If TLS is terminated by Passenger/Apache/Nginx, keep `https.enabled` off and set top-level `trustProxy` to `1` (or another exact proxy-hop/subnet value) so `req.secure`, secure cookies, and OAuth2 HTTPS checks work correctly.
|
|
123
|
+
- Only enable `https` in `settings.js` when Node itself should serve TLS directly.
|
|
124
|
+
|
|
125
|
+
How it works:
|
|
126
|
+
- `loader.cjs` imports `app.js`.
|
|
127
|
+
- `app.js` starts AegisNode with project root resolved from its own file location, so it works correctly under Passenger.
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
Generated routes are auto-wired into `apps/<app>/routes.js`.
|
|
131
|
+
`createapp` auto-detects the project root when run inside the project or from a parent folder containing exactly one AegisNode project.
|
|
132
|
+
After `createapp user`, `routes.js` is updated with central mapping style:
|
|
133
|
+
`route.use('/user', user);`
|
|
134
|
+
Only apps declared in `settings.apps` are allowed to load/mount. Startup fails when routes reference an undeclared app.
|
|
135
|
+
`--mount` accepts only safe path segments (`a-z`, `A-Z`, `0-9`, `_`, `-`, `:`).
|
|
136
|
+
|
|
137
|
+
By default, new app routes are API-ready:
|
|
138
|
+
- `GET /<mount>` list
|
|
139
|
+
- `POST /<mount>` create
|
|
140
|
+
- `GET /<mount>/:id` read
|
|
141
|
+
- `PUT /<mount>/:id` update
|
|
142
|
+
- `DELETE /<mount>/:id` delete
|
|
143
|
+
|
|
144
|
+
Default flow is `route -> validator -> service -> model`.
|
|
145
|
+
Default app tests generated by `createapp`:
|
|
146
|
+
- `apps/<app>/tests/models.test.js`
|
|
147
|
+
- `apps/<app>/tests/validators.test.js`
|
|
148
|
+
- `apps/<app>/tests/services.test.js`
|
|
149
|
+
- `apps/<app>/tests/routes.test.js`
|
|
150
|
+
|
|
151
|
+
Run all project tests:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
npm test
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Run project preflight checks:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
aegisnode doctor
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`doctor` checks:
|
|
164
|
+
- Project structure (`settings.js`, `routes.js`, app folders)
|
|
165
|
+
- App declarations vs filesystem
|
|
166
|
+
- Security baseline (`appSecret`, csrf/headers/ddos toggles)
|
|
167
|
+
- Auth safety checks (JWT secret, OAuth2 `allowHttp` in production)
|
|
168
|
+
- Template directory availability
|
|
169
|
+
|
|
170
|
+
Update project dependencies to the current npm `latest` dist-tag:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
aegisnode updatedeps
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`updatedeps` rewrites `dependencies`, `devDependencies`, `optionalDependencies`, and
|
|
177
|
+
`peerDependencies` in the project `package.json`, then runs the detected package manager's
|
|
178
|
+
`install`. It skips non-registry specs such as `file:`, `workspace:`, and git/http sources.
|
|
179
|
+
|
|
180
|
+
### Maintenance Mode
|
|
181
|
+
|
|
182
|
+
Enable maintenance mode in `settings.js` to serve a maintenance route with `503 Service Unavailable`.
|
|
183
|
+
If that route is missing or does not respond, AegisNode renders its internal maintenance fallback view:
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
export default {
|
|
187
|
+
maintenance: {
|
|
188
|
+
enabled: true,
|
|
189
|
+
route: '/maintenance',
|
|
190
|
+
excludePaths: ['/health'],
|
|
191
|
+
retryAfter: 120,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
```js
|
|
197
|
+
export default {
|
|
198
|
+
register(route) {
|
|
199
|
+
route.get('/maintenance', (req, res) => {
|
|
200
|
+
res.render('maintenance', {
|
|
201
|
+
title: 'Scheduled maintenance',
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Notes:
|
|
209
|
+
- `maintenance.route` is internally rewritten, so requests like `/users` can display your maintenance page without a redirect.
|
|
210
|
+
- If `maintenance.route` is not defined, or the route does not answer, the bundled fallback view is rendered.
|
|
211
|
+
- `excludePaths` lets selected endpoints keep running during maintenance.
|
|
212
|
+
- `retryAfter` sets the HTTP `Retry-After` header.
|
|
213
|
+
- `maintenance: true` uses the built-in default maintenance page.
|
|
214
|
+
- `maintenance: '<html>...</html>'` is still accepted as a shorthand for direct custom HTML.
|
|
215
|
+
|
|
216
|
+
### Generated Settings Config
|
|
217
|
+
|
|
218
|
+
`startproject` generates a minimal `settings.js` and runtime defaults fill the rest.
|
|
219
|
+
|
|
220
|
+
Access environment values anywhere with `process.env`:
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
export default {
|
|
224
|
+
port: process.env.PORT ? Number(process.env.PORT) : 3000,
|
|
225
|
+
security: {
|
|
226
|
+
appSecret: process.env.APP_SECRET || '',
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Injected app layers also receive `env`, so views/services/models/validators/controllers/subscribers/loaders can use `env.MY_NAME` without importing `process.env`.
|
|
232
|
+
|
|
233
|
+
`settings.js` (generated shape):
|
|
234
|
+
|
|
235
|
+
```js
|
|
236
|
+
export default {
|
|
237
|
+
appName: 'blog',
|
|
238
|
+
env: process.env.NODE_ENV || 'development',
|
|
239
|
+
host: process.env.HOST || '0.0.0.0',
|
|
240
|
+
port: process.env.PORT ? Number(process.env.PORT) : 3000,
|
|
241
|
+
trustProxy: false,
|
|
242
|
+
security: {
|
|
243
|
+
appSecret: process.env.APP_SECRET || '',
|
|
244
|
+
},
|
|
245
|
+
logging: {
|
|
246
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
247
|
+
},
|
|
248
|
+
database: {
|
|
249
|
+
enabled: false,
|
|
250
|
+
dialect: 'pg',
|
|
251
|
+
config: {},
|
|
252
|
+
options: {},
|
|
253
|
+
},
|
|
254
|
+
cache: {
|
|
255
|
+
enabled: true,
|
|
256
|
+
driver: 'memory',
|
|
257
|
+
options: {},
|
|
258
|
+
},
|
|
259
|
+
apps: [
|
|
260
|
+
// AEGIS_APPS_START
|
|
261
|
+
// AEGIS_APPS_END
|
|
262
|
+
],
|
|
263
|
+
};
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Notes:
|
|
267
|
+
- Keep `AEGIS_APPS_START/END` markers; `createapp` updates this list automatically.
|
|
268
|
+
- `startproject` also writes a local `.env` with a generated `APP_SECRET`.
|
|
269
|
+
- 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
|
+
- Any section you omit uses framework defaults from `src/runtime/config.js`.
|
|
271
|
+
|
|
272
|
+
<!-- SETTINGS_REFERENCE_START -->
|
|
273
|
+
## Full Settings Reference
|
|
274
|
+
|
|
275
|
+
All fields below are supported in `settings.js`. If you omit a field, AegisNode uses the runtime default.
|
|
276
|
+
|
|
277
|
+
Merge order used at startup:
|
|
278
|
+
1. Framework defaults (`defaultConfig`)
|
|
279
|
+
2. `settings.js`
|
|
280
|
+
3. Legacy `settings/index.js` (if present)
|
|
281
|
+
4. Legacy `settings/db.js` (merged into `database`)
|
|
282
|
+
5. Legacy `settings/cache.js` (merged into `cache`)
|
|
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`)
|
|
286
|
+
|
|
287
|
+
### Top-Level
|
|
288
|
+
|
|
289
|
+
| Key | Type / Default | Description |
|
|
290
|
+
| --- | --- | --- |
|
|
291
|
+
| `appName` | `string` / folder name | Application name used in logs and defaults. |
|
|
292
|
+
| `env` | `string` / `process.env.NODE_ENV || 'development'` | Active environment key for `environments` overrides. |
|
|
293
|
+
| `host` | `string` / `process.env.HOST || '0.0.0.0'` | Bind host for HTTP server. |
|
|
294
|
+
| `port` | `number` / `process.env.PORT || 3000` | Bind port for HTTP server. |
|
|
295
|
+
| `trustProxy` | `boolean \| number \| string` / `false` | Express `trust proxy` value. Set this when HTTPS is terminated by a reverse proxy/load balancer. |
|
|
296
|
+
| `https` | `object \| false` / see HTTPS table | Direct TLS server settings for Node-hosted HTTPS. |
|
|
297
|
+
| `staticDir` | `string \| null` / `null` | Static assets directory, relative to project root (if set). |
|
|
298
|
+
| `templates` | `object \| false` / see templates table | EJS template engine + layout settings. |
|
|
299
|
+
| `i18n` | `object` / see i18n table | Built-in locale detection + translator bridge (`req.aegis.t`, injected `i18n.t`). |
|
|
300
|
+
| `helpers` | `object` / see helpers table | Runtime helper defaults (for example currency/locale for `helpers.money`). |
|
|
301
|
+
| `security` | `object` / see security tables | Security headers, DDoS limiter, CSRF settings, app secret. |
|
|
302
|
+
| `logging` | `object` / `{ level: 'info' }` | Runtime logger level. |
|
|
303
|
+
| `database` | `object` / see database table | SQL or MongoDB connection settings. |
|
|
304
|
+
| `cache` | `object` / `{ enabled: true, driver: 'memory' }` | Cache backend settings. |
|
|
305
|
+
| `websocket` | `object` / `{ enabled: true, cors: { origin: false } }` | Socket.IO server options. |
|
|
306
|
+
| `uploads` | `object` / see uploads table | Built-in file upload middleware settings used by `route.upload`. |
|
|
307
|
+
| `mail` | `object` / see mail table | Nodemailer-backed mail manager available as injected `mail` and `req.aegis.mail`. |
|
|
308
|
+
| `api` | `object` / see API table | API-app middleware behavior (JSON enforcement, no-store, CSRF skip for API mounts). |
|
|
309
|
+
| `auth` | `object` / see auth tables | JWT or OAuth2 provider settings. |
|
|
310
|
+
| `swagger` | `object` / see swagger table | OpenAPI JSON + Swagger UI settings. |
|
|
311
|
+
| `architecture` | `object` / `{ strictLayers: false }` | Layering enforcement mode. |
|
|
312
|
+
| `autoMountApps` | `boolean` / `false` | Auto-mount each app route file from `settings.apps`. |
|
|
313
|
+
| `loaders` | `array` / `[]` | Startup loaders run before routes mounting. |
|
|
314
|
+
| `apps` | `array` / `[]` | Declared apps with mount points. |
|
|
315
|
+
| `environments` | `object` / `{}` | Environment-specific deep overrides. |
|
|
316
|
+
|
|
317
|
+
Notes:
|
|
318
|
+
- `rootDir` is internal and set by runtime; do not manage it manually.
|
|
319
|
+
- Arrays are replaced (not merged) during deep merge.
|
|
320
|
+
|
|
321
|
+
### HTTPS (`https`)
|
|
322
|
+
|
|
323
|
+
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.
|
|
324
|
+
|
|
325
|
+
| Key | Type / Default | Description |
|
|
326
|
+
| --- | --- | --- |
|
|
327
|
+
| `enabled` | `boolean` / `false` | Create an HTTPS server instead of HTTP. |
|
|
328
|
+
| `key` | `string \| Buffer` / `null` | TLS private key content. |
|
|
329
|
+
| `cert` | `string \| Buffer` / `null` | TLS certificate content. |
|
|
330
|
+
| `ca` | `string \| Buffer \| array` / `null` | Optional CA/intermediate certificate content. |
|
|
331
|
+
| `pfx` | `string \| Buffer` / `null` | PFX/PKCS#12 archive content. Use instead of `key` + `cert`. |
|
|
332
|
+
| `keyPath` | `string` / `''` | Path to TLS private key, relative to project root or absolute. |
|
|
333
|
+
| `certPath` | `string` / `''` | Path to TLS certificate, relative to project root or absolute. |
|
|
334
|
+
| `caPath` | `string \| string[]` / `null` | Optional CA/intermediate certificate path(s). |
|
|
335
|
+
| `pfxPath` | `string` / `''` | Path to PFX/PKCS#12 archive. |
|
|
336
|
+
| `passphrase` | `string` / `''` | Optional passphrase for encrypted key/PFX files. |
|
|
337
|
+
| `options` | `object` / `{}` | Extra Node `https.createServer` options (for example `minVersion`). |
|
|
338
|
+
|
|
339
|
+
Direct HTTPS example:
|
|
340
|
+
|
|
341
|
+
```js
|
|
342
|
+
export default {
|
|
343
|
+
host: '0.0.0.0',
|
|
344
|
+
port: 3443,
|
|
345
|
+
https: {
|
|
346
|
+
enabled: true,
|
|
347
|
+
keyPath: 'certs/localhost-key.pem',
|
|
348
|
+
certPath: 'certs/localhost-cert.pem',
|
|
349
|
+
options: {
|
|
350
|
+
minVersion: 'TLSv1.2',
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Reverse-proxy HTTPS example:
|
|
357
|
+
|
|
358
|
+
```js
|
|
359
|
+
export default {
|
|
360
|
+
host: '127.0.0.1',
|
|
361
|
+
port: 3000,
|
|
362
|
+
trustProxy: 1,
|
|
363
|
+
};
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Notes:
|
|
367
|
+
- `https` requires either `pfx`/`pfxPath` or both `key`/`keyPath` and `cert`/`certPath`.
|
|
368
|
+
- Paths resolve from project root unless absolute.
|
|
369
|
+
- `trustProxy` affects `req.secure`, `req.protocol`, secure cookies, and OAuth2 secure transport checks.
|
|
370
|
+
- Prefer `1`, a subnet, or another exact Express `trust proxy` value instead of `true` when rate limiting is enabled.
|
|
371
|
+
|
|
372
|
+
### Templates (`templates`)
|
|
373
|
+
|
|
374
|
+
| Key | Type / Default | Description |
|
|
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`. |
|
|
382
|
+
|
|
383
|
+
Layout notes:
|
|
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.
|
|
388
|
+
|
|
389
|
+
### Internationalization (`i18n`)
|
|
390
|
+
|
|
391
|
+
| Key | Type / Default | Description |
|
|
392
|
+
| --- | --- | --- |
|
|
393
|
+
| `enabled` | `boolean` / `false` | Enable built-in i18n translator bridge. |
|
|
394
|
+
| `defaultLocale` | `string` / `'en'` | Default locale used when detection fails. |
|
|
395
|
+
| `fallbackLocale` | `string` / `'en'` | Fallback locale used when key is missing in active locale. |
|
|
396
|
+
| `supported` | `string[]` / `['en']` | Allowed locales. Values normalize to lowercase (for example `en-US` -> `en-us`). |
|
|
397
|
+
| `queryParam` | `string` / `'lang'` | Query parameter used for locale selection (for example `?lang=fr`). |
|
|
398
|
+
| `cookieName` | `string` / `'aegis_locale'` | Cookie used to persist locale. |
|
|
399
|
+
| `detectFromHeader` | `boolean` / `true` | Enable locale detection from `Accept-Language` header. |
|
|
400
|
+
| `detectFromCookie` | `boolean` / `true` | Enable locale detection from configured cookie. |
|
|
401
|
+
| `detectFromQuery` | `boolean` / `true` | Enable locale detection from query parameter. |
|
|
402
|
+
| `translations` | `object` / `{}` | Translation map by locale. Values can be objects or JSON file paths: `{ en: { ... }, fr: 'locales/fr.json' }`. Alias keys `locales` and `messages` are also accepted. |
|
|
403
|
+
| `translationsFile` | `string` / unset | Path to a JSON file containing all locales (example: `{ "en": {...}, "fr": {...} }`). Inline `translations` overrides file values for same locale keys. |
|
|
404
|
+
|
|
405
|
+
i18n notes:
|
|
406
|
+
- Detection order: query -> cookie -> `Accept-Language` -> `defaultLocale`.
|
|
407
|
+
- Use dotted keys like `home.title`.
|
|
408
|
+
- Placeholder interpolation supports `{name}` style tokens.
|
|
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`.
|
|
412
|
+
|
|
413
|
+
### Helpers Defaults (`helpers`)
|
|
414
|
+
|
|
415
|
+
| Key | Type / Default | Description |
|
|
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`.
|
|
429
|
+
|
|
430
|
+
### Security (`security`)
|
|
431
|
+
|
|
432
|
+
| Key | Type / Default | Description |
|
|
433
|
+
| --- | --- | --- |
|
|
434
|
+
| `appSecret` | `string` / `''` | Shared secret for signing security artifacts. Use at least 16 chars. |
|
|
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. |
|
|
438
|
+
|
|
439
|
+
#### Security Headers (`security.headers`)
|
|
440
|
+
|
|
441
|
+
| Key | Type / Default | Description |
|
|
442
|
+
| --- | --- | --- |
|
|
443
|
+
| `enabled` | `boolean` / `true` | Enable Helmet middleware. |
|
|
444
|
+
| `csp` | `object` / see CSP table | Content Security Policy behavior. |
|
|
445
|
+
|
|
446
|
+
#### CSP (`security.headers.csp`)
|
|
447
|
+
|
|
448
|
+
| Key | Type / Default | Description |
|
|
449
|
+
| --- | --- | --- |
|
|
450
|
+
| `enabled` | `boolean` / `true` | Enable CSP header from Helmet. |
|
|
451
|
+
| `reportOnly` | `boolean` / `false` | Use report-only mode. |
|
|
452
|
+
| `directives` | `object` / `{}` | Directive overrides. Set a directive to `false`/`null` to remove it. |
|
|
453
|
+
|
|
454
|
+
Default CSP base includes safe defaults such as `defaultSrc 'self'`, `objectSrc 'none'`, `frameAncestors 'none'`, and websocket-aware `connectSrc`.
|
|
455
|
+
|
|
456
|
+
Allow multiple external domains by adding them per directive (not globally):
|
|
457
|
+
|
|
458
|
+
```js
|
|
459
|
+
security: {
|
|
460
|
+
headers: {
|
|
461
|
+
csp: {
|
|
462
|
+
directives: {
|
|
463
|
+
scriptSrc: ["'self'", 'https://cdn.jsdelivr.net', 'https://unpkg.com'],
|
|
464
|
+
scriptSrcElem: ["'self'", 'https://cdn.jsdelivr.net', 'https://unpkg.com'],
|
|
465
|
+
styleSrc: ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net'],
|
|
466
|
+
styleSrcElem: ["'self'", 'https://cdn.jsdelivr.net'],
|
|
467
|
+
imgSrc: ["'self'", 'data:', 'https://cdn.jsdelivr.net'],
|
|
468
|
+
connectSrc: ["'self'", 'https://api.example.com', 'wss://socket.example.com'],
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Notes:
|
|
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.
|
|
479
|
+
|
|
480
|
+
Google Fonts example:
|
|
481
|
+
|
|
482
|
+
```js
|
|
483
|
+
security: {
|
|
484
|
+
headers: {
|
|
485
|
+
csp: {
|
|
486
|
+
directives: {
|
|
487
|
+
styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
|
|
488
|
+
styleSrcElem: ["'self'", 'https://fonts.googleapis.com'],
|
|
489
|
+
fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
#### DDoS / Rate Limit (`security.ddos`)
|
|
497
|
+
|
|
498
|
+
| Key | Type / Default | Description |
|
|
499
|
+
| --- | --- | --- |
|
|
500
|
+
| `enabled` | `boolean` / `true` | Enable rate limiter. |
|
|
501
|
+
| `windowMs` | `number` / `60000` | Rate limit window in milliseconds. |
|
|
502
|
+
| `maxRequests` | `number` / `120` | Max requests per window per key. |
|
|
503
|
+
| `message` | `string` / `'Too many requests, please try again later.'` | JSON error message text. |
|
|
504
|
+
| `statusCode` | `number` / `429` | Response status code when limited. |
|
|
505
|
+
| `standardHeaders` | `boolean` / `true` | Emit modern rate-limit headers. |
|
|
506
|
+
| `legacyHeaders` | `boolean` / `false` | Emit legacy `X-RateLimit-*` headers. |
|
|
507
|
+
| `skipSuccessfulRequests` | `boolean` / `false` | Do not count successful responses. |
|
|
508
|
+
| `skipFailedRequests` | `boolean` / `false` | Do not count failed responses. |
|
|
509
|
+
| `trustProxy` | `boolean \| number \| string` / `false` | Legacy alias for top-level `trustProxy`. Still supported for backward compatibility. |
|
|
510
|
+
| `store` | `object \| null` / `null` | Custom rate-limit store implementation. |
|
|
511
|
+
| `skipPaths` | `string[]` / `['/health']` | Path prefixes excluded from limiter. |
|
|
512
|
+
|
|
513
|
+
#### CSRF (`security.csrf`)
|
|
514
|
+
|
|
515
|
+
| Key | Type / Default | Description |
|
|
516
|
+
| --- | --- | --- |
|
|
517
|
+
| `enabled` | `boolean` / `true` | Enable CSRF middleware. |
|
|
518
|
+
| `rejectForms` | `boolean` / `true` | Enforce CSRF on form submissions. |
|
|
519
|
+
| `rejectUnsafeMethods` | `boolean` / `true` | Enforce CSRF for unsafe methods (`POST/PUT/PATCH/DELETE`) in general. |
|
|
520
|
+
| `cookieName` | `string` / `'_aegis_csrf'` | CSRF cookie name. |
|
|
521
|
+
| `fieldName` | `string` / `'_csrf'` | Body form field name for CSRF token. |
|
|
522
|
+
| `headerName` | `string` / `'x-csrf-token'` | Header token key (normalized lowercase). |
|
|
523
|
+
| `requireSignedCookie` | `boolean` / `true` | Require signed CSRF cookie values. |
|
|
524
|
+
| `sameSite` | `'lax' \| 'strict' \| 'none' \| false` / `'lax'` | CSRF cookie same-site mode. |
|
|
525
|
+
| `secure` | `boolean \| 'auto'` / `'auto'` | Secure cookie flag (`auto` respects request security). |
|
|
526
|
+
| `httpOnly` | `boolean` / `true` | Make CSRF cookie httpOnly. |
|
|
527
|
+
| `path` | `string` / `'/'` | CSRF cookie path. |
|
|
528
|
+
|
|
529
|
+
CSRF notes:
|
|
530
|
+
- With `requireSignedCookie: true` (default), `security.appSecret` must be strong (minimum 16 chars) or startup fails.
|
|
531
|
+
- CSRF is skipped for configured API app mounts when `api.disableCsrf: true`.
|
|
532
|
+
- CSRF is skipped on built-in OAuth2 server endpoints.
|
|
533
|
+
|
|
534
|
+
### Logging (`logging`)
|
|
535
|
+
|
|
536
|
+
| Key | Type / Default | Description |
|
|
537
|
+
| --- | --- | --- |
|
|
538
|
+
| `level` | `'error' \| 'warn' \| 'info' \| 'debug' \| 'trace'` / `'info'` | Logger verbosity threshold. |
|
|
539
|
+
|
|
540
|
+
### Database (`database`)
|
|
541
|
+
|
|
542
|
+
| Key | Type / Default | Description |
|
|
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`. |
|
|
549
|
+
|
|
550
|
+
Mongo config shortcuts accepted in `database.config`:
|
|
551
|
+
- `connectionString` (preferred)
|
|
552
|
+
- or `server`/`host`, `port`, `database`/`dbName`, `user`/`username`, `password`
|
|
553
|
+
|
|
554
|
+
### Cache (`cache`)
|
|
555
|
+
|
|
556
|
+
| Key | Type / Default | Description |
|
|
557
|
+
| --- | --- | --- |
|
|
558
|
+
| `enabled` | `boolean` / `true` | Enable cache service. |
|
|
559
|
+
| `driver` | `string` / `'memory'` | Cache driver. Currently only `memory` is built-in. |
|
|
560
|
+
| `options` | `object` / `{}` | Reserved for future/custom drivers. |
|
|
561
|
+
|
|
562
|
+
### WebSocket (`websocket`)
|
|
563
|
+
|
|
564
|
+
| Key | Type / Default | Description |
|
|
565
|
+
| --- | --- | --- |
|
|
566
|
+
| `enabled` | `boolean` / `true` | Enable Socket.IO server. |
|
|
567
|
+
| `cors` | `object` / `{ origin: false }` | Passed directly to Socket.IO `cors` option. |
|
|
568
|
+
|
|
569
|
+
### Uploads (`uploads`)
|
|
570
|
+
|
|
571
|
+
| Key | Type / Default | Description |
|
|
572
|
+
| --- | --- | --- |
|
|
573
|
+
| `enabled` | `boolean` / `true` | Enable built-in upload manager. |
|
|
574
|
+
| `dir` | `string` / `'uploads'` | Upload destination folder (absolute or relative to project root). |
|
|
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. |
|
|
584
|
+
|
|
585
|
+
### Mail (`mail`)
|
|
586
|
+
|
|
587
|
+
| Key | Type / Default | Description |
|
|
588
|
+
| --- | --- | --- |
|
|
589
|
+
| `enabled` | `boolean` / `false` | Enable the built-in mail manager. |
|
|
590
|
+
| `defaults` | `object` / `{ from: '', replyTo: '' }` | Default message fields merged into every outgoing mail payload. |
|
|
591
|
+
| `defaults.from` | `string` / `''` | Default sender used when `mail.send(...)` omits `from`. |
|
|
592
|
+
| `defaults.replyTo` | `string` / `''` | Default `replyTo` header for outgoing mail. |
|
|
593
|
+
| `transport` | `object \| string` / `{}` | Nodemailer transport config or SMTP connection URL passed to `nodemailer.createTransport(...)`. |
|
|
594
|
+
| `transporter` | `object \| null` / `null` | Prebuilt transporter object with `sendMail()`. Useful in tests or custom integrations. |
|
|
595
|
+
| `transportFactory` | `function \| null` / `null` | Factory that returns a transporter object with `sendMail()`. |
|
|
596
|
+
| `verifyOnStartup` | `boolean` / `false` | Call `transporter.verify()` during boot when the transporter supports it. |
|
|
597
|
+
|
|
598
|
+
Mail notes:
|
|
599
|
+
- The runtime uses [Nodemailer](https://nodemailer.com/).
|
|
600
|
+
- `mail.send(payload)` and `mail.sendMail(payload)` are aliases.
|
|
601
|
+
- Each payload must include at least one of `to`, `cc`, or `bcc`.
|
|
602
|
+
- Each payload must include `from`, or configure `mail.defaults.from`.
|
|
603
|
+
- Injected `mail` is available in handlers, services, models, validators, controllers, subscribers, and loaders, plus `req.aegis.mail`.
|
|
604
|
+
|
|
605
|
+
### API (`api`)
|
|
606
|
+
|
|
607
|
+
| Key | Type / Default | Description |
|
|
608
|
+
| --- | --- | --- |
|
|
609
|
+
| `apps` | `string[]` / `[]` | App names treated as API apps. Mounts are resolved from `settings.apps`. |
|
|
610
|
+
| `disableCsrf` | `boolean` / `true` | Skip CSRF checks for API app mounts. |
|
|
611
|
+
| `requireJsonForUnsafeMethods` | `boolean` / `true` | Reject unsafe API payloads unless `Content-Type` is JSON (`415`). Multipart is allowed when `uploads.allowApiMultipart=true`. |
|
|
612
|
+
| `noStoreHeaders` | `boolean` / `true` | Set `Cache-Control: no-store` on API responses. |
|
|
613
|
+
|
|
614
|
+
API notes:
|
|
615
|
+
- `api.apps` contains app names from `settings.apps`, not URL paths.
|
|
616
|
+
- Marking an app as API does not generate REST routes or change the app file structure. It only applies API middleware to that app mount.
|
|
617
|
+
- The effective API mount comes from `settings.apps[].mount` (or `/${app}` by default). Keep that aligned with your `route.use(...)` mount when `autoMountApps` is off.
|
|
618
|
+
|
|
619
|
+
### Auth (`auth`)
|
|
620
|
+
|
|
621
|
+
| Key | Type / Default | Description |
|
|
622
|
+
| --- | --- | --- |
|
|
623
|
+
| `enabled` | `boolean` / `false` | Enable auth manager. |
|
|
624
|
+
| `provider` | `'jwt' \| 'oauth2'` / `'jwt'` | Active auth provider. |
|
|
625
|
+
| `tablePrefix` | `string` / `'aegisnode'` | Prefix for auth storage namespaces/tables; sanitized to lowercase `[a-z0-9_]`. |
|
|
626
|
+
| `storage` | `object` / see storage table | Persistence backend for auth state. |
|
|
627
|
+
| `jwt` | `object` / see jwt table | JWT configuration. |
|
|
628
|
+
| `oauth2` | `object` / see oauth2 table | OAuth2 server and token configuration. |
|
|
629
|
+
|
|
630
|
+
#### Auth Storage (`auth.storage`)
|
|
631
|
+
|
|
632
|
+
| Key | Type / Default | Description |
|
|
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`. |
|
|
638
|
+
|
|
639
|
+
#### JWT (`auth.jwt`)
|
|
640
|
+
|
|
641
|
+
| Key | Type / Default | Description |
|
|
642
|
+
| --- | --- | --- |
|
|
643
|
+
| `secret` | `string` / `security.appSecret` fallback | Signing secret. Required for JWT auth. |
|
|
644
|
+
| `algorithm` | `'HS256' \| 'HS384' \| 'HS512'` / `'HS256'` | JWT HMAC algorithm. |
|
|
645
|
+
| `issuer` | `string` / `appName` | JWT issuer claim. |
|
|
646
|
+
| `audience` | `string` / `appName` | JWT audience claim. |
|
|
647
|
+
| `expiresIn` | `string` / `'15m'` | Access token TTL. |
|
|
648
|
+
| `refreshExpiresIn` | `string` / `'7d'` | Refresh token TTL. |
|
|
649
|
+
|
|
650
|
+
#### OAuth2 Core (`auth.oauth2`)
|
|
651
|
+
|
|
652
|
+
| Key | Type / Default | Description |
|
|
653
|
+
| --- | --- | --- |
|
|
654
|
+
| `accessTokenTtlSeconds` | `number` / `3600` | Access token TTL in seconds. |
|
|
655
|
+
| `refreshTokenTtlSeconds` | `number` / `1209600` | Refresh token TTL in seconds. |
|
|
656
|
+
| `authorizationCodeTtlSeconds` | `number` / `600` | Authorization code TTL in seconds. |
|
|
657
|
+
| `rotateRefreshToken` | `boolean` / `true` | Rotate refresh token on refresh flow. |
|
|
658
|
+
| `requireClientSecret` | `boolean` / `true` | Require secret for confidential client flows. |
|
|
659
|
+
| `requirePkce` | `boolean` / `true` | Require PKCE for authorization_code flow. |
|
|
660
|
+
| `allowPlainPkce` | `boolean` / `false` | Allow PKCE `plain` method. |
|
|
661
|
+
| `grants` | `string[]` / `['authorization_code','refresh_token','client_credentials']` | Enabled OAuth2 grants. |
|
|
662
|
+
| `defaultScopes` | `string[]` / `[]` | Scopes assigned when none requested/provided. |
|
|
663
|
+
| `clientAuthMethod` | `'client_secret_basic' \| 'client_secret_post' \| 'none'` / `'client_secret_basic'` | Default client authentication method. |
|
|
664
|
+
| `server` | `object` / see OAuth2 server table | Built-in authorization server endpoint settings. |
|
|
665
|
+
|
|
666
|
+
#### OAuth2 Server (`auth.oauth2.server`)
|
|
667
|
+
|
|
668
|
+
| Key | Type / Default | Description |
|
|
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`. |
|
|
686
|
+
|
|
687
|
+
### Swagger (`swagger`)
|
|
688
|
+
|
|
689
|
+
| Key | Type / Default | Description |
|
|
690
|
+
| --- | --- | --- |
|
|
691
|
+
| `enabled` | `boolean` / `false` | Enable Swagger UI + OpenAPI JSON endpoints. |
|
|
692
|
+
| `docsPath` | `string` / `'/docs'` | Swagger UI route. |
|
|
693
|
+
| `jsonPath` | `string` / `'/openapi.json'` | OpenAPI JSON route. |
|
|
694
|
+
| `document` | `object \| null` / `null` | Inline OpenAPI document object. |
|
|
695
|
+
| `documentPath` | `string` / `'openapi.json'` | JSON file path used when `document` is not provided. |
|
|
696
|
+
| `explorer` | `boolean` / `true` | Enable Swagger UI explorer mode. |
|
|
697
|
+
|
|
698
|
+
### Architecture (`architecture`)
|
|
699
|
+
|
|
700
|
+
| Key | Type / Default | Description |
|
|
701
|
+
| --- | --- | --- |
|
|
702
|
+
| `strictLayers` | `boolean` / `false` | Enforce `route -> validator -> service -> model` restrictions. |
|
|
703
|
+
|
|
704
|
+
### Auto Mount (`autoMountApps`)
|
|
705
|
+
|
|
706
|
+
| Key | Type / Default | Description |
|
|
707
|
+
| --- | --- | --- |
|
|
708
|
+
| `autoMountApps` | `boolean` / `false` | If `true`, each `apps/<name>/routes.js` is mounted automatically from `settings.apps`. If `false`, central `routes.js` controls mounting. |
|
|
709
|
+
|
|
710
|
+
### Loaders (`loaders`)
|
|
711
|
+
|
|
712
|
+
`loaders` accepts an array of entries run in order during startup:
|
|
713
|
+
|
|
714
|
+
1. Function entry:
|
|
715
|
+
|
|
716
|
+
```js
|
|
717
|
+
loaders: [
|
|
718
|
+
async ({ logger, options, config }) => {
|
|
719
|
+
logger.info('loader ran');
|
|
720
|
+
},
|
|
721
|
+
]
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
2. Path entry (relative or absolute):
|
|
725
|
+
|
|
726
|
+
```js
|
|
727
|
+
loaders: ['loaders/init-db.js']
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
3. Object entry with options:
|
|
731
|
+
|
|
732
|
+
```js
|
|
733
|
+
loaders: [
|
|
734
|
+
{ path: 'loaders/init-queue.js', options: { queue: 'jobs' } },
|
|
735
|
+
]
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
Each loader module must export a function (default export preferred).
|
|
739
|
+
Each loader receives the shared runtime context documented in the injection matrix above, plus `options` from the loader entry.
|
|
740
|
+
|
|
741
|
+
### Apps (`apps`)
|
|
742
|
+
|
|
743
|
+
`apps` supports two forms:
|
|
744
|
+
|
|
745
|
+
```js
|
|
746
|
+
apps: ['users', 'billing']
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
or
|
|
750
|
+
|
|
751
|
+
```js
|
|
752
|
+
apps: [
|
|
753
|
+
{ name: 'users', mount: '/users' },
|
|
754
|
+
{ name: 'billing', mount: '/payments' },
|
|
755
|
+
]
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
Rules:
|
|
759
|
+
- `name` is required in object form.
|
|
760
|
+
- `mount` defaults to `/${name}`.
|
|
761
|
+
- Mounts are normalized to a single leading slash (except root `/`).
|
|
762
|
+
- App route modules must be declared in `settings.apps` before they can load/mount.
|
|
763
|
+
|
|
764
|
+
### Environment Overrides (`environments`)
|
|
765
|
+
|
|
766
|
+
Example:
|
|
767
|
+
|
|
768
|
+
```js
|
|
769
|
+
environments: {
|
|
770
|
+
default: {
|
|
771
|
+
logging: { level: 'debug' },
|
|
772
|
+
},
|
|
773
|
+
production: {
|
|
774
|
+
logging: { level: 'warn' },
|
|
775
|
+
security: { ddos: { maxRequests: 80 } },
|
|
776
|
+
},
|
|
777
|
+
}
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
Behavior:
|
|
781
|
+
- Base config loads first.
|
|
782
|
+
- `environments.default` is merged next (if present).
|
|
783
|
+
- `environments[env]` is merged last.
|
|
784
|
+
- Arrays in overrides replace full arrays from base.
|
|
785
|
+
|
|
786
|
+
<!-- SETTINGS_REFERENCE_END -->
|
|
787
|
+
|
|
788
|
+
## Core Concepts And App Structure
|
|
789
|
+
|
|
790
|
+
### App File Usage Examples
|
|
791
|
+
|
|
792
|
+
Each generated app usually contains:
|
|
793
|
+
- `apps/<app>/views.js`
|
|
794
|
+
- `apps/<app>/models.js`
|
|
795
|
+
- `apps/<app>/services.js`
|
|
796
|
+
- `apps/<app>/subscribers.js`
|
|
797
|
+
- `apps/<app>/routes.js`
|
|
798
|
+
|
|
799
|
+
Usage by file:
|
|
800
|
+
- `views.js`: HTTP handlers (`req`, `res`, `next`). Default signature can be context-first: `handler({ service, validator, services, validators, ... }, req, res, next)`.
|
|
801
|
+
- `models.js`: data access layer only (SQL/NoSQL operations).
|
|
802
|
+
- `services.js`: business logic layer; orchestrates models.
|
|
803
|
+
- `subscribers.js`: event listeners (for example `app.booted`, `ws.connection`, custom events).
|
|
804
|
+
- `routes.js`: route mapping only (`route.get(...)`, `route.post(...)`, `route.use(...)`) to view handlers.
|
|
805
|
+
|
|
806
|
+
Route modules are mapping-only (`register(route)`).
|
|
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>')`.
|
|
810
|
+
|
|
811
|
+
What “app-scoped” means:
|
|
812
|
+
- In app routes (for example inside `apps/users/routes.js`), `{ service }` resolves to that app service.
|
|
813
|
+
- 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>')`.
|
|
814
|
+
|
|
815
|
+
Injected runtime dependencies:
|
|
816
|
+
|
|
817
|
+
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.
|
|
818
|
+
|
|
819
|
+
Available by layer:
|
|
820
|
+
- 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`
|
|
821
|
+
- Services (`constructor({ ... })`): `appName`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `models`, `validators`, `services`
|
|
822
|
+
- Models (`constructor({ ... })`): `appName`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `helpers`, `jlive`, `dbClient`, `database`
|
|
823
|
+
- Validators (`constructor({ ... })`): `appName`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `dbClient`, `database`
|
|
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`
|
|
829
|
+
|
|
830
|
+
Key meanings:
|
|
831
|
+
|
|
832
|
+
| Key | Description |
|
|
833
|
+
| --- | --- |
|
|
834
|
+
| `config` | Resolved runtime config from `settings.js`, framework defaults, environment overrides, and runtime overrides. |
|
|
835
|
+
| `env` | Frozen environment snapshot (`process.env` plus runtime additions such as `APP_SECRET`). |
|
|
836
|
+
| `i18n` | Translator bridge. During a request it follows the active request locale; outside a request it falls back to `defaultLocale` unless you pass `{ locale }`. |
|
|
837
|
+
| `mail` | Mail manager. Use `mail.send({ to, subject, text/html })` or `mail.sendMail(...)`. |
|
|
838
|
+
| `logger` | Runtime logger instance. |
|
|
839
|
+
| `events` | Event bus used by subscribers and app code. |
|
|
840
|
+
| `cache` | Cache backend instance (memory by default). |
|
|
841
|
+
| `io` | Socket.IO server instance when websocket support is enabled. |
|
|
842
|
+
| `auth` | Auth manager for JWT/OAuth2 flows. |
|
|
843
|
+
| `helpers` | Runtime helper functions such as `money`, `number`, `dateTime`, and `timeElapsed`. |
|
|
844
|
+
| `jlive` | jlive bridge instance. |
|
|
845
|
+
| `upload` | Upload manager used by `route.upload`. |
|
|
846
|
+
| `services` | Layer accessor used to fetch services by app/name. |
|
|
847
|
+
| `models` | Layer accessor used to fetch models by app/name. |
|
|
848
|
+
| `validators` | Layer accessor used to fetch validators by app/name. |
|
|
849
|
+
| `service` | App-scoped convenience service for the current app only. |
|
|
850
|
+
| `model` | App-scoped convenience model for the current app only. |
|
|
851
|
+
| `validator` | App-scoped convenience validator for the current app only. |
|
|
852
|
+
| `database` | Database runtime wrapper. |
|
|
853
|
+
| `dbClient` | Low-level database/query client. |
|
|
854
|
+
| `appName` | Current app name. |
|
|
855
|
+
| `app` | Current app metadata/context. |
|
|
856
|
+
| `rootDir` | Absolute project root. |
|
|
857
|
+
| `server` | HTTP/HTTPS server instance. |
|
|
858
|
+
| `templates` | Resolved template-engine configuration. |
|
|
859
|
+
| `protocol` | Server protocol (`http` or `https`). |
|
|
860
|
+
| `container` | Internal DI container. |
|
|
861
|
+
| `declaredAppNames` | Set of apps declared in config/routes. |
|
|
862
|
+
| `options` | Loader-specific options object from a `{ path, options }` loader entry. |
|
|
863
|
+
| `locale` | Active request locale. Available on `req.aegis` and template locals. |
|
|
864
|
+
| `localeSource` | Where the current locale came from (`query`, `cookie`, `header`, `manual`, or `disabled`). |
|
|
865
|
+
| `t` | Convenience translator shortcut for the current request/template scope. |
|
|
866
|
+
| `setLocale` | Request helper used to change and optionally persist the active locale. |
|
|
867
|
+
|
|
868
|
+
```js
|
|
869
|
+
// routes.js (root/global)
|
|
870
|
+
export default {
|
|
871
|
+
register(route) {
|
|
872
|
+
route.get('/dashboard', async ({ services }, req, res, next) => {
|
|
873
|
+
try {
|
|
874
|
+
const usersService = services.forApp('users').get('users');
|
|
875
|
+
const ordersService = services.forApp('orders').get('orders');
|
|
876
|
+
res.json({
|
|
877
|
+
users: await usersService.list(),
|
|
878
|
+
orders: await ordersService.list(),
|
|
879
|
+
});
|
|
880
|
+
} catch (error) {
|
|
881
|
+
next(error);
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
},
|
|
885
|
+
};
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
Example `views.js`:
|
|
889
|
+
|
|
890
|
+
```js
|
|
891
|
+
class UsersView {
|
|
892
|
+
static async index({ service }, req, res, next) {
|
|
893
|
+
try {
|
|
894
|
+
const data = await service.listUsers();
|
|
895
|
+
res.json({ data });
|
|
896
|
+
} catch (error) {
|
|
897
|
+
next(error);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
static async create({ service, validator }, req, res, next) {
|
|
902
|
+
try {
|
|
903
|
+
const payload = validator.create(req.body || {});
|
|
904
|
+
const created = await service.createUser(payload);
|
|
905
|
+
res.status(201).json({ data: created });
|
|
906
|
+
} catch (error) {
|
|
907
|
+
next(error);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
static async tools({ service, helpers, jlive }, req, res, next) {
|
|
912
|
+
try {
|
|
913
|
+
const stats = await service.stats();
|
|
914
|
+
res.json({
|
|
915
|
+
stats,
|
|
916
|
+
total: helpers.money(1299.5, { currency: 'USD' }),
|
|
917
|
+
elapsed: helpers.timeElapsed(Date.now() - 60_000),
|
|
918
|
+
token: jlive.generate(16),
|
|
919
|
+
});
|
|
920
|
+
} catch (error) {
|
|
921
|
+
next(error);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
export default UsersView;
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
Example `models.js`:
|
|
930
|
+
|
|
931
|
+
```js
|
|
932
|
+
class UsersModel {
|
|
933
|
+
constructor({ dbClient }) {
|
|
934
|
+
this.dbClient = dbClient;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
async list() {
|
|
938
|
+
return [{ id: '1', name: 'Alice' }];
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
async create(payload) {
|
|
942
|
+
return { id: '2', ...payload };
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
export default { users: UsersModel };
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
Example `services.js`:
|
|
950
|
+
|
|
951
|
+
```js
|
|
952
|
+
class UsersService {
|
|
953
|
+
constructor({ models, env }) {
|
|
954
|
+
this.usersModel = models.get('users');
|
|
955
|
+
this.env = env;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
async listUsers() {
|
|
959
|
+
return this.usersModel.list();
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
async createUser(payload) {
|
|
963
|
+
return this.usersModel.create(payload);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
export default { users: UsersService };
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
Example `subscribers.js`:
|
|
971
|
+
|
|
972
|
+
```js
|
|
973
|
+
export default function registerUsersSubscribers({ events, logger }) {
|
|
974
|
+
events.subscribe('app.booted', ({ appName }) => {
|
|
975
|
+
logger.info('[users] booted: %s', appName);
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
Injected `env` is also available in:
|
|
981
|
+
- view handler context: `static index({ env }, req, res) { ... }`
|
|
982
|
+
- model constructors: `constructor({ dbClient, env }) { ... }`
|
|
983
|
+
- subscribers: `export default function ({ events, env }) { ... }`
|
|
984
|
+
- request runtime bridge: `req.aegis.env`
|
|
985
|
+
|
|
986
|
+
Example `routes.js`:
|
|
987
|
+
|
|
988
|
+
```js
|
|
989
|
+
import UsersView from './views.js';
|
|
990
|
+
|
|
991
|
+
export default {
|
|
992
|
+
appName: 'users',
|
|
993
|
+
register(route) {
|
|
994
|
+
route.get('/home', UsersView.home);
|
|
995
|
+
route.get('/', UsersView.index);
|
|
996
|
+
route.post('/', UsersView.create);
|
|
997
|
+
},
|
|
998
|
+
};
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
## Common Tasks And Feature Guides
|
|
1002
|
+
|
|
1003
|
+
### File Uploads
|
|
1004
|
+
|
|
1005
|
+
AegisNode provides built-in upload middleware on route API as `route.upload`.
|
|
1006
|
+
|
|
1007
|
+
Storage location:
|
|
1008
|
+
- Default folder: `<project-root>/uploads`
|
|
1009
|
+
- Change with `settings.uploads.dir` (relative or absolute path)
|
|
1010
|
+
|
|
1011
|
+
Recommended upload settings:
|
|
1012
|
+
|
|
1013
|
+
```js
|
|
1014
|
+
uploads: {
|
|
1015
|
+
enabled: true,
|
|
1016
|
+
dir: 'storage/uploads',
|
|
1017
|
+
createDir: true,
|
|
1018
|
+
preserveExtension: true,
|
|
1019
|
+
maxFileSize: '5mb',
|
|
1020
|
+
maxFiles: 5,
|
|
1021
|
+
maxFields: 50,
|
|
1022
|
+
maxFieldSize: '1mb',
|
|
1023
|
+
allowedMimeTypes: ['image/png', 'image/jpeg'],
|
|
1024
|
+
allowedExtensions: ['.png', '.jpg', '.jpeg'],
|
|
1025
|
+
allowApiMultipart: true,
|
|
1026
|
+
},
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
Route middleware modes:
|
|
1030
|
+
|
|
1031
|
+
```js
|
|
1032
|
+
import UsersView from './views.js';
|
|
1033
|
+
|
|
1034
|
+
export default {
|
|
1035
|
+
appName: 'users',
|
|
1036
|
+
register(route) {
|
|
1037
|
+
// One file -> req.file
|
|
1038
|
+
route.post('/avatar', route.upload.single('avatar'), UsersView.uploadAvatar);
|
|
1039
|
+
|
|
1040
|
+
// Many files from one input name -> req.files (array)
|
|
1041
|
+
route.post('/gallery', route.upload.array('photos', 6), UsersView.uploadGallery);
|
|
1042
|
+
|
|
1043
|
+
// Many named file inputs -> req.files.<fieldName> (array)
|
|
1044
|
+
route.post(
|
|
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);
|
|
1058
|
+
},
|
|
1059
|
+
};
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
`req` payload shape:
|
|
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';
|
|
1072
|
+
|
|
1073
|
+
export default {
|
|
1074
|
+
appName: 'users',
|
|
1075
|
+
register(route) {
|
|
1076
|
+
route.post('/profile/update', route.upload.single('avatar'), UsersView.updateProfile);
|
|
1077
|
+
},
|
|
1078
|
+
};
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
```js
|
|
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
|
+
}
|
|
1101
|
+
|
|
1102
|
+
export default UsersView;
|
|
1103
|
+
```
|
|
1104
|
+
|
|
1105
|
+
```html
|
|
1106
|
+
<form action="/users/profile/update" method="POST" enctype="multipart/form-data">
|
|
1107
|
+
<%= csrfToken %>
|
|
1108
|
+
<input name="username" />
|
|
1109
|
+
<textarea name="bio"></textarea>
|
|
1110
|
+
<input type="file" name="avatar" />
|
|
1111
|
+
<button type="submit">Save</button>
|
|
1112
|
+
</form>
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
Upload limits and rejections:
|
|
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`.
|
|
1119
|
+
|
|
1120
|
+
Important behavior:
|
|
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.
|
|
1124
|
+
|
|
1125
|
+
### API Apps
|
|
1126
|
+
|
|
1127
|
+
`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`.
|
|
1128
|
+
The `api` setting only changes middleware behavior for selected app mounts.
|
|
1129
|
+
|
|
1130
|
+
Think of it this way:
|
|
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.
|
|
1134
|
+
|
|
1135
|
+
Common combinations:
|
|
1136
|
+
- `api` only: public or internal JSON endpoints with no token auth.
|
|
1137
|
+
- `api` + JWT: first-party SPA/mobile/frontend calling your own backend.
|
|
1138
|
+
- `api` + OAuth2: third-party clients, machine-to-machine access, or standards-based authorization flows.
|
|
1139
|
+
|
|
1140
|
+
Quick start:
|
|
1141
|
+
|
|
1142
|
+
1. Declare the app in `settings.apps` and give it a mount.
|
|
1143
|
+
2. Add that app name to `api.apps`.
|
|
1144
|
+
3. Mount the app at the same path in `routes.js` when `autoMountApps` is off.
|
|
1145
|
+
4. Return JSON from your handlers.
|
|
1146
|
+
5. Send JSON for unsafe methods unless you intentionally allow multipart uploads.
|
|
1147
|
+
|
|
1148
|
+
Example `settings.js`:
|
|
1149
|
+
|
|
1150
|
+
```js
|
|
1151
|
+
export default {
|
|
1152
|
+
apps: [
|
|
1153
|
+
{ name: 'users', mount: '/users' },
|
|
1154
|
+
],
|
|
1155
|
+
api: {
|
|
1156
|
+
apps: ['users'],
|
|
1157
|
+
disableCsrf: true,
|
|
1158
|
+
requireJsonForUnsafeMethods: true,
|
|
1159
|
+
noStoreHeaders: true,
|
|
1160
|
+
},
|
|
1161
|
+
};
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
Example root `routes.js`:
|
|
1165
|
+
|
|
1166
|
+
```js
|
|
1167
|
+
import users from './apps/users/routes.js';
|
|
1168
|
+
|
|
1169
|
+
export default {
|
|
1170
|
+
register(route) {
|
|
1171
|
+
route.use('/users', users); // keep this aligned with settings.apps[].mount
|
|
1172
|
+
},
|
|
1173
|
+
};
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
Example `apps/users/routes.js`:
|
|
1177
|
+
|
|
1178
|
+
```js
|
|
1179
|
+
import UsersView from './views.js';
|
|
1180
|
+
|
|
1181
|
+
export default {
|
|
1182
|
+
appName: 'users',
|
|
1183
|
+
register(route) {
|
|
1184
|
+
route.get('/', UsersView.index);
|
|
1185
|
+
route.post('/', UsersView.create);
|
|
1186
|
+
},
|
|
1187
|
+
};
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
Example `apps/users/views.js`:
|
|
1191
|
+
|
|
1192
|
+
```js
|
|
1193
|
+
class UsersView {
|
|
1194
|
+
static async index({ service }, req, res, next) {
|
|
1195
|
+
try {
|
|
1196
|
+
const users = await service.list();
|
|
1197
|
+
res.json({ data: users });
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
next(error);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
static async create({ service, validator }, req, res, next) {
|
|
1204
|
+
try {
|
|
1205
|
+
const payload = validator.create(req.body || {});
|
|
1206
|
+
const created = await service.create(payload);
|
|
1207
|
+
res.status(201).json({ data: created });
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
next(error);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
export default UsersView;
|
|
1215
|
+
```
|
|
1216
|
+
|
|
1217
|
+
Example requests:
|
|
1218
|
+
|
|
1219
|
+
```bash
|
|
1220
|
+
curl http://127.0.0.1:3000/users
|
|
1221
|
+
|
|
1222
|
+
curl -X POST http://127.0.0.1:3000/users \
|
|
1223
|
+
-H "Content-Type: application/json" \
|
|
1224
|
+
-d '{"name":"Alice"}'
|
|
1225
|
+
```
|
|
1226
|
+
|
|
1227
|
+
What the API middleware changes:
|
|
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`.
|
|
1232
|
+
|
|
1233
|
+
What it does not change:
|
|
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.
|
|
1237
|
+
|
|
1238
|
+
### API And Auth Together
|
|
1239
|
+
|
|
1240
|
+
`api` and `auth` are separate features that are often used together:
|
|
1241
|
+
|
|
1242
|
+
- `api` makes an app behave like an API mount: JSON body enforcement, optional CSRF skip, and `Cache-Control: no-store`.
|
|
1243
|
+
- `auth` adds token issuance, token verification, route protection, client registration, and revocation/introspection behavior.
|
|
1244
|
+
|
|
1245
|
+
Examples:
|
|
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'`.
|
|
1249
|
+
|
|
1250
|
+
Rule of thumb:
|
|
1251
|
+
- If you only need JSON routes, use `api`.
|
|
1252
|
+
- If you need authenticated access, add `auth`.
|
|
1253
|
+
- If outside clients need a standard auth protocol, choose OAuth2 instead of rolling custom JWT login flows.
|
|
1254
|
+
|
|
1255
|
+
Quick comparison:
|
|
1256
|
+
|
|
1257
|
+
| Setup | Use it when | What you configure |
|
|
1258
|
+
| --- | --- | --- |
|
|
1259
|
+
| `api` only | You need JSON endpoints without token auth. Good for public read APIs or trusted internal services. | `api.apps = [...]` |
|
|
1260
|
+
| `api` + JWT | Your own frontend/mobile app talks to your backend and you control both sides. | `api.apps = [...]`, `auth.enabled = true`, `auth.provider = 'jwt'`, plus your own login/token routes |
|
|
1261
|
+
| `api` + OAuth2 | External clients, partner apps, or machine clients need standard token flows. | `api.apps = [...]`, `auth.enabled = true`, `auth.provider = 'oauth2'` |
|
|
1262
|
+
|
|
1263
|
+
### Database Config
|
|
1264
|
+
|
|
1265
|
+
Use `database.config` for every dialect (SQL and MongoDB/Mongoose):
|
|
1266
|
+
|
|
1267
|
+
```js
|
|
1268
|
+
database: {
|
|
1269
|
+
enabled: true,
|
|
1270
|
+
dialect: 'pg', // pg | mysql | mssql | sqlite | oracle | mongo | mongodb | mongoose
|
|
1271
|
+
config: {
|
|
1272
|
+
// SQL example:
|
|
1273
|
+
server: 'localhost',
|
|
1274
|
+
port: 5432,
|
|
1275
|
+
user: 'postgres',
|
|
1276
|
+
password: 'postgres',
|
|
1277
|
+
database: 'appdb',
|
|
1278
|
+
|
|
1279
|
+
// Mongo example:
|
|
1280
|
+
// connectionString: 'mongodb://localhost:27017/appdb',
|
|
1281
|
+
// or:
|
|
1282
|
+
// server: 'localhost',
|
|
1283
|
+
// port: 27017,
|
|
1284
|
+
// database: 'appdb',
|
|
1285
|
+
// user: 'mongo_user',
|
|
1286
|
+
// password: 'mongo_pass',
|
|
1287
|
+
},
|
|
1288
|
+
options: {},
|
|
1289
|
+
},
|
|
1290
|
+
```
|
|
1291
|
+
|
|
1292
|
+
Legacy note:
|
|
1293
|
+
- `database.uri` is still accepted for MongoDB, but `database.config.connectionString` is preferred.
|
|
1294
|
+
|
|
1295
|
+
Model usage for `mongo` / `mongodb` / `mongoose`:
|
|
1296
|
+
- `dbClient` is a QueryMesh client, so you can use the same fluent API as SQL models.
|
|
1297
|
+
|
|
1298
|
+
```js
|
|
1299
|
+
class UsersModel {
|
|
1300
|
+
constructor({ dbClient }) {
|
|
1301
|
+
this.db = dbClient;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
async list() {
|
|
1305
|
+
return this.db.table('users').select(['id', 'name']).get();
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
```
|
|
1309
|
+
|
|
1310
|
+
Mongo `_id` / `ObjectId` handling:
|
|
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.
|
|
1315
|
+
|
|
1316
|
+
```js
|
|
1317
|
+
class UsersModel {
|
|
1318
|
+
constructor({ dbClient, helpers }) {
|
|
1319
|
+
this.db = dbClient;
|
|
1320
|
+
this.helpers = helpers;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
async findById(id) {
|
|
1324
|
+
const _id = this.helpers.toObjectId(id);
|
|
1325
|
+
if (!_id) throw new Error('Invalid Mongo ObjectId');
|
|
1326
|
+
return this.db.table('users').where('_id', '=', _id).first();
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
```
|
|
1330
|
+
|
|
1331
|
+
### Environment Overrides (Single settings.js)
|
|
1332
|
+
|
|
1333
|
+
You can keep a single `settings.js` and define per-environment overrides:
|
|
1334
|
+
|
|
1335
|
+
```js
|
|
1336
|
+
export default {
|
|
1337
|
+
env: process.env.NODE_ENV || 'development',
|
|
1338
|
+
logging: {
|
|
1339
|
+
level: 'info',
|
|
1340
|
+
},
|
|
1341
|
+
security: {
|
|
1342
|
+
ddos: {
|
|
1343
|
+
maxRequests: 120,
|
|
1344
|
+
},
|
|
1345
|
+
},
|
|
1346
|
+
environments: {
|
|
1347
|
+
development: {
|
|
1348
|
+
logging: { level: 'debug' },
|
|
1349
|
+
},
|
|
1350
|
+
production: {
|
|
1351
|
+
logging: { level: 'warn' },
|
|
1352
|
+
security: { ddos: { maxRequests: 80 } },
|
|
1353
|
+
},
|
|
1354
|
+
},
|
|
1355
|
+
};
|
|
1356
|
+
```
|
|
1357
|
+
|
|
1358
|
+
Behavior:
|
|
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`.
|
|
1378
|
+
|
|
1379
|
+
Quick decision guide:
|
|
1380
|
+
- Use JWT when your own frontend/mobile app talks only to your backend.
|
|
1381
|
+
- Use OAuth2 when external clients, partner apps, or machine-to-machine integrations need standard token flows.
|
|
1382
|
+
- Use `auth.middleware()` to protect routes in both modes.
|
|
1383
|
+
|
|
1384
|
+
Enable auth in `settings.js`:
|
|
1385
|
+
|
|
1386
|
+
```js
|
|
1387
|
+
auth: {
|
|
1388
|
+
enabled: true,
|
|
1389
|
+
provider: 'jwt', // or 'oauth2'
|
|
1390
|
+
tablePrefix: 'aegisnode',
|
|
1391
|
+
storage: {
|
|
1392
|
+
// cache | memory | file | database
|
|
1393
|
+
driver: 'cache',
|
|
1394
|
+
filePath: 'storage/aegisnode-auth-store.json',
|
|
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.
|
|
1452
|
+
|
|
1453
|
+
JWT usage in routes:
|
|
1454
|
+
|
|
1455
|
+
- JWT does not create login routes for you.
|
|
1456
|
+
- You define the endpoints that authenticate users and issue tokens.
|
|
1457
|
+
- This is usually the simplest choice for a private API used only by your own frontend/mobile app.
|
|
1458
|
+
|
|
1459
|
+
```js
|
|
1460
|
+
export default {
|
|
1461
|
+
register(route) {
|
|
1462
|
+
const authGuard = (req, res, next) => req.aegis.auth.middleware()(req, res, next);
|
|
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
|
+
});
|
|
1468
|
+
|
|
1469
|
+
route.get('/auth/me', authGuard, (req, res) => {
|
|
1470
|
+
res.json({ user: req.auth });
|
|
1471
|
+
});
|
|
1472
|
+
},
|
|
1473
|
+
};
|
|
1474
|
+
```
|
|
1475
|
+
|
|
1476
|
+
OAuth2 built-in authorization server endpoints (when `auth.provider='oauth2'` and `auth.oauth2.server.enabled=true`):
|
|
1477
|
+
|
|
1478
|
+
- `GET /oauth/authorize`
|
|
1479
|
+
- `POST /oauth/authorize`
|
|
1480
|
+
- `POST /oauth/token`
|
|
1481
|
+
- `POST /oauth/introspect`
|
|
1482
|
+
- `POST /oauth/revoke`
|
|
1483
|
+
- `GET /.well-known/oauth-authorization-server`
|
|
1484
|
+
|
|
1485
|
+
Flows supported:
|
|
1486
|
+
- `authorization_code` (with PKCE)
|
|
1487
|
+
- `client_credentials`
|
|
1488
|
+
- `refresh_token`
|
|
1489
|
+
|
|
1490
|
+
OAuth2 is the better choice when:
|
|
1491
|
+
- you need standards-based client registration and token exchange,
|
|
1492
|
+
- you need machine clients as well as browser/mobile clients,
|
|
1493
|
+
- or third parties must integrate without depending on your custom JWT login route shape.
|
|
1494
|
+
|
|
1495
|
+
#### Route Usage (JWT vs OAuth2)
|
|
1496
|
+
|
|
1497
|
+
`startproject` gives you one root route file: `routes.js`.
|
|
1498
|
+
All your custom HTTP routes are defined there (or in app routes you mount with `route.use(...)`).
|
|
1499
|
+
|
|
1500
|
+
```js
|
|
1501
|
+
// routes.js
|
|
1502
|
+
import users from './apps/users/routes.js';
|
|
1503
|
+
|
|
1504
|
+
export default {
|
|
1505
|
+
register(route) {
|
|
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
|
+
});
|
|
1513
|
+
},
|
|
1514
|
+
};
|
|
1515
|
+
```
|
|
1516
|
+
|
|
1517
|
+
How this behaves:
|
|
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)
|
|
1533
|
+
|
|
1534
|
+
Register clients programmatically with `auth.registerClient(...)`.
|
|
1535
|
+
Do not expose this publicly in production without admin protection.
|
|
1536
|
+
|
|
1537
|
+
```js
|
|
1538
|
+
export default {
|
|
1539
|
+
register(route) {
|
|
1540
|
+
route.post('/admin/oauth/setup-clients', (req, res) => {
|
|
1541
|
+
const webClient = req.aegis.auth.registerClient({
|
|
1542
|
+
clientId: 'web',
|
|
1543
|
+
clientSecret: 'secret',
|
|
1544
|
+
redirectUris: ['https://client.example.com/callback'],
|
|
1545
|
+
grants: ['authorization_code', 'refresh_token'],
|
|
1546
|
+
scopes: ['read:users'],
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
const machineClient = req.aegis.auth.registerClient({
|
|
1550
|
+
clientId: 'machine',
|
|
1551
|
+
clientSecret: 'machine-secret',
|
|
1552
|
+
grants: ['client_credentials'],
|
|
1553
|
+
scopes: ['read:users'],
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
res.json({ webClient, machineClient });
|
|
1557
|
+
});
|
|
1558
|
+
},
|
|
1559
|
+
};
|
|
1560
|
+
```
|
|
1561
|
+
|
|
1562
|
+
Notes:
|
|
1563
|
+
- Secret is stored hashed (scrypt).
|
|
1564
|
+
- Returned client object does not include the secret/hash.
|
|
1565
|
+
- `authorization_code` clients must have at least one `redirectUri`.
|
|
1566
|
+
|
|
1567
|
+
2. Authorization Code + PKCE flow
|
|
1568
|
+
|
|
1569
|
+
Create PKCE verifier/challenge:
|
|
1570
|
+
|
|
1571
|
+
```js
|
|
1572
|
+
import crypto from 'crypto';
|
|
1573
|
+
|
|
1574
|
+
function b64url(buffer) {
|
|
1575
|
+
return Buffer.from(buffer).toString('base64')
|
|
1576
|
+
.replace(/\+/g, '-')
|
|
1577
|
+
.replace(/\//g, '_')
|
|
1578
|
+
.replace(/=+$/g, '');
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
const codeVerifier = b64url(crypto.randomBytes(48));
|
|
1582
|
+
const codeChallenge = b64url(crypto.createHash('sha256').update(codeVerifier).digest());
|
|
1583
|
+
```
|
|
1584
|
+
|
|
1585
|
+
Redirect user-agent to authorize endpoint:
|
|
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
|
+
```
|
|
1615
|
+
|
|
1616
|
+
Response shape:
|
|
1617
|
+
|
|
1618
|
+
```json
|
|
1619
|
+
{
|
|
1620
|
+
"access_token": "...",
|
|
1621
|
+
"token_type": "Bearer",
|
|
1622
|
+
"expires_in": 3600,
|
|
1623
|
+
"scope": "read:users",
|
|
1624
|
+
"refresh_token": "...",
|
|
1625
|
+
"refresh_expires_in": 1209600
|
|
1626
|
+
}
|
|
1627
|
+
```
|
|
1628
|
+
|
|
1629
|
+
3. Protect API routes with OAuth2 access tokens
|
|
1630
|
+
|
|
1631
|
+
```js
|
|
1632
|
+
export default {
|
|
1633
|
+
register(route) {
|
|
1634
|
+
const authGuard = (req, res, next) => req.aegis.auth.middleware()(req, res, next);
|
|
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
|
+
});
|
|
1642
|
+
},
|
|
1643
|
+
};
|
|
1644
|
+
```
|
|
1645
|
+
|
|
1646
|
+
Use token:
|
|
1647
|
+
|
|
1648
|
+
```bash
|
|
1649
|
+
curl http://127.0.0.1:3000/users/me \
|
|
1650
|
+
-H "Authorization: Bearer <ACCESS_TOKEN>"
|
|
1651
|
+
```
|
|
1652
|
+
|
|
1653
|
+
4. Refresh token flow
|
|
1654
|
+
|
|
1655
|
+
```bash
|
|
1656
|
+
curl -X POST http://127.0.0.1:3000/oauth/token \
|
|
1657
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
1658
|
+
-u web:secret \
|
|
1659
|
+
-d "grant_type=refresh_token" \
|
|
1660
|
+
-d "refresh_token=<REFRESH_TOKEN>"
|
|
1661
|
+
```
|
|
1662
|
+
|
|
1663
|
+
5. Client Credentials flow (machine-to-machine)
|
|
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
|
+
```
|
|
1672
|
+
|
|
1673
|
+
This returns access token only (no refresh token).
|
|
1674
|
+
|
|
1675
|
+
6. Introspection and revocation
|
|
1676
|
+
|
|
1677
|
+
Introspection:
|
|
1678
|
+
|
|
1679
|
+
```bash
|
|
1680
|
+
curl -X POST http://127.0.0.1:3000/oauth/introspect \
|
|
1681
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
1682
|
+
-u web:secret \
|
|
1683
|
+
-d "token=<ACCESS_TOKEN>"
|
|
1684
|
+
```
|
|
1685
|
+
|
|
1686
|
+
Revocation:
|
|
1687
|
+
|
|
1688
|
+
```bash
|
|
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
|
+
```
|
|
1694
|
+
|
|
1695
|
+
7. Custom subject/consent resolution
|
|
1696
|
+
|
|
1697
|
+
By default, `/oauth/authorize` resolves subject from:
|
|
1698
|
+
- `req.user.id`
|
|
1699
|
+
- `req.user.sub`
|
|
1700
|
+
- `req.auth.sub`
|
|
1701
|
+
- `subject`/`user_id` query/body params
|
|
1702
|
+
|
|
1703
|
+
You can override with hooks in `settings.js`:
|
|
1704
|
+
|
|
1705
|
+
```js
|
|
1706
|
+
auth: {
|
|
1707
|
+
provider: 'oauth2',
|
|
1708
|
+
oauth2: {
|
|
1709
|
+
server: {
|
|
1710
|
+
resolveSubject: ({ req }) => req.user?.id || '',
|
|
1711
|
+
resolveConsent: ({ req, client, subject }) => {
|
|
1712
|
+
// return true to approve, false to deny
|
|
1713
|
+
return true;
|
|
1714
|
+
},
|
|
1715
|
+
},
|
|
1716
|
+
},
|
|
1717
|
+
},
|
|
1718
|
+
```
|
|
1719
|
+
|
|
1720
|
+
8. Production checklist
|
|
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`:
|
|
1738
|
+
|
|
1739
|
+
```js
|
|
1740
|
+
swagger: {
|
|
1741
|
+
enabled: true,
|
|
1742
|
+
docsPath: '/docs',
|
|
1743
|
+
jsonPath: '/openapi.json',
|
|
1744
|
+
documentPath: 'openapi.json',
|
|
1745
|
+
explorer: true,
|
|
1746
|
+
},
|
|
1747
|
+
```
|
|
1748
|
+
|
|
1749
|
+
Behavior:
|
|
1750
|
+
- UI available at `docsPath` (default `/docs`).
|
|
1751
|
+
- OpenAPI JSON available at `jsonPath` (default `/openapi.json`).
|
|
1752
|
+
- If `openapi.json` exists in project root, it is loaded.
|
|
1753
|
+
- If no file is found, AegisNode serves a default minimal OpenAPI document.
|
|
1754
|
+
|
|
1755
|
+
### Templates (EJS + base.ejs)
|
|
1756
|
+
|
|
1757
|
+
Set template config in `settings.js`:
|
|
1758
|
+
|
|
1759
|
+
```js
|
|
1760
|
+
templates: {
|
|
1761
|
+
enabled: true,
|
|
1762
|
+
engine: 'ejs',
|
|
1763
|
+
dir: 'templates',
|
|
1764
|
+
base: 'base',
|
|
1765
|
+
appBases: {
|
|
1766
|
+
users: 'users/base',
|
|
1767
|
+
admin: 'admin/base',
|
|
1768
|
+
},
|
|
1769
|
+
}
|
|
1770
|
+
```
|
|
1771
|
+
|
|
1772
|
+
Then in a route handler:
|
|
1773
|
+
|
|
1774
|
+
```js
|
|
1775
|
+
route.get('/', (req, res) => {
|
|
1776
|
+
res.render('home', {
|
|
1777
|
+
title: 'Home',
|
|
1778
|
+
message: 'Welcome',
|
|
1779
|
+
});
|
|
1780
|
+
});
|
|
1781
|
+
```
|
|
1782
|
+
|
|
1783
|
+
`home.ejs` is rendered first, then injected into `base.ejs`.
|
|
1784
|
+
Use `<%- content %>` (or `<%- body %>`) in your `base.ejs` to print page content.
|
|
1785
|
+
|
|
1786
|
+
### Internationalization (i18n)
|
|
1787
|
+
|
|
1788
|
+
Configure i18n in `settings.js`:
|
|
1789
|
+
|
|
1790
|
+
```js
|
|
1791
|
+
i18n: {
|
|
1792
|
+
enabled: true,
|
|
1793
|
+
defaultLocale: 'en',
|
|
1794
|
+
fallbackLocale: 'en',
|
|
1795
|
+
supported: ['en', 'fr'],
|
|
1796
|
+
queryParam: 'lang',
|
|
1797
|
+
translations: {
|
|
1798
|
+
en: {
|
|
1799
|
+
home: {
|
|
1800
|
+
title: 'Welcome {name}',
|
|
1801
|
+
},
|
|
1802
|
+
},
|
|
1803
|
+
fr: {
|
|
1804
|
+
home: {
|
|
1805
|
+
title: 'Bienvenue {name}',
|
|
1806
|
+
},
|
|
1807
|
+
},
|
|
1808
|
+
},
|
|
1809
|
+
}
|
|
1810
|
+
```
|
|
1811
|
+
|
|
1812
|
+
You can also load locale JSON files directly (no import needed):
|
|
1813
|
+
|
|
1814
|
+
```js
|
|
1815
|
+
i18n: {
|
|
1816
|
+
enabled: true,
|
|
1817
|
+
defaultLocale: 'en',
|
|
1818
|
+
supported: ['en', 'fr'],
|
|
1819
|
+
translations: {
|
|
1820
|
+
en: 'locales/en.json',
|
|
1821
|
+
fr: 'locales/fr.json',
|
|
1822
|
+
},
|
|
1823
|
+
// optional single-file source (inline `translations` wins per locale key):
|
|
1824
|
+
// translationsFile: 'locales/all.json',
|
|
1825
|
+
}
|
|
1826
|
+
```
|
|
1827
|
+
|
|
1828
|
+
Route usage:
|
|
1829
|
+
|
|
1830
|
+
```js
|
|
1831
|
+
route.get('/i18n-demo', (req, res) => {
|
|
1832
|
+
// Auto-detected from query/cookie/header.
|
|
1833
|
+
res.json({
|
|
1834
|
+
locale: req.aegis.locale,
|
|
1835
|
+
title: req.aegis.t('home.title', { name: 'Jason' }),
|
|
1836
|
+
});
|
|
1837
|
+
});
|
|
1838
|
+
```
|
|
1839
|
+
|
|
1840
|
+
Choosing the API:
|
|
1841
|
+
|
|
1842
|
+
- `req.aegis.t('home.title')`
|
|
1843
|
+
Shortcut for `req.aegis.i18n.t('home.title')`. Use this in routes/views when you only need a translated string.
|
|
1844
|
+
- `req.aegis.i18n`
|
|
1845
|
+
Request-scoped i18n object. Use this when you also need locale metadata or helpers such as `locale`, `localeSource`, `setLocale(...)`, `resolveLocale(...)`, or `forLocale(...)`.
|
|
1846
|
+
- Injected `i18n` in handlers/services/models/validators/controllers/subscribers/loaders
|
|
1847
|
+
Runtime-injected i18n bridge. During an HTTP request, `i18n.t(...)` resolves with the same active locale as `req.aegis.i18n.t(...)`, so the translation result is the same.
|
|
1848
|
+
|
|
1849
|
+
Important differences:
|
|
1850
|
+
|
|
1851
|
+
- `req.aegis.t` and `req.aegis.i18n.t` return the same translation for the current request.
|
|
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' })`.
|
|
1855
|
+
|
|
1856
|
+
Service/model usage:
|
|
1857
|
+
|
|
1858
|
+
```js
|
|
1859
|
+
class UsersService {
|
|
1860
|
+
constructor({ i18n }) {
|
|
1861
|
+
this.i18n = i18n;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
greeting(name) {
|
|
1865
|
+
return this.i18n.t('home.title', { name });
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
```
|
|
1869
|
+
|
|
1870
|
+
Template usage:
|
|
1871
|
+
|
|
1872
|
+
```ejs
|
|
1873
|
+
<html lang="<%= locale %>">
|
|
1874
|
+
<body>
|
|
1875
|
+
<h1><%= t('home.title', { name: 'Jason' }) %></h1>
|
|
1876
|
+
</body>
|
|
1877
|
+
</html>
|
|
1878
|
+
```
|
|
1879
|
+
|
|
1880
|
+
Manual locale switch inside a request:
|
|
1881
|
+
|
|
1882
|
+
```js
|
|
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
|
+
```
|
|
1888
|
+
|
|
1889
|
+
Persist user-selected language as default:
|
|
1890
|
+
|
|
1891
|
+
```js
|
|
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
|
+
```
|
|
1898
|
+
|
|
1899
|
+
Language picker template (keeps selected option):
|
|
1900
|
+
|
|
1901
|
+
```ejs
|
|
1902
|
+
<form method="post" action="/lang">
|
|
1903
|
+
<select name="lang">
|
|
1904
|
+
<option value="en" <%= locale === 'en' ? 'selected' : '' %>>English</option>
|
|
1905
|
+
<option value="fr" <%= locale === 'fr' ? 'selected' : '' %>>Français</option>
|
|
1906
|
+
</select>
|
|
1907
|
+
<button type="submit">Change</button>
|
|
1908
|
+
</form>
|
|
1909
|
+
```
|
|
1910
|
+
|
|
1911
|
+
Notes:
|
|
1912
|
+
- `defaultLocale` is used only when user has no saved locale.
|
|
1913
|
+
- After selection, cookie locale becomes the default for that user on next requests.
|
|
1914
|
+
- `?lang=fr` also persists automatically when `detectFromQuery` is enabled.
|
|
1915
|
+
- Templates get `t`, `locale`, and `i18n` in locals.
|
|
1916
|
+
|
|
1917
|
+
### Mail
|
|
1918
|
+
|
|
1919
|
+
Configure mail transport in `settings.js`:
|
|
1920
|
+
|
|
1921
|
+
```js
|
|
1922
|
+
export default {
|
|
1923
|
+
mail: {
|
|
1924
|
+
enabled: true,
|
|
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,
|
|
1939
|
+
},
|
|
1940
|
+
};
|
|
1941
|
+
```
|
|
1942
|
+
|
|
1943
|
+
Available mail APIs:
|
|
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:
|
|
1951
|
+
|
|
1952
|
+
```js
|
|
1953
|
+
route.post('/contact', async ({ mail }, req, res, next) => {
|
|
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
|
+
});
|
|
1967
|
+
```
|
|
1968
|
+
|
|
1969
|
+
Service usage:
|
|
1970
|
+
|
|
1971
|
+
```js
|
|
1972
|
+
class UsersService {
|
|
1973
|
+
constructor({ mail }) {
|
|
1974
|
+
this.mail = mail;
|
|
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
|
+
}
|
|
1985
|
+
```
|
|
1986
|
+
|
|
1987
|
+
Notes:
|
|
1988
|
+
- `mail.send(...)` and `mail.sendMail(...)` are the same method.
|
|
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`.
|
|
1992
|
+
|
|
1993
|
+
### Helpers And jlive
|
|
1994
|
+
|
|
1995
|
+
Helpers and the `jlive` bridge are available in request context (`req.aegis`) and in EJS locals.
|
|
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`
|
|
2001
|
+
|
|
2002
|
+
Set helper defaults in `settings.js` (currency/locale):
|
|
2003
|
+
|
|
2004
|
+
```js
|
|
2005
|
+
helpers: {
|
|
2006
|
+
locale: 'fr-FR',
|
|
2007
|
+
money: {
|
|
2008
|
+
currency: 'EUR',
|
|
2009
|
+
currencyDisplay: 'code',
|
|
2010
|
+
},
|
|
2011
|
+
},
|
|
2012
|
+
```
|
|
2013
|
+
|
|
2014
|
+
Then `helpers.money(2500)` automatically uses your configured defaults unless you pass per-call overrides.
|
|
2015
|
+
|
|
2016
|
+
Route usage:
|
|
2017
|
+
|
|
2018
|
+
```js
|
|
2019
|
+
export default {
|
|
2020
|
+
register(route) {
|
|
2021
|
+
route.get('/tools', (req, res) => {
|
|
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
|
+
});
|
|
2034
|
+
},
|
|
2035
|
+
};
|
|
2036
|
+
```
|
|
2037
|
+
|
|
2038
|
+
Template usage (`res.render(...)`):
|
|
2039
|
+
|
|
2040
|
+
```ejs
|
|
2041
|
+
<h1><%= title %></h1>
|
|
2042
|
+
<p>Total: <%= money(1299.5, { currency: 'USD' }) %></p>
|
|
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
|
+
```
|
|
2048
|
+
|
|
2049
|
+
Available EJS locals:
|
|
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)
|
|
2064
|
+
|
|
2065
|
+
`timeElapsed` supports both styles:
|
|
2066
|
+
- `timeElapsed(value, { now, locale, numeric })` (Intl relative style)
|
|
2067
|
+
- `timeElapsed(unixTime, true)` (short legacy-style mode)
|
|
2068
|
+
|
|
2069
|
+
Mongo id helpers:
|
|
2070
|
+
- `isObjectId(value)` validates Mongo ObjectId format.
|
|
2071
|
+
- `toObjectId(value)` returns a Mongo ObjectId instance or `null` when invalid.
|
|
2072
|
+
|
|
2073
|
+
`jlive` behavior:
|
|
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`.
|
|
2076
|
+
|
|
2077
|
+
### Template Locals From Settings
|
|
2078
|
+
|
|
2079
|
+
You can inject custom functions/classes into all template renders from `settings.js`:
|
|
2080
|
+
|
|
2081
|
+
```js
|
|
2082
|
+
templates: {
|
|
2083
|
+
enabled: true,
|
|
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
|
+
},
|
|
2096
|
+
```
|
|
2097
|
+
|
|
2098
|
+
You can also pass already-defined class/function references by name (object shorthand):
|
|
2099
|
+
|
|
2100
|
+
```js
|
|
2101
|
+
import { formatCurrency, ViewBag } from './app/template-locals.js';
|
|
2102
|
+
|
|
2103
|
+
export default {
|
|
2104
|
+
templates: {
|
|
2105
|
+
locals: { formatCurrency, ViewBag },
|
|
2106
|
+
},
|
|
2107
|
+
};
|
|
2108
|
+
```
|
|
2109
|
+
|
|
2110
|
+
Note:
|
|
2111
|
+
- Use JS references/imports (as above).
|
|
2112
|
+
- String names like `{ formatCurrency: 'formatCurrency' }` are not auto-resolved.
|
|
2113
|
+
|
|
2114
|
+
Then in EJS:
|
|
2115
|
+
|
|
2116
|
+
```ejs
|
|
2117
|
+
<p><%= formatCurrency(123.45) %></p>
|
|
2118
|
+
<p><%= new ViewBag('Dashboard').title %></p>
|
|
2119
|
+
```
|
|
2120
|
+
|
|
2121
|
+
## Runtime Patterns And Advanced Topics
|
|
2122
|
+
|
|
2123
|
+
### Middleware
|
|
2124
|
+
|
|
2125
|
+
Route API supports Express-style middleware chains:
|
|
2126
|
+
|
|
2127
|
+
```js
|
|
2128
|
+
route.get('/secured', authGuard, UsersView.index);
|
|
2129
|
+
route.post('/users', validateBody, createUser);
|
|
2130
|
+
```
|
|
2131
|
+
|
|
2132
|
+
You can also use `route.use()`:
|
|
2133
|
+
|
|
2134
|
+
```js
|
|
2135
|
+
route.use(requestLogger);
|
|
2136
|
+
route.use('/admin', adminGuard, adminRoutes);
|
|
2137
|
+
```
|
|
2138
|
+
|
|
2139
|
+
### Validators (Between Route And Service)
|
|
2140
|
+
|
|
2141
|
+
Each app can define `apps/<app>/validators.js`.
|
|
2142
|
+
Routes should validate/sanitize input first, then call services.
|
|
2143
|
+
|
|
2144
|
+
```js
|
|
2145
|
+
// apps/users/validators.js
|
|
2146
|
+
class UsersValidator {
|
|
2147
|
+
id(value) {
|
|
2148
|
+
const normalized = String(value || '').trim();
|
|
2149
|
+
if (!normalized) {
|
|
2150
|
+
const error = new Error('Invalid id');
|
|
2151
|
+
error.statusCode = 400;
|
|
2152
|
+
throw error;
|
|
2153
|
+
}
|
|
2154
|
+
return normalized;
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
create(payload) {
|
|
2158
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
2159
|
+
const error = new Error('Payload must be an object');
|
|
2160
|
+
error.statusCode = 400;
|
|
2161
|
+
throw error;
|
|
2162
|
+
}
|
|
2163
|
+
return payload;
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
export default { users: UsersValidator };
|
|
2168
|
+
```
|
|
2169
|
+
|
|
2170
|
+
More complete example:
|
|
2171
|
+
|
|
2172
|
+
```js
|
|
2173
|
+
// apps/users/validators.js
|
|
2174
|
+
class UsersValidator {
|
|
2175
|
+
id(value) {
|
|
2176
|
+
const id = String(value || '').trim();
|
|
2177
|
+
if (!id) {
|
|
2178
|
+
const error = new Error('Invalid user id');
|
|
2179
|
+
error.statusCode = 400;
|
|
2180
|
+
throw error;
|
|
2181
|
+
}
|
|
2182
|
+
return id;
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
create(payload) {
|
|
2186
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
2187
|
+
const error = new Error('Payload must be an object');
|
|
2188
|
+
error.statusCode = 400;
|
|
2189
|
+
throw error;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
const name = String(payload.name || '').trim();
|
|
2193
|
+
const email = String(payload.email || '').trim().toLowerCase();
|
|
2194
|
+
|
|
2195
|
+
if (name.length < 2) {
|
|
2196
|
+
const error = new Error('name must be at least 2 chars');
|
|
2197
|
+
error.statusCode = 400;
|
|
2198
|
+
throw error;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)) {
|
|
2202
|
+
const error = new Error('Invalid email');
|
|
2203
|
+
error.statusCode = 400;
|
|
2204
|
+
throw error;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
return { name, email };
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
update(payload) {
|
|
2211
|
+
return this.create(payload);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
export default { users: UsersValidator };
|
|
2216
|
+
```
|
|
2217
|
+
|
|
2218
|
+
```js
|
|
2219
|
+
// apps/users/views.js
|
|
2220
|
+
class UsersView {
|
|
2221
|
+
static async create({ service, validator }, req, res, next) {
|
|
2222
|
+
try {
|
|
2223
|
+
const payload = validator.create(req.body || {});
|
|
2224
|
+
const created = await service.create(payload);
|
|
2225
|
+
res.status(201).json({ data: created });
|
|
2226
|
+
} catch (error) {
|
|
2227
|
+
next(error);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
export default UsersView;
|
|
2233
|
+
```
|
|
2234
|
+
|
|
2235
|
+
```js
|
|
2236
|
+
// apps/users/routes.js
|
|
2237
|
+
import UsersView from './views.js';
|
|
2238
|
+
|
|
2239
|
+
export default {
|
|
2240
|
+
appName: 'users',
|
|
2241
|
+
register(route) {
|
|
2242
|
+
route.post('/', UsersView.create);
|
|
2243
|
+
},
|
|
2244
|
+
};
|
|
2245
|
+
```
|
|
2246
|
+
|
|
2247
|
+
### Service And Model Flow (Strict Layers)
|
|
2248
|
+
|
|
2249
|
+
Enable strict layering in `settings.js`:
|
|
2250
|
+
|
|
2251
|
+
```js
|
|
2252
|
+
architecture: {
|
|
2253
|
+
strictLayers: true,
|
|
2254
|
+
},
|
|
2255
|
+
```
|
|
2256
|
+
|
|
2257
|
+
Rules when enabled:
|
|
2258
|
+
- Routes should register view handlers (for example `route.get('/home', UsersView.home)`).
|
|
2259
|
+
- Views should call validators/services (no model imports in route files).
|
|
2260
|
+
- Services must call models (no direct `dbClient/database` usage in service files).
|
|
2261
|
+
- Routes/services must not import DB libraries directly.
|
|
2262
|
+
|
|
2263
|
+
Example:
|
|
2264
|
+
|
|
2265
|
+
```js
|
|
2266
|
+
// apps/users/models.js
|
|
2267
|
+
class UsersModel {
|
|
2268
|
+
constructor({ dbClient }) {
|
|
2269
|
+
this.dbClient = dbClient;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
async list() {
|
|
2273
|
+
return [{ id: 1, name: 'Alice' }];
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
export default { users: UsersModel };
|
|
2278
|
+
```
|
|
2279
|
+
|
|
2280
|
+
```js
|
|
2281
|
+
// apps/users/services.js
|
|
2282
|
+
class UsersService {
|
|
2283
|
+
constructor({ models }) {
|
|
2284
|
+
this.usersModel = models.get('users');
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
async listUsers() {
|
|
2288
|
+
return this.usersModel.list();
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
export default { users: UsersService };
|
|
2293
|
+
```
|
|
2294
|
+
|
|
2295
|
+
```js
|
|
2296
|
+
// apps/users/views.js
|
|
2297
|
+
class UsersView {
|
|
2298
|
+
static async index({ services }, req, res, next) {
|
|
2299
|
+
try {
|
|
2300
|
+
const usersService = services.get('users');
|
|
2301
|
+
res.json(await usersService.listUsers());
|
|
2302
|
+
} catch (error) {
|
|
2303
|
+
next(error);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
export default UsersView;
|
|
2309
|
+
```
|
|
2310
|
+
|
|
2311
|
+
```js
|
|
2312
|
+
// apps/users/routes.js
|
|
2313
|
+
import UsersView from './views.js';
|
|
2314
|
+
|
|
2315
|
+
export default {
|
|
2316
|
+
appName: 'users',
|
|
2317
|
+
register(route) {
|
|
2318
|
+
route.get('/', UsersView.index);
|
|
2319
|
+
},
|
|
2320
|
+
};
|
|
2321
|
+
```
|
|
2322
|
+
|
|
2323
|
+
### Subscribers
|
|
2324
|
+
|
|
2325
|
+
Each app can define `apps/<app>/subscribers.js`.
|
|
2326
|
+
It should export one default function that registers one or many listeners.
|
|
2327
|
+
|
|
2328
|
+
```js
|
|
2329
|
+
// apps/users/subscribers.js
|
|
2330
|
+
export default function registerUsersSubscribers({ events, logger }) {
|
|
2331
|
+
events.subscribe('app.booted', ({ appName }) => {
|
|
2332
|
+
logger.debug('[users] app.booted => %s', appName);
|
|
2333
|
+
});
|
|
2334
|
+
|
|
2335
|
+
events.subscribe('user.created', ({ id }) => {
|
|
2336
|
+
logger.info('[users] user.created => %s', id);
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
```
|
|
2340
|
+
|
|
2341
|
+
Notes:
|
|
2342
|
+
- `subscribers.js` is called once during app boot (if the app is declared in `settings.js -> apps`).
|
|
2343
|
+
- You can register multiple subscribers inside the same file.
|
|
2344
|
+
- Built-in events emitted by framework core include: `app.booted`, `ws.connection`, `ws.disconnect`.
|
|
2345
|
+
- You can emit custom events from subscribers/services (`events.publish(...)`) and from views/handlers via `req.aegis.events.publish(...)`.
|
|
2346
|
+
|
|
2347
|
+
Example custom emit from a route:
|
|
2348
|
+
|
|
2349
|
+
```js
|
|
2350
|
+
export default {
|
|
2351
|
+
register(route) {
|
|
2352
|
+
route.post('/users', (req, res) => {
|
|
2353
|
+
const user = { id: 'u1' };
|
|
2354
|
+
req.aegis.events.publish('user.created', user);
|
|
2355
|
+
res.json(user);
|
|
2356
|
+
});
|
|
2357
|
+
},
|
|
2358
|
+
};
|
|
2359
|
+
```
|
|
2360
|
+
|
|
2361
|
+
### Example: Socket Lifecycle Via Subscribers
|
|
2362
|
+
|
|
2363
|
+
```js
|
|
2364
|
+
// apps/chat/subscribers.js
|
|
2365
|
+
const online = new Set();
|
|
2366
|
+
|
|
2367
|
+
export default function registerChatSubscribers({ events, logger, io }) {
|
|
2368
|
+
events.subscribe('ws.connection', ({ socket }) => {
|
|
2369
|
+
const userId = socket.handshake?.auth?.userId || socket.id;
|
|
2370
|
+
online.add(userId);
|
|
2371
|
+
|
|
2372
|
+
logger.info('[ws] connected user=%s socket=%s', userId, socket.id);
|
|
2373
|
+
io.emit('presence:update', { online: online.size });
|
|
2374
|
+
});
|
|
2375
|
+
|
|
2376
|
+
events.subscribe('ws.disconnect', ({ socket, reason }) => {
|
|
2377
|
+
const userId = socket.handshake?.auth?.userId || socket.id;
|
|
2378
|
+
online.delete(userId);
|
|
2379
|
+
|
|
2380
|
+
logger.info('[ws] disconnected user=%s socket=%s reason=%s', userId, socket.id, reason);
|
|
2381
|
+
io.emit('presence:update', { online: online.size });
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
```
|
|
2385
|
+
|
|
2386
|
+
### Security Headers
|
|
2387
|
+
|
|
2388
|
+
Project settings can control default security headers:
|
|
2389
|
+
|
|
2390
|
+
```js
|
|
2391
|
+
security: {
|
|
2392
|
+
appSecret: 'your-long-secret',
|
|
2393
|
+
headers: {
|
|
2394
|
+
enabled: true,
|
|
2395
|
+
csp: {
|
|
2396
|
+
enabled: true,
|
|
2397
|
+
reportOnly: false,
|
|
2398
|
+
directives: {},
|
|
2399
|
+
},
|
|
2400
|
+
},
|
|
2401
|
+
ddos: {
|
|
2402
|
+
enabled: true,
|
|
2403
|
+
windowMs: 60000,
|
|
2404
|
+
maxRequests: 120,
|
|
2405
|
+
message: 'Too many requests, please try again later.',
|
|
2406
|
+
statusCode: 429,
|
|
2407
|
+
standardHeaders: true,
|
|
2408
|
+
legacyHeaders: false,
|
|
2409
|
+
skipSuccessfulRequests: false,
|
|
2410
|
+
skipFailedRequests: false,
|
|
2411
|
+
trustProxy: false,
|
|
2412
|
+
skipPaths: ['/health'],
|
|
2413
|
+
},
|
|
2414
|
+
csrf: {
|
|
2415
|
+
enabled: true,
|
|
2416
|
+
rejectForms: true,
|
|
2417
|
+
rejectUnsafeMethods: true,
|
|
2418
|
+
cookieName: '_aegis_csrf',
|
|
2419
|
+
fieldName: '_csrf',
|
|
2420
|
+
headerName: 'x-csrf-token',
|
|
2421
|
+
requireSignedCookie: true,
|
|
2422
|
+
sameSite: 'lax',
|
|
2423
|
+
secure: 'auto',
|
|
2424
|
+
httpOnly: true,
|
|
2425
|
+
path: '/',
|
|
2426
|
+
},
|
|
2427
|
+
}
|
|
2428
|
+
```
|
|
2429
|
+
|
|
2430
|
+
`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.
|
|
2432
|
+
If neither `APP_SECRET` nor `security.appSecret` is set, AegisNode generates a fallback secret and persists it to `.aegis/app-secret`.
|
|
2433
|
+
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
|
+
|
|
2435
|
+
Set `security.headers.enabled = false` to disable Helmet.
|
|
2436
|
+
Set `security.headers.csp.enabled = false` to disable CSP only.
|
|
2437
|
+
If you need third-party assets, prefer adding allowed origins under `security.headers.csp.directives` instead of disabling CSP.
|
|
2438
|
+
Set `security.ddos.enabled = false` to disable rate limiting.
|
|
2439
|
+
Set `security.csrf.enabled = false` to disable CSRF middleware.
|
|
2440
|
+
|
|
2441
|
+
For forms, include token from `csrfToken`:
|
|
2442
|
+
|
|
2443
|
+
```ejs
|
|
2444
|
+
<form method="post" action="/submit">
|
|
2445
|
+
<%= csrfToken %>
|
|
2446
|
+
<button type="submit">Send</button>
|
|
2447
|
+
</form>
|
|
2448
|
+
```
|
|
2449
|
+
|
|
2450
|
+
If you need the raw token string (for AJAX header), use `csrfValue`.
|
|
2451
|
+
For API clients (Postman/mobile) you can either send `x-csrf-token` or set `security.csrf.rejectUnsafeMethods = false`.
|
|
2452
|
+
|
|
2453
|
+
WebSocket security note:
|
|
2454
|
+
- Default `websocket.cors.origin` is `false` (cross-origin blocked).
|
|
2455
|
+
- Set explicit allowed origins in `settings.js` when needed.
|
|
2456
|
+
|
|
2457
|
+
Note: framework rate limiting helps with basic abuse, but real DDoS protection should be done at edge (Cloudflare/AWS WAF/Nginx).
|
|
2458
|
+
|
|
2459
|
+
## License
|
|
2460
|
+
|
|
2461
|
+
MIT
|