@lenne.tech/nest-server 11.7.1 → 11.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/core/common/interfaces/server-options.interface.d.ts +18 -15
  2. package/dist/core/modules/auth/core-auth.controller.js +2 -2
  3. package/dist/core/modules/auth/core-auth.controller.js.map +1 -1
  4. package/dist/core/modules/auth/core-auth.resolver.js +2 -2
  5. package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
  6. package/dist/core/modules/auth/guards/roles.guard.d.ts +12 -2
  7. package/dist/core/modules/auth/guards/roles.guard.js +192 -5
  8. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  9. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +1 -1
  10. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -1
  11. package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js +1 -1
  12. package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js.map +1 -1
  13. package/dist/core/modules/better-auth/better-auth-user.mapper.js +7 -55
  14. package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +1 -1
  15. package/dist/core/modules/better-auth/better-auth.config.js +29 -10
  16. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  17. package/dist/core/modules/better-auth/better-auth.middleware.d.ts +1 -0
  18. package/dist/core/modules/better-auth/better-auth.middleware.js +55 -1
  19. package/dist/core/modules/better-auth/better-auth.middleware.js.map +1 -1
  20. package/dist/core/modules/better-auth/better-auth.module.d.ts +1 -1
  21. package/dist/core/modules/better-auth/better-auth.module.js +46 -18
  22. package/dist/core/modules/better-auth/better-auth.module.js.map +1 -1
  23. package/dist/core/modules/better-auth/better-auth.resolver.js +0 -11
  24. package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
  25. package/dist/core/modules/better-auth/better-auth.service.d.ts +22 -1
  26. package/dist/core/modules/better-auth/better-auth.service.js +209 -8
  27. package/dist/core/modules/better-auth/better-auth.service.js.map +1 -1
  28. package/dist/core/modules/better-auth/better-auth.types.d.ts +2 -0
  29. package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
  30. package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +5 -0
  31. package/dist/core/modules/better-auth/core-better-auth.resolver.js +58 -12
  32. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
  33. package/dist/core/modules/user/core-user.service.d.ts +1 -0
  34. package/dist/core/modules/user/core-user.service.js +12 -0
  35. package/dist/core/modules/user/core-user.service.js.map +1 -1
  36. package/dist/core.module.js +6 -3
  37. package/dist/core.module.js.map +1 -1
  38. package/dist/server/modules/better-auth/better-auth.module.d.ts +1 -1
  39. package/dist/server/modules/better-auth/better-auth.module.js +2 -1
  40. package/dist/server/modules/better-auth/better-auth.module.js.map +1 -1
  41. package/dist/server/modules/better-auth/better-auth.resolver.d.ts +3 -0
  42. package/dist/server/modules/better-auth/better-auth.resolver.js +14 -11
  43. package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -1
  44. package/dist/server/modules/user/user.controller.js +0 -8
  45. package/dist/server/modules/user/user.controller.js.map +1 -1
  46. package/dist/tsconfig.build.tsbuildinfo +1 -1
  47. package/package.json +1 -1
  48. package/src/core/common/interfaces/server-options.interface.ts +129 -58
  49. package/src/core/modules/auth/core-auth.controller.ts +2 -2
  50. package/src/core/modules/auth/core-auth.resolver.ts +2 -2
  51. package/src/core/modules/auth/guards/roles.guard.ts +298 -5
  52. package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +1 -1
  53. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +12 -11
  54. package/src/core/modules/better-auth/README.md +82 -43
  55. package/src/core/modules/better-auth/better-auth-rate-limiter.service.ts +1 -1
  56. package/src/core/modules/better-auth/better-auth-user.mapper.ts +9 -77
  57. package/src/core/modules/better-auth/better-auth.config.ts +45 -15
  58. package/src/core/modules/better-auth/better-auth.middleware.ts +85 -2
  59. package/src/core/modules/better-auth/better-auth.module.ts +83 -27
  60. package/src/core/modules/better-auth/better-auth.resolver.ts +0 -11
  61. package/src/core/modules/better-auth/better-auth.service.ts +367 -12
  62. package/src/core/modules/better-auth/better-auth.types.ts +16 -0
  63. package/src/core/modules/better-auth/core-better-auth.resolver.ts +111 -16
  64. package/src/core/modules/user/core-user.service.ts +27 -0
  65. package/src/core.module.ts +9 -3
  66. package/src/server/modules/better-auth/better-auth.module.ts +9 -3
  67. package/src/server/modules/better-auth/better-auth.resolver.ts +9 -11
  68. package/src/server/modules/user/user.controller.ts +1 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.7.1",
3
+ "version": "11.7.3",
4
4
  "description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
5
5
  "keywords": [
6
6
  "node",
@@ -274,22 +274,29 @@ export interface IBetterAuth {
274
274
 
275
275
  /**
276
276
  * JWT plugin configuration for API clients.
277
- * Enabled by default when this config block is present.
278
- * Set `enabled: false` to explicitly disable.
277
+ *
278
+ * **Default: Enabled** - JWT is enabled by default when BetterAuth is enabled.
279
+ * This ensures a minimal config (`betterAuth: true`) provides full functionality.
280
+ *
281
+ * Accepts:
282
+ * - `true` or `{}`: Enable with defaults (same as not specifying)
283
+ * - `{ expiresIn: '1h' }`: Enable with custom settings
284
+ * - `false` or `{ enabled: false }`: Explicitly disable
285
+ * - `undefined`: Enabled with defaults (JWT is on by default)
286
+ *
287
+ * @example
288
+ * ```typescript
289
+ * // JWT is enabled by default, no config needed
290
+ * betterAuth: true,
291
+ *
292
+ * // Customize JWT expiry
293
+ * betterAuth: { jwt: { expiresIn: '1h' } },
294
+ *
295
+ * // Explicitly disable JWT (session-only mode)
296
+ * betterAuth: { jwt: false },
297
+ * ```
279
298
  */
280
- jwt?: {
281
- /**
282
- * Whether JWT plugin is enabled.
283
- * @default true (when jwt config block is present)
284
- */
285
- enabled?: boolean;
286
-
287
- /**
288
- * JWT expiration time
289
- * @default '15m'
290
- */
291
- expiresIn?: string;
292
- };
299
+ jwt?: boolean | IBetterAuthJwtConfig;
293
300
 
294
301
  /**
295
302
  * Advanced Better-Auth options passthrough.
@@ -322,34 +329,22 @@ export interface IBetterAuth {
322
329
 
323
330
  /**
324
331
  * Passkey/WebAuthn configuration.
325
- * Enabled by default when this config block is present.
326
- * Set `enabled: false` to explicitly disable.
332
+ *
333
+ * Accepts:
334
+ * - `true` or `{}`: Enable with defaults
335
+ * - `{ rpName: 'My App' }`: Enable with custom settings
336
+ * - `false` or `{ enabled: false }`: Disable
337
+ * - `undefined`: Disabled (default)
338
+ *
339
+ * @example
340
+ * ```typescript
341
+ * passkey: true, // Enable with defaults
342
+ * passkey: {}, // Enable with defaults
343
+ * passkey: { rpName: 'My App', rpId: 'example.com' }, // Enable with custom settings
344
+ * passkey: false, // Disable
345
+ * ```
327
346
  */
328
- passkey?: {
329
- /**
330
- * Whether passkey authentication is enabled.
331
- * @default true (when passkey config block is present)
332
- */
333
- enabled?: boolean;
334
-
335
- /**
336
- * Origin URL for WebAuthn
337
- * e.g. 'http://localhost:3000'
338
- */
339
- origin?: string;
340
-
341
- /**
342
- * Relying Party ID (usually the domain)
343
- * e.g. 'localhost' or 'example.com'
344
- */
345
- rpId?: string;
346
-
347
- /**
348
- * Relying Party Name (displayed to users)
349
- * e.g. 'My Application'
350
- */
351
- rpName?: string;
352
- };
347
+ passkey?: boolean | IBetterAuthPasskeyConfig;
353
348
 
354
349
  /**
355
350
  * Additional Better-Auth plugins to include.
@@ -410,22 +405,68 @@ export interface IBetterAuth {
410
405
 
411
406
  /**
412
407
  * Two-factor authentication configuration.
413
- * Enabled by default when this config block is present.
414
- * Set `enabled: false` to explicitly disable.
408
+ *
409
+ * Accepts:
410
+ * - `true` or `{}`: Enable with defaults
411
+ * - `{ appName: 'My App' }`: Enable with custom settings
412
+ * - `false` or `{ enabled: false }`: Disable
413
+ * - `undefined`: Disabled (default)
414
+ *
415
+ * @example
416
+ * ```typescript
417
+ * twoFactor: true, // Enable with defaults
418
+ * twoFactor: {}, // Enable with defaults
419
+ * twoFactor: { appName: 'My App' }, // Enable with custom app name
420
+ * twoFactor: false, // Disable
421
+ * ```
415
422
  */
416
- twoFactor?: {
417
- /**
418
- * App name shown in authenticator apps
419
- * e.g. 'My Application'
420
- */
421
- appName?: string;
423
+ twoFactor?: boolean | IBetterAuthTwoFactorConfig;
424
+ }
422
425
 
423
- /**
424
- * Whether 2FA is enabled.
425
- * @default true (when twoFactor config block is present)
426
- */
427
- enabled?: boolean;
428
- };
426
+ /**
427
+ * JWT plugin configuration for Better-Auth
428
+ */
429
+ export interface IBetterAuthJwtConfig {
430
+ /**
431
+ * Whether JWT plugin is enabled.
432
+ * @default true (when config block is present)
433
+ */
434
+ enabled?: boolean;
435
+
436
+ /**
437
+ * JWT expiration time
438
+ * @default '15m'
439
+ */
440
+ expiresIn?: string;
441
+ }
442
+
443
+ /**
444
+ * Passkey/WebAuthn plugin configuration for Better-Auth
445
+ */
446
+ export interface IBetterAuthPasskeyConfig {
447
+ /**
448
+ * Whether passkey authentication is enabled.
449
+ * @default true (when config block is present)
450
+ */
451
+ enabled?: boolean;
452
+
453
+ /**
454
+ * Origin URL for WebAuthn
455
+ * e.g. 'http://localhost:3000'
456
+ */
457
+ origin?: string;
458
+
459
+ /**
460
+ * Relying Party ID (usually the domain)
461
+ * e.g. 'localhost' or 'example.com'
462
+ */
463
+ rpId?: string;
464
+
465
+ /**
466
+ * Relying Party Name (displayed to users)
467
+ * e.g. 'My Application'
468
+ */
469
+ rpName?: string;
429
470
  }
430
471
 
431
472
  /**
@@ -505,6 +546,23 @@ export interface IBetterAuthSocialProvider {
505
546
  enabled?: boolean;
506
547
  }
507
548
 
549
+ /**
550
+ * Two-factor authentication plugin configuration for Better-Auth
551
+ */
552
+ export interface IBetterAuthTwoFactorConfig {
553
+ /**
554
+ * App name shown in authenticator apps
555
+ * e.g. 'My Application'
556
+ */
557
+ appName?: string;
558
+
559
+ /**
560
+ * Whether 2FA is enabled.
561
+ * @default true (when config block is present)
562
+ */
563
+ enabled?: boolean;
564
+ }
565
+
508
566
  /**
509
567
  * Interface for additional user fields in Better-Auth
510
568
  * @see https://www.better-auth.com/docs/concepts/users-accounts#additional-fields
@@ -598,10 +656,23 @@ export interface IServerOptions {
598
656
  automaticObjectIdFiltering?: boolean;
599
657
 
600
658
  /**
601
- * Configuration for better-auth authentication framework
659
+ * Configuration for better-auth authentication framework.
602
660
  * See: https://better-auth.com
661
+ *
662
+ * Accepts:
663
+ * - `true`: Enable with all defaults (including JWT)
664
+ * - `false`: Disable BetterAuth completely
665
+ * - `{ ... }`: Enable with custom configuration
666
+ * - `undefined`: Disabled (default for backward compatibility)
667
+ *
668
+ * @example
669
+ * ```typescript
670
+ * betterAuth: true, // Enable with defaults (JWT enabled)
671
+ * betterAuth: { baseUrl: 'https://example.com' }, // Custom config
672
+ * betterAuth: false, // Explicitly disabled
673
+ * ```
603
674
  */
604
- betterAuth?: IBetterAuth;
675
+ betterAuth?: boolean | IBetterAuth;
605
676
 
606
677
  /**
607
678
  * Configuration for Brevo
@@ -101,8 +101,8 @@ export class CoreAuthController {
101
101
  @ApiQuery({ description: 'If all devices should be logged out,', name: 'allDevices', required: false, type: Boolean })
102
102
  @ApiTooManyRequestsResponse({ description: 'Rate limit exceeded' })
103
103
  @Get('logout')
104
- @Roles(RoleEnum.S_EVERYONE)
105
- @UseGuards(LegacyAuthRateLimitGuard, AuthGuard(AuthGuardStrategy.JWT))
104
+ @Roles(RoleEnum.S_USER)
105
+ @UseGuards(LegacyAuthRateLimitGuard)
106
106
  async logout(
107
107
  @CurrentUser() currentUser: ICoreAuthUser,
108
108
  @Tokens('token') token: string,
@@ -92,8 +92,8 @@ export class CoreAuthResolver {
92
92
  * @throws LegacyAuthDisabledException if legacy endpoints are disabled
93
93
  */
94
94
  @Mutation(() => Boolean, { description: 'Logout user (from specific device)' })
95
- @Roles(RoleEnum.S_EVERYONE)
96
- @UseGuards(LegacyAuthRateLimitGuard, AuthGuard(AuthGuardStrategy.JWT))
95
+ @Roles(RoleEnum.S_USER)
96
+ @UseGuards(LegacyAuthRateLimitGuard)
97
97
  async logout(
98
98
  @CurrentUser() currentUser: ICoreAuthUser,
99
99
  @Context() ctx: { res: ResponseType },
@@ -1,8 +1,12 @@
1
- import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
2
- import { Reflector } from '@nestjs/core';
1
+ import { ExecutionContext, Injectable, Logger, Optional, UnauthorizedException } from '@nestjs/common';
2
+ import { ModuleRef, Reflector } from '@nestjs/core';
3
3
  import { GqlExecutionContext } from '@nestjs/graphql';
4
+ import { getConnectionToken } from '@nestjs/mongoose';
5
+ import { Connection, Types } from 'mongoose';
6
+ import { firstValueFrom, isObservable } from 'rxjs';
4
7
 
5
8
  import { RoleEnum } from '../../../common/enums/role.enum';
9
+ import { BetterAuthService } from '../../better-auth/better-auth.service';
6
10
  import { AuthGuardStrategy } from '../auth-guard-strategy.enum';
7
11
  import { ExpiredTokenException } from '../exceptions/expired-token.exception';
8
12
  import { InvalidTokenException } from '../exceptions/invalid-token.exception';
@@ -14,16 +18,305 @@ import { AuthGuard } from './auth.guard';
14
18
  * The RoleGuard is activated by the Role decorator. It checks whether the current user has at least one of the
15
19
  * specified roles or is logged in when the S_USER role is set.
16
20
  * If this is not the case, an UnauthorizedException is thrown.
21
+ *
22
+ * MULTI-TOKEN SUPPORT:
23
+ * This guard supports multiple authentication token types:
24
+ * 1. Legacy JWT tokens (Passport JWT strategy)
25
+ * 2. BetterAuth JWT tokens (verified via BetterAuth service)
26
+ * 3. BetterAuth session tokens (verified via database lookup)
27
+ *
28
+ * When Passport JWT validation fails, the guard falls back to BetterAuth verification:
29
+ * - First tries JWT verification if the JWT plugin is enabled
30
+ * - Then tries session token lookup via MongoDB
31
+ *
32
+ * This enables users who sign in via IAM (/iam/sign-in/email) to access all protected endpoints,
33
+ * regardless of whether they use JWT or session-based authentication.
17
34
  */
18
35
  @Injectable()
19
36
  export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
37
+ private readonly logger = new Logger(RolesGuard.name);
38
+ private betterAuthService: BetterAuthService | null = null;
39
+ private mongoConnection: Connection | null = null;
40
+ private servicesResolved = false;
41
+
20
42
  /**
21
- * Integrate reflector
43
+ * Integrate reflector and moduleRef for lazy service resolution
22
44
  */
23
- constructor(protected readonly reflector: Reflector) {
45
+ constructor(
46
+ protected readonly reflector: Reflector,
47
+ @Optional() private readonly moduleRef?: ModuleRef,
48
+ ) {
24
49
  super();
25
50
  }
26
51
 
52
+ /**
53
+ * Lazily resolve BetterAuth service and MongoDB connection
54
+ */
55
+ private resolveServices(): void {
56
+ if (this.servicesResolved || !this.moduleRef) {
57
+ return;
58
+ }
59
+
60
+ try {
61
+ this.betterAuthService = this.moduleRef.get(BetterAuthService, { strict: false });
62
+ } catch {
63
+ // BetterAuth not available - that's fine, we'll use Legacy JWT only
64
+ }
65
+
66
+ try {
67
+ // Get the Mongoose connection to query users directly
68
+ this.mongoConnection = this.moduleRef.get(getConnectionToken(), { strict: false });
69
+ } catch {
70
+ // MongoDB connection not available
71
+ }
72
+
73
+ this.servicesResolved = true;
74
+ }
75
+
76
+ /**
77
+ * Override canActivate to add BetterAuth JWT fallback
78
+ *
79
+ * Flow:
80
+ * 1. Try Passport JWT authentication (Legacy JWT)
81
+ * 2. If that fails, try BetterAuth JWT verification
82
+ * 3. If BetterAuth succeeds, load the user and proceed
83
+ */
84
+ override async canActivate(context: ExecutionContext): Promise<boolean> {
85
+ // Resolve services lazily
86
+ this.resolveServices();
87
+
88
+ // First, try the parent canActivate (Passport JWT)
89
+ try {
90
+ const result = super.canActivate(context);
91
+ return isObservable(result) ? await firstValueFrom(result) : await result;
92
+ } catch (passportError) {
93
+ // Passport JWT validation failed - try BetterAuth token fallback (JWT or session)
94
+ if (!this.betterAuthService?.isEnabled()) {
95
+ // BetterAuth not available - rethrow original error
96
+ throw passportError;
97
+ }
98
+
99
+ // Try to verify the token via BetterAuth (JWT or session token)
100
+ const user = await this.verifyBetterAuthTokenFromContext(context);
101
+ if (!user) {
102
+ // BetterAuth verification also failed - rethrow original Passport error
103
+ throw passportError;
104
+ }
105
+
106
+ // BetterAuth token is valid - set the user on the request
107
+ const request = this.getRequest(context);
108
+ if (request) {
109
+ request.user = user;
110
+ }
111
+
112
+ // Now call handleRequest with the BetterAuth-authenticated user to check roles
113
+ this.handleRequest(null, user, null, context);
114
+
115
+ return true;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Verify BetterAuth token (JWT or session) and load the corresponding user
121
+ *
122
+ * This method tries multiple verification strategies:
123
+ * 1. BetterAuth JWT verification (if JWT plugin is enabled)
124
+ * 2. BetterAuth session token lookup (database lookup)
125
+ *
126
+ * @param context - ExecutionContext to extract request from
127
+ * @returns User object if verification succeeds, null otherwise
128
+ */
129
+ private async verifyBetterAuthTokenFromContext(context: ExecutionContext): Promise<any> {
130
+ if (!this.betterAuthService || !this.mongoConnection) {
131
+ return null;
132
+ }
133
+
134
+ try {
135
+ // Get the raw HTTP request from multiple possible sources
136
+ let authHeader: string | undefined;
137
+
138
+ // Try GraphQL context first
139
+ try {
140
+ const gqlContext = GqlExecutionContext.create(context);
141
+ const ctx = gqlContext.getContext();
142
+ if (ctx?.req?.headers) {
143
+ authHeader = ctx.req.headers.authorization || ctx.req.headers.Authorization;
144
+ }
145
+ } catch {
146
+ // GraphQL context not available
147
+ }
148
+
149
+ // Fallback to HTTP context
150
+ if (!authHeader) {
151
+ try {
152
+ const httpRequest = context.switchToHttp().getRequest();
153
+ if (httpRequest?.headers) {
154
+ authHeader = httpRequest.headers.authorization || httpRequest.headers.Authorization;
155
+ }
156
+ } catch {
157
+ // HTTP context not available
158
+ }
159
+ }
160
+
161
+ let token: string | undefined;
162
+
163
+ if (authHeader?.startsWith('Bearer ')) {
164
+ token = authHeader.substring(7);
165
+ } else if (authHeader?.startsWith('bearer ')) {
166
+ // Handle lowercase 'bearer' as well
167
+ token = authHeader.substring(7);
168
+ }
169
+
170
+ if (!token) {
171
+ return null;
172
+ }
173
+
174
+ // Strategy 1: Try JWT verification (if JWT plugin is enabled)
175
+ if (this.betterAuthService.isJwtEnabled()) {
176
+ try {
177
+ const payload = await this.betterAuthService.verifyJwtToken(token);
178
+ if (payload?.sub) {
179
+ const user = await this.loadUserFromPayload(payload);
180
+ if (user) {
181
+ return user;
182
+ }
183
+ }
184
+ } catch {
185
+ // JWT verification failed - try session token next
186
+ }
187
+ }
188
+
189
+ // Strategy 2: Try session token lookup (database lookup)
190
+ try {
191
+ const sessionResult = await this.betterAuthService.getSessionByToken(token);
192
+ if (sessionResult?.user) {
193
+ return this.loadUserFromSessionResult(sessionResult.user);
194
+ }
195
+ } catch {
196
+ // Session lookup failed
197
+ }
198
+
199
+ return null;
200
+ } catch (error) {
201
+ this.logger.debug(
202
+ `BetterAuth token fallback failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
203
+ );
204
+ return null;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Load user from JWT payload using direct MongoDB query
210
+ *
211
+ * @param payload - JWT payload with sub (user ID or iamId)
212
+ * @returns User object with hasRole method
213
+ */
214
+ private async loadUserFromPayload(payload: { [key: string]: any; sub: string }): Promise<any> {
215
+ if (!this.mongoConnection) {
216
+ return null;
217
+ }
218
+
219
+ try {
220
+ const usersCollection = this.mongoConnection.collection('users');
221
+ let user: any = null;
222
+
223
+ // Try to find by MongoDB _id first
224
+ if (Types.ObjectId.isValid(payload.sub)) {
225
+ user = await usersCollection.findOne({ _id: new Types.ObjectId(payload.sub) });
226
+ }
227
+
228
+ // If not found, try by iamId
229
+ if (!user) {
230
+ user = await usersCollection.findOne({ iamId: payload.sub });
231
+ }
232
+
233
+ if (!user) {
234
+ return null;
235
+ }
236
+
237
+ // Convert MongoDB document to user-like object with hasRole method
238
+ const userObject = {
239
+ ...user,
240
+ _authenticatedViaBetterAuth: true,
241
+ // Add hasRole method for role checking
242
+ hasRole: (roles: string[]): boolean => {
243
+ if (!user.roles || !Array.isArray(user.roles)) {
244
+ return false;
245
+ }
246
+ return roles.some((role) => user.roles.includes(role));
247
+ },
248
+ id: user._id?.toString(),
249
+ };
250
+
251
+ return userObject;
252
+ } catch (error) {
253
+ this.logger.debug(
254
+ `Failed to load user from payload: ${error instanceof Error ? error.message : 'Unknown error'}`,
255
+ );
256
+ return null;
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Load user from session result (from getSessionByToken)
262
+ *
263
+ * @param sessionUser - User object from session lookup
264
+ * @returns User object with hasRole method
265
+ */
266
+ private async loadUserFromSessionResult(sessionUser: any): Promise<any> {
267
+ if (!this.mongoConnection || !sessionUser) {
268
+ return null;
269
+ }
270
+
271
+ try {
272
+ const usersCollection = this.mongoConnection.collection('users');
273
+
274
+ // The sessionUser might have id (BetterAuth ID) or email
275
+ // We need to find the corresponding user in our users collection
276
+ let user: any = null;
277
+
278
+ // Try to find by email (most reliable)
279
+ if (sessionUser.email) {
280
+ user = await usersCollection.findOne({ email: sessionUser.email });
281
+ }
282
+
283
+ // If not found by email, try by iamId
284
+ if (!user && sessionUser.id) {
285
+ user = await usersCollection.findOne({ iamId: sessionUser.id });
286
+ }
287
+
288
+ // If still not found, try by _id (if the ID looks like a MongoDB ObjectId)
289
+ if (!user && sessionUser.id && Types.ObjectId.isValid(sessionUser.id)) {
290
+ user = await usersCollection.findOne({ _id: new Types.ObjectId(sessionUser.id) });
291
+ }
292
+
293
+ if (!user) {
294
+ return null;
295
+ }
296
+
297
+ // Convert MongoDB document to user-like object with hasRole method
298
+ const userObject = {
299
+ ...user,
300
+ _authenticatedViaBetterAuth: true,
301
+ // Add hasRole method for role checking
302
+ hasRole: (roles: string[]): boolean => {
303
+ if (!user.roles || !Array.isArray(user.roles)) {
304
+ return false;
305
+ }
306
+ return roles.some((role) => user.roles.includes(role));
307
+ },
308
+ id: user._id?.toString(),
309
+ };
310
+
311
+ return userObject;
312
+ } catch (error) {
313
+ this.logger.debug(
314
+ `Failed to load user from session: ${error instanceof Error ? error.message : 'Unknown error'}`,
315
+ );
316
+ return null;
317
+ }
318
+ }
319
+
27
320
  /**
28
321
  * Handle request
29
322
  */
@@ -42,7 +335,7 @@ export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
42
335
  }
43
336
 
44
337
  // Check roles
45
- if (!roles || !roles.some(value => !!value)) {
338
+ if (!roles || !roles.some((value) => !!value)) {
46
339
  return user;
47
340
  }
48
341
 
@@ -114,7 +114,7 @@ export class LegacyAuthRateLimiter implements OnModuleInit {
114
114
  };
115
115
 
116
116
  if (this.config.enabled) {
117
- this.logger.log(
117
+ this.logger.debug(
118
118
  `Legacy Auth rate limiting enabled: ${this.config.max} requests per ${this.config.windowSeconds}s`,
119
119
  );
120
120
  }
@@ -54,7 +54,9 @@ https://github.com/lenneTech/nest-server/tree/develop/src/server/modules/better-
54
54
  **Copy from:** `node_modules/@lenne.tech/nest-server/src/server/modules/better-auth/better-auth.resolver.ts`
55
55
 
56
56
  **WHY must ALL decorators be re-declared?**
57
- GraphQL schema is built from decorators at compile time. The parent class (`CoreBetterAuthResolver`) is marked as `isAbstract: true`, so its methods are not registered in the schema. You MUST re-declare `@Query`, `@Mutation`, `@Roles`, `@UseGuards` decorators in the child class for the methods to appear in the GraphQL schema.
57
+ GraphQL schema is built from decorators at compile time. The parent class (`CoreBetterAuthResolver`) is marked as `isAbstract: true`, so its methods are not registered in the schema. You MUST re-declare `@Query`, `@Mutation`, `@Roles` decorators in the child class for the methods to appear in the GraphQL schema.
58
+
59
+ **Note:** `@UseGuards(AuthGuard(JWT))` is NOT needed when using `@Roles(S_USER)` or `@Roles(ADMIN)` because `RolesGuard` already extends `AuthGuard(JWT)` internally.
58
60
 
59
61
  ---
60
62
 
@@ -136,13 +138,14 @@ const config = {
136
138
  enabled: false,
137
139
  },
138
140
  },
139
- // BetterAuth configuration
141
+ // BetterAuth configuration (minimal - JWT enabled by default)
142
+ betterAuth: true, // or betterAuth: {} for same effect
143
+
144
+ // OR with optional features:
140
145
  betterAuth: {
141
- // enabled: true (default)
142
- // basePath: '/iam' (default)
143
- jwt: {}, // Enable JWT tokens
144
- twoFactor: {}, // Enable 2FA
145
- passkey: {}, // Enable Passkeys
146
+ twoFactor: {}, // Enable 2FA (opt-in)
147
+ passkey: {}, // Enable Passkeys (opt-in)
148
+ // JWT is already enabled by default
146
149
  },
147
150
  };
148
151
  ```
@@ -156,10 +159,8 @@ const config = {
156
159
  enabled: true, // Default - can disable after migration
157
160
  },
158
161
  },
159
- // BetterAuth configuration
160
- betterAuth: {
161
- // ... same as above
162
- },
162
+ // BetterAuth configuration (JWT enabled by default)
163
+ betterAuth: true, // Minimal config, or use object for more options
163
164
  };
164
165
  ```
165
166