@lenne.tech/nest-server 11.21.3 → 11.22.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/.claude/rules/architecture.md +79 -0
- package/.claude/rules/better-auth.md +262 -0
- package/.claude/rules/configurable-features.md +308 -0
- package/.claude/rules/core-modules.md +205 -0
- package/.claude/rules/framework-compatibility.md +79 -0
- package/.claude/rules/migration-guides.md +149 -0
- package/.claude/rules/module-deprecation.md +214 -0
- package/.claude/rules/module-inheritance.md +97 -0
- package/.claude/rules/package-management.md +112 -0
- package/.claude/rules/role-system.md +146 -0
- package/.claude/rules/testing.md +120 -0
- package/.claude/rules/versioning.md +53 -0
- package/CLAUDE.md +174 -0
- package/FRAMEWORK-API.md +231 -0
- package/dist/core/common/interfaces/server-options.interface.d.ts +10 -0
- package/dist/core/modules/error-code/error-code.module.js.map +1 -1
- package/dist/core.module.d.ts +3 -3
- package/dist/core.module.js +17 -4
- package/dist/core.module.js.map +1 -1
- package/dist/server/modules/file/file-info.model.d.ts +1 -5
- package/dist/server/modules/user/user.model.d.ts +1 -5
- package/dist/server/server.module.js +6 -6
- package/dist/server/server.module.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +1256 -0
- package/docs/error-codes.md +446 -0
- package/migration-guides/11.10.x-to-11.11.x.md +266 -0
- package/migration-guides/11.11.x-to-11.12.x.md +323 -0
- package/migration-guides/11.12.x-to-11.13.0.md +612 -0
- package/migration-guides/11.13.x-to-11.14.0.md +348 -0
- package/migration-guides/11.14.x-to-11.15.0.md +262 -0
- package/migration-guides/11.15.0-to-11.15.3.md +118 -0
- package/migration-guides/11.15.x-to-11.16.0.md +497 -0
- package/migration-guides/11.16.x-to-11.17.0.md +130 -0
- package/migration-guides/11.17.x-to-11.18.0.md +393 -0
- package/migration-guides/11.18.x-to-11.19.0.md +151 -0
- package/migration-guides/11.19.x-to-11.20.0.md +170 -0
- package/migration-guides/11.20.x-to-11.21.0.md +216 -0
- package/migration-guides/11.21.0-to-11.21.1.md +194 -0
- package/migration-guides/11.21.1-to-11.21.2.md +114 -0
- package/migration-guides/11.21.2-to-11.21.3.md +175 -0
- package/migration-guides/11.21.x-to-11.22.0.md +224 -0
- package/migration-guides/11.22.0-to-11.22.1.md +105 -0
- package/migration-guides/11.3.x-to-11.4.x.md +233 -0
- package/migration-guides/11.6.x-to-11.7.x.md +394 -0
- package/migration-guides/11.7.x-to-11.8.x.md +318 -0
- package/migration-guides/11.8.x-to-11.9.x.md +322 -0
- package/migration-guides/11.9.x-to-11.10.x.md +571 -0
- package/migration-guides/TEMPLATE.md +113 -0
- package/package.json +25 -18
- package/src/core/common/interfaces/server-options.interface.ts +83 -16
- package/src/core/modules/better-auth/CUSTOMIZATION.md +24 -17
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +5 -5
- package/src/core/modules/error-code/INTEGRATION-CHECKLIST.md +42 -12
- package/src/core/modules/error-code/error-code.module.ts +4 -9
- package/src/core.module.ts +52 -10
- package/src/server/server.module.ts +7 -9
|
@@ -0,0 +1,1256 @@
|
|
|
1
|
+
# Request Lifecycle & Security Architecture
|
|
2
|
+
|
|
3
|
+
This document explains the complete lifecycle of a request through `@lenne.tech/nest-server`, covering both REST and GraphQL flows, all security mechanisms, and the interaction between CrudService and the Safety Net.
|
|
4
|
+
|
|
5
|
+
> **Audience:** Developers and AI agents building on nest-server who want to understand what features are available, how data flows, where security is enforced, and how to use or extend the framework correctly.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Features Overview](#features-overview)
|
|
12
|
+
- [Architecture Overview](#architecture-overview)
|
|
13
|
+
- [Request Flow Diagram](#request-flow-diagram)
|
|
14
|
+
- [Phase 1: Incoming Request](#phase-1-incoming-request)
|
|
15
|
+
- [Phase 2: Authorization & Validation](#phase-2-authorization--validation)
|
|
16
|
+
- [Phase 3: Handler Execution](#phase-3-handler-execution)
|
|
17
|
+
- [Phase 4: Response Processing](#phase-4-response-processing)
|
|
18
|
+
- [CrudService.process() Pipeline](#crudserviceprocess-pipeline)
|
|
19
|
+
- [Safety Net Architecture](#safety-net-architecture)
|
|
20
|
+
- [Decorators Reference](#decorators-reference)
|
|
21
|
+
- [Model System](#model-system)
|
|
22
|
+
- [REST vs GraphQL Differences](#rest-vs-graphql-differences)
|
|
23
|
+
- [Configuration Reference](#configuration-reference)
|
|
24
|
+
- [NestJS Documentation Links](#nestjs-documentation-links)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Features Overview
|
|
29
|
+
|
|
30
|
+
`@lenne.tech/nest-server` extends NestJS with a complete application framework for GraphQL + REST APIs with MongoDB. The following sections list **all features** available out of the box.
|
|
31
|
+
|
|
32
|
+
### Core Module
|
|
33
|
+
|
|
34
|
+
The `CoreModule` is a dynamic module that bootstraps the entire framework:
|
|
35
|
+
|
|
36
|
+
| Feature | Description |
|
|
37
|
+
|---------|-------------|
|
|
38
|
+
| **GraphQL Integration** | Apollo Server with auto-schema generation (disable via `graphQl: false`) |
|
|
39
|
+
| **MongoDB Integration** | Mongoose ODM with automatic connection management |
|
|
40
|
+
| **Dual API Support** | GraphQL and REST in the same application |
|
|
41
|
+
| **Security Pipeline** | 4 global interceptors, global validation pipe, middleware stack |
|
|
42
|
+
| **Mongoose Plugins** | Auto-registration of ID, password, audit, and role guard plugins |
|
|
43
|
+
| **GraphQL Subscriptions** | WebSocket support with JWT/session authentication |
|
|
44
|
+
| **Configuration System** | `config.env.ts` with ENV variables, `NEST_SERVER_CONFIG` JSON, `NSC__*` prefixes |
|
|
45
|
+
| **Dual Auth Modes** | IAM-Only (BetterAuth) or Legacy+IAM for migration periods |
|
|
46
|
+
|
|
47
|
+
### Authentication & Authorization
|
|
48
|
+
|
|
49
|
+
#### BetterAuth Module (recommended)
|
|
50
|
+
|
|
51
|
+
Modern OAuth-compatible authentication with plugin architecture:
|
|
52
|
+
|
|
53
|
+
| Feature | Description |
|
|
54
|
+
|---------|-------------|
|
|
55
|
+
| **Session Management** | Secure session-based auth with automatic token rotation |
|
|
56
|
+
| **JWT Tokens** | Stateless API authentication (plugin) |
|
|
57
|
+
| **2FA / TOTP** | Two-factor authentication (plugin) |
|
|
58
|
+
| **Passkey / WebAuthn** | Passwordless authentication (plugin) |
|
|
59
|
+
| **Social Login** | OAuth providers: Google, GitHub, Apple, Discord, etc. (plugin) |
|
|
60
|
+
| **Email Verification** | Configurable email verification flow |
|
|
61
|
+
| **Sign-Up Validation** | Custom validation hooks for registration |
|
|
62
|
+
| **Rate Limiting** | Per-endpoint rate limits (configurable) |
|
|
63
|
+
| **Cross-Subdomain Cookies** | Automatic cookie domain configuration |
|
|
64
|
+
| **Organization / Multi-Tenant** | Teams and organization management (plugin) |
|
|
65
|
+
| **3 Registration Patterns** | Zero-config, overrides parameter, or manual (`autoRegister: false`) |
|
|
66
|
+
|
|
67
|
+
#### Legacy Auth Module (backward compatible)
|
|
68
|
+
|
|
69
|
+
JWT-based authentication for existing projects:
|
|
70
|
+
|
|
71
|
+
| Feature | Description |
|
|
72
|
+
|---------|-------------|
|
|
73
|
+
| **JWT Authentication** | Bearer token auth with Passport strategies |
|
|
74
|
+
| **Refresh Tokens** | Automatic token renewal |
|
|
75
|
+
| **Sign In / Sign Up / Logout** | GraphQL mutations + REST endpoints |
|
|
76
|
+
| **Rate Limiting** | Configurable per-endpoint rate limits |
|
|
77
|
+
| **Legacy Endpoint Controls** | Disable legacy endpoints after migration (`auth.legacyEndpoints`) |
|
|
78
|
+
| **Migration Tracking** | `betterAuthMigrationStatus` query for monitoring |
|
|
79
|
+
|
|
80
|
+
#### Role System
|
|
81
|
+
|
|
82
|
+
| Feature | Description |
|
|
83
|
+
|---------|-------------|
|
|
84
|
+
| **Real Roles** | `ADMIN` (stored in `user.roles`) |
|
|
85
|
+
| **System Roles** | `S_USER`, `S_VERIFIED`, `S_CREATOR`, `S_SELF`, `S_EVERYONE`, `S_NO_ONE` (runtime-only, never stored) |
|
|
86
|
+
| **Hierarchy Roles** | Configurable via `multiTenancy.roleHierarchy` (default: `member`, `manager`, `owner`). Level comparison: higher includes lower. Use `DefaultHR` or `createHierarchyRoles()`. |
|
|
87
|
+
| **Method-Level Auth** | `@Roles()` decorator on resolvers/controllers |
|
|
88
|
+
| **Field-Level Auth** | `@Restricted()` decorator on model properties |
|
|
89
|
+
| **Membership Checks** | `@Restricted({ memberOf: 'teamMembers' })` |
|
|
90
|
+
| **Input/Output Restriction** | `@Restricted({ processType: ProcessType.INPUT })` |
|
|
91
|
+
|
|
92
|
+
### Security Features
|
|
93
|
+
|
|
94
|
+
| Feature | Description |
|
|
95
|
+
|---------|-------------|
|
|
96
|
+
| **Input Whitelisting** | `MapAndValidatePipe` strips/rejects unknown properties |
|
|
97
|
+
| **Input Validation** | `class-validator` integration via `@UnifiedField()` |
|
|
98
|
+
| **Password Hashing Plugin** | Automatic BCrypt hashing on all Mongoose write operations |
|
|
99
|
+
| **Role Guard Plugin** | Prevents unauthorized role escalation at database level |
|
|
100
|
+
| **Audit Fields Plugin** | Automatic `createdBy`/`updatedBy` tracking |
|
|
101
|
+
| **Response Model Interceptor** | Auto-converts plain objects to CoreModel instances |
|
|
102
|
+
| **Security Check Interceptor** | Calls `securityCheck()` + removes secret fields |
|
|
103
|
+
| **Response Filter Interceptor** | Enforces `@Restricted()` field-level access |
|
|
104
|
+
| **Translation Interceptor** | Applies `_translations` based on `Accept-Language` |
|
|
105
|
+
| **Secret Fields Removal** | Configurable fallback removal of password, tokens, etc. |
|
|
106
|
+
| **RequestContext** | `AsyncLocalStorage`-based context for current user in Mongoose hooks |
|
|
107
|
+
| **Query Complexity** | GraphQL query complexity analysis to prevent DoS |
|
|
108
|
+
| **Tenant Isolation** | Header-based multi-tenant isolation with membership validation (opt-in) |
|
|
109
|
+
| **Tenant Guard** | `CoreTenantGuard` validates tenant membership; system roles (`S_EVERYONE`, `S_USER`, `S_VERIFIED`) are checked as OR alternatives before real roles; hierarchy roles (`@Roles(DefaultHR.MEMBER)`), `@SkipTenantCheck()`, BetterAuth auto-skip (`betterAuth.skipTenantCheck`) |
|
|
110
|
+
| **Tenant Plugin Safety Net** | Mongoose tenant plugin throws `ForbiddenException` when tenant-schema is accessed without valid tenant context |
|
|
111
|
+
|
|
112
|
+
### Data & CRUD
|
|
113
|
+
|
|
114
|
+
| Feature | Description |
|
|
115
|
+
|---------|-------------|
|
|
116
|
+
| **CrudService** | Abstract CRUD with `process()` pipeline (input/output security) |
|
|
117
|
+
| **Filtering** | `FilterArgs` with comparison operators (`eq`, `ne`, `gt`, `in`, `contains`, etc.) |
|
|
118
|
+
| **Pagination** | `PaginationArgs` with `limit`/`offset`, returns `PaginationInfo` |
|
|
119
|
+
| **Sorting** | `SortInput` with `ASC`/`DESC` |
|
|
120
|
+
| **Population** | `@GraphQLPopulate()` for automatic relation loading |
|
|
121
|
+
| **Field Selection** | GraphQL field selection drives Mongoose population |
|
|
122
|
+
| **Aggregation** | Pipeline support via CrudService |
|
|
123
|
+
| **Bulk Operations** | Batch create/update/delete |
|
|
124
|
+
| **Force Mode** | `force: true` bypasses all security checks |
|
|
125
|
+
| **Raw Mode** | `raw: true` skips prepareInput/prepareOutput |
|
|
126
|
+
|
|
127
|
+
### Models & Inputs
|
|
128
|
+
|
|
129
|
+
| Feature | Description |
|
|
130
|
+
|---------|-------------|
|
|
131
|
+
| **CoreModel** | Base class with `map()`, `securityCheck()`, `hasRole()` |
|
|
132
|
+
| **CorePersistenceModel** | Adds `id`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` |
|
|
133
|
+
| **CoreInput** | Base input type for validation |
|
|
134
|
+
| **@UnifiedField()** | Combines `@Field()`, `@ApiProperty()`, `@IsOptional()` in one decorator |
|
|
135
|
+
| **Nested Validation** | Recursive object/array validation via `nestedTypeRegistry` |
|
|
136
|
+
| **Exclude/Include** | `@UnifiedField({ exclude: true/false })` for inheritance control |
|
|
137
|
+
|
|
138
|
+
### Custom Decorators
|
|
139
|
+
|
|
140
|
+
| Decorator | Purpose |
|
|
141
|
+
|-----------|---------|
|
|
142
|
+
| `@Roles(...roles)` | Method-level authorization (includes JWT auth) |
|
|
143
|
+
| `@Restricted(...roles)` | Field-level access control |
|
|
144
|
+
| `@CurrentUser()` | Inject authenticated user (REST + GraphQL) |
|
|
145
|
+
| `@UnifiedField(options)` | Combined schema, validation, and API metadata |
|
|
146
|
+
| `@GraphQLPopulate(config)` | Mongoose populate configuration |
|
|
147
|
+
| `@GraphQLServiceOptions()` | Service options injection (GraphQL) |
|
|
148
|
+
| `@RestServiceOptions()` | Service options injection (REST) |
|
|
149
|
+
| `@ResponseModel(Model)` | REST response type hint for auto-conversion |
|
|
150
|
+
| `@Translatable()` | Multi-language field metadata |
|
|
151
|
+
| `@CommonError(code)` | Error code registration |
|
|
152
|
+
| `@SkipTenantCheck()` | Opt out of CoreTenantGuard validation on a method |
|
|
153
|
+
|
|
154
|
+
### File Handling
|
|
155
|
+
|
|
156
|
+
| Feature | Description |
|
|
157
|
+
|---------|-------------|
|
|
158
|
+
| **File Module** | Upload/download with MongoDB GridFS storage |
|
|
159
|
+
| **REST Endpoints** | `GET /files/:id`, `POST /files/upload`, `DELETE /files/:id` |
|
|
160
|
+
| **GraphQL Endpoints** | `uploadFile`, `file`, `fileByFilename`, `deleteFile` |
|
|
161
|
+
| **TUS Module** | Resumable uploads via tus.io protocol (creation, termination, expiration) |
|
|
162
|
+
| **GridFS Migration** | Completed TUS uploads auto-migrate to GridFS |
|
|
163
|
+
| **CORS Support** | Automatic CORS headers for browser uploads |
|
|
164
|
+
|
|
165
|
+
### Email & Templates
|
|
166
|
+
|
|
167
|
+
| Feature | Description |
|
|
168
|
+
|---------|-------------|
|
|
169
|
+
| **EmailService** | Multi-provider email sending |
|
|
170
|
+
| **Mailjet / Brevo** | API-based email providers |
|
|
171
|
+
| **SMTP** | Standard SMTP email sending |
|
|
172
|
+
| **TemplateService** | EJS template rendering for emails |
|
|
173
|
+
| **Template Inheritance** | Project templates override nest-server fallbacks |
|
|
174
|
+
| **Multi-Language** | Locale-aware template resolution (`template-de.ejs` → `template.ejs`) |
|
|
175
|
+
|
|
176
|
+
### Database & Migration
|
|
177
|
+
|
|
178
|
+
| Feature | Description |
|
|
179
|
+
|---------|-------------|
|
|
180
|
+
| **Mongoose Plugins** | ID handling, password hashing, audit fields, role guard |
|
|
181
|
+
| **Migration Module** | MongoDB migration state management with cluster locking |
|
|
182
|
+
| **Synchronized Migrations** | `synchronizedMigration()` with distributed locks |
|
|
183
|
+
| **Migration CLI** | TypeScript-based migration scripts with `getDb()` helper |
|
|
184
|
+
| **GridFS Helper** | Direct GridFS file access and migration utilities |
|
|
185
|
+
|
|
186
|
+
### GraphQL Features
|
|
187
|
+
|
|
188
|
+
| Feature | Description |
|
|
189
|
+
|---------|-------------|
|
|
190
|
+
| **Apollo Server** | Full GraphQL server with schema-first or code-first |
|
|
191
|
+
| **Custom Scalars** | `Date`, `DateTime` (timestamp), `JSON`, `Any` |
|
|
192
|
+
| **Subscriptions** | WebSocket support via `graphql-ws` with auth |
|
|
193
|
+
| **Complexity Analysis** | Query cost calculation to prevent DoS attacks |
|
|
194
|
+
| **Enum Registration** | `registerEnum()` helper for GraphQL enum types |
|
|
195
|
+
| **Upload Support** | `graphqlUploadExpress()` for multipart file uploads |
|
|
196
|
+
|
|
197
|
+
### Development & Operations
|
|
198
|
+
|
|
199
|
+
| Feature | Description |
|
|
200
|
+
|---------|-------------|
|
|
201
|
+
| **Health Check Module** | `GET /health` + GraphQL `healthCheck` query |
|
|
202
|
+
| **Error Code Module** | Centralized error registry with unique IDs |
|
|
203
|
+
| **Permissions Report** | Interactive HTML dashboard, JSON, and Markdown reports |
|
|
204
|
+
| **System Setup Module** | Initial admin creation for fresh deployments |
|
|
205
|
+
| **Cron Jobs** | `CoreCronJobsService` with timezone/UTC offset support |
|
|
206
|
+
| **Model Documentation** | Auto-generated model docs via `ModelDocService` |
|
|
207
|
+
| **SCIM Support** | SCIM filtering and query parsing utilities |
|
|
208
|
+
|
|
209
|
+
### Testing Utilities
|
|
210
|
+
|
|
211
|
+
| Feature | Description |
|
|
212
|
+
|---------|-------------|
|
|
213
|
+
| **TestHelper** | API testing helper for GraphQL and REST |
|
|
214
|
+
| **Cookie Support** | Session and JWT token testing |
|
|
215
|
+
| **Dynamic Ports** | `httpServer.listen(0)` for parallel test execution |
|
|
216
|
+
| **Database Cleanup** | Test data management in `afterAll` hooks |
|
|
217
|
+
|
|
218
|
+
### Configuration Patterns
|
|
219
|
+
|
|
220
|
+
| Pattern | Use Case | Example |
|
|
221
|
+
|---------|----------|---------|
|
|
222
|
+
| **Presence Implies Enabled** | Object config = enabled | `rateLimit: {}` enables with defaults |
|
|
223
|
+
| **Boolean Shorthand** | Simple toggle | `jwt: true` or `jwt: { expiresIn: '1h' }` |
|
|
224
|
+
| **Explicit Disable** | Pre-configured but off | `{ enabled: false, max: 10 }` |
|
|
225
|
+
| **Backward Compatible** | Undefined = disabled | No config = feature off |
|
|
226
|
+
|
|
227
|
+
### Key TypeScript Utilities
|
|
228
|
+
|
|
229
|
+
| Type | Purpose |
|
|
230
|
+
|------|---------|
|
|
231
|
+
| `IServerOptions` | Complete framework configuration interface |
|
|
232
|
+
| `IServiceOptions` | Service method options (`force`, `raw`, `currentUser`) |
|
|
233
|
+
| `PlainObject` / `PlainInput` | Type-safe plain object types |
|
|
234
|
+
| `ID` / `IDs` | MongoDB ObjectId or string types |
|
|
235
|
+
| `MaybePromise<T>` | Sync or async return type |
|
|
236
|
+
| `RequireOnlyOne<T>` | Require exactly one property |
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Architecture Overview
|
|
241
|
+
|
|
242
|
+
nest-server implements **defense-in-depth security** with three complementary layers:
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
+===================================================================+
|
|
246
|
+
| HTTP Request |
|
|
247
|
+
+===================================================================+
|
|
248
|
+
| |
|
|
249
|
+
| Layer 1: Guardian Gates (Middleware -> Guards -> Pipes) |
|
|
250
|
+
| +------------------+ +-----------+ +--------+ +------------+ |
|
|
251
|
+
| | RequestContext | | Roles | | Tenant | | MapAndValid| |
|
|
252
|
+
| | BetterAuth |->| Guard |->| Guard |->| atePipe | |
|
|
253
|
+
| | Middleware | | | | | | | |
|
|
254
|
+
| +------------------+ +-----------+ +--------+ +------------+ |
|
|
255
|
+
| |
|
|
256
|
+
| Layer 2: Application Logic (Controllers/Resolvers -> Services) |
|
|
257
|
+
| +----------------+ +---------------------------------------+ |
|
|
258
|
+
| | Controller / | | CrudService.process() | |
|
|
259
|
+
| | Resolver |->| prepareInput -> serviceFunc -> | |
|
|
260
|
+
| | | | processFieldSelection -> prepareOutput | |
|
|
261
|
+
| +----------------+ +---------------------------------------+ |
|
|
262
|
+
| |
|
|
263
|
+
| Layer 3: Safety Net (Mongoose Plugins + Response Interceptors) |
|
|
264
|
+
| +--------------------------+ +------------------------------+ |
|
|
265
|
+
| | Mongoose Plugins | | Response Interceptors | |
|
|
266
|
+
| | - Password Hashing | | - ResponseModelInterceptor | |
|
|
267
|
+
| | - Role Guard | | - TranslateResponse | |
|
|
268
|
+
| | - Audit Fields | | - CheckSecurity (secrets) | |
|
|
269
|
+
| | - Tenant Isolation | | - CheckResponse (@Restrict) | |
|
|
270
|
+
| | (Safety Net: 403) | | | |
|
|
271
|
+
| +--------------------------+ +------------------------------+ |
|
|
272
|
+
| |
|
|
273
|
+
+===================================================================+
|
|
274
|
+
| HTTP Response |
|
|
275
|
+
+===================================================================+
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Key principle:** Layer 2 (CrudService) provides the primary security pipeline. Layer 3 (Safety Net) catches anything that bypasses Layer 2, ensuring security even when developers use direct Mongoose queries.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Request Flow Diagram
|
|
283
|
+
|
|
284
|
+
The following diagram shows the exact order of execution from HTTP request to response:
|
|
285
|
+
|
|
286
|
+
```
|
|
287
|
+
+---------------------+
|
|
288
|
+
| HTTP Request |
|
|
289
|
+
| (REST or GQL) |
|
|
290
|
+
+----------+----------+
|
|
291
|
+
|
|
|
292
|
+
+----------------------------v----------------------------+
|
|
293
|
+
| MIDDLEWARE CHAIN |
|
|
294
|
+
| |
|
|
295
|
+
| 1. RequestContextMiddleware |
|
|
296
|
+
| - AsyncLocalStorage context |
|
|
297
|
+
| - Lazy currentUser getter (from req.user) |
|
|
298
|
+
| - Accept-Language for translations |
|
|
299
|
+
| |
|
|
300
|
+
| 2. CoreBetterAuthMiddleware |
|
|
301
|
+
| - Strategy 1: Auth header (JWT/Session) |
|
|
302
|
+
| - Strategy 2: JWT cookie |
|
|
303
|
+
| - Strategy 3: Session cookie |
|
|
304
|
+
| - Sets req.user |
|
|
305
|
+
| |
|
|
306
|
+
| 3. graphqlUploadExpress() [GraphQL only] |
|
|
307
|
+
| - Handles multipart file uploads |
|
|
308
|
+
+----------------------------+----------------------------+
|
|
309
|
+
|
|
|
310
|
+
+----------------------------v----------------------------+
|
|
311
|
+
| GUARDS |
|
|
312
|
+
| |
|
|
313
|
+
| 4. RolesGuard / BetterAuthRolesGuard |
|
|
314
|
+
| - Reads @Roles() metadata |
|
|
315
|
+
| - Validates JWT / session token |
|
|
316
|
+
| - Checks real roles (ADMIN) |
|
|
317
|
+
| - Evaluates system roles (S_USER, ...) |
|
|
318
|
+
| - Throws 401 (Unauthorized) or 403 (Forbidden) |
|
|
319
|
+
| |
|
|
320
|
+
| 4b. CoreTenantGuard [if multiTenancy enabled] |
|
|
321
|
+
| - Reads X-Tenant-Id header |
|
|
322
|
+
| - Validates membership via hierarchy roles (level comparison) |
|
|
323
|
+
| - Non-admin + header + no membership = always 403 |
|
|
324
|
+
| - Checks configurable roleHierarchy levels |
|
|
325
|
+
| - Admin bypass: sets isAdminBypass (sees all data) |
|
|
326
|
+
| - Sets tenantId in RequestContext |
|
|
327
|
+
| - @SkipTenantCheck() opts out of tenant validation |
|
|
328
|
+
| - BetterAuth auto-skip: IAM handlers skip tenant |
|
|
329
|
+
| validation when no X-Tenant-Id header is present |
|
|
330
|
+
| (betterAuth.skipTenantCheck, default: true) |
|
|
331
|
+
| - Throws 403 (Forbidden) on failure |
|
|
332
|
+
+----------------------------+----------------------------+
|
|
333
|
+
|
|
|
334
|
+
+----------------------------v----------------------------+
|
|
335
|
+
| PIPES |
|
|
336
|
+
| |
|
|
337
|
+
| 5. MapAndValidatePipe |
|
|
338
|
+
| - Transform plain object -> class instance |
|
|
339
|
+
| - Whitelist: strip/reject unknown fields |
|
|
340
|
+
| - Validate via class-validator decorators |
|
|
341
|
+
| - Inheritance-aware (child overrides) |
|
|
342
|
+
+----------------------------+----------------------------+
|
|
343
|
+
|
|
|
344
|
+
+----------------------------v----------------------------+
|
|
345
|
+
| HANDLER EXECUTION |
|
|
346
|
+
| |
|
|
347
|
+
| 6. Controller method / Resolver method |
|
|
348
|
+
| - @CurrentUser() injects authenticated user |
|
|
349
|
+
| - Calls service methods |
|
|
350
|
+
| - Service uses CrudService.process() |
|
|
351
|
+
| OR direct Mongoose queries |
|
|
352
|
+
| |
|
|
353
|
+
| +-----------------------------------------------+ |
|
|
354
|
+
| | Mongoose Plugins (fire on DB operations) | |
|
|
355
|
+
| | - mongoosePasswordPlugin (hash password) | |
|
|
356
|
+
| | - mongooseRoleGuardPlugin (block roles) | |
|
|
357
|
+
| | - mongooseAuditFieldsPlugin (set by/at) | |
|
|
358
|
+
| | - mongooseTenantPlugin (tenant isolation) | |
|
|
359
|
+
| +-----------------------------------------------+ |
|
|
360
|
+
+----------------------------+----------------------------+
|
|
361
|
+
|
|
|
362
|
+
| <-- Response data flows back
|
|
363
|
+
|
|
|
364
|
+
+----------------------------v----------------------------+
|
|
365
|
+
| RESPONSE INTERCEPTORS |
|
|
366
|
+
| (NestJS runs in REVERSE registration order) |
|
|
367
|
+
| |
|
|
368
|
+
| 7. ResponseModelInterceptor [runs 1st] |
|
|
369
|
+
| - Plain object -> CoreModel instance |
|
|
370
|
+
| - Enables securityCheck() on output |
|
|
371
|
+
| - Resolves model via @Query/@Mutation type, |
|
|
372
|
+
| @ResponseModel(), or @ApiOkResponse() |
|
|
373
|
+
| |
|
|
374
|
+
| 8. TranslateResponseInterceptor [runs 2nd] |
|
|
375
|
+
| - Applies _translations for Accept-Language |
|
|
376
|
+
| - Skips when no _translations present |
|
|
377
|
+
| |
|
|
378
|
+
| 9. CheckSecurityInterceptor [runs 3rd] |
|
|
379
|
+
| - Calls securityCheck(user) on models |
|
|
380
|
+
| - Fallback: removes secret fields |
|
|
381
|
+
| (password, tokens, etc.) |
|
|
382
|
+
| |
|
|
383
|
+
| 10. CheckResponseInterceptor [runs 4th] |
|
|
384
|
+
| - Filters @Restricted() fields |
|
|
385
|
+
| - Role-based: removes fields user can't see |
|
|
386
|
+
| - Membership-based: checks memberOf |
|
|
387
|
+
+----------------------------+----------------------------+
|
|
388
|
+
|
|
|
389
|
+
+----------v----------+
|
|
390
|
+
| HTTP Response |
|
|
391
|
+
| (filtered & |
|
|
392
|
+
| secured) |
|
|
393
|
+
+---------------------+
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Phase 1: Incoming Request
|
|
399
|
+
|
|
400
|
+
### Middleware Chain
|
|
401
|
+
|
|
402
|
+
Middleware runs for **every request** before any NestJS component. Registration happens in `CoreModule.configure()`:
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
// src/core.module.ts
|
|
406
|
+
configure(consumer: MiddlewareConsumer) {
|
|
407
|
+
consumer.apply(RequestContextMiddleware).forRoutes('*');
|
|
408
|
+
consumer.apply(graphqlUploadExpress()).forRoutes('graphql');
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
#### 1. RequestContextMiddleware
|
|
413
|
+
|
|
414
|
+
Wraps the entire request in an `AsyncLocalStorage` context, making the current user and language available anywhere — including Mongoose hooks — without dependency injection.
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
// src/core/common/middleware/request-context.middleware.ts
|
|
418
|
+
use(req: Request, _res: Response, next: NextFunction) {
|
|
419
|
+
const context: IRequestContext = {
|
|
420
|
+
get currentUser() {
|
|
421
|
+
return (req as any).user || undefined; // Lazy: evaluated when accessed
|
|
422
|
+
},
|
|
423
|
+
get language() {
|
|
424
|
+
return req.headers?.['accept-language'] || undefined;
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
RequestContext.run(context, () => next());
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
**Key design:** The `currentUser` getter is **lazy**. At middleware time, `req.user` is not yet set (auth middleware hasn't run). By using a getter, the value is resolved at access time, after authentication.
|
|
432
|
+
|
|
433
|
+
#### 2. CoreBetterAuthMiddleware
|
|
434
|
+
|
|
435
|
+
Authenticates the request using three strategies in priority order:
|
|
436
|
+
|
|
437
|
+
| Priority | Strategy | Source | Token Type |
|
|
438
|
+
|----------|----------|--------|------------|
|
|
439
|
+
| 1 | Authorization header | `Bearer <token>` | JWT or Session token |
|
|
440
|
+
| 2 | JWT cookie | `better-auth.jwt_token` | JWT token |
|
|
441
|
+
| 3 | Session cookie | `better-auth.session_token` | Session token |
|
|
442
|
+
|
|
443
|
+
If authentication succeeds, `req.user` is set with the authenticated user (including `hasRole()` method).
|
|
444
|
+
|
|
445
|
+
#### 3. graphqlUploadExpress
|
|
446
|
+
|
|
447
|
+
Only for GraphQL routes. Handles multipart file upload requests according to the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec).
|
|
448
|
+
|
|
449
|
+
> **NestJS docs:** [Middleware](https://docs.nestjs.com/middleware)
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## Phase 2: Authorization & Validation
|
|
454
|
+
|
|
455
|
+
### Guards — @Roles() Enforcement
|
|
456
|
+
|
|
457
|
+
Guards run after middleware but before the handler. The `@Roles()` decorator specifies who can access a method:
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
@Query(() => User)
|
|
461
|
+
@Roles(RoleEnum.ADMIN) // Only admins
|
|
462
|
+
async getUser(@Args('id') id: string): Promise<User> { ... }
|
|
463
|
+
|
|
464
|
+
@Mutation(() => User)
|
|
465
|
+
@Roles(RoleEnum.S_USER) // Any authenticated user
|
|
466
|
+
async updateUser(...): Promise<User> { ... }
|
|
467
|
+
|
|
468
|
+
@Query(() => [User])
|
|
469
|
+
@Roles(RoleEnum.S_EVERYONE) // Public access (no auth required)
|
|
470
|
+
async getPublicUsers(): Promise<User[]> { ... }
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
**Important:** `@Roles()` already handles JWT authentication internally. Do NOT add `@UseGuards(AuthGuard(JWT))` — it is redundant.
|
|
474
|
+
|
|
475
|
+
#### System Roles (S_ prefix)
|
|
476
|
+
|
|
477
|
+
System roles are evaluated at runtime and must **never** be stored in `user.roles`.
|
|
478
|
+
|
|
479
|
+
**OR semantics in CoreTenantGuard:** When multiTenancy is active, system roles are checked as OR alternatives in priority order (`S_EVERYONE` → `S_USER` → `S_VERIFIED`) before real roles. If ANY system role in `@Roles()` is satisfied, access is granted immediately — real roles in the same `@Roles()` are treated as alternatives, not additional requirements.
|
|
480
|
+
|
|
481
|
+
Example: `@Roles(RoleEnum.S_USER, DefaultHR.OWNER)` — any authenticated user passes (owner is an alternative).
|
|
482
|
+
|
|
483
|
+
When `X-Tenant-Id` header is present and a system role grants access, membership is still validated to set tenant context (`tenantId`, `tenantRole`). A non-member gets 403 even with `S_USER` or `S_VERIFIED` satisfied.
|
|
484
|
+
|
|
485
|
+
| System Role | Check Logic | Use Case |
|
|
486
|
+
|-------------|-------------|----------|
|
|
487
|
+
| `S_EVERYONE` | Always true | Public endpoints |
|
|
488
|
+
| `S_NO_ONE` | Always false | Permanently locked |
|
|
489
|
+
| `S_USER` | `currentUser` exists | Any authenticated user |
|
|
490
|
+
| `S_VERIFIED` | `user.verified \|\| user.verifiedAt \|\| user.emailVerified` | Email-verified users |
|
|
491
|
+
| `S_CREATOR` | `object.createdBy === user.id` | Creator of the resource (object-level, checked by interceptor) |
|
|
492
|
+
| `S_SELF` | `object.id === user.id` | User accessing own data (object-level, checked by interceptor) |
|
|
493
|
+
| `DefaultHR.MEMBER` (`'member'`) | Active membership in current tenant (level >= 1) | Tenant member access |
|
|
494
|
+
| `DefaultHR.MANAGER` (`'manager'`) | At least manager-level role (level >= 2) | Tenant manager access |
|
|
495
|
+
| `DefaultHR.OWNER` (`'owner'`) | Highest role level (level >= 3) | Tenant owner access |
|
|
496
|
+
| Custom hierarchy roles | Configurable via `createHierarchyRoles()` | Level comparison |
|
|
497
|
+
| Normal (non-hierarchy) roles | Exact match against membership.role or user.roles | No level compensation |
|
|
498
|
+
|
|
499
|
+
> **NestJS docs:** [Guards](https://docs.nestjs.com/guards), [Authorization](https://docs.nestjs.com/security/authorization)
|
|
500
|
+
|
|
501
|
+
### Pipes — Input Validation & Whitelisting
|
|
502
|
+
|
|
503
|
+
The `MapAndValidatePipe` runs on every incoming argument/body:
|
|
504
|
+
|
|
505
|
+
```
|
|
506
|
+
Plain Object --> Transform to Class Instance --> Whitelist Check --> Validation --> Clean Input
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
#### Whitelisting via @UnifiedField()
|
|
510
|
+
|
|
511
|
+
Properties **without** `@UnifiedField()` are subject to the whitelist policy:
|
|
512
|
+
|
|
513
|
+
| Mode | Config Value | Behavior |
|
|
514
|
+
|------|-------------|----------|
|
|
515
|
+
| **Strip** (default) | `'strip'` | Unknown properties silently removed |
|
|
516
|
+
| **Error** | `'error'` | Throws `400 Bad Request` with property names |
|
|
517
|
+
| **Disabled** | `false` | All properties accepted |
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
// config.env.ts
|
|
521
|
+
security: {
|
|
522
|
+
mapAndValidatePipe: {
|
|
523
|
+
nonWhitelistedFields: 'strip', // 'strip' | 'error' | false
|
|
524
|
+
},
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
#### @UnifiedField() Decorator
|
|
529
|
+
|
|
530
|
+
Combines GraphQL `@Field()`, Swagger `@ApiProperty()`, and class-validator decorators into one:
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
export class CreateUserInput extends CoreInput {
|
|
534
|
+
@UnifiedField({ description: 'Email address' })
|
|
535
|
+
email: string = undefined;
|
|
536
|
+
|
|
537
|
+
@UnifiedField({ isOptional: true, description: 'Display name' })
|
|
538
|
+
displayName?: string = undefined;
|
|
539
|
+
|
|
540
|
+
@UnifiedField({ exclude: true }) // Hidden from schema, rejected at runtime
|
|
541
|
+
internalFlag?: boolean = undefined;
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
> **NestJS docs:** [Pipes](https://docs.nestjs.com/pipes), [Validation](https://docs.nestjs.com/techniques/validation)
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
## Phase 3: Handler Execution
|
|
550
|
+
|
|
551
|
+
### Controllers (REST) vs Resolvers (GraphQL)
|
|
552
|
+
|
|
553
|
+
```
|
|
554
|
+
+----------------------------+ +----------------------------+
|
|
555
|
+
| REST Controller | | GraphQL Resolver |
|
|
556
|
+
+----------------------------+ +----------------------------+
|
|
557
|
+
| @Controller('/users') | | @Resolver(() => User) |
|
|
558
|
+
| @Get(':id') | | @Query(() => User) |
|
|
559
|
+
| @Post() | | @Mutation(() => User) |
|
|
560
|
+
| @Patch(':id') | | |
|
|
561
|
+
| @Delete(':id') | | |
|
|
562
|
+
+----------------------------+ +----------------------------+
|
|
563
|
+
| Input: @Body(), @Param() | | Input: @Args() |
|
|
564
|
+
| User: @CurrentUser() | | User: @CurrentUser() |
|
|
565
|
+
| Type: @ApiOkResponse() | | Type: @Query(() => User) |
|
|
566
|
+
| @ResponseModel() | | @Mutation(() => User)|
|
|
567
|
+
+----------------------------+ +----------------------------+
|
|
568
|
+
| |
|
|
569
|
+
+----------------+------------------+
|
|
570
|
+
|
|
|
571
|
+
+----------------v------------------+
|
|
572
|
+
| Service Layer |
|
|
573
|
+
| |
|
|
574
|
+
| A: CrudService.process() |
|
|
575
|
+
| Full pipeline with security |
|
|
576
|
+
| |
|
|
577
|
+
| B: Direct Mongoose query |
|
|
578
|
+
| Safety Net catches |
|
|
579
|
+
| |
|
|
580
|
+
| C: processResult() |
|
|
581
|
+
| Population + output only |
|
|
582
|
+
+-----------------------------------+
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
### @CurrentUser() Decorator
|
|
586
|
+
|
|
587
|
+
Injects the authenticated user into the handler. This is a **custom parameter decorator** that bypasses the pipe (no validation/whitelist applied):
|
|
588
|
+
|
|
589
|
+
```typescript
|
|
590
|
+
@Query(() => User)
|
|
591
|
+
@Roles(RoleEnum.S_USER)
|
|
592
|
+
async getMe(
|
|
593
|
+
@CurrentUser() currentUser: User,
|
|
594
|
+
@GraphQLServiceOptions() serviceOptions: ServiceOptions,
|
|
595
|
+
): Promise<User> {
|
|
596
|
+
return this.userService.get(currentUser.id, serviceOptions);
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### Mongoose Plugins (Write Operations)
|
|
601
|
+
|
|
602
|
+
When the service performs write operations (save, update), Mongoose plugins fire **at the database level**:
|
|
603
|
+
|
|
604
|
+
#### Password Hashing Plugin
|
|
605
|
+
|
|
606
|
+
```
|
|
607
|
+
+------------------+
|
|
608
|
+
| Input password |
|
|
609
|
+
+--------+---------+
|
|
610
|
+
|
|
|
611
|
+
+--------v---------+
|
|
612
|
+
| Already hashed? |
|
|
613
|
+
| (BCrypt pattern) |
|
|
614
|
+
+--------+---------+
|
|
615
|
+
Yes / \ No
|
|
616
|
+
/ \
|
|
617
|
+
+------------+ +---------v---------+
|
|
618
|
+
| Skip | | Sentinel value? |
|
|
619
|
+
| pass thru | | (skipPatterns) |
|
|
620
|
+
+------------+ +---------+---------+
|
|
621
|
+
Yes / \ No
|
|
622
|
+
/ \
|
|
623
|
+
+------------+ +---------v---------+
|
|
624
|
+
| Skip | | SHA256 -> BCrypt |
|
|
625
|
+
| pass thru | | hash -> MongoDB |
|
|
626
|
+
+------------+ +-------------------+
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
#### Role Guard Plugin
|
|
630
|
+
|
|
631
|
+
```
|
|
632
|
+
+---------------------+
|
|
633
|
+
| Write includes |
|
|
634
|
+
| roles? |
|
|
635
|
+
+----------+----------+
|
|
636
|
+
No / \ Yes
|
|
637
|
+
/ \
|
|
638
|
+
+------------+ +------------v-----------+
|
|
639
|
+
| Pass | | No currentUser? |
|
|
640
|
+
| through | | (system operation) |
|
|
641
|
+
+------------+ +------------+-----------+
|
|
642
|
+
Yes / \ No
|
|
643
|
+
/ \
|
|
644
|
+
+------------+ +------------v-----------+
|
|
645
|
+
| Allow | | bypassRoleGuard |
|
|
646
|
+
| | | active? |
|
|
647
|
+
+------------+ +------------+-----------+
|
|
648
|
+
Yes / \ No
|
|
649
|
+
/ \
|
|
650
|
+
+------------+ +------------v-----------+
|
|
651
|
+
| Allow | | User is ADMIN? |
|
|
652
|
+
| | | |
|
|
653
|
+
+------------+ +------------+-----------+
|
|
654
|
+
Yes / \ No
|
|
655
|
+
/ \
|
|
656
|
+
+------------+ +------------v-----------+
|
|
657
|
+
| Allow | | User in allowedRoles? |
|
|
658
|
+
| | | |
|
|
659
|
+
+------------+ +------------+-----------+
|
|
660
|
+
Yes / \ No
|
|
661
|
+
/ \
|
|
662
|
+
+------------+ +-------------+
|
|
663
|
+
| Allow | | Block |
|
|
664
|
+
| | | (strip |
|
|
665
|
+
+------------+ | roles) |
|
|
666
|
+
+-------------+
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
#### Audit Fields Plugin
|
|
670
|
+
|
|
671
|
+
```
|
|
672
|
+
+---------------------+
|
|
673
|
+
| Write operation |
|
|
674
|
+
+----------+----------+
|
|
675
|
+
|
|
|
676
|
+
+----------v----------+
|
|
677
|
+
| currentUser exists? |
|
|
678
|
+
+----------+----------+
|
|
679
|
+
No / \ Yes
|
|
680
|
+
/ \
|
|
681
|
+
+------------+ +------------v-----------+
|
|
682
|
+
| Skip | | New document? |
|
|
683
|
+
+------------+ +------------+-----------+
|
|
684
|
+
Yes / \ No
|
|
685
|
+
/ \
|
|
686
|
+
+------------+ +------------v-----------+
|
|
687
|
+
| Set | | Set updatedBy |
|
|
688
|
+
| createdBy + | | only |
|
|
689
|
+
| updatedBy | | |
|
|
690
|
+
+-------------+ +-----------------------+
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
#### Tenant Isolation Plugin (opt-in)
|
|
694
|
+
|
|
695
|
+
Enabled via `multiTenancy: {}` in config. Auto-activates only on schemas with a `tenantId` field.
|
|
696
|
+
The `tenantId` is read from RequestContext (set by CoreTenantGuard via `req.tenantId`), not the raw header.
|
|
697
|
+
|
|
698
|
+
**Safety Net:** If a tenant-schema is accessed without a valid tenant context (no `tenantId` and no bypass), the plugin throws a `ForbiddenException`. This prevents accidental cross-tenant data leaks when developers forget to set the tenant header or bypass.
|
|
699
|
+
|
|
700
|
+
```
|
|
701
|
+
+---------------------+
|
|
702
|
+
| DB operation |
|
|
703
|
+
| (query/save/agg) |
|
|
704
|
+
+----------+----------+
|
|
705
|
+
|
|
|
706
|
+
+----------v----------+
|
|
707
|
+
| multiTenancy config |
|
|
708
|
+
| enabled? |
|
|
709
|
+
+----------+----------+
|
|
710
|
+
No / \ Yes
|
|
711
|
+
/ \
|
|
712
|
+
+------------+ +------------v-----------+
|
|
713
|
+
| Skip | | RequestContext exists? |
|
|
714
|
+
+------------+ +------------+-----------+
|
|
715
|
+
No / \ Yes
|
|
716
|
+
/ \
|
|
717
|
+
+------------+ +------------v-----------+
|
|
718
|
+
| No filter | | bypassTenantGuard? |
|
|
719
|
+
| (system op)| | |
|
|
720
|
+
+------------+ +------------+-----------+
|
|
721
|
+
Yes / \ No
|
|
722
|
+
/ \
|
|
723
|
+
+------------+ +------------v-----------+
|
|
724
|
+
| No filter | | Schema in |
|
|
725
|
+
| | | excludeSchemas? |
|
|
726
|
+
+------------+ +------------+-----------+
|
|
727
|
+
Yes / \ No
|
|
728
|
+
/ \
|
|
729
|
+
+------------+ +------------v-----------+
|
|
730
|
+
| No filter | | isAdminBypass? |
|
|
731
|
+
+------------+ +------------+-----------+
|
|
732
|
+
Yes / \ No
|
|
733
|
+
/ \
|
|
734
|
+
+------------+ +------------v-----------+
|
|
735
|
+
| No filter | | tenantId? |
|
|
736
|
+
| (admin sees| +------------+-----------+
|
|
737
|
+
| all data) | Yes / \ No
|
|
738
|
+
+------------+ / \
|
|
739
|
+
+------------+ +-------------+
|
|
740
|
+
| Filter by | | FORBIDDEN |
|
|
741
|
+
| tenantId | | (Safety Net)|
|
|
742
|
+
+------------+ +-------------+
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
**Important:** When `multiTenancy.adminBypass` is `true` (default), system admins without a tenant header get `isAdminBypass` set in RequestContext and see all data (no tenant filter). Non-admin users with a tenant header but no membership always get 403. For cross-tenant admin operations, use `RequestContext.runWithBypassTenantGuard()`.
|
|
746
|
+
|
|
747
|
+
> **NestJS docs:** [Custom decorators](https://docs.nestjs.com/custom-decorators), [Mongoose](https://docs.nestjs.com/techniques/mongodb)
|
|
748
|
+
|
|
749
|
+
---
|
|
750
|
+
|
|
751
|
+
## Phase 4: Response Processing
|
|
752
|
+
|
|
753
|
+
NestJS runs interceptors in **reverse registration order** on the response. Since `ResponseModelInterceptor` is registered last in `CoreModule`, it runs **first** on the response:
|
|
754
|
+
|
|
755
|
+
```
|
|
756
|
+
Handler return value
|
|
757
|
+
|
|
|
758
|
+
+--------v-------------------------------------------------+
|
|
759
|
+
| Step 7: ResponseModelInterceptor |
|
|
760
|
+
| |
|
|
761
|
+
| Resolves the expected model class: |
|
|
762
|
+
| 1. @ResponseModel(User) decorator (explicit) |
|
|
763
|
+
| 2. @Query(() => User) / @Mutation() return type (GQL) |
|
|
764
|
+
| 3. @ApiOkResponse({ type: User }) (Swagger/REST) |
|
|
765
|
+
| |
|
|
766
|
+
| Converts plain objects -> CoreModel instances via .map() |
|
|
767
|
+
| Skips if already instanceof or _objectAlreadyChecked |
|
|
768
|
+
| Enables securityCheck() and @Restricted on the result |
|
|
769
|
+
+--------+--------------------------------------------------+
|
|
770
|
+
|
|
|
771
|
+
+--------v-------------------------------------------------+
|
|
772
|
+
| Step 8: TranslateResponseInterceptor |
|
|
773
|
+
| |
|
|
774
|
+
| Checks Accept-Language header |
|
|
775
|
+
| If _translations exists on response objects: |
|
|
776
|
+
| -> Applies matching translation to base fields |
|
|
777
|
+
| Early bailout when no _translations present |
|
|
778
|
+
+--------+--------------------------------------------------+
|
|
779
|
+
|
|
|
780
|
+
+--------v-------------------------------------------------+
|
|
781
|
+
| Step 9: CheckSecurityInterceptor |
|
|
782
|
+
| |
|
|
783
|
+
| Calls securityCheck(user, force) on model instances |
|
|
784
|
+
| Recursively processes nested objects |
|
|
785
|
+
| |
|
|
786
|
+
| Fallback: Removes secret fields from ALL objects |
|
|
787
|
+
| (password, verificationToken, refreshTokens, etc.) |
|
|
788
|
+
| Even if object is NOT a CoreModel instance |
|
|
789
|
+
+--------+--------------------------------------------------+
|
|
790
|
+
|
|
|
791
|
+
+--------v-------------------------------------------------+
|
|
792
|
+
| Step 10: CheckResponseInterceptor |
|
|
793
|
+
| |
|
|
794
|
+
| Reads @Restricted() metadata from each property |
|
|
795
|
+
| For each field: |
|
|
796
|
+
| - Role check: Does user have required role? |
|
|
797
|
+
| - Membership check: Is user.id in field's memberOf? |
|
|
798
|
+
| - System role check: S_CREATOR, S_SELF, etc. |
|
|
799
|
+
| Removes fields the user is not allowed to see |
|
|
800
|
+
| Sets _objectAlreadyCheckedForRestrictions = true |
|
|
801
|
+
+--------+--------------------------------------------------+
|
|
802
|
+
|
|
|
803
|
+
+----v-----------+
|
|
804
|
+
| HTTP Response |
|
|
805
|
+
+----------------+
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
> **NestJS docs:** [Interceptors](https://docs.nestjs.com/interceptors)
|
|
809
|
+
|
|
810
|
+
---
|
|
811
|
+
|
|
812
|
+
## CrudService.process() Pipeline
|
|
813
|
+
|
|
814
|
+
The `process()` method in `ModuleService` is the **primary** way to handle CRUD operations with full security. It orchestrates input preparation, authorization, the database operation, and output preparation:
|
|
815
|
+
|
|
816
|
+
```
|
|
817
|
+
+---------------------------------------------------------------+
|
|
818
|
+
| CrudService.process() |
|
|
819
|
+
| |
|
|
820
|
+
| +----------------------------------------------------------+ |
|
|
821
|
+
| | 1. prepareInput() | |
|
|
822
|
+
| | - Hash password (SHA256 + BCrypt) | |
|
|
823
|
+
| | - Check roles (if checkRoles: true) | |
|
|
824
|
+
| | - Convert ObjectIds to strings | |
|
|
825
|
+
| | - Map to target model type | |
|
|
826
|
+
| | - Remove undefined properties | |
|
|
827
|
+
| +----------------------------+-----------------------------+ |
|
|
828
|
+
| | |
|
|
829
|
+
| +----------------------------v-----------------------------+ |
|
|
830
|
+
| | 2. checkRights(INPUT) | |
|
|
831
|
+
| | - Evaluate @Restricted() on input properties | |
|
|
832
|
+
| | - Verify user has required roles/memberships | |
|
|
833
|
+
| | - Strip/reject unauthorized input fields | |
|
|
834
|
+
| +----------------------------+-----------------------------+ |
|
|
835
|
+
| | |
|
|
836
|
+
| +----------------------------v-----------------------------+ |
|
|
837
|
+
| | 3. serviceFunc() <-- Your database operation | |
|
|
838
|
+
| | - findById, create, findByIdAndUpdate, aggregate... | |
|
|
839
|
+
| | - Mongoose plugins fire here (password, roles, audit)| |
|
|
840
|
+
| | - If force: true -> runs inside bypassRoleGuard | |
|
|
841
|
+
| +----------------------------+-----------------------------+ |
|
|
842
|
+
| | |
|
|
843
|
+
| +----------------------------v-----------------------------+ |
|
|
844
|
+
| | 4. processFieldSelection() | |
|
|
845
|
+
| | - Populate referenced documents (GraphQL selections) | |
|
|
846
|
+
| +----------------------------+-----------------------------+ |
|
|
847
|
+
| | |
|
|
848
|
+
| +----------------------------v-----------------------------+ |
|
|
849
|
+
| | 5. prepareOutput() | |
|
|
850
|
+
| | - Map Mongoose document -> model instance (.map()) | |
|
|
851
|
+
| | - Convert ObjectIds to strings | |
|
|
852
|
+
| | - Remove secrets (if removeSecrets: true) | |
|
|
853
|
+
| | - Apply custom transformations (overridable) | |
|
|
854
|
+
| +----------------------------+-----------------------------+ |
|
|
855
|
+
| | |
|
|
856
|
+
| +----------------------------v-----------------------------+ |
|
|
857
|
+
| | 6. checkRights(OUTPUT, throwError: false) | |
|
|
858
|
+
| | - Filter output properties based on @Restricted() | |
|
|
859
|
+
| | - Non-throwing: strips fields instead of erroring | |
|
|
860
|
+
| +----------------------------+-----------------------------+ |
|
|
861
|
+
| | |
|
|
862
|
+
| Return processed result |
|
|
863
|
+
+---------------------------------------------------------------+
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
### Key Options
|
|
867
|
+
|
|
868
|
+
| Option | Type | Default | Effect |
|
|
869
|
+
|--------|------|---------|--------|
|
|
870
|
+
| `force` | boolean | `false` | Disables checkRights, checkRoles, removeSecrets, bypasses role guard plugin |
|
|
871
|
+
| `raw` | boolean | `false` | Disables prepareInput and prepareOutput entirely |
|
|
872
|
+
| `checkRights` | boolean | `true` | Enable/disable authorization checks |
|
|
873
|
+
| `populate` | object | - | Field selection for population |
|
|
874
|
+
| `currentUser` | object | from request | Override the current user |
|
|
875
|
+
|
|
876
|
+
### Alternative: processResult()
|
|
877
|
+
|
|
878
|
+
For direct Mongoose queries that need population and output preparation but not the full pipeline:
|
|
879
|
+
|
|
880
|
+
```typescript
|
|
881
|
+
// Direct query + simplified processing
|
|
882
|
+
const doc = await this.mainDbModel.findById(id).exec();
|
|
883
|
+
return this.processResult(doc, serviceOptions);
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
`processResult()` handles population and `prepareOutput()` only. Security is handled by the Safety Net (Mongoose plugins for input, interceptors for output).
|
|
887
|
+
|
|
888
|
+
---
|
|
889
|
+
|
|
890
|
+
## Safety Net Architecture
|
|
891
|
+
|
|
892
|
+
The Safety Net ensures security even when developers bypass `CrudService.process()` and use direct Mongoose queries. It consists of two complementary layers:
|
|
893
|
+
|
|
894
|
+
```
|
|
895
|
+
+-----------------------------------------------------------+
|
|
896
|
+
| Developer writes direct query |
|
|
897
|
+
| |
|
|
898
|
+
| const user = await this.mainDbModel.findById(id).exec(); |
|
|
899
|
+
| return user; // Plain Mongoose document |
|
|
900
|
+
+----------------------------+------------------------------+
|
|
901
|
+
|
|
|
902
|
+
+-------------------v--------------------+
|
|
903
|
+
| INPUT PROTECTION |
|
|
904
|
+
| (Mongoose Plugins -- on write) |
|
|
905
|
+
| |
|
|
906
|
+
| - Password auto-hashed |
|
|
907
|
+
| (mongoosePasswordPlugin) |
|
|
908
|
+
| |
|
|
909
|
+
| - Roles guarded |
|
|
910
|
+
| (mongooseRoleGuardPlugin) |
|
|
911
|
+
| |
|
|
912
|
+
| - Audit fields set |
|
|
913
|
+
| (mongooseAuditFieldsPlugin) |
|
|
914
|
+
| |
|
|
915
|
+
| - Tenant isolation enforced |
|
|
916
|
+
| (mongooseTenantPlugin) |
|
|
917
|
+
| 403 if no valid tenant context |
|
|
918
|
+
+-------------------+--------------------+
|
|
919
|
+
|
|
|
920
|
+
+-------------------v--------------------+
|
|
921
|
+
| OUTPUT PROTECTION |
|
|
922
|
+
| (Response Interceptors) |
|
|
923
|
+
| |
|
|
924
|
+
| - Plain -> Model conversion |
|
|
925
|
+
| (ResponseModelInterceptor) |
|
|
926
|
+
| |
|
|
927
|
+
| - Translations applied |
|
|
928
|
+
| (TranslateResponseInterceptor) |
|
|
929
|
+
| |
|
|
930
|
+
| - securityCheck() called |
|
|
931
|
+
| (CheckSecurityInterceptor) |
|
|
932
|
+
| |
|
|
933
|
+
| - @Restricted fields filtered |
|
|
934
|
+
| (CheckResponseInterceptor) |
|
|
935
|
+
| |
|
|
936
|
+
| - Secret fields removed (fallback) |
|
|
937
|
+
| (CheckSecurityInterceptor) |
|
|
938
|
+
+-------------------+--------------------+
|
|
939
|
+
|
|
|
940
|
+
+--------v--------+
|
|
941
|
+
| Secure Response |
|
|
942
|
+
+-----------------+
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
### When is process() vs Safety Net used?
|
|
946
|
+
|
|
947
|
+
| Approach | Input Security | Output Security | Population | Custom Logic |
|
|
948
|
+
|----------|---------------|----------------|------------|--------------|
|
|
949
|
+
| `process()` | prepareInput + plugins | prepareOutput + interceptors | Yes | checkRights, serviceOptions |
|
|
950
|
+
| Direct query + `return` | Plugins only | Interceptors only | No | None |
|
|
951
|
+
| Direct query + `processResult()` | Plugins only | prepareOutput + interceptors | Yes | Custom prepareOutput |
|
|
952
|
+
|
|
953
|
+
**Recommendation:** Use `process()` for full CRUD operations. Use direct queries + Safety Net for simple read-only queries, aggregations, or performance-critical paths.
|
|
954
|
+
|
|
955
|
+
---
|
|
956
|
+
|
|
957
|
+
## Decorators Reference
|
|
958
|
+
|
|
959
|
+
### @Roles() — Method-Level Authorization
|
|
960
|
+
|
|
961
|
+
Controls who can access a resolver/controller method. Evaluated by the RolesGuard.
|
|
962
|
+
|
|
963
|
+
```typescript
|
|
964
|
+
// Only admins
|
|
965
|
+
@Roles(RoleEnum.ADMIN)
|
|
966
|
+
|
|
967
|
+
// Any authenticated user
|
|
968
|
+
@Roles(RoleEnum.S_USER)
|
|
969
|
+
|
|
970
|
+
// Public access
|
|
971
|
+
@Roles(RoleEnum.S_EVERYONE)
|
|
972
|
+
|
|
973
|
+
// Multiple roles (OR logic — user needs at least one)
|
|
974
|
+
@Roles(RoleEnum.ADMIN, 'MANAGER')
|
|
975
|
+
|
|
976
|
+
// Tenant roles — validated by CoreTenantGuard when multiTenancy is enabled
|
|
977
|
+
@Roles(DefaultHR.MEMBER) // Any active tenant member (level >= 1)
|
|
978
|
+
@Roles(DefaultHR.MANAGER) // At least manager-level (level >= 2)
|
|
979
|
+
@Roles(DefaultHR.OWNER) // Highest role level (level >= 3)
|
|
980
|
+
@Roles('auditor') // Normal role: exact match only
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
**Note:** `@Roles()` includes JWT authentication. Do NOT add `@UseGuards(AuthGuard(JWT))`. When multiTenancy is enabled, `CoreTenantGuard` checks system roles as OR alternatives first (`S_EVERYONE` → `S_USER` → `S_VERIFIED`). Hierarchy roles use level comparison. Normal (non-hierarchy) roles use exact match. When `X-Tenant-Id` header is present, only `membership.role` is checked (user.roles ignored, except ADMIN bypass). With a system role granting access + header present, membership is still validated to set tenant context.
|
|
984
|
+
|
|
985
|
+
### @Restricted() — Field-Level Access Control
|
|
986
|
+
|
|
987
|
+
Controls who can see or modify specific properties. Evaluated by `CheckResponseInterceptor` (output) and `checkRights()` (input).
|
|
988
|
+
|
|
989
|
+
```typescript
|
|
990
|
+
export class User extends CorePersistenceModel {
|
|
991
|
+
// Only admins or the user themselves can see the email
|
|
992
|
+
@Restricted(RoleEnum.ADMIN, RoleEnum.S_SELF)
|
|
993
|
+
email: string = undefined;
|
|
994
|
+
|
|
995
|
+
// Only admins can see roles
|
|
996
|
+
@Restricted(RoleEnum.ADMIN)
|
|
997
|
+
roles: string[] = undefined;
|
|
998
|
+
|
|
999
|
+
// Only users who are members of the 'teamMembers' array
|
|
1000
|
+
@Restricted({ memberOf: 'teamMembers' })
|
|
1001
|
+
internalNotes: string = undefined;
|
|
1002
|
+
|
|
1003
|
+
// Restrict for input only (anyone can read, but only admins can write)
|
|
1004
|
+
@Restricted({ roles: RoleEnum.ADMIN, processType: ProcessType.INPUT })
|
|
1005
|
+
status: string = undefined;
|
|
1006
|
+
}
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
### @UnifiedField() — Schema & Validation
|
|
1010
|
+
|
|
1011
|
+
Single decorator that replaces `@Field()`, `@ApiProperty()`, `@IsOptional()`, and more:
|
|
1012
|
+
|
|
1013
|
+
```typescript
|
|
1014
|
+
export class CreateUserInput extends CoreInput {
|
|
1015
|
+
// Required string field (shown in both GraphQL and Swagger)
|
|
1016
|
+
@UnifiedField({ description: 'User email address' })
|
|
1017
|
+
email: string = undefined;
|
|
1018
|
+
|
|
1019
|
+
// Optional field
|
|
1020
|
+
@UnifiedField({ isOptional: true })
|
|
1021
|
+
displayName?: string = undefined;
|
|
1022
|
+
|
|
1023
|
+
// Enum field
|
|
1024
|
+
@UnifiedField({ enum: RoleEnum, isOptional: true })
|
|
1025
|
+
role?: RoleEnum = undefined;
|
|
1026
|
+
|
|
1027
|
+
// Excluded from input (hidden from schema, rejected at runtime)
|
|
1028
|
+
@UnifiedField({ exclude: true })
|
|
1029
|
+
internalId?: string = undefined;
|
|
1030
|
+
|
|
1031
|
+
// Re-include a field excluded by parent class
|
|
1032
|
+
@UnifiedField({ exclude: false })
|
|
1033
|
+
parentExcludedField?: string = undefined;
|
|
1034
|
+
}
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1037
|
+
### @ResponseModel() — REST Response Type Hint
|
|
1038
|
+
|
|
1039
|
+
For REST controllers, specifies the model class for automatic response conversion:
|
|
1040
|
+
|
|
1041
|
+
```typescript
|
|
1042
|
+
@ResponseModel(User)
|
|
1043
|
+
@Get(':id')
|
|
1044
|
+
async getUser(@Param('id') id: string): Promise<User> {
|
|
1045
|
+
return this.mainDbModel.findById(id).exec();
|
|
1046
|
+
// Even though this returns a Mongoose document,
|
|
1047
|
+
// ResponseModelInterceptor converts it to User model
|
|
1048
|
+
}
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
**Note:** For GraphQL, the return type is resolved automatically from `@Query(() => User)`.
|
|
1052
|
+
|
|
1053
|
+
### @ApiOkResponse() — Swagger + Response Type
|
|
1054
|
+
|
|
1055
|
+
For REST controllers with Swagger. Also serves as response type hint for `ResponseModelInterceptor`:
|
|
1056
|
+
|
|
1057
|
+
```typescript
|
|
1058
|
+
@ApiOkResponse({ type: User })
|
|
1059
|
+
@Get(':id')
|
|
1060
|
+
async getUser(@Param('id') id: string): Promise<User> { ... }
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
> **NestJS docs:** [Custom decorators](https://docs.nestjs.com/custom-decorators), [OpenAPI](https://docs.nestjs.com/openapi/introduction)
|
|
1064
|
+
|
|
1065
|
+
---
|
|
1066
|
+
|
|
1067
|
+
## Model System
|
|
1068
|
+
|
|
1069
|
+
### Class Hierarchy
|
|
1070
|
+
|
|
1071
|
+
```
|
|
1072
|
+
CoreModel Abstract base (map, securityCheck, hasRole)
|
|
1073
|
+
|
|
|
1074
|
+
+-- CorePersistenceModel Adds id, createdAt, updatedAt, createdBy, updatedBy
|
|
1075
|
+
|
|
|
1076
|
+
+-- User Your concrete model
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
### Key Methods
|
|
1080
|
+
|
|
1081
|
+
#### `Model.map(data)` — Static Factory
|
|
1082
|
+
|
|
1083
|
+
Creates a model instance from a plain object or Mongoose document. Copies only properties that exist on the model class (defined with `= undefined`):
|
|
1084
|
+
|
|
1085
|
+
```typescript
|
|
1086
|
+
const user = User.map(mongooseDoc);
|
|
1087
|
+
// user is now a User instance with securityCheck(), hasRole(), etc.
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
This is what `prepareOutput()` and `ResponseModelInterceptor` call internally.
|
|
1091
|
+
|
|
1092
|
+
#### `model.securityCheck(user, force)` — Instance Security
|
|
1093
|
+
|
|
1094
|
+
Called by `CheckSecurityInterceptor` on every response object. Override this in your model to implement custom security logic:
|
|
1095
|
+
|
|
1096
|
+
```typescript
|
|
1097
|
+
export class User extends CorePersistenceModel {
|
|
1098
|
+
password: string = undefined;
|
|
1099
|
+
internalScore: number = undefined;
|
|
1100
|
+
|
|
1101
|
+
override securityCheck(user: any, force?: boolean): this {
|
|
1102
|
+
// Remove password from output (should never be returned)
|
|
1103
|
+
this.password = undefined;
|
|
1104
|
+
|
|
1105
|
+
// Only admins can see internalScore
|
|
1106
|
+
if (!force && !user?.hasRole?.([RoleEnum.ADMIN])) {
|
|
1107
|
+
this.internalScore = undefined;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
return this;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
**When securityCheck runs:**
|
|
1116
|
+
1. `CheckSecurityInterceptor` calls it on every response object
|
|
1117
|
+
2. `CrudService.process()` runs it via `prepareOutput()` (before the interceptor)
|
|
1118
|
+
3. Safety Net: `ResponseModelInterceptor` converts to model first → then `CheckSecurityInterceptor` calls securityCheck
|
|
1119
|
+
|
|
1120
|
+
### prepareOutput() in Services
|
|
1121
|
+
|
|
1122
|
+
Override `prepareOutput()` in your service for custom output transformations:
|
|
1123
|
+
|
|
1124
|
+
```typescript
|
|
1125
|
+
export class UserService extends CoreUserService<User> {
|
|
1126
|
+
override async prepareOutput(output: any, options?: any): Promise<User> {
|
|
1127
|
+
// Call parent (handles mapping, ObjectId conversion)
|
|
1128
|
+
output = await super.prepareOutput(output, options);
|
|
1129
|
+
|
|
1130
|
+
// Custom transformations
|
|
1131
|
+
if (output && !options?.force) {
|
|
1132
|
+
output.fullName = `${output.firstName} ${output.lastName}`;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
return output;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
```
|
|
1139
|
+
|
|
1140
|
+
---
|
|
1141
|
+
|
|
1142
|
+
## REST vs GraphQL Differences
|
|
1143
|
+
|
|
1144
|
+
| Aspect | REST | GraphQL |
|
|
1145
|
+
|--------|------|---------|
|
|
1146
|
+
| **Entry point** | `@Controller()` class | `@Resolver()` class |
|
|
1147
|
+
| **Method decorators** | `@Get()`, `@Post()`, `@Patch()`, `@Delete()` | `@Query()`, `@Mutation()` |
|
|
1148
|
+
| **Input** | `@Body()`, `@Param()`, `@Query()` | `@Args()` |
|
|
1149
|
+
| **User injection** | `@CurrentUser()` (same) | `@CurrentUser()` (same) |
|
|
1150
|
+
| **Response type resolution** | `@ResponseModel()` or `@ApiOkResponse()` | Automatic from `@Query(() => Type)` |
|
|
1151
|
+
| **Context extraction** | `context.switchToHttp().getRequest()` | `GqlExecutionContext.create(context)` |
|
|
1152
|
+
| **Field selection** | Not available (all fields returned) | GraphQL field selection → population |
|
|
1153
|
+
| **File uploads** | Standard multipart | `graphqlUploadExpress()` middleware |
|
|
1154
|
+
| **Subscriptions** | Not supported | WebSocket via `graphql-ws` |
|
|
1155
|
+
|
|
1156
|
+
### Guard Context Detection
|
|
1157
|
+
|
|
1158
|
+
Guards handle both REST and GraphQL contexts:
|
|
1159
|
+
|
|
1160
|
+
```typescript
|
|
1161
|
+
// Inside RolesGuard
|
|
1162
|
+
const gqlContext = GqlExecutionContext.create(context).getContext();
|
|
1163
|
+
const request = gqlContext?.req || context.switchToHttp().getRequest();
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
> **NestJS docs:** [REST Controllers](https://docs.nestjs.com/controllers), [GraphQL Resolvers](https://docs.nestjs.com/graphql/resolvers)
|
|
1167
|
+
|
|
1168
|
+
---
|
|
1169
|
+
|
|
1170
|
+
## Configuration Reference
|
|
1171
|
+
|
|
1172
|
+
All security features are configured in `config.env.ts` under the `security` key:
|
|
1173
|
+
|
|
1174
|
+
### Guardian Gates
|
|
1175
|
+
|
|
1176
|
+
| Config Path | Type | Default | Description |
|
|
1177
|
+
|-------------|------|---------|-------------|
|
|
1178
|
+
| `security.checkResponseInterceptor` | `boolean \| object` | `true` | Enable @Restricted field filtering |
|
|
1179
|
+
| `security.checkSecurityInterceptor` | `boolean \| object` | `true` | Enable securityCheck() calls |
|
|
1180
|
+
| `security.mapAndValidatePipe` | `boolean \| object` | `true` | Enable input validation |
|
|
1181
|
+
| `security.mapAndValidatePipe.nonWhitelistedFields` | `'strip' \| 'error' \| false` | `'strip'` | Whitelist behavior |
|
|
1182
|
+
|
|
1183
|
+
### Safety Net — Mongoose Plugins
|
|
1184
|
+
|
|
1185
|
+
| Config Path | Type | Default | Description |
|
|
1186
|
+
|-------------|------|---------|-------------|
|
|
1187
|
+
| `security.mongoosePasswordPlugin` | `boolean \| { skipPatterns }` | `true` | Auto password hashing |
|
|
1188
|
+
| `security.mongooseRoleGuardPlugin` | `boolean \| { allowedRoles }` | `true` | Role escalation prevention |
|
|
1189
|
+
| `security.mongooseAuditFieldsPlugin` | `boolean` | `true` | Auto createdBy/updatedBy |
|
|
1190
|
+
| `multiTenancy` | `IMultiTenancy` | `undefined` (disabled) | Tenant-based data isolation (header + membership) |
|
|
1191
|
+
|
|
1192
|
+
### Safety Net — Response Interceptors
|
|
1193
|
+
|
|
1194
|
+
| Config Path | Type | Default | Description |
|
|
1195
|
+
|-------------|------|---------|-------------|
|
|
1196
|
+
| `security.responseModelInterceptor` | `boolean \| { debug }` | `true` | Plain → Model auto-conversion |
|
|
1197
|
+
| `security.translateResponseInterceptor` | `boolean` | `true` | Auto translation application |
|
|
1198
|
+
| `security.secretFields` | `string[]` | `['password', ...]` | Global secret field removal list |
|
|
1199
|
+
| `security.checkSecurityInterceptor.removeSecretFields` | `boolean` | `true` | Fallback secret removal |
|
|
1200
|
+
|
|
1201
|
+
### Role Guard Bypass
|
|
1202
|
+
|
|
1203
|
+
```typescript
|
|
1204
|
+
// Option 1: Programmatic bypass in service code
|
|
1205
|
+
import { RequestContext } from '@lenne.tech/nest-server';
|
|
1206
|
+
|
|
1207
|
+
await RequestContext.runWithBypassRoleGuard(async () => {
|
|
1208
|
+
await this.mainDbModel.create({ roles: ['EMPLOYEE'] });
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
// Option 2: CrudService force mode
|
|
1212
|
+
this.process(serviceFunc, { serviceOptions, force: true });
|
|
1213
|
+
|
|
1214
|
+
// Option 3: Config-based (permanently allow roles)
|
|
1215
|
+
security: {
|
|
1216
|
+
mongooseRoleGuardPlugin: { allowedRoles: ['HR_MANAGER'] },
|
|
1217
|
+
}
|
|
1218
|
+
```
|
|
1219
|
+
|
|
1220
|
+
### Tenant Guard Bypass
|
|
1221
|
+
|
|
1222
|
+
```typescript
|
|
1223
|
+
// Cross-tenant admin operations
|
|
1224
|
+
import { RequestContext } from '@lenne.tech/nest-server';
|
|
1225
|
+
|
|
1226
|
+
const allOrders = await RequestContext.runWithBypassTenantGuard(async () => {
|
|
1227
|
+
return this.orderService.find(); // sees all tenants
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
// Exclude specific schemas from tenant filtering
|
|
1231
|
+
multiTenancy: {
|
|
1232
|
+
excludeSchemas: ['User', 'Session'], // model names, not collection names
|
|
1233
|
+
}
|
|
1234
|
+
```
|
|
1235
|
+
|
|
1236
|
+
---
|
|
1237
|
+
|
|
1238
|
+
## NestJS Documentation Links
|
|
1239
|
+
|
|
1240
|
+
| Topic | URL |
|
|
1241
|
+
|-------|-----|
|
|
1242
|
+
| **Request Lifecycle** | https://docs.nestjs.com/faq/request-lifecycle |
|
|
1243
|
+
| **Middleware** | https://docs.nestjs.com/middleware |
|
|
1244
|
+
| **Guards** | https://docs.nestjs.com/guards |
|
|
1245
|
+
| **Interceptors** | https://docs.nestjs.com/interceptors |
|
|
1246
|
+
| **Pipes** | https://docs.nestjs.com/pipes |
|
|
1247
|
+
| **Custom Decorators** | https://docs.nestjs.com/custom-decorators |
|
|
1248
|
+
| **Validation** | https://docs.nestjs.com/techniques/validation |
|
|
1249
|
+
| **Authentication** | https://docs.nestjs.com/security/authentication |
|
|
1250
|
+
| **Authorization** | https://docs.nestjs.com/security/authorization |
|
|
1251
|
+
| **MongoDB / Mongoose** | https://docs.nestjs.com/techniques/mongodb |
|
|
1252
|
+
| **GraphQL** | https://docs.nestjs.com/graphql/quick-start |
|
|
1253
|
+
| **GraphQL Resolvers** | https://docs.nestjs.com/graphql/resolvers |
|
|
1254
|
+
| **REST Controllers** | https://docs.nestjs.com/controllers |
|
|
1255
|
+
| **OpenAPI / Swagger** | https://docs.nestjs.com/openapi/introduction |
|
|
1256
|
+
| **Dynamic Modules** | https://docs.nestjs.com/fundamentals/dynamic-modules |
|