@lenne.tech/nest-server 11.21.2 → 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/better-auth/core-better-auth-user.mapper.js +25 -25
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.service.js +8 -4
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/core/modules/error-code/error-code.module.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.guard.d.ts +1 -0
- package/dist/core/modules/tenant/core-tenant.guard.js +59 -4
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.helpers.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/test/test.helper.d.ts +6 -2
- package/dist/test/test.helper.js +28 -6
- package/dist/test/test.helper.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/better-auth/core-better-auth-user.mapper.ts +29 -25
- package/src/core/modules/better-auth/core-better-auth.service.ts +13 -9
- 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/modules/tenant/INTEGRATION-CHECKLIST.md +13 -2
- package/src/core/modules/tenant/README.md +26 -1
- package/src/core/modules/tenant/core-tenant.guard.ts +142 -11
- package/src/core/modules/tenant/core-tenant.helpers.ts +6 -2
- package/src/core.module.ts +52 -10
- package/src/server/server.module.ts +7 -9
- package/src/test/README.md +47 -0
- package/src/test/test.helper.ts +55 -6
|
@@ -47,6 +47,10 @@ interface CachedTenantIds {
|
|
|
47
47
|
* - Plugin level: Safety net — ForbiddenException when tenantId-schema accessed without context
|
|
48
48
|
*
|
|
49
49
|
* Role check semantics:
|
|
50
|
+
* - System roles are OR alternatives, checked in order before real roles:
|
|
51
|
+
* S_EVERYONE → immediate pass; S_USER → pass if authenticated; S_VERIFIED → pass if verified
|
|
52
|
+
* - When a system role grants access and X-Tenant-Id header is present, membership is still
|
|
53
|
+
* validated to set tenant context (tenantId + tenantRole) on the request.
|
|
50
54
|
* - Hierarchy roles (in roleHierarchy config): level comparison — higher includes lower
|
|
51
55
|
* - Normal roles (not in roleHierarchy): exact match — no compensation by higher role
|
|
52
56
|
* - Tenant context (header present): checks against membership.role only (user.roles ignored)
|
|
@@ -61,9 +65,13 @@ interface CachedTenantIds {
|
|
|
61
65
|
* Flow:
|
|
62
66
|
* 1. Config check: multiTenancy enabled?
|
|
63
67
|
* 2. Parse header (X-Tenant-Id, max 128 chars, trimmed)
|
|
64
|
-
* 3. @
|
|
65
|
-
* 4.
|
|
66
|
-
*
|
|
68
|
+
* 3. Read @Roles() metadata (method + class level)
|
|
69
|
+
* 4. System role early-exit checks (OR alternatives):
|
|
70
|
+
* S_EVERYONE → pass immediately
|
|
71
|
+
* S_USER → pass if authenticated (+ optional membership check when header present)
|
|
72
|
+
* S_VERIFIED → pass if user is verified (+ optional membership check when header present)
|
|
73
|
+
* 5. @SkipTenantCheck → role check against user.roles, no tenant context
|
|
74
|
+
* 6. BetterAuth auto-skip (betterAuth.skipTenantCheck config + no header) → skip, no tenant context
|
|
67
75
|
*
|
|
68
76
|
* HEADER PRESENT:
|
|
69
77
|
* - System ADMIN (adminBypass: true) → set req.tenantId + isAdminBypass
|
|
@@ -184,24 +192,111 @@ export class CoreTenantGuard implements CanActivate, OnModuleDestroy {
|
|
|
184
192
|
const headerTenantId =
|
|
185
193
|
rawHeader && typeof rawHeader === 'string' && rawHeader.length <= 128 ? rawHeader.trim() : undefined;
|
|
186
194
|
|
|
187
|
-
//
|
|
195
|
+
// Two role sets for different purposes:
|
|
196
|
+
//
|
|
197
|
+
// 1. systemCheckRoles (method-takes-precedence): Used for system role early-returns.
|
|
198
|
+
// Method-level system roles override class-level ones to prevent e.g. class @Roles(S_EVERYONE)
|
|
199
|
+
// from making a method @Roles(S_USER) endpoint public.
|
|
200
|
+
//
|
|
201
|
+
// 2. roles (OR/merged): Used for real role checks (checkableRoles).
|
|
202
|
+
// Class-level roles serve as a base that method-level roles extend.
|
|
203
|
+
// E.g., class @Roles(ADMIN) + method @Roles('editor') → both are alternatives.
|
|
204
|
+
//
|
|
205
|
+
// S_EVERYONE check — access is always granted; no authentication or membership required.
|
|
206
|
+
//
|
|
207
|
+
// Header handling for S_EVERYONE:
|
|
208
|
+
// - No header → return true immediately (no tenant context needed)
|
|
209
|
+
// - Header present + authenticated user that IS a member → optionally enrich with tenant
|
|
210
|
+
// context (sets request.tenantId/tenantRole) so downstream consumers can use it.
|
|
211
|
+
// Access is NOT blocked if user is not a member — S_EVERYONE means public access.
|
|
188
212
|
const rolesMetadata = this.reflector.getAll<string[][]>('roles', [context.getHandler(), context.getClass()]);
|
|
189
213
|
const roles = mergeRolesMetadata(rolesMetadata);
|
|
214
|
+
const methodRoles: string[] = rolesMetadata[0] ?? [];
|
|
215
|
+
const systemCheckRoles = methodRoles.length > 0 ? methodRoles : roles;
|
|
216
|
+
|
|
217
|
+
// Defense-in-depth: S_NO_ONE is normally caught by RolesGuard/BetterAuthRolesGuard upstream,
|
|
218
|
+
// but guard it here too in case CoreTenantGuard runs standalone (e.g., custom guard chains).
|
|
219
|
+
if (roles.includes(RoleEnum.S_NO_ONE)) {
|
|
220
|
+
throw new ForbiddenException('Access denied');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const sEveryoneGrantsAccess: boolean = systemCheckRoles.includes(RoleEnum.S_EVERYONE);
|
|
224
|
+
if (sEveryoneGrantsAccess) {
|
|
225
|
+
// Optionally enrich with tenant context when header is present and user is an active member.
|
|
226
|
+
// Never block access — S_EVERYONE endpoints are always public.
|
|
227
|
+
if (headerTenantId && request.user?.id) {
|
|
228
|
+
const membership = await this.findMembershipCached(request.user.id, headerTenantId);
|
|
229
|
+
if (membership) {
|
|
230
|
+
request.tenantId = headerTenantId;
|
|
231
|
+
request.tenantRole = membership.role as string;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
190
236
|
|
|
191
237
|
const user = request.user;
|
|
192
238
|
const adminBypass = config.adminBypass !== false;
|
|
193
239
|
const isAdmin = adminBypass && user?.roles?.includes(RoleEnum.ADMIN);
|
|
194
240
|
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const minRequiredLevel = checkableRoles.length > 0 ? getMinRequiredLevel(checkableRoles) : undefined;
|
|
199
|
-
|
|
200
|
-
// @SkipTenantCheck decorator → no tenant context, but role check against user.roles
|
|
241
|
+
// Read @SkipTenantCheck early — it suppresses tenant membership validation for system roles too.
|
|
242
|
+
// When set, S_USER and S_VERIFIED still enforce authentication/verification, but no membership
|
|
243
|
+
// check is performed even when a tenant header is present.
|
|
201
244
|
const hasSkipDecorator = this.reflector.getAllAndOverride<boolean>(SKIP_TENANT_CHECK_KEY, [
|
|
202
245
|
context.getHandler(),
|
|
203
246
|
context.getClass(),
|
|
204
247
|
]);
|
|
248
|
+
|
|
249
|
+
// S_USER check — any authenticated user satisfies this system role.
|
|
250
|
+
//
|
|
251
|
+
// OR semantics: if S_USER is in the active role set, a logged-in user gets through.
|
|
252
|
+
// Real roles in the same @Roles() are ignored when S_USER is satisfied (they are alternatives).
|
|
253
|
+
// Example: @Roles(S_USER, 'owner') → a plain logged-in user passes (owner is an alternative, not required).
|
|
254
|
+
//
|
|
255
|
+
// Tenant header behavior: when X-Tenant-Id is present and @SkipTenantCheck is NOT set,
|
|
256
|
+
// membership is validated so that tenant context (tenantId, tenantRole) is set on the request.
|
|
257
|
+
// A non-member will still get 403 when a tenant header is provided (unless @SkipTenantCheck).
|
|
258
|
+
const sUserGrantsAccess: boolean = systemCheckRoles.includes(RoleEnum.S_USER);
|
|
259
|
+
if (sUserGrantsAccess) {
|
|
260
|
+
if (!user) {
|
|
261
|
+
throw new ForbiddenException('Authentication required');
|
|
262
|
+
}
|
|
263
|
+
if (headerTenantId && !hasSkipDecorator) {
|
|
264
|
+
return this.handleSystemRoleWithTenantHeader(user, headerTenantId, request, isAdmin);
|
|
265
|
+
}
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// S_VERIFIED check — any verified authenticated user satisfies this system role.
|
|
270
|
+
//
|
|
271
|
+
// A user is considered verified when any of these properties is truthy:
|
|
272
|
+
// user.verified, user.verifiedAt, user.emailVerified
|
|
273
|
+
//
|
|
274
|
+
// Tenant header behavior: same as S_USER — membership is validated when header is present
|
|
275
|
+
// (unless @SkipTenantCheck is set).
|
|
276
|
+
const sVerifiedGrantsAccess: boolean = systemCheckRoles.includes(RoleEnum.S_VERIFIED);
|
|
277
|
+
if (sVerifiedGrantsAccess) {
|
|
278
|
+
if (!user) {
|
|
279
|
+
throw new ForbiddenException('Authentication required');
|
|
280
|
+
}
|
|
281
|
+
const isVerified = !!(user.verified || user.verifiedAt || user.emailVerified);
|
|
282
|
+
if (!isVerified) {
|
|
283
|
+
throw new ForbiddenException('Verification required');
|
|
284
|
+
}
|
|
285
|
+
if (headerTenantId && !hasSkipDecorator) {
|
|
286
|
+
return this.handleSystemRoleWithTenantHeader(user, headerTenantId, request, isAdmin);
|
|
287
|
+
}
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Extract checkable (non-system) roles from the merged set.
|
|
292
|
+
// System roles that grant access (S_EVERYONE, S_USER, S_VERIFIED) have been
|
|
293
|
+
// early-returned above. Remaining system roles (S_SELF, S_CREATOR) are object-level
|
|
294
|
+
// and handled by interceptors.
|
|
295
|
+
const checkableRoles = roles.filter((r: string) => !isSystemRole(r));
|
|
296
|
+
|
|
297
|
+
const minRequiredLevel = checkableRoles.length > 0 ? getMinRequiredLevel(checkableRoles) : undefined;
|
|
298
|
+
|
|
299
|
+
// @SkipTenantCheck decorator → no tenant context, but role check against user.roles
|
|
205
300
|
if (hasSkipDecorator) {
|
|
206
301
|
return this.skipWithUserRoleCheck(checkableRoles, user, isAdmin);
|
|
207
302
|
}
|
|
@@ -238,7 +333,7 @@ export class CoreTenantGuard implements CanActivate, OnModuleDestroy {
|
|
|
238
333
|
const requiredRole = checkableRoles.length > 0 ? checkableRoles.join(',') : 'none';
|
|
239
334
|
// Sanitize control characters to prevent log injection
|
|
240
335
|
const safeTenantId = headerTenantId.replace(/[\r\n\t]/g, '_');
|
|
241
|
-
this.logger.
|
|
336
|
+
this.logger.debug(`Admin bypass: user ${user.id} accessing tenant ${safeTenantId} (required: ${requiredRole})`);
|
|
242
337
|
return true;
|
|
243
338
|
}
|
|
244
339
|
|
|
@@ -465,6 +560,42 @@ export class CoreTenantGuard implements CanActivate, OnModuleDestroy {
|
|
|
465
560
|
}
|
|
466
561
|
}
|
|
467
562
|
|
|
563
|
+
/**
|
|
564
|
+
* Validate tenant membership for a request that was granted access via a system role
|
|
565
|
+
* (S_USER or S_VERIFIED). When a tenant header is present, the user must be an active member
|
|
566
|
+
* of that tenant — unless the user is an admin with adminBypass enabled.
|
|
567
|
+
*
|
|
568
|
+
* On success, sets request.tenantId and request.tenantRole for downstream consumers.
|
|
569
|
+
*
|
|
570
|
+
* @param user - The authenticated request user
|
|
571
|
+
* @param headerTenantId - The validated, non-empty tenant ID from the request header
|
|
572
|
+
* @param request - The HTTP/GraphQL request object
|
|
573
|
+
* @param isAdmin - Whether the user has admin bypass privileges
|
|
574
|
+
*/
|
|
575
|
+
private async handleSystemRoleWithTenantHeader(
|
|
576
|
+
user: any,
|
|
577
|
+
headerTenantId: string,
|
|
578
|
+
request: any,
|
|
579
|
+
isAdmin: boolean,
|
|
580
|
+
): Promise<true> {
|
|
581
|
+
// Admin bypass: same behavior as the HEADER PRESENT admin path below
|
|
582
|
+
if (isAdmin) {
|
|
583
|
+
request.tenantId = headerTenantId;
|
|
584
|
+
request.isAdminBypass = true;
|
|
585
|
+
const safeTenantId = headerTenantId.replace(/[\r\n\t]/g, '_');
|
|
586
|
+
this.logger.debug(`Admin bypass (system-role path): user ${user.id} accessing tenant ${safeTenantId}`);
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const membership = await this.findMembershipCached(user.id, headerTenantId);
|
|
591
|
+
if (!membership) {
|
|
592
|
+
throw new ForbiddenException('Not a member of this tenant');
|
|
593
|
+
}
|
|
594
|
+
request.tenantId = headerTenantId;
|
|
595
|
+
request.tenantRole = membership.role as string;
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
|
|
468
599
|
/**
|
|
469
600
|
* Skip tenant validation but still check non-system roles against user.roles.
|
|
470
601
|
* Shared by @SkipTenantCheck decorator path and BetterAuth auto-skip path.
|
|
@@ -5,7 +5,10 @@ const SYSTEM_ROLE_PREFIX = 's_';
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Merge handler-level and class-level @Roles() metadata arrays into a single flat array.
|
|
8
|
-
* Used by RolesGuard, BetterAuthRolesGuard, and CoreTenantGuard
|
|
8
|
+
* Used by RolesGuard, BetterAuthRolesGuard, and CoreTenantGuard.
|
|
9
|
+
*
|
|
10
|
+
* OR semantics: class-level roles serve as a base that method-level roles extend.
|
|
11
|
+
* Example: class @Roles(ADMIN) + method @Roles(S_USER) → [S_USER, ADMIN] — both are alternatives.
|
|
9
12
|
*
|
|
10
13
|
* @param meta - Two-element tuple [handlerRoles, classRoles] from Reflector.getAll or Reflect.getMetadata
|
|
11
14
|
*/
|
|
@@ -22,7 +25,8 @@ export function getRoleHierarchy(): Record<string, number> {
|
|
|
22
25
|
|
|
23
26
|
/**
|
|
24
27
|
* Check if a role is a system role (S_USER, S_EVERYONE, etc.).
|
|
25
|
-
* System roles are
|
|
28
|
+
* System roles are checked by RolesGuard/BetterAuthRolesGuard for authentication
|
|
29
|
+
* and by CoreTenantGuard as OR alternatives before real role checks.
|
|
26
30
|
*/
|
|
27
31
|
export function isSystemRole(role: string): boolean {
|
|
28
32
|
return role.startsWith(SYSTEM_ROLE_PREFIX);
|
package/src/core.module.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { CheckResponseInterceptor } from './core/common/interceptors/check-respo
|
|
|
12
12
|
import { CheckSecurityInterceptor } from './core/common/interceptors/check-security.interceptor';
|
|
13
13
|
import { ResponseModelInterceptor } from './core/common/interceptors/response-model.interceptor';
|
|
14
14
|
import { TranslateResponseInterceptor } from './core/common/interceptors/translate-response.interceptor';
|
|
15
|
-
import { IServerOptions } from './core/common/interfaces/server-options.interface';
|
|
15
|
+
import { ICoreModuleOverrides, IServerOptions } from './core/common/interfaces/server-options.interface';
|
|
16
16
|
import { RequestContextMiddleware } from './core/common/middleware/request-context.middleware';
|
|
17
17
|
import { MapAndValidatePipe } from './core/common/pipes/map-and-validate.pipe';
|
|
18
18
|
import { ComplexityPlugin } from './core/common/plugins/complexity.plugin';
|
|
@@ -95,6 +95,12 @@ export class CoreModule implements NestModule {
|
|
|
95
95
|
*
|
|
96
96
|
* ```typescript
|
|
97
97
|
* CoreModule.forRoot(envConfig)
|
|
98
|
+
*
|
|
99
|
+
* // With module overrides (custom controllers/resolvers/services)
|
|
100
|
+
* CoreModule.forRoot(envConfig, {
|
|
101
|
+
* errorCode: { controller: ErrorCodeController, service: ErrorCodeService },
|
|
102
|
+
* betterAuth: { resolver: BetterAuthResolver },
|
|
103
|
+
* })
|
|
98
104
|
* ```
|
|
99
105
|
*
|
|
100
106
|
* Use this for new projects that only use BetterAuth (IAM) for authentication.
|
|
@@ -109,6 +115,11 @@ export class CoreModule implements NestModule {
|
|
|
109
115
|
*
|
|
110
116
|
* ```typescript
|
|
111
117
|
* CoreModule.forRoot(CoreAuthService, AuthModule.forRoot(envConfig.jwt), envConfig)
|
|
118
|
+
*
|
|
119
|
+
* // With module overrides
|
|
120
|
+
* CoreModule.forRoot(CoreAuthService, AuthModule.forRoot(envConfig.jwt), envConfig, {
|
|
121
|
+
* errorCode: { controller: ErrorCodeController, service: ErrorCodeService },
|
|
122
|
+
* })
|
|
112
123
|
* ```
|
|
113
124
|
*
|
|
114
125
|
* @deprecated This 3-parameter signature is deprecated for new projects.
|
|
@@ -127,22 +138,36 @@ export class CoreModule implements NestModule {
|
|
|
127
138
|
*
|
|
128
139
|
* @see https://github.com/lenneTech/nest-server/blob/develop/.claude/rules/module-deprecation.md
|
|
129
140
|
*/
|
|
130
|
-
static forRoot(options: Partial<IServerOptions
|
|
141
|
+
static forRoot(options: Partial<IServerOptions>, overrides?: ICoreModuleOverrides): DynamicModule;
|
|
131
142
|
/**
|
|
132
143
|
* @deprecated Use the single-parameter signature `CoreModule.forRoot(envConfig)` for new projects.
|
|
133
144
|
* This 3-parameter signature is for existing projects during migration to IAM.
|
|
134
145
|
*/
|
|
135
|
-
static forRoot(
|
|
146
|
+
static forRoot(
|
|
147
|
+
AuthService: any,
|
|
148
|
+
AuthModule: any,
|
|
149
|
+
options: Partial<IServerOptions>,
|
|
150
|
+
overrides?: ICoreModuleOverrides,
|
|
151
|
+
): DynamicModule;
|
|
136
152
|
static forRoot(
|
|
137
153
|
authServiceOrOptions: any,
|
|
138
154
|
authModuleOrUndefined?: any,
|
|
139
155
|
optionsOrUndefined?: Partial<IServerOptions>,
|
|
156
|
+
overridesOrUndefined?: ICoreModuleOverrides,
|
|
140
157
|
): DynamicModule {
|
|
141
|
-
// Detect which signature was used
|
|
142
|
-
|
|
158
|
+
// Detect which signature was used:
|
|
159
|
+
// IAM-only: forRoot(config, overrides?) — first arg is a plain object (config)
|
|
160
|
+
// Legacy: forRoot(AuthService, AuthModule, config, overrides?) — first arg is a class (function)
|
|
161
|
+
const isIamOnlyMode = typeof authServiceOrOptions !== 'function';
|
|
143
162
|
const AuthService = isIamOnlyMode ? null : authServiceOrOptions;
|
|
144
163
|
const AuthModule = isIamOnlyMode ? null : authModuleOrUndefined;
|
|
145
164
|
const options: Partial<IServerOptions> = isIamOnlyMode ? authServiceOrOptions : optionsOrUndefined;
|
|
165
|
+
// For IAM-only mode: overrides is the 2nd param; for legacy mode: it's the 4th param.
|
|
166
|
+
// The cast is safe: the public overloads guarantee the 2nd arg is ICoreModuleOverrides | undefined
|
|
167
|
+
// in IAM-only mode (typeof first arg !== 'function'), never a DynamicModule (AuthModule).
|
|
168
|
+
const overrides: ICoreModuleOverrides | undefined = isIamOnlyMode
|
|
169
|
+
? (authModuleOrUndefined as ICoreModuleOverrides | undefined)
|
|
170
|
+
: overridesOrUndefined;
|
|
146
171
|
|
|
147
172
|
// Process config
|
|
148
173
|
let cors = {};
|
|
@@ -316,11 +341,21 @@ export class CoreModule implements NestModule {
|
|
|
316
341
|
const errorCodeConfig = config.errorCode;
|
|
317
342
|
const isErrorCodeAutoRegister = errorCodeConfig?.autoRegister !== false;
|
|
318
343
|
|
|
344
|
+
if (!isErrorCodeAutoRegister && (overrides?.errorCode?.controller || overrides?.errorCode?.service)) {
|
|
345
|
+
console.warn(
|
|
346
|
+
'CoreModule: errorCode overrides are ignored because errorCode.autoRegister is false. ' +
|
|
347
|
+
'Either remove autoRegister: false or pass controller/service to your own ErrorCodeModule.forRoot() call.',
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
319
351
|
if (isErrorCodeAutoRegister) {
|
|
320
352
|
// Always use forRoot() - it registers the controller and handles configuration
|
|
353
|
+
// Overrides take precedence over config for controller/service
|
|
321
354
|
imports.push(
|
|
322
355
|
ErrorCodeModule.forRoot({
|
|
323
356
|
additionalErrorRegistry: errorCodeConfig?.additionalErrorRegistry,
|
|
357
|
+
controller: overrides?.errorCode?.controller,
|
|
358
|
+
service: overrides?.errorCode?.service,
|
|
324
359
|
}),
|
|
325
360
|
);
|
|
326
361
|
}
|
|
@@ -357,24 +392,31 @@ export class CoreModule implements NestModule {
|
|
|
357
392
|
// autoRegister: false means the project imports its own BetterAuthModule separately
|
|
358
393
|
const isAutoRegisterDisabled = typeof betterAuthConfig === 'object' && betterAuthConfig?.autoRegister === false;
|
|
359
394
|
|
|
360
|
-
// Extract custom controller/resolver
|
|
395
|
+
// Extract custom controller/resolver: overrides take precedence over config fields
|
|
361
396
|
const configController = typeof betterAuthConfig === 'object' ? betterAuthConfig?.controller : undefined;
|
|
362
397
|
const configResolver = typeof betterAuthConfig === 'object' ? betterAuthConfig?.resolver : undefined;
|
|
363
398
|
|
|
399
|
+
if (isAutoRegisterDisabled && (overrides?.betterAuth?.controller || overrides?.betterAuth?.resolver)) {
|
|
400
|
+
console.warn(
|
|
401
|
+
'CoreModule: betterAuth overrides are ignored because betterAuth.autoRegister is false. ' +
|
|
402
|
+
'Either remove autoRegister: false or pass controller/resolver to your own BetterAuthModule.forRoot() call.',
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
364
406
|
if (isBetterAuthEnabled) {
|
|
365
407
|
if ((isIamOnlyMode && !isAutoRegisterDisabled) || isAutoRegister) {
|
|
366
408
|
imports.push(
|
|
367
409
|
CoreBetterAuthModule.forRoot({
|
|
368
410
|
config: betterAuthConfig === true ? {} : betterAuthConfig || {},
|
|
369
|
-
//
|
|
370
|
-
controller: configController,
|
|
411
|
+
// Overrides take precedence over config fields (backward compatible)
|
|
412
|
+
controller: overrides?.betterAuth?.controller || configController,
|
|
371
413
|
// Pass JWT secrets for backwards compatibility fallback
|
|
372
414
|
fallbackSecrets: [config.jwt?.secret, config.jwt?.refresh?.secret],
|
|
373
415
|
// In IAM-only mode, register RolesGuard globally to enforce @Roles() decorators
|
|
374
416
|
// In Legacy mode (autoRegister), RolesGuard is already registered via CoreAuthModule
|
|
375
417
|
registerRolesGuardGlobally: isIamOnlyMode,
|
|
376
|
-
//
|
|
377
|
-
resolver: configResolver,
|
|
418
|
+
// Overrides take precedence over config fields (backward compatible)
|
|
419
|
+
resolver: overrides?.betterAuth?.resolver || configResolver,
|
|
378
420
|
// Pass server-level URLs for Passkey auto-detection
|
|
379
421
|
// When env: 'local', defaults are: baseUrl=localhost:3000, appUrl=localhost:3001
|
|
380
422
|
serverAppUrl: config.appUrl,
|
|
@@ -7,7 +7,6 @@ import { Any } from '../core/common/scalars/any.scalar';
|
|
|
7
7
|
import { DateScalar } from '../core/common/scalars/date.scalar';
|
|
8
8
|
import { JSON } from '../core/common/scalars/json.scalar';
|
|
9
9
|
import { CoreAuthService } from '../core/modules/auth/services/core-auth.service';
|
|
10
|
-
import { ErrorCodeModule } from '../core/modules/error-code/error-code.module';
|
|
11
10
|
import { TusModule } from '../core/modules/tus';
|
|
12
11
|
import { CronJobs } from './common/services/cron-jobs.service';
|
|
13
12
|
import { AuthController } from './modules/auth/auth.controller';
|
|
@@ -35,7 +34,13 @@ import { ServerController } from './server.controller';
|
|
|
35
34
|
imports: [
|
|
36
35
|
// Include CoreModule for standard processes
|
|
37
36
|
// Note: BetterAuthModule is imported manually below (autoRegister defaults to false)
|
|
38
|
-
CoreModule
|
|
37
|
+
// ErrorCodeModule is auto-registered by CoreModule with overrides for custom controller/service
|
|
38
|
+
CoreModule.forRoot(CoreAuthService, AuthModule.forRoot(envConfig.jwt), envConfig, {
|
|
39
|
+
errorCode: {
|
|
40
|
+
controller: ErrorCodeController,
|
|
41
|
+
service: ErrorCodeService,
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
39
44
|
|
|
40
45
|
// Include cron job handling
|
|
41
46
|
ScheduleModule.forRoot(),
|
|
@@ -49,13 +54,6 @@ import { ServerController } from './server.controller';
|
|
|
49
54
|
// This allows project-specific customization via BetterAuthResolver
|
|
50
55
|
BetterAuthModule.forRoot({}),
|
|
51
56
|
|
|
52
|
-
// Include ErrorCodeModule with project-specific error codes
|
|
53
|
-
// Uses Core ErrorCodeModule.forRoot() with custom service and controller
|
|
54
|
-
ErrorCodeModule.forRoot({
|
|
55
|
-
controller: ErrorCodeController,
|
|
56
|
-
service: ErrorCodeService,
|
|
57
|
-
}),
|
|
58
|
-
|
|
59
57
|
// Include FileModule for file handling
|
|
60
58
|
FileModule,
|
|
61
59
|
|
package/src/test/README.md
CHANGED
|
@@ -174,6 +174,53 @@ await testHelper.rest('/protected-endpoint', {
|
|
|
174
174
|
});
|
|
175
175
|
```
|
|
176
176
|
|
|
177
|
+
## File Download Testing (`testHelper.download()` / `testHelper.downloadBuffer()`)
|
|
178
|
+
|
|
179
|
+
### `download(url, tokenOrOptions?)`
|
|
180
|
+
|
|
181
|
+
Download a file and return the response with a `data` string property for content comparison.
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
// No authentication
|
|
185
|
+
const res = await testHelper.download('/files/id/abc123');
|
|
186
|
+
expect(res.statusCode).toEqual(200);
|
|
187
|
+
expect(res.data).toEqual('file content');
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### `downloadBuffer(url, tokenOrOptions?)`
|
|
191
|
+
|
|
192
|
+
Download a file and return a `Buffer` for binary comparison or saving.
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
const buffer = await testHelper.downloadBuffer('/files/id/abc123', jwtToken);
|
|
196
|
+
await fs.promises.writeFile('/tmp/downloaded.bin', buffer);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### TestDownloadOptions
|
|
200
|
+
|
|
201
|
+
The second parameter accepts either a plain token string or a `TestDownloadOptions` object:
|
|
202
|
+
|
|
203
|
+
| Option | Type | Description |
|
|
204
|
+
| --------- | -------- | ------------------------------------------------------------- |
|
|
205
|
+
| `token` | `string` | Bearer token via Authorization header (JWT) |
|
|
206
|
+
| `cookies` | `string` | Plain session token, converted via `buildBetterAuthCookies()` |
|
|
207
|
+
|
|
208
|
+
Both can be used simultaneously — `token` sets the Authorization header while `cookies` sets the Cookie header.
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
// String form (backward compatible) — sets Authorization: bearer <token>
|
|
212
|
+
await testHelper.download('/files/id/abc123', jwtToken);
|
|
213
|
+
|
|
214
|
+
// Options object with JWT token
|
|
215
|
+
await testHelper.download('/files/id/abc123', { token: jwtToken });
|
|
216
|
+
|
|
217
|
+
// Options object with cookie-based session auth
|
|
218
|
+
await testHelper.download('/files/id/abc123', { cookies: sessionToken });
|
|
219
|
+
|
|
220
|
+
// Both simultaneously
|
|
221
|
+
await testHelper.download('/files/id/abc123', { cookies: sessionToken, token: jwtToken });
|
|
222
|
+
```
|
|
223
|
+
|
|
177
224
|
## GraphQL Testing (`testHelper.graphQl()`)
|
|
178
225
|
|
|
179
226
|
```typescript
|
package/src/test/test.helper.ts
CHANGED
|
@@ -154,6 +154,25 @@ export interface TestRestOptions {
|
|
|
154
154
|
token?: string;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Options for download/downloadBuffer requests
|
|
159
|
+
*/
|
|
160
|
+
export interface TestDownloadOptions {
|
|
161
|
+
/**
|
|
162
|
+
* Cookie-based authentication. Pass a plain session token string
|
|
163
|
+
* which is converted via buildBetterAuthCookies() to all relevant cookie names
|
|
164
|
+
* (iam.session_token, token).
|
|
165
|
+
*/
|
|
166
|
+
cookies?: string;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Bearer token via Authorization header (JWT authentication).
|
|
170
|
+
* Can be used simultaneously with `cookies` — token sets the Authorization header
|
|
171
|
+
* while cookies sets the Cookie header.
|
|
172
|
+
*/
|
|
173
|
+
token?: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
157
176
|
/**
|
|
158
177
|
* Test helper
|
|
159
178
|
*/
|
|
@@ -176,13 +195,28 @@ export class TestHelper {
|
|
|
176
195
|
/**
|
|
177
196
|
* Download file from URL
|
|
178
197
|
* To compare content data via string comparison
|
|
198
|
+
* @param url - URL to download from
|
|
199
|
+
* @param tokenOrOptions - Bearer token string, or {@link TestDownloadOptions} with `cookies` and/or `token`
|
|
179
200
|
* @return Superagent response with additional data field containing the content of the file
|
|
180
201
|
*/
|
|
181
|
-
download(url: string,
|
|
202
|
+
download(url: string, tokenOrOptions?: string | TestDownloadOptions): Promise<any> {
|
|
182
203
|
return new Promise((resolve, reject) => {
|
|
183
204
|
const request = supertest((this.app as INestApplication).getHttpServer()).get(url);
|
|
184
|
-
if (
|
|
185
|
-
request.set('Authorization', `bearer ${
|
|
205
|
+
if (typeof tokenOrOptions === 'string') {
|
|
206
|
+
request.set('Authorization', `bearer ${tokenOrOptions}`);
|
|
207
|
+
} else if (tokenOrOptions) {
|
|
208
|
+
if (tokenOrOptions.cookies) {
|
|
209
|
+
const cookieRecord = TestHelper.buildBetterAuthCookies(tokenOrOptions.cookies);
|
|
210
|
+
request.set(
|
|
211
|
+
'Cookie',
|
|
212
|
+
Object.entries(cookieRecord)
|
|
213
|
+
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
|
|
214
|
+
.join('; '),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
if (tokenOrOptions.token) {
|
|
218
|
+
request.set('Authorization', `bearer ${tokenOrOptions.token}`);
|
|
219
|
+
}
|
|
186
220
|
}
|
|
187
221
|
let data = '';
|
|
188
222
|
request
|
|
@@ -207,12 +241,27 @@ export class TestHelper {
|
|
|
207
241
|
/**
|
|
208
242
|
* Download file from URL and get buffer
|
|
209
243
|
* To compare content data via buffer comparison and with the possibility to save the file
|
|
244
|
+
* @param url - URL to download from
|
|
245
|
+
* @param tokenOrOptions - Bearer token string, or {@link TestDownloadOptions} with `cookies` and/or `token`
|
|
210
246
|
*/
|
|
211
|
-
downloadBuffer(url: string,
|
|
247
|
+
downloadBuffer(url: string, tokenOrOptions?: string | TestDownloadOptions): Promise<Buffer> {
|
|
212
248
|
return new Promise((resolve, reject) => {
|
|
213
249
|
const request = supertest(this.app.getHttpServer()).get(url);
|
|
214
|
-
if (
|
|
215
|
-
request.set('Authorization', `bearer ${
|
|
250
|
+
if (typeof tokenOrOptions === 'string') {
|
|
251
|
+
request.set('Authorization', `bearer ${tokenOrOptions}`);
|
|
252
|
+
} else if (tokenOrOptions) {
|
|
253
|
+
if (tokenOrOptions.cookies) {
|
|
254
|
+
const cookieRecord = TestHelper.buildBetterAuthCookies(tokenOrOptions.cookies);
|
|
255
|
+
request.set(
|
|
256
|
+
'Cookie',
|
|
257
|
+
Object.entries(cookieRecord)
|
|
258
|
+
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
|
|
259
|
+
.join('; '),
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
if (tokenOrOptions.token) {
|
|
263
|
+
request.set('Authorization', `bearer ${tokenOrOptions.token}`);
|
|
264
|
+
}
|
|
216
265
|
}
|
|
217
266
|
|
|
218
267
|
// Array to store the data chunks
|