@appweaver/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +7 -0
  3. package/build/build-command.d.ts +2 -0
  4. package/build/build-command.js +15 -0
  5. package/build/build-project.d.ts +8 -0
  6. package/build/build-project.js +19 -0
  7. package/build/index.d.ts +2 -0
  8. package/build/index.js +18 -0
  9. package/generate/generate-command.d.ts +2 -0
  10. package/generate/generate-command.js +38 -0
  11. package/generate/generate-schema.d.ts +12 -0
  12. package/generate/generate-schema.js +475 -0
  13. package/generate/generate-types.d.ts +10 -0
  14. package/generate/generate-types.js +86 -0
  15. package/generate/index.d.ts +3 -0
  16. package/generate/index.js +19 -0
  17. package/migrate/index.d.ts +1 -0
  18. package/migrate/index.js +17 -0
  19. package/migrate/migrate-command.d.ts +2 -0
  20. package/migrate/migrate-command.js +13 -0
  21. package/migration/index.d.ts +1 -0
  22. package/migration/index.js +17 -0
  23. package/migration/migration-command.d.ts +2 -0
  24. package/migration/migration-command.js +34 -0
  25. package/openapi/index.d.ts +1 -0
  26. package/openapi/index.js +17 -0
  27. package/openapi/openapi-command.d.ts +2 -0
  28. package/openapi/openapi-command.js +46 -0
  29. package/package.json +56 -0
  30. package/seed/index.d.ts +1 -0
  31. package/seed/index.js +17 -0
  32. package/seed/seed-command.d.ts +2 -0
  33. package/seed/seed-command.js +33 -0
  34. package/skill/GUIDELINES.md +298 -0
  35. package/skill/SKILL.md +593 -0
  36. package/skill/references/cache.md +207 -0
  37. package/skill/references/cli.md +213 -0
  38. package/skill/references/client.md +507 -0
  39. package/skill/references/configuration.md +402 -0
  40. package/skill/references/database.md +134 -0
  41. package/skill/references/dependency-injection.md +214 -0
  42. package/skill/references/events.md +152 -0
  43. package/skill/references/mailer.md +235 -0
  44. package/skill/references/queue.md +196 -0
  45. package/skill/references/resources.md +961 -0
  46. package/skill/references/scheduler.md +184 -0
  47. package/skill/references/security.md +694 -0
  48. package/skill/references/storage.md +251 -0
  49. package/start/index.d.ts +2 -0
  50. package/start/index.js +18 -0
  51. package/start/start-command.d.ts +2 -0
  52. package/start/start-command.js +17 -0
  53. package/start/start-project.d.ts +8 -0
  54. package/start/start-project.js +147 -0
  55. package/testing/index.d.ts +1 -0
  56. package/testing/index.js +17 -0
  57. package/testing/testing-command.d.ts +2 -0
  58. package/testing/testing-command.js +96 -0
  59. package/update/index.d.ts +2 -0
  60. package/update/index.js +18 -0
  61. package/update/update-command.d.ts +2 -0
  62. package/update/update-command.js +84 -0
  63. package/update/update-packages.d.ts +10 -0
  64. package/update/update-packages.js +45 -0
  65. package/update/update-skill.d.ts +8 -0
  66. package/update/update-skill.js +93 -0
  67. package/utils/index.d.ts +3 -0
  68. package/utils/index.js +19 -0
  69. package/utils/loader-util.d.ts +29 -0
  70. package/utils/loader-util.js +132 -0
  71. package/utils/path-util.d.ts +41 -0
  72. package/utils/path-util.js +98 -0
  73. package/utils/process-util.d.ts +39 -0
  74. package/utils/process-util.js +92 -0
  75. package/weaver.d.ts +2 -0
  76. package/weaver.js +53 -0
@@ -0,0 +1,694 @@
1
+ # Security
2
+
3
+ Appweaver provides a comprehensive security system with multiple authentication methods, role-based authorization,
4
+ OAuth2 social login, two-factor authentication, reCAPTCHA, and account management. All security features are
5
+ configurable and can be enabled/disabled independently.
6
+
7
+ ---
8
+
9
+ ## Authentication methods
10
+
11
+ Appweaver supports four authentication methods that can be used independently or combined:
12
+
13
+ | Method | Config flag | Header/mechanism |
14
+ |--------------|----------------------------|--------------------------------------|
15
+ | JWT (Bearer) | Always enabled | `Authorization: Bearer <token>` |
16
+ | HTTP Basic | `SECURITY_BASIC_ENABLED` | `Authorization: Basic <base64>` |
17
+ | API Key | `SECURITY_API_KEY_ENABLED` | `x-api-key: <id><delimiter><secret>` |
18
+ | OAuth2 | Per-provider flags | Browser redirect flow |
19
+
20
+ When multiple methods are enabled, the authentication middleware tries each in order. A request is authenticated if
21
+ any one method succeeds. If no credentials are present for any method, a 401 error is returned.
22
+
23
+ ### Route authentication configuration
24
+
25
+ Routes specify which auth methods they accept:
26
+
27
+ ```ts
28
+ const config = {
29
+ // Accept only JWT
30
+ create: {
31
+ auth: ['jwt']
32
+ },
33
+
34
+ // Accept JWT or API key
35
+ query: {
36
+ auth: ['jwt', 'apiKey']
37
+ },
38
+
39
+ // Public route (no auth required)
40
+ find: {
41
+ public: true
42
+ }
43
+ };
44
+
45
+ ```
46
+
47
+ ---
48
+
49
+ ## JWT authentication
50
+
51
+ JWT is the primary authentication method. Appweaver uses RSA (RS256) by default but supports symmetric HMAC (HS256)
52
+ if `SECURITY_JWT_SECRET` is set.
53
+
54
+ ### Key management
55
+
56
+ - RSA 2048-bit key pair generated automatically if `SECURITY_JWT_AUTO_GENERATE_KEYS` is `true` and key files are
57
+ missing
58
+ - Keys stored at `SECURITY_JWT_PUBLIC_KEY_PATH` and `SECURITY_JWT_PRIVATE_KEY_PATH` (default: `./storage/keys/`)
59
+ - If `SECURITY_JWT_SECRET` is set, HMAC signing is used instead of RSA
60
+
61
+ ### Token types and scopes
62
+
63
+ | Scope | Purpose | Access |
64
+ |-----------|-----------------------|------------------------------------------------------------------------|
65
+ | `Auth` | Full API access | All routes except `/refresh`, `/2fa-send-code`, `/verify-2fa-code` |
66
+ | `Refresh` | Token renewal only | Only `POST /auth/refresh` |
67
+ | `TwoFA` | 2FA verification only | Only `POST /account/send-2fa-code` and `POST /account/verify-2fa-code` |
68
+
69
+ ### JWT payload
70
+
71
+ ```json
72
+ {
73
+ "scope": "auth | refresh | 2fa",
74
+ "source": "password | oauth2Google | oauth2Facebook | oauth2Custom | apiKey | basic",
75
+ "username": "User email (e.g. admin@example.com)",
76
+ "sub": "User ID (e.g. 123)",
77
+ "iat": "Issued at timestamp (e.g. 1774623924234)"
78
+ }
79
+ ```
80
+
81
+ ### Token validation
82
+
83
+ On every authenticated request, the server:
84
+
85
+ 1. Verifies the JWT signature
86
+ 2. Loads the user from the database by `sub` (user ID)
87
+ 3. Checks that the user is enabled
88
+ 4. Validates `logoutAt` is before the token's `iat` (tokens issued before logout are rejected)
89
+ 5. Checks that the token scope allows access to the requested URL
90
+
91
+ ### Auth routes
92
+
93
+ | Method | Path | Auth | Description |
94
+ |--------|-------------------------|---------------------|-------------------------------------------------------------------------------------------------------------|
95
+ | `POST` | `/auth/login` | Public | Login with email and password. Returns access + refresh tokens. Rate limited: 12/window. |
96
+ | `POST` | `/auth/refresh` | JWT (Refresh scope) | Exchange a refresh token for a new access token. Rate limited: 12/window. |
97
+ | `POST` | `/auth/logout` | JWT | Logout. Sets `logoutAt` timestamp to invalidate all existing tokens. |
98
+ | `GET` | `/auth/me` | JWT | Get the current authenticated user's profile. |
99
+ | `POST` | `/auth/change-password` | Any | Change password. Requires current password + new password. Invalidates all tokens. Rate limited: 12/window. |
100
+ | `POST` | `/auth/exchange-token` | Public | Exchange a one-time token (OTT) for JWT access + refresh tokens. Rate limited: 12/window. |
101
+
102
+ ---
103
+
104
+ ## HTTP Basic authentication
105
+
106
+ When enabled, requests with an `Authorization: Basic` header are authenticated against the user database.
107
+
108
+ **Configuration:**
109
+
110
+ ```json
111
+ {
112
+ "config": {
113
+ "security": {
114
+ "basic": {
115
+ "enabled": true,
116
+ "realm": "My App",
117
+ "proxyMode": false
118
+ }
119
+ }
120
+ }
121
+ }
122
+ ```
123
+
124
+ Uses `@fastify/basic-auth` plugin. Extracts Base64-encoded `username:password` from the header and validates against
125
+ the user's stored password hash.
126
+
127
+ ---
128
+
129
+ ## API key authentication
130
+
131
+ API keys provide long-lived, per-user credentials for programmatic access.
132
+
133
+ ### How it works
134
+
135
+ 1. An authenticated user creates an API key via the CRUD endpoints
136
+ 2. The server generates a 64-character random secret and stores its SHA256 hash
137
+ 3. The full key is returned once as `{id}{delimiter}{secret}` (e.g. `42AKa1b2c3d4...`)
138
+ 4. Subsequent reads show only a masked version: `{id}...{last6chars}`
139
+ 5. On each request, the server extracts the key from the header, parses `id` and `secret`, hashes the secret, and
140
+ compares against the stored hash
141
+
142
+ ### API key format
143
+
144
+ ```
145
+ {id}{SECURITY_API_KEY_DELIMITER}{secret}
146
+ ```
147
+
148
+ Default delimiter is `AK`, so a key looks like: `42AKa1b2c3d4e5f6...`
149
+
150
+ ### API key model
151
+
152
+ | Field | Type | Description |
153
+ |---------------|-----------|------------------------------------------------------|
154
+ | `id` | int | Auto-generated ID. |
155
+ | `key` | string | 64-char secret (shown only at creation). |
156
+ | `keyHash` | string | SHA256 hash of the key (stored, hidden from API). |
157
+ | `name` | string? | Optional friendly name. |
158
+ | `description` | string? | Optional description. |
159
+ | `enabled` | boolean | Whether the key is active. |
160
+ | `expiresAt` | dateTime? | Optional expiration date. Enforced on every request. |
161
+
162
+ ### API key policy
163
+
164
+ - Users can only see and manage their own API keys
165
+ - `SECURITY_API_KEY_MAX_DURATION` limits how far in the future `expiresAt` can be set
166
+
167
+ **Configuration:**
168
+
169
+ ```json
170
+ {
171
+ "config": {
172
+ "security": {
173
+ "apiKey": {
174
+ "enabled": true,
175
+ "headerName": "x-api-key",
176
+ "delimiter": "AK",
177
+ "maxDuration": 7776000000
178
+ }
179
+ }
180
+ }
181
+ }
182
+ ```
183
+
184
+ ---
185
+
186
+ ## OAuth2 authentication
187
+
188
+ Appweaver supports OAuth2 login with Google, Facebook, and a custom OpenID Connect provider. All OAuth2 providers
189
+ follow the same flow pattern.
190
+
191
+ ### OAuth2 flow
192
+
193
+ ```
194
+ 1. Client redirects to:
195
+ GET /auth/login/{provider}?redirectToUrl=https://myapp.com/dashboard
196
+
197
+ 2. Server validates redirectToUrl against SECURITY_ALLOWED_REDIRECT_HOSTS
198
+
199
+ 3. Server generates a state token (OTT) and redirects to provider:
200
+ -> https://accounts.google.com/o/oauth2/v2/auth?
201
+ client_id=...&
202
+ redirect_uri=https://myapi.com/auth/login/google/callback&
203
+ state={stateToken}&
204
+ scope=profile email&
205
+ response_type=code
206
+
207
+ 4. User authenticates with the provider
208
+
209
+ 5. Provider redirects back to callback:
210
+ GET /auth/login/{provider}/callback?code={authCode}&state={stateToken}
211
+
212
+ 6. Server verifies the state token (one-time use)
213
+ 7. Server exchanges the code for an access token with the provider
214
+ 8. Server fetches user info from the provider
215
+ 9. Server creates or finds the user by email
216
+ 10. Server generates an authentication OTT
217
+ 11. Server redirects to the original URL with the token:
218
+ -> https://myapp.com/dashboard?token={ott}
219
+
220
+ 12. Client exchanges the OTT for JWT tokens:
221
+ POST /auth/exchange-token { token: "{ott}" }
222
+ -> { accessToken, refreshToken }
223
+ ```
224
+
225
+ ### Google OAuth2
226
+
227
+ **Configuration:**
228
+
229
+ ```json
230
+ {
231
+ "config": {
232
+ "security": {
233
+ "oauth2": {
234
+ "google": {
235
+ "enabled": true,
236
+ "clientId": "your-google-client-id",
237
+ "clientSecret": "your-google-client-secret"
238
+ }
239
+ }
240
+ }
241
+ }
242
+ }
243
+ ```
244
+
245
+ Or via environment variables:
246
+
247
+ ```env
248
+ SECURITY_OAUTH2_GOOGLE_ENABLED=true
249
+ SECURITY_OAUTH2_GOOGLE_CLIENT_ID=your-google-client-id
250
+ SECURITY_OAUTH2_GOOGLE_CLIENT_SECRET=your-google-client-secret
251
+ ```
252
+
253
+ **Routes:**
254
+
255
+ | Method | Path | Description |
256
+ |--------|-------------------------------|--------------------------------------------------------------------------|
257
+ | `GET` | `/auth/login/google` | Redirect to Google consent screen. Query: `redirectToUrl`. |
258
+ | `GET` | `/auth/login/google/callback` | Google callback. Exchanges code, creates/finds user, redirects with OTT. |
259
+
260
+ **Scopes**: `profile`, `email`
261
+
262
+ **User info extracted**: `email`, `given_name` (firstName), `family_name` (lastName)
263
+
264
+ **Google Cloud Console setup:**
265
+
266
+ 1. Create OAuth 2.0 credentials in the Google Cloud Console
267
+ 2. Set the authorized redirect URI to: `{APP_HOSTNAME}{SERVER_API_PREFIX}/auth/login/google/callback`
268
+ (e.g. `https://api.myapp.com/api/auth/login/google/callback`)
269
+
270
+ ### Facebook OAuth2
271
+
272
+ **Configuration:**
273
+
274
+ ```json
275
+ {
276
+ "config": {
277
+ "security": {
278
+ "oauth2": {
279
+ "facebook": {
280
+ "enabled": true,
281
+ "clientId": "your-facebook-app-id",
282
+ "clientSecret": "your-facebook-app-secret"
283
+ }
284
+ }
285
+ }
286
+ }
287
+ }
288
+ ```
289
+
290
+ Or via environment variables:
291
+
292
+ ```env
293
+ SECURITY_OAUTH2_FACEBOOK_ENABLED=true
294
+ SECURITY_OAUTH2_FACEBOOK_CLIENT_ID=your-facebook-app-id
295
+ SECURITY_OAUTH2_FACEBOOK_CLIENT_SECRET=your-facebook-app-secret
296
+ ```
297
+
298
+ **Routes:**
299
+
300
+ | Method | Path | Description |
301
+ |--------|---------------------------------|----------------------------------------------------------------------------|
302
+ | `GET` | `/auth/login/facebook` | Redirect to Facebook login. Query: `redirectToUrl`. |
303
+ | `GET` | `/auth/login/facebook/callback` | Facebook callback. Exchanges code, creates/finds user, redirects with OTT. |
304
+
305
+ **Scopes**: `public_profile`, `email`
306
+
307
+ **User info extracted**: `email`, `name` (split into firstName/lastName)
308
+
309
+ **Facebook Developer Console setup:**
310
+
311
+ 1. Create an app in the Facebook Developer Console
312
+ 2. Add Facebook Login product
313
+ 3. Set the valid OAuth redirect URI to: `{APP_HOSTNAME}{SERVER_API_PREFIX}/auth/login/facebook/callback`
314
+
315
+ ### Custom OAuth2 (OpenID Connect)
316
+
317
+ For any OpenID Connect-compatible provider (Keycloak, Auth0, etc.).
318
+
319
+ **Configuration:**
320
+
321
+ ```json
322
+ {
323
+ "config": {
324
+ "security": {
325
+ "oauth2": {
326
+ "custom": {
327
+ "enabled": true,
328
+ "clientId": "your-client-id",
329
+ "clientSecret": "your-client-secret",
330
+ "issuer": "https://keycloak.example.com/realms/myrealm"
331
+ }
332
+ }
333
+ }
334
+ }
335
+ }
336
+ ```
337
+
338
+ **Routes:**
339
+
340
+ | Method | Path | Description |
341
+ |--------|-------------------------------|------------------------------------------------------|
342
+ | `GET` | `/auth/login/custom` | Redirect to custom provider. Query: `redirectToUrl`. |
343
+ | `GET` | `/auth/login/custom/callback` | Custom provider callback. |
344
+
345
+ **Scopes**: `openid`, `profile`, `email`
346
+
347
+ **User info endpoint**: `{issuer}/protocol/openid-connect/userinfo`
348
+
349
+ **Standard claims expected**: `sub`, `email`, `given_name`, `family_name`
350
+
351
+ ### Client-side OAuth2 integration example
352
+
353
+ ```ts
354
+ // 1. Redirect user to OAuth2 login
355
+ window.location.href = 'https://api.myapp.com/api/auth/login/google?redirectToUrl=https://myapp.com/auth/callback';
356
+
357
+ // 2. On the callback page, extract the token from URL params
358
+ const params = new URLSearchParams(window.location.search);
359
+ const token = params.get('token');
360
+
361
+ // 3. Exchange the OTT for JWT tokens
362
+ const response = await fetch('https://api.myapp.com/api/auth/exchange-token', {
363
+ method: 'POST',
364
+ headers: { 'Content-Type': 'application/json' },
365
+ body: JSON.stringify({ token })
366
+ });
367
+
368
+ const { accessToken, refreshToken } = await response.json();
369
+
370
+ // 4. Use the access token for subsequent requests
371
+ fetch('https://api.myapp.com/api/products/query', {
372
+ headers: { Authorization: `Bearer ${accessToken}` }
373
+ });
374
+ ```
375
+
376
+ ### Redirect URL validation
377
+
378
+ All redirect URLs (for OAuth2, email verification, password reset) are validated against
379
+ `SECURITY_ALLOWED_REDIRECT_HOSTS`. Set this to specific domains in production:
380
+
381
+ ```json
382
+ {
383
+ "config": {
384
+ "security": {
385
+ "allowedRedirectHosts": [
386
+ "myapp.com",
387
+ "admin.myapp.com"
388
+ ]
389
+ }
390
+ }
391
+ }
392
+ ```
393
+
394
+ Default is `['*']` (all hosts allowed).
395
+
396
+ ---
397
+
398
+ ## Authorization
399
+
400
+ ### Role-based access control (RBAC)
401
+
402
+ Appweaver uses a role-permission model:
403
+
404
+ - **Roles** have a unique name and contain zero or more **permissions**
405
+ - **Users** are assigned zero or more roles
406
+ - Routes can require specific roles or permissions
407
+
408
+ ### Route authorization
409
+
410
+ ```ts
411
+ // Require any of these roles (OR logic)
412
+ create: {
413
+ roles: ['Admin', 'Editor']
414
+ }
415
+
416
+ // Require any of these permissions (OR logic)
417
+ update: {
418
+ permissions: ['product:update', 'product:manage']
419
+ }
420
+ ```
421
+
422
+ ### Authorization check order
423
+
424
+ On every authenticated request:
425
+
426
+ 1. Verify user exists and is enabled
427
+ 2. Verify `logoutAt` is before token `iat`
428
+ 3. Verify JWT scope allows access to the URL
429
+ 4. Verify the user has required roles (if configured)
430
+ 5. Verify the user has required permissions (if configured)
431
+
432
+ ### Helper functions
433
+
434
+ ```ts
435
+ import { hasRole, hasRoles, hasPermission, hasPermissions, currentAuthUser } from '@appweaver/core';
436
+
437
+ const user = currentAuthUser();
438
+
439
+ hasRole(user, 'Admin'); // boolean
440
+ hasRoles(user, ['Admin', 'Editor']); // OR: has at least one
441
+ hasRoles(user, ['Admin', 'Editor'], 'and'); // AND: has all
442
+
443
+ hasPermission(user, 'product:create'); // boolean
444
+ hasPermissions(user, ['product:create', 'product:update']); // OR
445
+ hasPermissions(user, ['product:create', 'product:update'], 'and'); // AND
446
+ ```
447
+
448
+ ### Request context helpers
449
+
450
+ ```ts
451
+ import { currentAuthUser, currentAuthType, currentAuthSource } from '@appweaver/core';
452
+
453
+ const user = currentAuthUser(); // Current authenticated user
454
+ const type = currentAuthType(); // 'jwt' | 'apiKey' | 'basic'
455
+ const source = currentAuthSource(); // 'Password' | 'OAuth2Google' | 'ApiKey' | etc.
456
+ ```
457
+
458
+ ---
459
+
460
+ ## Two-factor authentication (2FA)
461
+
462
+ When enabled, 2FA adds an extra verification step after password login using email-based one-time codes.
463
+
464
+ ### Configuration
465
+
466
+ ```json
467
+ {
468
+ "config": {
469
+ "security": {
470
+ "account": {
471
+ "2fa": {
472
+ "enabled": true,
473
+ "forced": false,
474
+ "ottTtl": 300000
475
+ }
476
+ }
477
+ }
478
+ }
479
+ }
480
+ ```
481
+
482
+ - `enabled` - Allow users to opt into 2FA
483
+ - `forced` - Require 2FA for all users regardless of their preference
484
+ - `ottTtl` - Code expiration time in milliseconds (default 5 minutes)
485
+
486
+ ### User setting
487
+
488
+ Users set their 2FA preference via the `twoFactorAuth` field on their profile:
489
+
490
+ - `'None'` - 2FA disabled for this user
491
+ - `'Email'` - 2FA enabled via email codes
492
+
493
+ ### Login flow with 2FA
494
+
495
+ ```
496
+ 1. POST /auth/login { email, password }
497
+ -> If 2FA required: returns JWT with TwoFA scope (restricted access)
498
+ -> If 2FA not required: returns JWT with Auth scope (full access)
499
+
500
+ 2. POST /account/send-2fa-code
501
+ -> Generates 6-digit code, emails it to user
502
+ -> Returns { challengeId }
503
+
504
+ 3. POST /account/verify-2fa-code { challengeId, code }
505
+ -> Validates code against stored hash
506
+ -> Returns { token } (one-time authentication token)
507
+
508
+ 4. POST /auth/exchange-token { token }
509
+ -> Returns full JWT with Auth scope { accessToken, refreshToken }
510
+ ```
511
+
512
+ ### 2FA routes
513
+
514
+ | Method | Path | Auth | Rate limit | Description |
515
+ |--------|----------------------------|-------------------|------------|--------------------------------------------------|
516
+ | `POST` | `/account/send-2fa-code` | JWT (TwoFA scope) | 10/15min | Send 2FA code to user's email. |
517
+ | `POST` | `/account/verify-2fa-code` | JWT (TwoFA scope) | 12/window | Verify 2FA code, returns OTT for token exchange. |
518
+
519
+ ---
520
+
521
+ ## reCAPTCHA
522
+
523
+ Appweaver integrates Google reCAPTCHA v3 for bot protection on sensitive endpoints.
524
+
525
+ ### Configuration
526
+
527
+ ```json
528
+ {
529
+ "config": {
530
+ "security": {
531
+ "recaptcha": {
532
+ "enabled": true,
533
+ "secret": "your-recaptcha-secret-key",
534
+ "headerName": "x-recaptcha-token",
535
+ "minScore": 0.4
536
+ }
537
+ }
538
+ }
539
+ }
540
+ ```
541
+
542
+ ### How it works
543
+
544
+ 1. Client gets a reCAPTCHA token from the Google reCAPTCHA v3 widget
545
+ 2. Client includes the token in the request header: `x-recaptcha-token: <token>`
546
+ 3. Server sends the token to Google's verification API along with the secret key and client IP
547
+ 4. Server validates:
548
+ - `success` flag is `true`
549
+ - `action` matches the expected action (if configured on the route)
550
+ - `score` is at or above `SECURITY_RECAPTCHA_MIN_SCORE`
551
+
552
+ ### Using reCAPTCHA on custom routes
553
+
554
+ ```ts
555
+ registerRoute(
556
+ async (router) => {
557
+ router.post('/contact', { handler: contactHandler });
558
+ },
559
+ { recaptcha: true, recaptchaAction: 'contact_form', public: true }
560
+ );
561
+ ```
562
+
563
+ On resource routes:
564
+
565
+ ```ts
566
+ createRoutes({
567
+ modelName: 'Comment',
568
+ create: { recaptcha: true, recaptchaAction: 'create_comment' }
569
+ });
570
+ ```
571
+
572
+ ### Routes with reCAPTCHA by default
573
+
574
+ - `POST /account/send-reset-password` (action: `send-reset-password`)
575
+ - `POST /account/reset-password` (action: `reset-password`)
576
+
577
+ ---
578
+
579
+ ## Account management
580
+
581
+ ### Email verification
582
+
583
+ | Method | Path | Auth | Description |
584
+ |--------|----------------------------------|--------|--------------------------------------------------------------------------------------------------------------------|
585
+ | `POST` | `/account/send-verify-email` | JWT | Send verification email. Takes `redirectUrl` and optional `type` (`'auto'` or `'manual'`). Rate limited: 10/15min. |
586
+ | `POST` | `/account/verify-email` | Public | Verify email with token from body. Rate limited: 12/window. |
587
+ | `GET` | `/account/verify-email-redirect` | Public | Auto-verify and redirect. Token from query param. Redirects to `{redirectUrl}?status=ok\|error&message=...`. |
588
+
589
+ **Verification types:**
590
+
591
+ - `auto` (default) – Generates a link that auto-verifies on click and redirects with a status query parameter
592
+ - `manual` - Generates a link where the client must POST the token to the verified endpoint
593
+
594
+ ### Password reset
595
+
596
+ | Method | Path | Auth | reCAPTCHA | Description |
597
+ |--------|--------------------------------|--------|-----------|-------------------------------------------------------------------------------------|
598
+ | `POST` | `/account/send-reset-password` | Public | Yes | Send password reset email. Takes `email` and `redirectUrl`. Rate limited: 10/15min. |
599
+ | `POST` | `/account/reset-password` | Public | Yes | Reset password with token and new password. Rate limited: 12/window. |
600
+
601
+ **Reset flow:**
602
+
603
+ 1. User requests reset: `POST /account/send-reset-password { email, redirectUrl }`
604
+ 2. Server generates OTT, emails a link: `{redirectUrl}?token={ott}`
605
+ 3. User clicks a link, enters a new password
606
+ 4. Client sends: `POST /account/reset-password { token, password }`
607
+ 5. Server validates password complexity, updates hash, sets `logoutAt` (invalidates all sessions)
608
+
609
+ ---
610
+
611
+ ## One-time tokens (OTT)
612
+
613
+ One-time tokens are used internally for various verification flows. They are single-use, purpose-bound, and
614
+ time-limited.
615
+
616
+ ### Purposes
617
+
618
+ | Purpose | TTL config | Used for |
619
+ |---------------------|---------------------------------------------------|---------------------------------------|
620
+ | `Authentication` | `SECURITY_AUTH_OTT_TTL` (120s) | OAuth2 token exchange, 2FA completion |
621
+ | `EmailVerification` | `SECURITY_ACCOUNT_VERIFY_EMAIL_OTT_TTL` (2h) | Email verification links |
622
+ | `PasswordReset` | `SECURITY_ACCOUNT_RESET_PASSWORD_OTT_TTL` (30min) | Password reset links |
623
+ | `TwoFAVerification` | `SECURITY_ACCOUNT_2FA_OTT_TTL` (5min) | 2FA code challenges |
624
+ | `OAuth2State` | `SECURITY_OAUTH2_STATE_TTL` (10min) | CSRF protection in OAuth2 flow |
625
+
626
+ ### Storage
627
+
628
+ OTTs are stored in the configured security store:
629
+
630
+ - **Redis** (default): `@appweaver/core/security/store/redis-security-store`
631
+ - **Database**: `@appweaver/core/security/store/database-security-store`
632
+
633
+ ---
634
+
635
+ ## Auth user model
636
+
637
+ The security module automatically adds the following fields to the user model:
638
+
639
+ | Field | Type | Default | Description |
640
+ |-----------------|------------------|----------|--------------------------------------------------------------|
641
+ | `email` | string (unique) | - | User's email address. |
642
+ | `passwordHash` | string? (hidden) | - | Bcrypt password hash (never exposed in API). |
643
+ | `verifiedEmail` | boolean | `false` | Whether the user's email is verified. |
644
+ | `twoFactorAuth` | enum | `'None'` | 2FA setting. Values: `'None'`, `'Email'`. |
645
+ | `enabled` | boolean | `true` | Whether the account is active. |
646
+ | `logoutAt` | dateTime? | - | Timestamp used to invalidate tokens issued before this time. |
647
+
648
+ **Relations:**
649
+
650
+ | Relation | Type | Description |
651
+ |-----------|----------|---------------------------------------------------------------------|
652
+ | `roles` | Role[] | Assigned roles with nested permissions (always included in output). |
653
+ | `apiKeys` | ApiKey[] | User's API keys (if API key auth is enabled). |
654
+
655
+ ---
656
+
657
+ ## Password handling
658
+
659
+ ### Hashing
660
+
661
+ Passwords are hashed using bcrypt with automatic salt generation.
662
+
663
+ ### Complexity validation
664
+
665
+ Password validation is configurable via `SECURITY_PASSWORD_*` properties:
666
+
667
+ - Minimum length (default: 8)
668
+ - Maximum length (default: 100)
669
+ - Require an uppercase letter (default: true)
670
+ - Require a lowercase letter (default: true)
671
+ - Require a digit (default: true)
672
+ - Require special character (default: true)
673
+
674
+ ### Token invalidation on password change
675
+
676
+ Both `change-password` and `reset-password` set `logoutAt` to the current timestamp, which invalidates all existing
677
+ JWT tokens across all devices. The `change-password` endpoint returns new tokens so the current session stays active.
678
+
679
+ ---
680
+
681
+ ## Rate limiting on security routes
682
+
683
+ | Endpoint | Limit |
684
+ |-------------------------------------|-------------------|
685
+ | `POST /auth/login` | 12 per window |
686
+ | `POST /auth/refresh` | 12 per window |
687
+ | `POST /auth/change-password` | 12 per window |
688
+ | `POST /auth/exchange-token` | 12 per window |
689
+ | `POST /account/send-verify-email` | 10 per 15 minutes |
690
+ | `POST /account/verify-email` | 12 per window |
691
+ | `POST /account/send-reset-password` | 10 per 15 minutes |
692
+ | `POST /account/reset-password` | 12 per window |
693
+ | `POST /account/send-2fa-code` | 10 per 15 minutes |
694
+ | `POST /account/verify-2fa-code` | 12 per window |