create-forgeon 0.3.15 → 0.3.17
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/package.json +4 -2
- package/src/cli/add-options.test.mjs +5 -2
- package/src/cli/options.test.mjs +1 -0
- package/src/cli/prompt-select.test.mjs +1 -0
- package/src/core/docs.test.mjs +80 -40
- package/src/core/scaffold.test.mjs +100 -0
- package/src/core/validate.test.mjs +1 -0
- package/src/modules/accounts.mjs +416 -0
- package/src/modules/db-prisma.mjs +23 -55
- package/src/modules/dependencies.test.mjs +71 -29
- package/src/modules/executor.mjs +3 -2
- package/src/modules/executor.test.mjs +631 -500
- package/src/modules/files-access.mjs +36 -105
- package/src/modules/files-image.mjs +35 -107
- package/src/modules/files-local.mjs +15 -6
- package/src/modules/files-quotas.mjs +75 -93
- package/src/modules/files-s3.mjs +17 -6
- package/src/modules/files.mjs +56 -125
- package/src/modules/i18n.mjs +17 -121
- package/src/modules/idempotency.test.mjs +180 -0
- package/src/modules/logger.mjs +0 -9
- package/src/modules/probes.test.mjs +204 -0
- package/src/modules/queue.mjs +325 -440
- package/src/modules/rate-limit.mjs +36 -76
- package/src/modules/rbac.mjs +39 -78
- package/src/modules/registry.mjs +22 -35
- package/src/modules/scheduler.mjs +51 -171
- package/src/modules/shared/files-runtime-wiring.mjs +81 -0
- package/src/modules/shared/nest-runtime-wiring.mjs +110 -0
- package/src/modules/shared/patch-utils.mjs +29 -1
- package/src/modules/shared/probes.mjs +235 -0
- package/src/modules/sync-integrations.mjs +109 -396
- package/src/modules/sync-integrations.test.mjs +141 -0
- package/src/run-add-module.test.mjs +154 -0
- package/templates/base/README.md +7 -55
- package/templates/base/apps/web/src/App.tsx +70 -42
- package/templates/base/apps/web/src/probes.ts +61 -0
- package/templates/base/apps/web/src/styles.css +86 -25
- package/templates/base/package.json +21 -15
- package/templates/base/scripts/forgeon-sync-integrations.mjs +65 -281
- package/templates/module-fragments/{jwt-auth → accounts}/00_title.md +2 -1
- package/templates/module-fragments/{jwt-auth → accounts}/10_overview.md +5 -5
- package/templates/module-fragments/accounts/20_scope.md +29 -0
- package/templates/module-fragments/accounts/90_status_implemented.md +8 -0
- package/templates/module-fragments/accounts/90_status_planned.md +7 -0
- package/templates/module-fragments/rbac/30_what_it_adds.md +3 -2
- package/templates/module-fragments/rbac/40_how_it_works.md +2 -1
- package/templates/module-fragments/rbac/50_how_to_use.md +2 -1
- package/templates/module-fragments/swagger/20_scope.md +2 -1
- package/templates/module-presets/accounts/apps/api/prisma/migrations/0002_accounts_core/migration.sql +97 -0
- package/templates/module-presets/accounts/apps/api/src/accounts/forgeon-accounts-db-prisma.module.ts +17 -0
- package/templates/module-presets/accounts/apps/api/src/accounts/prisma-accounts-persistence.store.ts +332 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/package.json +5 -5
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-email.port.ts +13 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-persistence.port.ts +67 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-rbac.port.ts +14 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.loader.ts +7 -7
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.service.ts +7 -7
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +318 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-env.schema.ts +4 -4
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-jwt.service.ts +58 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-password.service.ts +21 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.controller.ts +93 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.service.ts +48 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.types.ts +17 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/change-password.dto.ts +13 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/confirm-password-reset.dto.ts +12 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/index.ts +10 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/login.dto.ts +1 -1
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/refresh.dto.ts +1 -1
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/register.dto.ts +23 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/request-password-reset.dto.ts +7 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-profile.dto.ts +16 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-settings.dto.ts +16 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user.dto.ts +8 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/verify-email.dto.ts +8 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +82 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +24 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/jwt.strategy.ts +3 -3
- package/templates/module-presets/accounts/packages/accounts-api/src/owner-access.guard.ts +39 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users-config.ts +13 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users.controller.ts +65 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users.service.ts +87 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users.types.ts +65 -0
- package/templates/module-presets/{jwt-auth/packages/auth-contracts → accounts/packages/accounts-contracts}/package.json +1 -1
- package/templates/module-presets/accounts/packages/accounts-contracts/src/index.ts +119 -0
- package/templates/module-presets/files/apps/api/src/files/forgeon-files-db-prisma.module.ts +17 -0
- package/templates/module-presets/files/apps/api/src/files/prisma-files-persistence.store.ts +164 -0
- package/templates/module-presets/files/packages/files/package.json +1 -2
- package/templates/module-presets/files/packages/files/src/files.ports.ts +107 -0
- package/templates/module-presets/files/packages/files/src/files.service.ts +81 -395
- package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +126 -2
- package/templates/module-presets/files/packages/files/src/index.ts +2 -1
- package/templates/module-presets/files-local/packages/files-local/src/forgeon-files-local-storage.module.ts +18 -0
- package/templates/module-presets/files-local/packages/files-local/src/index.ts +2 -0
- package/templates/module-presets/files-local/packages/files-local/src/local-files-storage.adapter.ts +53 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +12 -4
- package/templates/module-presets/files-s3/packages/files-s3/src/forgeon-files-s3-storage.module.ts +18 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/index.ts +2 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/s3-files-storage.adapter.ts +130 -0
- package/templates/module-presets/i18n/apps/web/src/App.tsx +68 -41
- package/templates/module-presets/logger/packages/logger/src/index.ts +0 -1
- package/src/modules/jwt-auth.mjs +0 -390
- package/templates/base/docs/AI/ARCHITECTURE.md +0 -85
- package/templates/base/docs/AI/MODULE_CHECKS.md +0 -28
- package/templates/base/docs/AI/MODULE_SPEC.md +0 -77
- package/templates/base/docs/AI/PROJECT.md +0 -43
- package/templates/base/docs/AI/ROADMAP.md +0 -171
- package/templates/base/docs/AI/TASKS.md +0 -60
- package/templates/base/docs/AI/VALIDATION.md +0 -31
- package/templates/base/docs/README.md +0 -18
- package/templates/module-fragments/jwt-auth/20_scope.md +0 -19
- package/templates/module-fragments/jwt-auth/90_status_implemented.md +0 -8
- package/templates/module-fragments/jwt-auth/90_status_planned.md +0 -3
- package/templates/module-presets/jwt-auth/apps/api/prisma/migrations/0002_auth_refresh_token_hash/migration.sql +0 -3
- package/templates/module-presets/jwt-auth/apps/api/src/auth/prisma-auth-refresh-token.store.ts +0 -36
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-refresh-token.store.ts +0 -23
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.controller.ts +0 -71
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.service.ts +0 -175
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.types.ts +0 -6
- package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/index.ts +0 -2
- package/templates/module-presets/jwt-auth/packages/auth-api/src/forgeon-auth.module.ts +0 -47
- package/templates/module-presets/jwt-auth/packages/auth-api/src/index.ts +0 -12
- package/templates/module-presets/jwt-auth/packages/auth-contracts/src/index.ts +0 -47
- package/templates/module-presets/logger/packages/logger/src/http-logging.interceptor.ts +0 -94
- /package/templates/module-presets/{jwt-auth/packages/auth-api/src/jwt-auth.guard.ts → accounts/packages/accounts-api/src/access-token.guard.ts} +0 -0
- /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.module.ts +0 -0
- /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/tsconfig.json +0 -0
- /package/templates/module-presets/{jwt-auth/packages/auth-contracts → accounts/packages/accounts-contracts}/tsconfig.json +0 -0
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
# ROADMAP
|
|
2
|
-
|
|
3
|
-
This is a living plan. Scope and priorities may change.
|
|
4
|
-
|
|
5
|
-
## Current Foundation (Implemented)
|
|
6
|
-
|
|
7
|
-
- [x] Canonical scaffold: NestJS API + React web + Docker (+ default-on `db-prisma` module)
|
|
8
|
-
- [x] Proxy preset selection: `caddy | nginx | none`
|
|
9
|
-
- [x] `@forgeon/core`:
|
|
10
|
-
- [x] `core-config` (typed env config + validation)
|
|
11
|
-
- [x] `core-errors` (global envelope + exception filter)
|
|
12
|
-
- [x] `core-validation` (global validation pipe)
|
|
13
|
-
- [x] `@forgeon/db-prisma` as default-applied DB module
|
|
14
|
-
- [x] i18n add-module baseline:
|
|
15
|
-
- [x] `@forgeon/i18n`, `@forgeon/i18n-contracts`, `@forgeon/i18n-web`
|
|
16
|
-
- [x] shared dictionaries in `resources/i18n/*`
|
|
17
|
-
- [x] tooling: `i18n:sync`, `i18n:check`, `i18n:types`, `i18n:add`
|
|
18
|
-
- [x] module diagnostics probes pattern (`/api/health/*` + web test buttons)
|
|
19
|
-
|
|
20
|
-
## Standards (Accepted)
|
|
21
|
-
|
|
22
|
-
- [x] `*-contracts` and `*-web` packages are ESM-first
|
|
23
|
-
- [x] API runtime modules use Node-oriented TS config
|
|
24
|
-
- [x] no cross-package imports via `/src/*`; only package entrypoints
|
|
25
|
-
|
|
26
|
-
## Updated Priority Backlog
|
|
27
|
-
|
|
28
|
-
### P0 (Immediate Must-Have)
|
|
29
|
-
|
|
30
|
-
- [ ] `logger`
|
|
31
|
-
- [ ] canonical logger module
|
|
32
|
-
- [ ] requestId / correlationId propagation
|
|
33
|
-
- [ ] structured log conventions
|
|
34
|
-
|
|
35
|
-
- [ ] `openapi / swagger`
|
|
36
|
-
- [ ] env toggle: `SWAGGER_ENABLED`
|
|
37
|
-
- [ ] standard setup
|
|
38
|
-
- [ ] bearer integration hook for jwt-auth
|
|
39
|
-
- [ ] `/docs` route
|
|
40
|
-
|
|
41
|
-
- [ ] `jwt-auth`
|
|
42
|
-
- [ ] module split: contracts/api/web
|
|
43
|
-
- [ ] access + refresh baseline
|
|
44
|
-
- [ ] guards/strategy integration
|
|
45
|
-
|
|
46
|
-
- [ ] `rbac / permissions`
|
|
47
|
-
- [ ] decorators: `@Roles()`, `@Permissions()`
|
|
48
|
-
- [ ] guard + policy helper
|
|
49
|
-
- [ ] contracts: `Role`, `Permission`
|
|
50
|
-
- [ ] integration with jwt-auth claims
|
|
51
|
-
|
|
52
|
-
- [ ] `redis/queue foundation`
|
|
53
|
-
- [ ] base Redis config/service
|
|
54
|
-
- [ ] queue baseline (BullMQ or equivalent)
|
|
55
|
-
- [ ] retry and dead-letter conventions
|
|
56
|
-
|
|
57
|
-
- [ ] `rate-limit`
|
|
58
|
-
- [ ] Nest Throttler add-module
|
|
59
|
-
- [ ] policies: route / user / ip
|
|
60
|
-
- [ ] error code: `TOO_MANY_REQUESTS`
|
|
61
|
-
- [ ] reverse-proxy-aware mode (`trust proxy`)
|
|
62
|
-
|
|
63
|
-
- [ ] `files` (upload + storage)
|
|
64
|
-
- [ ] upload endpoints + DTO + guards
|
|
65
|
-
- [ ] storage presets: local + S3-compatible (MinIO/R2)
|
|
66
|
-
- [ ] MIME/size validation
|
|
67
|
-
- [ ] optional image processing subpackage (`sharp`)
|
|
68
|
-
- [ ] error codes: `UPLOAD_INVALID_TYPE`, `UPLOAD_TOO_LARGE`, `UPLOAD_QUOTA`
|
|
69
|
-
|
|
70
|
-
### P1 (Strongly Recommended)
|
|
71
|
-
|
|
72
|
-
- [ ] `testing baseline`
|
|
73
|
-
- [ ] unit + e2e presets
|
|
74
|
-
- [ ] test helpers for add-modules
|
|
75
|
-
- [ ] smoke test template for generated project
|
|
76
|
-
|
|
77
|
-
- [ ] `CI quality gates`
|
|
78
|
-
- [ ] `typecheck`, `lint`, `test`, docker build smoke
|
|
79
|
-
- [ ] release gate checklist
|
|
80
|
-
|
|
81
|
-
- [ ] `cache` (Redis)
|
|
82
|
-
- [ ] CacheModule preset
|
|
83
|
-
- [ ] key naming conventions
|
|
84
|
-
- [ ] shared wrapper/service
|
|
85
|
-
|
|
86
|
-
- [ ] `scheduler`
|
|
87
|
-
- [ ] `@nestjs/schedule` integration
|
|
88
|
-
- [ ] task template
|
|
89
|
-
- [ ] optional distributed lock (Redis)
|
|
90
|
-
|
|
91
|
-
- [ ] `mail`
|
|
92
|
-
- [ ] at least one provider preset (SMTP/Resend/SendGrid)
|
|
93
|
-
- [ ] templates: verify email, reset password
|
|
94
|
-
- [ ] optional outbox with queue
|
|
95
|
-
|
|
96
|
-
- [ ] workspace `eslint/prettier` config package
|
|
97
|
-
|
|
98
|
-
### P2 (Later)
|
|
99
|
-
|
|
100
|
-
- [ ] frontend `http-client` module
|
|
101
|
-
- [ ] frontend UI kit package
|
|
102
|
-
- [ ] migrate reusable parts from `eso-dt` (when available)
|
|
103
|
-
- [ ] extend missing primitives
|
|
104
|
-
- [ ] `realtime` (ws)
|
|
105
|
-
- [ ] gateway baseline
|
|
106
|
-
- [ ] jwt auth for ws
|
|
107
|
-
- [ ] rooms + basic events
|
|
108
|
-
- [ ] `webhooks` module (subject to scope validation)
|
|
109
|
-
- [ ] signed inbound verify (HMAC)
|
|
110
|
-
- [ ] signed outbound sender
|
|
111
|
-
- [ ] replay protection (timestamp/nonce)
|
|
112
|
-
|
|
113
|
-
## Execution Plan (3 Sprints)
|
|
114
|
-
|
|
115
|
-
### Sprint 1: Platform Baseline and Security Start
|
|
116
|
-
|
|
117
|
-
Scope:
|
|
118
|
-
- `logger`
|
|
119
|
-
- `openapi/swagger`
|
|
120
|
-
- `jwt-auth`
|
|
121
|
-
- `testing baseline`
|
|
122
|
-
- `CI quality gates`
|
|
123
|
-
|
|
124
|
-
Definition of Done:
|
|
125
|
-
- add-modules install cleanly via `create-forgeon add <module>`
|
|
126
|
-
- local dev (`pnpm dev`) and docker build both pass on fresh generated project
|
|
127
|
-
- each module has probe endpoint and web probe UI hook when applicable
|
|
128
|
-
- docs updated in both root and template docs
|
|
129
|
-
|
|
130
|
-
### Sprint 2: Authorization and Traffic Control
|
|
131
|
-
|
|
132
|
-
Scope:
|
|
133
|
-
- `rbac/permissions`
|
|
134
|
-
- `redis/queue foundation`
|
|
135
|
-
- `rate-limit`
|
|
136
|
-
- `files`
|
|
137
|
-
- `cache`
|
|
138
|
-
|
|
139
|
-
Definition of Done:
|
|
140
|
-
- claims/roles/permissions flow validated end-to-end (api + web contracts)
|
|
141
|
-
- rate-limit and files include standardized error codes and envelope mapping
|
|
142
|
-
- Redis-backed modules run in docker profile with documented env keys
|
|
143
|
-
- at least one e2e happy-path per module
|
|
144
|
-
|
|
145
|
-
### Sprint 3: Async Integrations and Frontend Foundation
|
|
146
|
-
|
|
147
|
-
Scope:
|
|
148
|
-
- `scheduler`
|
|
149
|
-
- `mail`
|
|
150
|
-
- workspace `eslint/prettier` config package
|
|
151
|
-
- frontend `http-client`
|
|
152
|
-
|
|
153
|
-
Definition of Done:
|
|
154
|
-
- queue/scheduler/mail basic scenarios work in local + docker
|
|
155
|
-
- frontend http-client consumes api contracts with typed errors
|
|
156
|
-
- lint/typecheck/test/build pass through CI gate preset
|
|
157
|
-
- docs include migration notes and extension points
|
|
158
|
-
|
|
159
|
-
## Explicit Dependencies and Order Constraints
|
|
160
|
-
|
|
161
|
-
- `rbac` depends on `jwt-auth`
|
|
162
|
-
- `rate-limit` should follow Redis/queue foundation for scalable mode
|
|
163
|
-
- `mail` should reuse queue foundation where possible
|
|
164
|
-
- `openapi` is most useful before/with `jwt-auth` and `http-client`
|
|
165
|
-
- `realtime` and `webhooks` stay post-MVP unless a concrete use-case appears
|
|
166
|
-
|
|
167
|
-
## i18n Policy For Add-Modules
|
|
168
|
-
|
|
169
|
-
- [ ] each add-module that introduces user-facing text defines its own namespace templates
|
|
170
|
-
- [ ] if i18n is already enabled, namespace files are added during module installation
|
|
171
|
-
- [ ] if module is installed first and i18n later, namespaces are merged during i18n installation
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
# TASKS
|
|
2
|
-
|
|
3
|
-
## Feature Discovery Matrix
|
|
4
|
-
|
|
5
|
-
```text
|
|
6
|
-
Scan this monorepo and build a backend feature matrix by app/package.
|
|
7
|
-
Use only evidence from code and dependencies.
|
|
8
|
-
Output:
|
|
9
|
-
1) taxonomy by category
|
|
10
|
-
2) feature comparison table
|
|
11
|
-
3) common core
|
|
12
|
-
4) unique features
|
|
13
|
-
5) architectural inconsistencies
|
|
14
|
-
Include file references for every feature.
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## Add Module Package
|
|
18
|
-
|
|
19
|
-
```text
|
|
20
|
-
Create a new reusable package under packages/ for <feature-name>.
|
|
21
|
-
Requirements:
|
|
22
|
-
- minimal API
|
|
23
|
-
- NestJS-compatible module
|
|
24
|
-
- docs in package README
|
|
25
|
-
- wire into apps/api conditionally via env flag
|
|
26
|
-
- keep backward compatibility
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Refactor Core
|
|
30
|
-
|
|
31
|
-
```text
|
|
32
|
-
Move shared backend logic from apps/api into packages/core.
|
|
33
|
-
Do not change behavior.
|
|
34
|
-
Update imports, package dependencies, and docs.
|
|
35
|
-
Run build checks and show changed files.
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Generate Preset
|
|
39
|
-
|
|
40
|
-
```text
|
|
41
|
-
Create or update create-forgeon preset flow:
|
|
42
|
-
- keep canonical stack fixed: NestJS + React + Prisma/Postgres + Docker
|
|
43
|
-
- allow only runtime proxy choice: caddy/nginx/none
|
|
44
|
-
- update generated docs fragments
|
|
45
|
-
- update docs/AI/ARCHITECTURE.md and docs/AI/MODULE_SPEC.md when scope changes
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Add Fullstack Module
|
|
49
|
-
|
|
50
|
-
```text
|
|
51
|
-
Implement `create-forgeon add <module-id>` for a fullstack feature.
|
|
52
|
-
Requirements:
|
|
53
|
-
- split module into contracts/api/web packages
|
|
54
|
-
- contracts is source of truth for routes, DTOs, errors
|
|
55
|
-
- if feasible, add module probe hooks in API (`/api/health/*`) and web diagnostics UI
|
|
56
|
-
- if i18n is enabled, add module namespace files and wire them for both API and web
|
|
57
|
-
- add docs note under docs/AI/MODULES/<module-id>.md
|
|
58
|
-
- keep backward compatibility
|
|
59
|
-
```
|
|
60
|
-
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# VALIDATION
|
|
2
|
-
|
|
3
|
-
## Backend DTO Validation Standard
|
|
4
|
-
|
|
5
|
-
- Use `class-validator` decorators on DTO classes.
|
|
6
|
-
- Global validation is centralized in `@forgeon/core` via `createValidationPipe()`.
|
|
7
|
-
- Current defaults:
|
|
8
|
-
- `whitelist: true`
|
|
9
|
-
- `transform: true`
|
|
10
|
-
- `validationError.target: false`
|
|
11
|
-
- `validationError.value: false`
|
|
12
|
-
- Keep DTO validation messages stable and explicit.
|
|
13
|
-
- For required values, use a consistent key or message pattern.
|
|
14
|
-
|
|
15
|
-
## Config Validation Standard
|
|
16
|
-
|
|
17
|
-
- Use Zod for env/config validation.
|
|
18
|
-
- Core env is validated by `@forgeon/core` (`core-config`).
|
|
19
|
-
- Each add-module validates only its own env keys with its own Zod schema.
|
|
20
|
-
- Avoid one global env schema for all modules; keep schemas modular.
|
|
21
|
-
|
|
22
|
-
## Error Details for Validation
|
|
23
|
-
|
|
24
|
-
- Error envelope stays consistent:
|
|
25
|
-
- `error.code`
|
|
26
|
-
- `error.message`
|
|
27
|
-
- `error.status`
|
|
28
|
-
- optional `error.details`
|
|
29
|
-
- Validation details should be structured (not `any`).
|
|
30
|
-
- `core-validation` formats validation details as:
|
|
31
|
-
- `{ field?: string, message: string }[]`
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# Documentation Index
|
|
2
|
-
|
|
3
|
-
- `AI/PROJECT.md` - project overview and run modes
|
|
4
|
-
- `AI/ARCHITECTURE.md` - monorepo design and extension model
|
|
5
|
-
- `AI/ROADMAP.md` - implementation roadmap and feature priorities
|
|
6
|
-
- `AI/MODULE_SPEC.md` - fullstack module contract (`contracts/api/web`)
|
|
7
|
-
- `AI/MODULE_CHECKS.md` - required runtime probe hooks for modules
|
|
8
|
-
- `AI/VALIDATION.md` - DTO/env validation standards
|
|
9
|
-
- `AI/TASKS.md` - ready-to-use Codex prompts
|
|
10
|
-
|
|
11
|
-
## i18n Language Workflow
|
|
12
|
-
|
|
13
|
-
Add a new language from existing namespaces:
|
|
14
|
-
- `pnpm i18n:add uk`
|
|
15
|
-
|
|
16
|
-
Useful follow-up commands:
|
|
17
|
-
- `pnpm i18n:sync`
|
|
18
|
-
- `pnpm i18n:check`
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
## Scope
|
|
2
|
-
|
|
3
|
-
Implemented scope:
|
|
4
|
-
|
|
5
|
-
1. Split into reusable packages:
|
|
6
|
-
- `@forgeon/auth-contracts`
|
|
7
|
-
- `@forgeon/auth-api`
|
|
8
|
-
2. API runtime:
|
|
9
|
-
- JWT login/refresh/logout/me endpoints
|
|
10
|
-
- `JwtStrategy` + `JwtAuthGuard`
|
|
11
|
-
- `authConfig` + `authEnvSchema` wiring through root `ConfigModule` validator chain
|
|
12
|
-
3. DB behavior:
|
|
13
|
-
- module install stays stateless by default
|
|
14
|
-
- refresh token hash persistence is enabled later through the `db-adapter` capability via `pnpm forgeon:sync-integrations`
|
|
15
|
-
- current DB adapter implementation for this integration is `db-prisma`
|
|
16
|
-
- if no DB adapter is installed, the module stays stateless and prints an optional integration warning with follow-up commands
|
|
17
|
-
4. Module checks:
|
|
18
|
-
- API probe endpoint: `GET /api/health/auth`
|
|
19
|
-
- default web probe button + result block
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
## Current State
|
|
2
|
-
|
|
3
|
-
Status: implemented.
|
|
4
|
-
|
|
5
|
-
Notes:
|
|
6
|
-
- The persistence boundary is `db-adapter`, not a hard dependency on one concrete DB module.
|
|
7
|
-
- The current DB adapter implementation for auth persistence is `db-prisma`.
|
|
8
|
-
- If no DB adapter is installed, jwt-auth stays in stateless refresh mode and surfaces an optional integration warning.
|
package/templates/module-presets/jwt-auth/apps/api/src/auth/prisma-auth-refresh-token.store.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AuthRefreshTokenStore,
|
|
3
|
-
} from '@forgeon/auth-api';
|
|
4
|
-
import { PrismaService } from '@forgeon/db-prisma';
|
|
5
|
-
import { Injectable } from '@nestjs/common';
|
|
6
|
-
|
|
7
|
-
@Injectable()
|
|
8
|
-
export class PrismaAuthRefreshTokenStore implements AuthRefreshTokenStore {
|
|
9
|
-
readonly kind = 'prisma';
|
|
10
|
-
|
|
11
|
-
constructor(private readonly prisma: PrismaService) {}
|
|
12
|
-
|
|
13
|
-
async saveRefreshTokenHash(subject: string, hash: string): Promise<void> {
|
|
14
|
-
await this.prisma.user.upsert({
|
|
15
|
-
where: { email: subject },
|
|
16
|
-
create: { email: subject, refreshTokenHash: hash },
|
|
17
|
-
update: { refreshTokenHash: hash },
|
|
18
|
-
select: { id: true },
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async getRefreshTokenHash(subject: string): Promise<string | null> {
|
|
23
|
-
const user = await this.prisma.user.findUnique({
|
|
24
|
-
where: { email: subject },
|
|
25
|
-
select: { refreshTokenHash: true },
|
|
26
|
-
});
|
|
27
|
-
return user?.refreshTokenHash ?? null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async removeRefreshTokenHash(subject: string): Promise<void> {
|
|
31
|
-
await this.prisma.user.updateMany({
|
|
32
|
-
where: { email: subject },
|
|
33
|
-
data: { refreshTokenHash: null },
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { Injectable } from '@nestjs/common';
|
|
2
|
-
|
|
3
|
-
export const AUTH_REFRESH_TOKEN_STORE = Symbol('AUTH_REFRESH_TOKEN_STORE');
|
|
4
|
-
|
|
5
|
-
export interface AuthRefreshTokenStore {
|
|
6
|
-
readonly kind: string;
|
|
7
|
-
saveRefreshTokenHash(subject: string, hash: string): Promise<void>;
|
|
8
|
-
getRefreshTokenHash(subject: string): Promise<string | null>;
|
|
9
|
-
removeRefreshTokenHash(subject: string): Promise<void>;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
@Injectable()
|
|
13
|
-
export class NoopAuthRefreshTokenStore implements AuthRefreshTokenStore {
|
|
14
|
-
readonly kind = 'none';
|
|
15
|
-
|
|
16
|
-
async saveRefreshTokenHash(): Promise<void> {}
|
|
17
|
-
|
|
18
|
-
async getRefreshTokenHash(): Promise<string | null> {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async removeRefreshTokenHash(): Promise<void> {}
|
|
23
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import type { AuthUser } from '@forgeon/auth-contracts';
|
|
2
|
-
import {
|
|
3
|
-
Body,
|
|
4
|
-
Controller,
|
|
5
|
-
Get,
|
|
6
|
-
Post,
|
|
7
|
-
Req,
|
|
8
|
-
UseGuards,
|
|
9
|
-
} from '@nestjs/common';
|
|
10
|
-
import { AuthService } from './auth.service';
|
|
11
|
-
import { LoginDto, RefreshDto } from './dto';
|
|
12
|
-
import { JwtAuthGuard } from './jwt-auth.guard';
|
|
13
|
-
import { AuthJwtPayload } from './auth.types';
|
|
14
|
-
|
|
15
|
-
type RequestWithUser = { user?: AuthJwtPayload };
|
|
16
|
-
|
|
17
|
-
@Controller('auth')
|
|
18
|
-
export class AuthController {
|
|
19
|
-
constructor(private readonly authService: AuthService) {}
|
|
20
|
-
|
|
21
|
-
@Post('login')
|
|
22
|
-
login(@Body() body: LoginDto) {
|
|
23
|
-
return this.authService.login(body);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
@Post('refresh')
|
|
27
|
-
refresh(@Body() body: RefreshDto) {
|
|
28
|
-
return this.authService.refresh(body);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
@UseGuards(JwtAuthGuard)
|
|
32
|
-
@Post('logout')
|
|
33
|
-
async logout(@Req() request: RequestWithUser) {
|
|
34
|
-
const user = this.getRequestUser(request);
|
|
35
|
-
await this.authService.logout(user);
|
|
36
|
-
return {
|
|
37
|
-
status: 'ok',
|
|
38
|
-
tokenStore: this.authService.getTokenStoreKind(),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
@UseGuards(JwtAuthGuard)
|
|
43
|
-
@Get('me')
|
|
44
|
-
me(@Req() request: RequestWithUser) {
|
|
45
|
-
const user = this.getRequestUser(request);
|
|
46
|
-
return {
|
|
47
|
-
user: this.toAuthUser(user),
|
|
48
|
-
tokenStore: this.authService.getTokenStoreKind(),
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
private getRequestUser(request: RequestWithUser): AuthJwtPayload {
|
|
53
|
-
const user = request.user;
|
|
54
|
-
if (!user || typeof user.sub !== 'string' || typeof user.email !== 'string') {
|
|
55
|
-
return {
|
|
56
|
-
sub: 'unknown',
|
|
57
|
-
email: 'unknown@invalid.local',
|
|
58
|
-
roles: ['user'],
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
return user;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
private toAuthUser(payload: AuthJwtPayload): AuthUser {
|
|
65
|
-
return {
|
|
66
|
-
sub: payload.sub,
|
|
67
|
-
email: payload.email,
|
|
68
|
-
roles: Array.isArray(payload.roles) ? payload.roles : ['user'],
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
}
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AuthErrorCode,
|
|
3
|
-
AuthUser,
|
|
4
|
-
LoginResponse,
|
|
5
|
-
RefreshResponse,
|
|
6
|
-
TokenPair,
|
|
7
|
-
} from '@forgeon/auth-contracts';
|
|
8
|
-
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
|
|
9
|
-
import { JwtService } from '@nestjs/jwt';
|
|
10
|
-
import type { JwtSignOptions } from '@nestjs/jwt';
|
|
11
|
-
import { compare, hash } from 'bcryptjs';
|
|
12
|
-
import {
|
|
13
|
-
AUTH_REFRESH_TOKEN_STORE,
|
|
14
|
-
AuthRefreshTokenStore,
|
|
15
|
-
} from './auth-refresh-token.store';
|
|
16
|
-
import { AuthConfigService } from './auth-config.service';
|
|
17
|
-
import { LoginDto, RefreshDto } from './dto';
|
|
18
|
-
import { AuthJwtPayload } from './auth.types';
|
|
19
|
-
|
|
20
|
-
type JwtExpiresIn = NonNullable<JwtSignOptions['expiresIn']>;
|
|
21
|
-
|
|
22
|
-
const AUTH_ERROR_CODES: Record<
|
|
23
|
-
'invalidCredentials' | 'tokenExpired' | 'refreshInvalid',
|
|
24
|
-
AuthErrorCode
|
|
25
|
-
> = {
|
|
26
|
-
invalidCredentials: 'AUTH_INVALID_CREDENTIALS',
|
|
27
|
-
tokenExpired: 'AUTH_TOKEN_EXPIRED',
|
|
28
|
-
refreshInvalid: 'AUTH_REFRESH_INVALID',
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
@Injectable()
|
|
32
|
-
export class AuthService {
|
|
33
|
-
constructor(
|
|
34
|
-
private readonly jwtService: JwtService,
|
|
35
|
-
private readonly configService: AuthConfigService,
|
|
36
|
-
@Inject(AUTH_REFRESH_TOKEN_STORE)
|
|
37
|
-
private readonly refreshTokenStore: AuthRefreshTokenStore,
|
|
38
|
-
) {}
|
|
39
|
-
|
|
40
|
-
getTokenStoreKind(): string {
|
|
41
|
-
return this.refreshTokenStore.kind;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async login(input: LoginDto): Promise<LoginResponse> {
|
|
45
|
-
const email = input.email.trim().toLowerCase();
|
|
46
|
-
if (email !== this.configService.demoEmail || input.password !== this.configService.demoPassword) {
|
|
47
|
-
throw new UnauthorizedException({
|
|
48
|
-
message: 'Invalid credentials',
|
|
49
|
-
details: { code: AUTH_ERROR_CODES.invalidCredentials },
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const user: AuthUser = {
|
|
54
|
-
sub: email,
|
|
55
|
-
email,
|
|
56
|
-
roles: ['user'],
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const tokens = await this.issueTokens(user);
|
|
60
|
-
return {
|
|
61
|
-
...tokens,
|
|
62
|
-
user,
|
|
63
|
-
tokenStore: this.refreshTokenStore.kind,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async refresh(input: RefreshDto): Promise<RefreshResponse> {
|
|
68
|
-
let payload: AuthJwtPayload;
|
|
69
|
-
try {
|
|
70
|
-
payload = await this.jwtService.verifyAsync<AuthJwtPayload>(input.refreshToken, {
|
|
71
|
-
secret: this.configService.refreshSecret,
|
|
72
|
-
});
|
|
73
|
-
} catch (error) {
|
|
74
|
-
const code =
|
|
75
|
-
error instanceof Error && error.name === 'TokenExpiredError'
|
|
76
|
-
? AUTH_ERROR_CODES.tokenExpired
|
|
77
|
-
: AUTH_ERROR_CODES.refreshInvalid;
|
|
78
|
-
|
|
79
|
-
throw new UnauthorizedException({
|
|
80
|
-
message: 'Refresh token is invalid or expired',
|
|
81
|
-
details: { code },
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (this.refreshTokenStore.kind !== 'none') {
|
|
86
|
-
const storedHash = await this.refreshTokenStore.getRefreshTokenHash(payload.sub);
|
|
87
|
-
if (!storedHash) {
|
|
88
|
-
throw new UnauthorizedException({
|
|
89
|
-
message: 'Refresh token is invalid or expired',
|
|
90
|
-
details: { code: AUTH_ERROR_CODES.refreshInvalid },
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const matched = await compare(input.refreshToken, storedHash);
|
|
95
|
-
if (!matched) {
|
|
96
|
-
throw new UnauthorizedException({
|
|
97
|
-
message: 'Refresh token is invalid or expired',
|
|
98
|
-
details: { code: AUTH_ERROR_CODES.refreshInvalid },
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const user = this.toAuthUser(payload);
|
|
104
|
-
const tokens = await this.issueTokens(user);
|
|
105
|
-
return {
|
|
106
|
-
...tokens,
|
|
107
|
-
user,
|
|
108
|
-
tokenStore: this.refreshTokenStore.kind,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async logout(user: AuthJwtPayload): Promise<void> {
|
|
113
|
-
if (this.refreshTokenStore.kind === 'none') {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
await this.refreshTokenStore.removeRefreshTokenHash(user.sub);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
getProbeStatus() {
|
|
120
|
-
return {
|
|
121
|
-
status: 'ok',
|
|
122
|
-
feature: 'jwt-auth',
|
|
123
|
-
tokenStore: this.refreshTokenStore.kind,
|
|
124
|
-
demoEmail: this.configService.demoEmail,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
private async issueTokens(user: AuthUser): Promise<TokenPair> {
|
|
129
|
-
const payload: AuthJwtPayload = {
|
|
130
|
-
sub: user.sub,
|
|
131
|
-
email: user.email,
|
|
132
|
-
roles: user.roles,
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const [accessToken, refreshToken] = await Promise.all([
|
|
136
|
-
this.jwtService.signAsync(payload, {
|
|
137
|
-
secret: this.configService.accessSecret,
|
|
138
|
-
expiresIn: this.toJwtExpiresIn(this.configService.accessExpiresIn),
|
|
139
|
-
}),
|
|
140
|
-
this.jwtService.signAsync(payload, {
|
|
141
|
-
secret: this.configService.refreshSecret,
|
|
142
|
-
expiresIn: this.toJwtExpiresIn(this.configService.refreshExpiresIn),
|
|
143
|
-
}),
|
|
144
|
-
]);
|
|
145
|
-
|
|
146
|
-
if (this.refreshTokenStore.kind !== 'none') {
|
|
147
|
-
const tokenHash = await hash(refreshToken, this.configService.bcryptRounds);
|
|
148
|
-
await this.refreshTokenStore.saveRefreshTokenHash(user.sub, tokenHash);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
accessToken,
|
|
153
|
-
refreshToken,
|
|
154
|
-
tokenType: 'Bearer',
|
|
155
|
-
accessTtl: this.configService.accessExpiresIn,
|
|
156
|
-
refreshTtl: this.configService.refreshExpiresIn,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
private toAuthUser(payload: AuthJwtPayload): AuthUser {
|
|
161
|
-
return {
|
|
162
|
-
sub: payload.sub,
|
|
163
|
-
email: payload.email,
|
|
164
|
-
roles: Array.isArray(payload.roles) ? payload.roles : ['user'],
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
private toJwtExpiresIn(value: string): JwtExpiresIn {
|
|
169
|
-
const trimmed = value.trim();
|
|
170
|
-
if (/^\d+$/.test(trimmed)) {
|
|
171
|
-
return Number(trimmed);
|
|
172
|
-
}
|
|
173
|
-
return trimmed as JwtExpiresIn;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DynamicModule,
|
|
3
|
-
Module,
|
|
4
|
-
ModuleMetadata,
|
|
5
|
-
Provider,
|
|
6
|
-
} from '@nestjs/common';
|
|
7
|
-
import { JwtModule } from '@nestjs/jwt';
|
|
8
|
-
import { PassportModule } from '@nestjs/passport';
|
|
9
|
-
import {
|
|
10
|
-
AUTH_REFRESH_TOKEN_STORE,
|
|
11
|
-
NoopAuthRefreshTokenStore,
|
|
12
|
-
} from './auth-refresh-token.store';
|
|
13
|
-
import { AuthConfigModule } from './auth-config.module';
|
|
14
|
-
import { AuthController } from './auth.controller';
|
|
15
|
-
import { AuthService } from './auth.service';
|
|
16
|
-
import { JwtAuthGuard } from './jwt-auth.guard';
|
|
17
|
-
import { JwtStrategy } from './jwt.strategy';
|
|
18
|
-
|
|
19
|
-
export interface ForgeonAuthModuleOptions {
|
|
20
|
-
imports?: ModuleMetadata['imports'];
|
|
21
|
-
refreshTokenStoreProvider?: Provider;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
@Module({})
|
|
25
|
-
export class ForgeonAuthModule {
|
|
26
|
-
static register(options: ForgeonAuthModuleOptions = {}): DynamicModule {
|
|
27
|
-
const refreshTokenStoreProvider =
|
|
28
|
-
options.refreshTokenStoreProvider ??
|
|
29
|
-
({
|
|
30
|
-
provide: AUTH_REFRESH_TOKEN_STORE,
|
|
31
|
-
useClass: NoopAuthRefreshTokenStore,
|
|
32
|
-
} satisfies Provider);
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
module: ForgeonAuthModule,
|
|
36
|
-
imports: [
|
|
37
|
-
AuthConfigModule,
|
|
38
|
-
PassportModule.register({ defaultStrategy: 'jwt' }),
|
|
39
|
-
JwtModule.register({}),
|
|
40
|
-
...(options.imports ?? []),
|
|
41
|
-
],
|
|
42
|
-
controllers: [AuthController],
|
|
43
|
-
providers: [AuthService, JwtStrategy, JwtAuthGuard, refreshTokenStoreProvider],
|
|
44
|
-
exports: [AuthConfigModule, AuthService, JwtAuthGuard, AUTH_REFRESH_TOKEN_STORE],
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
}
|