@lenne.tech/nest-server 11.21.3 → 11.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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 +172 -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/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.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 +8 -3
- 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,170 @@
|
|
|
1
|
+
# Migration Guide: 11.19.x → 11.20.0
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Category | Details |
|
|
6
|
+
|----------|---------|
|
|
7
|
+
| **Breaking Changes** | None |
|
|
8
|
+
| **New Features** | Multi-Tenancy Plugin (opt-in) |
|
|
9
|
+
| **Bugfixes** | None |
|
|
10
|
+
| **Migration Effort** | 0 min (feature is opt-in, no changes required for existing projects) |
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Quick Migration (No Breaking Changes)
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Update package
|
|
18
|
+
pnpm add @lenne.tech/nest-server@11.20.0
|
|
19
|
+
|
|
20
|
+
# Verify build
|
|
21
|
+
pnpm run build
|
|
22
|
+
|
|
23
|
+
# Run tests
|
|
24
|
+
pnpm test
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
No code changes required. Multi-tenancy is disabled by default and has zero overhead when not configured.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## What's New in 11.20.0
|
|
32
|
+
|
|
33
|
+
### Multi-Tenancy: Tenant-Based Data Isolation
|
|
34
|
+
|
|
35
|
+
A global Mongoose plugin that automatically filters all database queries by `tenantId`, providing tenant isolation in a shared MongoDB database.
|
|
36
|
+
|
|
37
|
+
**Key features:**
|
|
38
|
+
- Auto-filters queries (find, update, delete, aggregate, count, distinct)
|
|
39
|
+
- Auto-sets `tenantId` on new documents (save, insertMany)
|
|
40
|
+
- Zero overhead when disabled (plugin not registered)
|
|
41
|
+
- Works with both Legacy Auth (JWT) and BetterAuth (IAM)
|
|
42
|
+
- Follows the "Presence Implies Enabled" configuration pattern
|
|
43
|
+
|
|
44
|
+
#### Step 1: Enable in config
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// config.env.ts
|
|
48
|
+
{
|
|
49
|
+
// Enable with defaults (userField: 'tenantId')
|
|
50
|
+
multiTenancy: {},
|
|
51
|
+
|
|
52
|
+
// Or with options
|
|
53
|
+
multiTenancy: {
|
|
54
|
+
excludeSchemas: ['User', 'Session'], // Model names to exclude from filtering
|
|
55
|
+
userField: 'tenantId', // Field on req.user (default: 'tenantId')
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// Pre-configured but disabled
|
|
59
|
+
multiTenancy: {
|
|
60
|
+
enabled: false,
|
|
61
|
+
excludeSchemas: ['User'],
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### Step 2: Add `tenantId` to your User model
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// In your project's User model (NOT in nest-server!)
|
|
70
|
+
export class User extends CoreUserModel {
|
|
71
|
+
@UnifiedField({
|
|
72
|
+
mongoose: { type: String, index: true },
|
|
73
|
+
roles: RoleEnum.S_NO_ONE, // Users cannot modify their own tenantId
|
|
74
|
+
})
|
|
75
|
+
tenantId: string = undefined;
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
#### Step 3: Add `tenantId` to domain models
|
|
80
|
+
|
|
81
|
+
> **Security Warning:** You **MUST** use `@UnifiedField` with `roles: RoleEnum.S_NO_ONE` on the `tenantId` field of every domain model. Without this, API consumers can inject arbitrary tenant IDs via request body, bypassing tenant isolation on writes. The plugin auto-sets `tenantId` from the user context — manual input should be blocked.
|
|
82
|
+
>
|
|
83
|
+
> **This is a developer responsibility that the plugin cannot enforce.** The tenant plugin operates at the Mongoose middleware layer and has no knowledge of your GraphQL/REST input schema. If you omit the `roles: RoleEnum.S_NO_ONE` declaration, the field will be writable via API input, silently undermining tenant isolation on write paths. There is no runtime warning — the plugin will simply preserve whatever `tenantId` value was provided in the request body instead of auto-assigning from the user context.
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
@MongooseSchema({ timestamps: true })
|
|
87
|
+
export class Order extends CorePersistenceModel {
|
|
88
|
+
@UnifiedField({
|
|
89
|
+
mongoose: { type: String, index: true },
|
|
90
|
+
roles: RoleEnum.S_NO_ONE, // Prevent user from setting tenantId via API input
|
|
91
|
+
})
|
|
92
|
+
tenantId: string = undefined;
|
|
93
|
+
|
|
94
|
+
// ... other fields
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The plugin automatically activates on any schema that has a `tenantId` field. Schemas without `tenantId` are not affected.
|
|
99
|
+
|
|
100
|
+
#### Cross-Tenant Admin Operations
|
|
101
|
+
|
|
102
|
+
**Important:** ADMIN users are also filtered by their own `tenantId`. For admin dashboards or cross-tenant queries, use the explicit bypass:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { RequestContext } from '@lenne.tech/nest-server';
|
|
106
|
+
|
|
107
|
+
// Admin dashboard: view all tenants
|
|
108
|
+
const allOrders = await RequestContext.runWithBypassTenantGuard(async () => {
|
|
109
|
+
return this.orderService.find();
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### Behavior Reference
|
|
114
|
+
|
|
115
|
+
| Situation | Behavior |
|
|
116
|
+
|-----------|----------|
|
|
117
|
+
| No `multiTenancy` config | Plugin not registered, zero overhead |
|
|
118
|
+
| No RequestContext (cron, migration) | No filter applied |
|
|
119
|
+
| `bypassTenantGuard: true` | No filter applied |
|
|
120
|
+
| Schema in `excludeSchemas` | No filter applied |
|
|
121
|
+
| User with `tenantId: "abc"` | Filter: `{ tenantId: "abc" }` |
|
|
122
|
+
| User without `tenantId` | Filter: `{ tenantId: null }` (sees only unassigned data) |
|
|
123
|
+
| User with `tenantId: ""` (empty string) | Same as without — empty string is falsy, filter: `{ tenantId: null }` |
|
|
124
|
+
| No user (public endpoint) | No filter applied |
|
|
125
|
+
|
|
126
|
+
#### Known Limitations
|
|
127
|
+
|
|
128
|
+
1. **`populate()`**: Referenced models with `tenantId` are also filtered. Shared models (User, Config) should be in `excludeSchemas`.
|
|
129
|
+
2. **`estimatedDocumentCount`**: Does not support query filters (MongoDB limitation).
|
|
130
|
+
3. **`$lookup` in aggregates**: Joined collections are not automatically filtered. Add `$match` in `$lookup.pipeline` manually.
|
|
131
|
+
4. **Legacy data**: Existing documents without `tenantId` are not auto-assigned. Backfill tenantIds via migration script.
|
|
132
|
+
5. **Write-path protection**: The plugin auto-sets `tenantId` on new documents but does not enforce it on explicit values. Use `@UnifiedField({ roles: RoleEnum.S_NO_ONE })` on domain model `tenantId` fields to prevent cross-tenant write injection via API input.
|
|
133
|
+
6. **Empty string tenantId**: If `req.user.tenantId` is an empty string (`''`), it is treated as falsy — the user sees only documents with `tenantId: null`. Ensure tenant IDs are never stored as empty strings.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Compatibility Notes
|
|
138
|
+
|
|
139
|
+
### Pattern: Existing projects without multi-tenancy
|
|
140
|
+
|
|
141
|
+
**Status: Fully compatible, no action required**
|
|
142
|
+
|
|
143
|
+
If your project does not configure `multiTenancy`, the plugin is never registered. There is zero performance impact.
|
|
144
|
+
|
|
145
|
+
### Pattern: Projects with custom Mongoose plugins
|
|
146
|
+
|
|
147
|
+
**Status: Compatible**
|
|
148
|
+
|
|
149
|
+
The tenant plugin is registered after the audit fields plugin in `connectionFactory`. It does not interfere with other plugins.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Module Documentation
|
|
154
|
+
|
|
155
|
+
| Module | Documentation |
|
|
156
|
+
|--------|--------------|
|
|
157
|
+
| Multi-Tenancy Plugin | `src/core/common/plugins/mongoose-tenant.plugin.ts` |
|
|
158
|
+
| RequestContext | `src/core/common/services/request-context.service.ts` |
|
|
159
|
+
| Configuration | `IMultiTenancy` in `src/core/common/interfaces/server-options.interface.ts` |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Troubleshooting
|
|
164
|
+
|
|
165
|
+
| Issue | Solution |
|
|
166
|
+
|-------|----------|
|
|
167
|
+
| Admin can't see all tenants | Use `RequestContext.runWithBypassTenantGuard()` — ADMIN users are also filtered |
|
|
168
|
+
| `populate()` returns null for cross-tenant refs | Add referenced model to `excludeSchemas` |
|
|
169
|
+
| Legacy data invisible after enabling MT | Backfill `tenantId` on existing documents |
|
|
170
|
+
| Cron job sees all data | Expected — no RequestContext = no filter |
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Migration Guide: 11.20.x → 11.21.0
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Category | Changes | Effort |
|
|
6
|
+
|----------|---------|--------|
|
|
7
|
+
| **Breaking** | `userField` removed from `IMultiTenancy`, tenant filtering behavior changed | Medium |
|
|
8
|
+
| **Breaking** | Safety Net: tenantId-schemas throw `ForbiddenException` without valid tenant context | Low |
|
|
9
|
+
| **New Feature** | Header-based multi-tenant membership with configurable hierarchy roles | Low-Medium |
|
|
10
|
+
|
|
11
|
+
## Not Using Multi-Tenancy?
|
|
12
|
+
|
|
13
|
+
If your project does **not** configure `multiTenancy` in `config.env.ts`, this update requires **no action**. All changes are fully backward compatible — just update the package version.
|
|
14
|
+
|
|
15
|
+
## Breaking Changes
|
|
16
|
+
|
|
17
|
+
### 1. `userField` Removed from IMultiTenancy
|
|
18
|
+
|
|
19
|
+
The `userField` property has been removed. Tenant ID is now read from the `X-Tenant-Id` request header instead of from `req.user`.
|
|
20
|
+
|
|
21
|
+
**Before (11.20.x):**
|
|
22
|
+
```typescript
|
|
23
|
+
multiTenancy: {
|
|
24
|
+
userField: 'organizationId',
|
|
25
|
+
excludeSchemas: ['User'],
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**After (11.21.0):**
|
|
30
|
+
```typescript
|
|
31
|
+
multiTenancy: {
|
|
32
|
+
headerName: 'x-tenant-id', // Optional, this is the default
|
|
33
|
+
excludeSchemas: ['User'],
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Action Required:** Remove any `userField` references from your `config.env.ts`.
|
|
38
|
+
|
|
39
|
+
### 2. Tenant Filtering Behavior Change
|
|
40
|
+
|
|
41
|
+
**Before (11.20.x):**
|
|
42
|
+
- Logged-in user without `tenantId` → queries filtered by `{ tenantId: null }` (sees only unassigned data)
|
|
43
|
+
|
|
44
|
+
**After (11.21.0):**
|
|
45
|
+
- Unauthenticated request with `X-Tenant-Id` header → **403 "Authentication required for tenant access"**
|
|
46
|
+
- Authenticated request without `X-Tenant-Id` header + hierarchy role → checks `user.roles`, filters `tenantIds` by level
|
|
47
|
+
- Authenticated user with no memberships → **Safety Net: `ForbiddenException`** on tenantId-schemas
|
|
48
|
+
- Use `@Roles(DefaultHR.MEMBER)` to require tenant membership on specific endpoints
|
|
49
|
+
|
|
50
|
+
**Action Required:** If you relied on the null-filter behavior, use `@Roles(DefaultHR.MEMBER)` on endpoints that should require tenant context.
|
|
51
|
+
|
|
52
|
+
### 3. Safety Net (Defense-in-Depth)
|
|
53
|
+
|
|
54
|
+
The Mongoose tenant plugin now throws `ForbiddenException` instead of returning unfiltered data when a tenantId-scoped schema is accessed without valid tenant context.
|
|
55
|
+
|
|
56
|
+
**Affected scenarios:**
|
|
57
|
+
- Public endpoints (`@Roles(S_EVERYONE)`) accessing tenantId-scoped models → now throw 403
|
|
58
|
+
- System operations without `RequestContext` → unaffected (no context = no filter, as before)
|
|
59
|
+
- Operations with `bypassTenantGuard` → unaffected (bypass skips all filtering)
|
|
60
|
+
|
|
61
|
+
**Action Required:** Public routes that access tenantId-scoped models must use one of:
|
|
62
|
+
- `@SkipTenantCheck()` decorator + `RequestContext.runWithBypassTenantGuard()` in the service
|
|
63
|
+
- Or ensure a valid tenant context is provided
|
|
64
|
+
|
|
65
|
+
### 4. New IMultiTenancy Properties
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
export interface IMultiTenancy {
|
|
69
|
+
enabled?: boolean;
|
|
70
|
+
excludeSchemas?: string[];
|
|
71
|
+
// NEW
|
|
72
|
+
headerName?: string; // Default: 'x-tenant-id'
|
|
73
|
+
membershipModel?: string; // Default: 'TenantMember'
|
|
74
|
+
adminBypass?: boolean; // Default: true
|
|
75
|
+
roleHierarchy?: Record<string, number>; // Default: { member: 1, manager: 2, owner: 3 }
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## New Feature: Multi-Tenant Membership
|
|
80
|
+
|
|
81
|
+
### What's New
|
|
82
|
+
|
|
83
|
+
- **CoreTenantModule** — Auto-registered when `multiTenancy` is configured
|
|
84
|
+
- **CoreTenantMemberModel** — Join table for user-tenant membership with roles
|
|
85
|
+
- **CoreTenantGuard** — APP_GUARD validating tenant header and membership
|
|
86
|
+
- **CoreTenantService** — Membership CRUD operations
|
|
87
|
+
- **Hierarchy Roles** — Configurable role levels with level comparison (`@Roles(DefaultHR.MEMBER)`)
|
|
88
|
+
- **Normal Roles** — Non-hierarchy roles with exact match (`@Roles('auditor')`)
|
|
89
|
+
- **`createHierarchyRoles()`** — Generate type-safe constants from hierarchy config
|
|
90
|
+
- **`DefaultHR`** — Pre-built constants for default hierarchy (MEMBER, MANAGER, OWNER)
|
|
91
|
+
- **`@SkipTenantCheck()`** — Skip tenant validation for specific endpoints
|
|
92
|
+
- **`@CurrentTenant()`** — Parameter decorator for tenant ID extraction
|
|
93
|
+
- **`TenantMemberStatus`** enum — ACTIVE, INVITED, SUSPENDED
|
|
94
|
+
|
|
95
|
+
### Quick Setup
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// config.env.ts — enable tenant system
|
|
99
|
+
multiTenancy: {},
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Protecting Endpoints
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { Roles, CurrentTenant, DefaultHR } from '@lenne.tech/nest-server';
|
|
106
|
+
|
|
107
|
+
@Controller('projects')
|
|
108
|
+
export class ProjectController {
|
|
109
|
+
@Get()
|
|
110
|
+
@Roles(DefaultHR.MEMBER) // any active member
|
|
111
|
+
async list(@CurrentTenant() tenantId: string) {
|
|
112
|
+
return this.projectService.find({ tenantId });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@Delete(':id')
|
|
116
|
+
@Roles(DefaultHR.OWNER) // highest level only
|
|
117
|
+
async delete(@Param('id') id: string) {
|
|
118
|
+
return this.projectService.delete(id);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Custom Hierarchy (5+ Levels)
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// config.env.ts
|
|
127
|
+
multiTenancy: {
|
|
128
|
+
roleHierarchy: { viewer: 1, editor: 2, manager: 2, admin: 3, owner: 4 },
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// roles.ts
|
|
132
|
+
import { createHierarchyRoles } from '@lenne.tech/nest-server';
|
|
133
|
+
export const HR = createHierarchyRoles({ viewer: 1, editor: 2, manager: 2, admin: 3, owner: 4 });
|
|
134
|
+
|
|
135
|
+
// resolver.ts
|
|
136
|
+
@Roles(HR.EDITOR) // requires level >= 2 (editor, manager, admin, owner)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Tenant Context Rule
|
|
140
|
+
|
|
141
|
+
When `X-Tenant-Id` header is present: only `membership.role` is checked (user.roles ignored, except ADMIN bypass).
|
|
142
|
+
When no header: `user.roles` is checked instead (same hierarchy/normal semantics).
|
|
143
|
+
|
|
144
|
+
### Frontend: Send Tenant Header
|
|
145
|
+
|
|
146
|
+
All tenant-scoped API calls must include:
|
|
147
|
+
```
|
|
148
|
+
X-Tenant-Id: <tenant-id>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### How It Works
|
|
152
|
+
|
|
153
|
+
1. `RequestContextMiddleware` provides lazy getters for tenant context
|
|
154
|
+
2. `CoreTenantGuard` validates membership → sets `req.tenantId` (only after successful check)
|
|
155
|
+
3. Mongoose tenant plugin reads `tenantId` from `RequestContext` → filters queries
|
|
156
|
+
4. **Safety Net:** Plugin throws `ForbiddenException` if tenantId-schema accessed without valid context
|
|
157
|
+
|
|
158
|
+
### Admin Bypass
|
|
159
|
+
|
|
160
|
+
System admins (`RoleEnum.ADMIN`) skip membership validation by default.
|
|
161
|
+
Disable: `multiTenancy: { adminBypass: false }`.
|
|
162
|
+
|
|
163
|
+
## Compatibility Notes
|
|
164
|
+
|
|
165
|
+
| Pattern | Status | Notes |
|
|
166
|
+
|---------|--------|-------|
|
|
167
|
+
| `multiTenancy: {}` | Compatible | Enables tenant system with defaults |
|
|
168
|
+
| `multiTenancy: { enabled: false }` | Compatible | Pre-configured but disabled |
|
|
169
|
+
| No `multiTenancy` config | Compatible | Zero overhead, no changes needed |
|
|
170
|
+
| `excludeSchemas` | Compatible | Works as before; `TenantMember` auto-added |
|
|
171
|
+
| `RequestContext.runWithBypassTenantGuard()` | Compatible | Unchanged |
|
|
172
|
+
| Public endpoints on non-tenant schemas | Compatible | Unaffected by Safety Net |
|
|
173
|
+
| Public endpoints on tenant schemas | **Breaking** | Safety Net throws 403 — use `@SkipTenantCheck()` + bypass |
|
|
174
|
+
|
|
175
|
+
## Troubleshooting
|
|
176
|
+
|
|
177
|
+
| Issue | Cause | Fix |
|
|
178
|
+
|-------|-------|-----|
|
|
179
|
+
| 403 "Authentication required for tenant access" | Header present but no user | Authenticate before accessing tenant endpoints |
|
|
180
|
+
| 403 "Insufficient tenant role" | Membership role too low for hierarchy requirement | Upgrade membership role or use a lower-level decorator |
|
|
181
|
+
| 403 "Insufficient role" | No header, user.roles don't meet requirement | Add appropriate role to user.roles or send tenant header |
|
|
182
|
+
| 403 "Not a member of this tenant" | User has no active membership | Add membership via `CoreTenantService.addMember()` |
|
|
183
|
+
| 403 "Tenant context required" | Safety Net: tenantId-schema accessed without valid context | Add `@SkipTenantCheck()` + `runWithBypassTenantGuard()`, or provide tenant header |
|
|
184
|
+
| Data not filtered by tenant | Model missing `tenantId` field | Add `@Prop({ type: String }) tenantId: string` |
|
|
185
|
+
| TypeScript error: `userField` not found | Breaking change in IMultiTenancy | Remove `userField` from config |
|
|
186
|
+
|
|
187
|
+
## Rollback / Downgrade
|
|
188
|
+
|
|
189
|
+
If you need to revert to 11.20.x after upgrading:
|
|
190
|
+
|
|
191
|
+
1. **Downgrade the package:**
|
|
192
|
+
```bash
|
|
193
|
+
pnpm add @lenne.tech/nest-server@11.20.1
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
2. **Restore `userField` in config:**
|
|
197
|
+
```typescript
|
|
198
|
+
multiTenancy: {
|
|
199
|
+
userField: 'tenantId', // Re-add the removed property
|
|
200
|
+
excludeSchemas: ['User'],
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
3. **Remove tenant decorators** from any endpoints where you added `@Roles(DefaultHR.*)`, `@SkipTenantCheck()`, or `@CurrentTenant()`.
|
|
205
|
+
|
|
206
|
+
4. **Remove `X-Tenant-Id` header** from frontend API calls if already added.
|
|
207
|
+
|
|
208
|
+
5. **Note:** The `TenantMember` collection created by 11.21.0 will remain in MongoDB but is unused by 11.20.x. You can drop it manually if desired:
|
|
209
|
+
```
|
|
210
|
+
db.tenantmembers.drop()
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Module Documentation
|
|
214
|
+
|
|
215
|
+
- [Tenant Module README](../src/core/modules/tenant/README.md)
|
|
216
|
+
- [Integration Checklist](../src/core/modules/tenant/INTEGRATION-CHECKLIST.md)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Migration Guide: 11.21.0 → 11.21.1
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Category | Changes | Effort |
|
|
6
|
+
|----------|---------|--------|
|
|
7
|
+
| **Breaking Changes** | None | None |
|
|
8
|
+
| **New Features** | Tenant membership cache with configurable TTL, cache invalidation APIs, DB indices for BetterAuth | Low (opt-in) |
|
|
9
|
+
| **Performance Improvements** | CheckSecurityInterceptor, processDeep, EmailService, RequestContext bypass | None (automatic) |
|
|
10
|
+
| **Migration Effort** | Update package version | ~1 minute |
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Quick Migration
|
|
15
|
+
|
|
16
|
+
This is a PATCH release with no breaking changes. All improvements are backward compatible and most activate automatically.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Update package
|
|
20
|
+
pnpm add @lenne.tech/nest-server@11.21.1
|
|
21
|
+
|
|
22
|
+
# Verify build
|
|
23
|
+
pnpm run build
|
|
24
|
+
|
|
25
|
+
# Run tests
|
|
26
|
+
pnpm test
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
No code changes required.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## What's New in 11.21.1
|
|
34
|
+
|
|
35
|
+
### 1. Tenant Membership Cache (`multiTenancy.cacheTtlMs`)
|
|
36
|
+
|
|
37
|
+
The `CoreTenantGuard` now caches membership lookups in-process to avoid repeated database queries when the same user accesses the same tenant. This is especially beneficial for high-traffic multi-tenant applications.
|
|
38
|
+
|
|
39
|
+
**Default behavior:** Cache is enabled with a 30-second TTL. In test environments (`VITEST=true`, `NODE_ENV=test`, or `NODE_ENV=e2e`), the cache is automatically disabled to avoid stale data between test cases.
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// config.env.ts — default (no changes needed, cache is on)
|
|
43
|
+
multiTenancy: {}
|
|
44
|
+
|
|
45
|
+
// config.env.ts — custom TTL (e.g., 60 seconds)
|
|
46
|
+
multiTenancy: {
|
|
47
|
+
cacheTtlMs: 60_000,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// config.env.ts — disable cache entirely
|
|
51
|
+
multiTenancy: {
|
|
52
|
+
cacheTtlMs: 0,
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Important for horizontally scaled deployments:** The cache is process-local. When a membership is added or removed on one server instance, other instances will not see the change until the TTL expires. For security-critical deployments where immediate revocation is required, reduce the TTL or set it to `0`.
|
|
57
|
+
|
|
58
|
+
### 2. Cache Invalidation APIs
|
|
59
|
+
|
|
60
|
+
Two new public methods on `CoreTenantGuard` allow programmatic cache invalidation:
|
|
61
|
+
|
|
62
|
+
| Method | Description |
|
|
63
|
+
|--------|-------------|
|
|
64
|
+
| `invalidateUser(userId)` | Clears all cached entries for a specific user |
|
|
65
|
+
| `invalidateAll()` | Clears the entire membership cache |
|
|
66
|
+
|
|
67
|
+
These are called automatically by `CoreTenantService` whenever memberships are added, removed, or updated. **No action required** unless your project extends `CoreTenantService` with custom membership mutation methods.
|
|
68
|
+
|
|
69
|
+
**Action required only if:** You have a custom service that modifies tenant memberships directly (bypassing `CoreTenantService`). In that case, inject `CoreTenantGuard` and call `invalidateUser()` after changes:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { CoreTenantGuard } from '@lenne.tech/nest-server';
|
|
73
|
+
|
|
74
|
+
@Injectable()
|
|
75
|
+
export class CustomTenantService extends CoreTenantService {
|
|
76
|
+
constructor(
|
|
77
|
+
// ... other deps
|
|
78
|
+
private tenantGuard: CoreTenantGuard,
|
|
79
|
+
) {
|
|
80
|
+
super(/* ... */);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async customMembershipChange(userId: string, tenantId: string): Promise<void> {
|
|
84
|
+
// ... your custom logic ...
|
|
85
|
+
this.tenantGuard.invalidateUser(userId);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 3. User Role Cache Invalidation (`CoreBetterAuthUserMapper`)
|
|
91
|
+
|
|
92
|
+
A new public method `invalidateUserCache(iamId)` is available on `CoreBetterAuthUserMapper`. It clears the cached user data (15-second TTL) used during session-to-user mapping.
|
|
93
|
+
|
|
94
|
+
This is called automatically from `CoreUserService.setRoles()`. **No action required** unless your project modifies user roles outside of `CoreUserService.setRoles()`.
|
|
95
|
+
|
|
96
|
+
**Action required only if:** You have custom logic that changes user roles directly in the database:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { CoreBetterAuthUserMapper } from '@lenne.tech/nest-server';
|
|
100
|
+
|
|
101
|
+
@Injectable()
|
|
102
|
+
export class CustomUserService {
|
|
103
|
+
constructor(private userMapper: CoreBetterAuthUserMapper) {}
|
|
104
|
+
|
|
105
|
+
async customRoleChange(userId: string, iamId: string): Promise<void> {
|
|
106
|
+
// ... your custom role update logic ...
|
|
107
|
+
this.userMapper.invalidateUserCache(iamId);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 4. Automatic Database Indices
|
|
113
|
+
|
|
114
|
+
The following indices are now auto-created by `CoreBetterAuthService.onModuleInit()` when BetterAuth is enabled:
|
|
115
|
+
|
|
116
|
+
| Collection | Index | Purpose |
|
|
117
|
+
|------------|-------|---------|
|
|
118
|
+
| `session` | `{ token: 1 }` | Fast session token lookups |
|
|
119
|
+
| `session` | `{ userId: 1, expiresAt: 1 }` | Active session queries per user |
|
|
120
|
+
| `users` | `{ iamId: 1 }` (sparse) | IAM ID lookups during session mapping |
|
|
121
|
+
|
|
122
|
+
No action required. Index creation is idempotent — calling `createIndex` on an existing index is a no-op. These indices improve query performance for BetterAuth session validation and user mapping.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Performance Improvements
|
|
127
|
+
|
|
128
|
+
These optimizations apply automatically. No configuration or code changes needed.
|
|
129
|
+
|
|
130
|
+
### 1. CheckSecurityInterceptor: Conditional JSON.stringify
|
|
131
|
+
|
|
132
|
+
`JSON.stringify` on response data now only runs when `debug: true` is configured. Previously, every response was serialized even when debug output was not used. This reduces CPU overhead on every request that passes through the security interceptor.
|
|
133
|
+
|
|
134
|
+
### 2. processDeep: Options Object Reuse
|
|
135
|
+
|
|
136
|
+
The `processDeep()` helper (used by `CheckSecurityInterceptor` and input validation) now reuses its options object across recursive calls instead of recreating it for each nested property. This reduces garbage collection pressure for deeply nested response objects.
|
|
137
|
+
|
|
138
|
+
### 3. EmailService: SMTP Transport Reuse
|
|
139
|
+
|
|
140
|
+
The SMTP transport created by `nodemailer.createTransport()` is now cached as a singleton per configuration. Previously, a new transport was created for each email send operation. The cache key is derived from the SMTP configuration hash, so transport changes at runtime are handled correctly.
|
|
141
|
+
|
|
142
|
+
### 4. RequestContext.runWithBypass*: Skip Redundant Context
|
|
143
|
+
|
|
144
|
+
`RequestContext.runWithBypassRoleGuard()` and `RequestContext.runWithBypassTenantGuard()` now skip creating a new `AsyncLocalStorage` context when the bypass flag is already set. This avoids unnecessary object spread operations in nested bypass calls.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Compatibility Notes
|
|
149
|
+
|
|
150
|
+
| Pattern | Status | Notes |
|
|
151
|
+
|---------|--------|-------|
|
|
152
|
+
| `multiTenancy: {}` | Compatible | Cache enabled at 30s TTL by default |
|
|
153
|
+
| `multiTenancy: { cacheTtlMs: 0 }` | Compatible | Cache disabled, same behavior as before 11.21.1 |
|
|
154
|
+
| No `multiTenancy` config | Compatible | No changes, no overhead |
|
|
155
|
+
| Custom `CoreTenantService` extensions | Compatible | Automatic invalidation if using `super.addMember()` etc. |
|
|
156
|
+
| Direct membership DB mutations | **Check** | Must call `invalidateUser()` manually (see section 2 above) |
|
|
157
|
+
| Custom role-change logic (bypassing `CoreUserService.setRoles`) | **Check** | Must call `invalidateUserCache()` manually (see section 3 above) |
|
|
158
|
+
| BetterAuth disabled | Compatible | DB indices are not created when BetterAuth is disabled |
|
|
159
|
+
| Test environments | Compatible | Cache auto-disabled in test/e2e environments |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Troubleshooting
|
|
164
|
+
|
|
165
|
+
| Issue | Cause | Fix |
|
|
166
|
+
|-------|-------|-----|
|
|
167
|
+
| Stale membership data after role change | Membership cache TTL not expired | Call `CoreTenantGuard.invalidateUser(userId)` after custom membership changes |
|
|
168
|
+
| Stale user roles after `setRoles()` | Should not happen (auto-invalidated) | Verify you are using `CoreUserService.setRoles()`, not direct DB updates |
|
|
169
|
+
| Membership changes not visible across server instances | Process-local cache | Reduce `cacheTtlMs` or set to `0` for immediate consistency |
|
|
170
|
+
| Tests see stale membership data | Cache enabled in test env | Verify `NODE_ENV=test` or `VITEST=true` is set (auto-disables cache) |
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Module Documentation
|
|
175
|
+
|
|
176
|
+
### Tenant Module
|
|
177
|
+
|
|
178
|
+
- **README:** [src/core/modules/tenant/README.md](../src/core/modules/tenant/README.md)
|
|
179
|
+
- **Integration Checklist:** [src/core/modules/tenant/INTEGRATION-CHECKLIST.md](../src/core/modules/tenant/INTEGRATION-CHECKLIST.md)
|
|
180
|
+
- **Key File:** `src/core/modules/tenant/core-tenant.guard.ts` — cache implementation and `invalidateUser()`/`invalidateAll()` methods
|
|
181
|
+
|
|
182
|
+
### BetterAuth Module
|
|
183
|
+
|
|
184
|
+
- **README:** [src/core/modules/better-auth/README.md](../src/core/modules/better-auth/README.md)
|
|
185
|
+
- **Integration Checklist:** [src/core/modules/better-auth/INTEGRATION-CHECKLIST.md](../src/core/modules/better-auth/INTEGRATION-CHECKLIST.md)
|
|
186
|
+
- **Key File:** `src/core/modules/better-auth/core-better-auth.service.ts` — DB index creation in `onModuleInit()`
|
|
187
|
+
- **Key File:** `src/core/modules/better-auth/core-better-auth-user.mapper.ts` — `invalidateUserCache()` method
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## References
|
|
192
|
+
|
|
193
|
+
- [nest-server-starter](https://github.com/lenneTech/nest-server-starter) (reference implementation)
|
|
194
|
+
- [11.20.x → 11.21.0 Migration Guide](./11.20.x-to-11.21.0.md) (previous release with multi-tenancy breaking changes)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Migration Guide: 11.21.1 → 11.21.2
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Category | Details |
|
|
6
|
+
|----------|---------|
|
|
7
|
+
| **Breaking Changes** | None |
|
|
8
|
+
| **New Features** | BetterAuth auto-skip for tenant validation on IAM endpoints |
|
|
9
|
+
| **Bugfixes** | lodash security fix (CVE HIGH + MODERATE) |
|
|
10
|
+
| **Migration Effort** | < 5 minutes |
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Quick Migration
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Update package
|
|
18
|
+
pnpm add @lenne.tech/nest-server@11.21.2
|
|
19
|
+
|
|
20
|
+
# Verify build
|
|
21
|
+
pnpm run build
|
|
22
|
+
|
|
23
|
+
# Run tests
|
|
24
|
+
pnpm test
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
No code changes required. The new feature activates automatically with safe defaults.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## What's New in 11.21.2
|
|
32
|
+
|
|
33
|
+
### 1. BetterAuth Auto-Skip for Tenant Validation
|
|
34
|
+
|
|
35
|
+
**Affects:** Projects with both `multiTenancy` and `betterAuth` active.
|
|
36
|
+
|
|
37
|
+
When both features are enabled, IAM endpoints (sign-in, sign-up, sign-out, session, etc.)
|
|
38
|
+
now automatically skip `CoreTenantGuard` tenant validation when no `X-Tenant-Id` header is
|
|
39
|
+
present. This solves the chicken-and-egg problem where users couldn't authenticate because
|
|
40
|
+
they weren't yet a member of any tenant.
|
|
41
|
+
|
|
42
|
+
**Default behavior (`skipTenantCheck: true`):**
|
|
43
|
+
|
|
44
|
+
| Scenario | Behavior |
|
|
45
|
+
|----------|----------|
|
|
46
|
+
| No `X-Tenant-Id` header | Skip tenant validation (auth before tenant) |
|
|
47
|
+
| `X-Tenant-Id` header present | Validate normally (membership check + tenant context) |
|
|
48
|
+
|
|
49
|
+
The auto-skip applies to both REST controllers (`CoreBetterAuthController` subclasses)
|
|
50
|
+
and GraphQL resolvers (`CoreBetterAuthResolver` subclasses).
|
|
51
|
+
|
|
52
|
+
**If you previously used `@SkipTenantCheck()` on your IAM resolver/controller to work
|
|
53
|
+
around this issue, you can remove it** — the framework now handles this automatically.
|
|
54
|
+
|
|
55
|
+
### 2. Opt-Out for Tenant-Aware Authentication
|
|
56
|
+
|
|
57
|
+
If your project requires tenant context at login time (e.g., subdomain-based tenancy,
|
|
58
|
+
invite links with embedded tenant, SSO per tenant), disable the auto-skip:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// config.env.ts
|
|
62
|
+
betterAuth: {
|
|
63
|
+
skipTenantCheck: false, // IAM endpoints require valid X-Tenant-Id header
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 3. Security: lodash Update
|
|
68
|
+
|
|
69
|
+
`lodash` updated from 4.17.23 to 4.18.1, fixing:
|
|
70
|
+
- GHSA-r5fr-rjxr-66jc (HIGH — prototype pollution)
|
|
71
|
+
- GHSA-f23m-r3pf-42rh (MODERATE — prototype pollution)
|
|
72
|
+
|
|
73
|
+
A `pnpm.overrides` entry ensures all transitive dependencies also use the patched version.
|
|
74
|
+
|
|
75
|
+
### 4. Dependency Updates
|
|
76
|
+
|
|
77
|
+
| Package | From | To | Type |
|
|
78
|
+
|---------|------|----|------|
|
|
79
|
+
| `lodash` | 4.17.23 | 4.18.1 | Security fix |
|
|
80
|
+
| `dotenv` | 17.3.1 | 17.4.0 | Minor feature |
|
|
81
|
+
| `@swc/cli` | 0.8.0 | 0.8.1 | Patch |
|
|
82
|
+
|
|
83
|
+
Three unnecessary pnpm overrides were removed (`file-type`, `yauzl`, `flatted`).
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Compatibility Notes
|
|
88
|
+
|
|
89
|
+
- **Projects without `multiTenancy`:** Not affected. No behavior change.
|
|
90
|
+
- **Projects without `betterAuth`:** Not affected. No behavior change.
|
|
91
|
+
- **Projects with both features + default config:** IAM endpoints now work without `X-Tenant-Id` header. Previously, these would fail if the user wasn't a tenant member.
|
|
92
|
+
- **Projects with `@SkipTenantCheck()` on IAM endpoints:** The decorator continues to work but is no longer needed for this use case. Safe to remove.
|
|
93
|
+
- **Subdomain-based / invite-link tenancy:** Set `betterAuth.skipTenantCheck: false` to require tenant context on IAM endpoints.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Module Documentation
|
|
98
|
+
|
|
99
|
+
### Tenant Module
|
|
100
|
+
|
|
101
|
+
- **README:** [src/core/modules/tenant/README.md](../src/core/modules/tenant/README.md) — See "BetterAuth (IAM) Integration" section
|
|
102
|
+
- **Integration Checklist:** [src/core/modules/tenant/INTEGRATION-CHECKLIST.md](../src/core/modules/tenant/INTEGRATION-CHECKLIST.md) — See § 9 "BetterAuth (IAM) Coexistence"
|
|
103
|
+
|
|
104
|
+
### BetterAuth Module
|
|
105
|
+
|
|
106
|
+
- **README:** [src/core/modules/better-auth/README.md](../src/core/modules/better-auth/README.md) — See "Multi-Tenancy Integration" section
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## References
|
|
111
|
+
|
|
112
|
+
- [Configurable Features](../.claude/rules/configurable-features.md) — `betterAuth.skipTenantCheck` entry
|
|
113
|
+
- [Request Lifecycle](../docs/REQUEST-LIFECYCLE.md) — CoreTenantGuard flow with auto-skip
|
|
114
|
+
- [nest-server-starter](https://github.com/lenneTech/nest-server-starter) (reference implementation)
|