@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.
- package/dist/core/common/interfaces/server-options.interface.d.ts +18 -15
- package/dist/core/modules/auth/core-auth.controller.js +2 -2
- package/dist/core/modules/auth/core-auth.controller.js.map +1 -1
- package/dist/core/modules/auth/core-auth.resolver.js +2 -2
- package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
- package/dist/core/modules/auth/guards/roles.guard.d.ts +12 -2
- package/dist/core/modules/auth/guards/roles.guard.js +192 -5
- package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +1 -1
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js +1 -1
- package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth-user.mapper.js +7 -55
- package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.config.js +29 -10
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.middleware.d.ts +1 -0
- package/dist/core/modules/better-auth/better-auth.middleware.js +55 -1
- package/dist/core/modules/better-auth/better-auth.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.module.d.ts +1 -1
- package/dist/core/modules/better-auth/better-auth.module.js +46 -18
- package/dist/core/modules/better-auth/better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.resolver.js +0 -11
- package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.service.d.ts +22 -1
- package/dist/core/modules/better-auth/better-auth.service.js +209 -8
- package/dist/core/modules/better-auth/better-auth.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.types.d.ts +2 -0
- package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +5 -0
- package/dist/core/modules/better-auth/core-better-auth.resolver.js +58 -12
- package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
- package/dist/core/modules/user/core-user.service.d.ts +1 -0
- package/dist/core/modules/user/core-user.service.js +12 -0
- package/dist/core/modules/user/core-user.service.js.map +1 -1
- package/dist/core.module.js +6 -3
- package/dist/core.module.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.module.d.ts +1 -1
- package/dist/server/modules/better-auth/better-auth.module.js +2 -1
- package/dist/server/modules/better-auth/better-auth.module.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.resolver.d.ts +3 -0
- package/dist/server/modules/better-auth/better-auth.resolver.js +14 -11
- package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -1
- package/dist/server/modules/user/user.controller.js +0 -8
- package/dist/server/modules/user/user.controller.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/core/common/interfaces/server-options.interface.ts +129 -58
- package/src/core/modules/auth/core-auth.controller.ts +2 -2
- package/src/core/modules/auth/core-auth.resolver.ts +2 -2
- package/src/core/modules/auth/guards/roles.guard.ts +298 -5
- package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +1 -1
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +12 -11
- package/src/core/modules/better-auth/README.md +82 -43
- package/src/core/modules/better-auth/better-auth-rate-limiter.service.ts +1 -1
- package/src/core/modules/better-auth/better-auth-user.mapper.ts +9 -77
- package/src/core/modules/better-auth/better-auth.config.ts +45 -15
- package/src/core/modules/better-auth/better-auth.middleware.ts +85 -2
- package/src/core/modules/better-auth/better-auth.module.ts +83 -27
- package/src/core/modules/better-auth/better-auth.resolver.ts +0 -11
- package/src/core/modules/better-auth/better-auth.service.ts +367 -12
- package/src/core/modules/better-auth/better-auth.types.ts +16 -0
- package/src/core/modules/better-auth/core-better-auth.resolver.ts +111 -16
- package/src/core/modules/user/core-user.service.ts +27 -0
- package/src/core.module.ts +9 -3
- package/src/server/modules/better-auth/better-auth.module.ts +9 -3
- package/src/server/modules/better-auth/better-auth.resolver.ts +9 -11
- 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.
|
|
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
|
-
*
|
|
278
|
-
*
|
|
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
|
-
*
|
|
326
|
-
*
|
|
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
|
-
*
|
|
414
|
-
*
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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.
|
|
105
|
-
@UseGuards(LegacyAuthRateLimitGuard
|
|
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.
|
|
96
|
-
@UseGuards(LegacyAuthRateLimitGuard
|
|
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(
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
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
|
|