@lenne.tech/nest-server 11.8.0 → 11.10.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/dist/config.env.js +5 -0
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/logging.helper.d.ts +6 -0
- package/dist/core/common/helpers/logging.helper.js +55 -0
- package/dist/core/common/helpers/logging.helper.js.map +1 -0
- package/dist/core/common/interfaces/server-options.interface.d.ts +50 -19
- package/dist/core/modules/auth/guards/roles.guard.js +37 -5
- package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
- package/dist/core/modules/auth/services/core-auth.service.d.ts +5 -5
- package/dist/core/modules/auth/services/core-auth.service.js +9 -8
- package/dist/core/modules/auth/services/core-auth.service.js.map +1 -1
- package/dist/core/modules/auth/tokens.decorator.d.ts +1 -1
- package/dist/core/modules/better-auth/better-auth.config.js +32 -10
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.resolver.d.ts +16 -16
- package/dist/core/modules/better-auth/better-auth.resolver.js +34 -34
- package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.types.d.ts +2 -1
- package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.d.ts +10 -0
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +91 -0
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-auth.model.d.ts +9 -0
- package/dist/core/modules/better-auth/{better-auth-auth.model.js → core-better-auth-auth.model.js} +17 -17
- package/dist/core/modules/better-auth/core-better-auth-auth.model.js.map +1 -0
- package/dist/core/modules/better-auth/{better-auth-migration-status.model.d.ts → core-better-auth-migration-status.model.d.ts} +1 -1
- package/dist/core/modules/better-auth/{better-auth-migration-status.model.js → core-better-auth-migration-status.model.js} +14 -14
- package/dist/core/modules/better-auth/core-better-auth-migration-status.model.js.map +1 -0
- package/dist/core/modules/better-auth/{better-auth-models.d.ts → core-better-auth-models.d.ts} +8 -8
- package/dist/core/modules/better-auth/{better-auth-models.js → core-better-auth-models.js} +61 -61
- package/dist/core/modules/better-auth/core-better-auth-models.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.d.ts +12 -0
- package/dist/core/modules/better-auth/{better-auth-rate-limit.middleware.js → core-better-auth-rate-limit.middleware.js} +10 -10
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -0
- package/dist/core/modules/better-auth/{better-auth-rate-limiter.service.d.ts → core-better-auth-rate-limiter.service.d.ts} +1 -1
- package/dist/core/modules/better-auth/{better-auth-rate-limiter.service.js → core-better-auth-rate-limiter.service.js} +8 -8
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -0
- package/dist/core/modules/better-auth/{better-auth-user.mapper.d.ts → core-better-auth-user.mapper.d.ts} +1 -1
- package/dist/core/modules/better-auth/{better-auth-user.mapper.js → core-better-auth-user.mapper.js} +10 -9
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-web.helper.d.ts +19 -0
- package/dist/core/modules/better-auth/core-better-auth-web.helper.js +152 -0
- package/dist/core/modules/better-auth/core-better-auth-web.helper.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +23 -32
- package/dist/core/modules/better-auth/core-better-auth.controller.js +184 -201
- package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.middleware.d.ts +22 -0
- package/dist/core/modules/better-auth/{better-auth.middleware.js → core-better-auth.middleware.js} +45 -18
- package/dist/core/modules/better-auth/core-better-auth.middleware.js.map +1 -0
- package/dist/core/modules/better-auth/{better-auth.module.d.ts → core-better-auth.module.d.ts} +6 -6
- package/dist/core/modules/better-auth/{better-auth.module.js → core-better-auth.module.js} +65 -60
- package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +19 -19
- package/dist/core/modules/better-auth/core-better-auth.resolver.js +18 -18
- package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
- package/dist/core/modules/better-auth/{better-auth.service.d.ts → core-better-auth.service.d.ts} +3 -2
- package/dist/core/modules/better-auth/{better-auth.service.js → core-better-auth.service.js} +75 -35
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -0
- package/dist/core/modules/better-auth/index.d.ts +11 -9
- package/dist/core/modules/better-auth/index.js +11 -9
- package/dist/core/modules/better-auth/index.js.map +1 -1
- package/dist/core/modules/error-code/core-error-code.controller.d.ts +7 -0
- package/dist/core/modules/error-code/core-error-code.controller.js +45 -0
- package/dist/core/modules/error-code/core-error-code.controller.js.map +1 -0
- package/dist/core/modules/error-code/core-error-code.service.d.ts +16 -0
- package/dist/core/modules/error-code/core-error-code.service.js +65 -0
- package/dist/core/modules/error-code/core-error-code.service.js.map +1 -0
- package/dist/core/modules/error-code/error-code.module.d.ts +7 -0
- package/dist/core/modules/error-code/error-code.module.js +64 -0
- package/dist/core/modules/error-code/error-code.module.js.map +1 -0
- package/dist/core/modules/error-code/error-codes.d.ts +219 -0
- package/dist/core/modules/error-code/error-codes.js +204 -0
- package/dist/core/modules/error-code/error-codes.js.map +1 -0
- package/dist/core/modules/error-code/index.d.ts +5 -0
- package/dist/core/modules/error-code/index.js +22 -0
- package/dist/core/modules/error-code/index.js.map +1 -0
- package/dist/core/modules/error-code/interfaces/error-code.interfaces.d.ts +12 -0
- package/dist/core/modules/error-code/interfaces/error-code.interfaces.js +3 -0
- package/dist/core/modules/error-code/interfaces/error-code.interfaces.js.map +1 -0
- package/dist/core/modules/user/interfaces/core-user-service-options.interface.d.ts +2 -2
- package/dist/core.module.js +14 -6
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.controller.d.ts +5 -5
- package/dist/server/modules/better-auth/better-auth.controller.js +4 -4
- package/dist/server/modules/better-auth/better-auth.controller.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.module.js +3 -3
- package/dist/server/modules/better-auth/better-auth.module.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.resolver.d.ts +17 -17
- package/dist/server/modules/better-auth/better-auth.resolver.js +18 -18
- package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -1
- package/dist/server/modules/error-code/error-code.controller.d.ts +8 -0
- package/dist/server/modules/error-code/error-code.controller.js +55 -0
- package/dist/server/modules/error-code/error-code.controller.js.map +1 -0
- package/dist/server/modules/error-code/error-code.service.d.ts +4 -0
- package/dist/server/modules/error-code/error-code.service.js +27 -0
- package/dist/server/modules/error-code/error-code.service.js.map +1 -0
- package/dist/server/modules/error-code/error-codes.d.ts +45 -0
- package/dist/server/modules/error-code/error-codes.js +24 -0
- package/dist/server/modules/error-code/error-codes.js.map +1 -0
- package/dist/server/modules/error-code/index.d.ts +3 -0
- package/dist/server/modules/error-code/index.js +20 -0
- package/dist/server/modules/error-code/index.js.map +1 -0
- package/dist/server/modules/user/user.service.d.ts +2 -2
- package/dist/server/modules/user/user.service.js +2 -2
- package/dist/server/modules/user/user.service.js.map +1 -1
- package/dist/server/server.module.js +7 -0
- package/dist/server/server.module.js.map +1 -1
- package/dist/test/test.helper.d.ts +1 -0
- package/dist/test/test.helper.js +5 -1
- package/dist/test/test.helper.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +6 -4
- package/src/config.env.ts +19 -0
- package/src/core/common/helpers/logging.helper.ts +134 -0
- package/src/core/common/interfaces/server-options.interface.ts +511 -237
- package/src/core/modules/auth/guards/roles.guard.ts +49 -7
- package/src/core/modules/auth/services/core-auth.service.ts +9 -8
- package/src/core/modules/better-auth/ARCHITECTURE.md +102 -0
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +277 -8
- package/src/core/modules/better-auth/README.md +97 -53
- package/src/core/modules/better-auth/better-auth.config.ts +66 -18
- package/src/core/modules/better-auth/better-auth.resolver.ts +32 -32
- package/src/core/modules/better-auth/better-auth.types.ts +3 -2
- package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +134 -0
- package/src/core/modules/better-auth/{better-auth-auth.model.ts → core-better-auth-auth.model.ts} +6 -6
- package/src/core/modules/better-auth/{better-auth-migration-status.model.ts → core-better-auth-migration-status.model.ts} +1 -1
- package/src/core/modules/better-auth/{better-auth-models.ts → core-better-auth-models.ts} +9 -9
- package/src/core/modules/better-auth/{better-auth-rate-limit.middleware.ts → core-better-auth-rate-limit.middleware.ts} +5 -5
- package/src/core/modules/better-auth/{better-auth-rate-limiter.service.ts → core-better-auth-rate-limiter.service.ts} +2 -2
- package/src/core/modules/better-auth/{better-auth-user.mapper.ts → core-better-auth-user.mapper.ts} +4 -3
- package/src/core/modules/better-auth/core-better-auth-web.helper.ts +272 -0
- package/src/core/modules/better-auth/core-better-auth.controller.ts +386 -230
- package/src/core/modules/better-auth/{better-auth.middleware.ts → core-better-auth.middleware.ts} +57 -17
- package/src/core/modules/better-auth/{better-auth.module.ts → core-better-auth.module.ts} +77 -66
- package/src/core/modules/better-auth/core-better-auth.resolver.ts +42 -42
- package/src/core/modules/better-auth/{better-auth.service.ts → core-better-auth.service.ts} +86 -40
- package/src/core/modules/better-auth/index.ts +18 -11
- package/src/core/modules/error-code/INTEGRATION-CHECKLIST.md +291 -0
- package/src/core/modules/error-code/core-error-code.controller.ts +55 -0
- package/src/core/modules/error-code/core-error-code.service.ts +135 -0
- package/src/core/modules/error-code/error-code.module.ts +119 -0
- package/src/core/modules/error-code/error-codes.ts +405 -0
- package/src/core/modules/error-code/index.ts +14 -0
- package/src/core/modules/error-code/interfaces/error-code.interfaces.ts +99 -0
- package/src/core/modules/user/interfaces/core-user-service-options.interface.ts +3 -3
- package/src/core.module.ts +28 -12
- package/src/index.ts +7 -0
- package/src/server/modules/better-auth/better-auth.controller.ts +4 -4
- package/src/server/modules/better-auth/better-auth.module.ts +1 -1
- package/src/server/modules/better-auth/better-auth.resolver.ts +31 -31
- package/src/server/modules/error-code/README.md +131 -0
- package/src/server/modules/error-code/error-code.controller.ts +91 -0
- package/src/server/modules/error-code/error-code.service.ts +42 -0
- package/src/server/modules/error-code/error-codes.ts +65 -0
- package/src/server/modules/error-code/index.ts +8 -0
- package/src/server/modules/user/user.service.ts +2 -2
- package/src/server/server.module.ts +10 -0
- package/src/test/test.helper.ts +13 -1
- package/dist/core/modules/better-auth/better-auth-auth.model.d.ts +0 -9
- package/dist/core/modules/better-auth/better-auth-auth.model.js.map +0 -1
- package/dist/core/modules/better-auth/better-auth-migration-status.model.js.map +0 -1
- package/dist/core/modules/better-auth/better-auth-models.js.map +0 -1
- package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.d.ts +0 -12
- package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.js.map +0 -1
- package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js.map +0 -1
- package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +0 -1
- package/dist/core/modules/better-auth/better-auth.middleware.d.ts +0 -21
- package/dist/core/modules/better-auth/better-auth.middleware.js.map +0 -1
- package/dist/core/modules/better-auth/better-auth.module.js.map +0 -1
- package/dist/core/modules/better-auth/better-auth.service.js.map +0 -1
|
@@ -1,41 +1,41 @@
|
|
|
1
1
|
import {
|
|
2
|
+
All,
|
|
2
3
|
BadRequestException,
|
|
3
4
|
Body,
|
|
4
5
|
Controller,
|
|
5
6
|
Get,
|
|
6
7
|
HttpCode,
|
|
7
8
|
HttpStatus,
|
|
9
|
+
InternalServerErrorException,
|
|
8
10
|
Logger,
|
|
9
11
|
Post,
|
|
10
12
|
Req,
|
|
11
13
|
Res,
|
|
12
14
|
UnauthorizedException,
|
|
13
15
|
} from '@nestjs/common';
|
|
14
|
-
import { ApiBody, ApiCreatedResponse, ApiOkResponse, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
|
|
16
|
+
import { ApiBody, ApiCreatedResponse, ApiExcludeEndpoint, ApiOkResponse, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
|
|
15
17
|
import { Request, Response } from 'express';
|
|
16
18
|
|
|
17
19
|
import { Roles } from '../../common/decorators/roles.decorator';
|
|
18
20
|
import { RoleEnum } from '../../common/enums/role.enum';
|
|
21
|
+
import { isProduction, maskToken } from '../../common/helpers/logging.helper';
|
|
19
22
|
import { ConfigService } from '../../common/services/config.service';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
+
import { BetterAuthSignInResponse, hasSession, hasUser, requires2FA } from './better-auth.types';
|
|
24
|
+
import { BetterAuthSessionUser, CoreBetterAuthUserMapper } from './core-better-auth-user.mapper';
|
|
25
|
+
import { sendWebResponse, toWebRequest } from './core-better-auth-web.helper';
|
|
26
|
+
import { CoreBetterAuthService } from './core-better-auth.service';
|
|
23
27
|
|
|
24
28
|
// ===================================================================================================================
|
|
25
29
|
// Response Models
|
|
26
30
|
// ===================================================================================================================
|
|
27
31
|
|
|
28
|
-
/**
|
|
29
|
-
* Token response interface for JWT tokens
|
|
30
|
-
*/
|
|
31
|
-
interface TokenResponse {
|
|
32
|
-
token?: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
32
|
/**
|
|
36
33
|
* Session info for REST responses
|
|
34
|
+
*
|
|
35
|
+
* NOTE: The session token is NOT included in this response for security reasons.
|
|
36
|
+
* It is set as an httpOnly cookie instead.
|
|
37
37
|
*/
|
|
38
|
-
export class
|
|
38
|
+
export class CoreBetterAuthSessionInfo {
|
|
39
39
|
@ApiProperty({ description: 'Session expiration time' })
|
|
40
40
|
expiresAt: string;
|
|
41
41
|
|
|
@@ -46,7 +46,7 @@ export class BetterAuthSessionInfo {
|
|
|
46
46
|
/**
|
|
47
47
|
* User model for REST responses
|
|
48
48
|
*/
|
|
49
|
-
export class
|
|
49
|
+
export class CoreBetterAuthUserResponse {
|
|
50
50
|
@ApiProperty({ description: 'User email address' })
|
|
51
51
|
email: string;
|
|
52
52
|
|
|
@@ -66,15 +66,15 @@ export class BetterAuthUserResponse {
|
|
|
66
66
|
/**
|
|
67
67
|
* Standard auth response
|
|
68
68
|
*/
|
|
69
|
-
export class
|
|
69
|
+
export class CoreBetterAuthResponse {
|
|
70
70
|
@ApiProperty({ description: 'Error message if failed', required: false })
|
|
71
71
|
error?: string;
|
|
72
72
|
|
|
73
73
|
@ApiProperty({ description: 'Whether 2FA is required', required: false })
|
|
74
74
|
requiresTwoFactor?: boolean;
|
|
75
75
|
|
|
76
|
-
@ApiProperty({ description: 'Session information', required: false, type:
|
|
77
|
-
session?:
|
|
76
|
+
@ApiProperty({ description: 'Session information', required: false, type: CoreBetterAuthSessionInfo })
|
|
77
|
+
session?: CoreBetterAuthSessionInfo;
|
|
78
78
|
|
|
79
79
|
@ApiProperty({ description: 'Whether operation succeeded' })
|
|
80
80
|
success: boolean;
|
|
@@ -82,8 +82,8 @@ export class BetterAuthResponse {
|
|
|
82
82
|
@ApiProperty({ description: 'JWT token (if JWT plugin enabled)', required: false })
|
|
83
83
|
token?: string;
|
|
84
84
|
|
|
85
|
-
@ApiProperty({ description: 'User information', required: false, type:
|
|
86
|
-
user?:
|
|
85
|
+
@ApiProperty({ description: 'User information', required: false, type: CoreBetterAuthUserResponse })
|
|
86
|
+
user?: CoreBetterAuthUserResponse;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// ===================================================================================================================
|
|
@@ -93,7 +93,7 @@ export class BetterAuthResponse {
|
|
|
93
93
|
/**
|
|
94
94
|
* Sign-in input DTO
|
|
95
95
|
*/
|
|
96
|
-
export class
|
|
96
|
+
export class CoreBetterAuthSignInInput {
|
|
97
97
|
@ApiProperty({ description: 'User email address', example: 'user@example.com' })
|
|
98
98
|
email: string;
|
|
99
99
|
|
|
@@ -104,7 +104,7 @@ export class BetterAuthSignInInput {
|
|
|
104
104
|
/**
|
|
105
105
|
* Sign-up input DTO
|
|
106
106
|
*/
|
|
107
|
-
export class
|
|
107
|
+
export class CoreBetterAuthSignUpInput {
|
|
108
108
|
@ApiProperty({ description: 'User email address', example: 'user@example.com' })
|
|
109
109
|
email: string;
|
|
110
110
|
|
|
@@ -115,31 +115,6 @@ export class BetterAuthSignUpInput {
|
|
|
115
115
|
password: string;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
/**
|
|
119
|
-
* 2FA verification input DTO
|
|
120
|
-
*/
|
|
121
|
-
export class BetterAuthTwoFactorInput {
|
|
122
|
-
@ApiProperty({ description: 'TOTP code from authenticator app', example: '123456' })
|
|
123
|
-
code: string;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* 2FA setup response
|
|
128
|
-
*/
|
|
129
|
-
export class BetterAuthTwoFactorSetupResponse {
|
|
130
|
-
@ApiProperty({ description: 'Backup codes for recovery' })
|
|
131
|
-
backupCodes: string[];
|
|
132
|
-
|
|
133
|
-
@ApiProperty({ description: 'Whether operation succeeded' })
|
|
134
|
-
success: boolean;
|
|
135
|
-
|
|
136
|
-
@ApiProperty({ description: 'TOTP secret for manual entry' })
|
|
137
|
-
totpSecret: string;
|
|
138
|
-
|
|
139
|
-
@ApiProperty({ description: 'QR code URI for authenticator apps' })
|
|
140
|
-
totpUri: string;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
118
|
// ===================================================================================================================
|
|
144
119
|
// Controller
|
|
145
120
|
// ===================================================================================================================
|
|
@@ -151,21 +126,47 @@ export class BetterAuthTwoFactorSetupResponse {
|
|
|
151
126
|
* This controller follows the same pattern as CoreAuthController and can be
|
|
152
127
|
* extended by project-specific implementations.
|
|
153
128
|
*
|
|
129
|
+
* ## Why Custom Controller Instead of Native Better-Auth Endpoints?
|
|
130
|
+
*
|
|
131
|
+
* This controller implements custom endpoints rather than directly using Better-Auth's
|
|
132
|
+
* native API. This architecture is **necessary** for nest-server's requirements:
|
|
133
|
+
*
|
|
134
|
+
* ### 1. Better-Auth Hooks Cannot:
|
|
135
|
+
* - Access plaintext passwords in after-hooks (needed for Legacy sync)
|
|
136
|
+
* - Modify HTTP responses (needed for custom response format)
|
|
137
|
+
* - Set cookies (needed for multi-cookie auth strategy)
|
|
138
|
+
* - Access NestJS Dependency Injection (needed for UserService, etc.)
|
|
139
|
+
*
|
|
140
|
+
* ### 2. Custom Endpoints Enable:
|
|
141
|
+
* - **Hybrid Auth**: Bidirectional Legacy Auth ↔ Better-Auth synchronization
|
|
142
|
+
* - **Password Normalization**: SHA256 pre-hashing for security
|
|
143
|
+
* - **Legacy Migration**: Automatic migration of legacy users on sign-in
|
|
144
|
+
* - **Multi-Cookie Support**: Setting multiple auth cookies for compatibility
|
|
145
|
+
* - **Role Mapping**: Integration with nest-server's role-based access control
|
|
146
|
+
*
|
|
147
|
+
* ### 3. Native Handler Where Possible:
|
|
148
|
+
* Despite custom endpoints, we use `authInstance.handler()` for:
|
|
149
|
+
* - Plugin routes (Passkey, 2FA, OAuth)
|
|
150
|
+
* - 2FA verification (for correct cookie handling)
|
|
151
|
+
* - All plugin-provided functionality
|
|
152
|
+
*
|
|
153
|
+
* See README.md section "Architecture: Why Custom Controllers?" for details.
|
|
154
|
+
*
|
|
154
155
|
* @example
|
|
155
156
|
* ```typescript
|
|
156
157
|
* // In your project - src/server/modules/better-auth/better-auth.controller.ts
|
|
157
158
|
* @Controller('iam')
|
|
158
159
|
* export class BetterAuthController extends CoreBetterAuthController {
|
|
159
160
|
* constructor(
|
|
160
|
-
* betterAuthService:
|
|
161
|
-
* userMapper:
|
|
161
|
+
* betterAuthService: CoreBetterAuthService,
|
|
162
|
+
* userMapper: CoreBetterAuthUserMapper,
|
|
162
163
|
* configService: ConfigService,
|
|
163
164
|
* private readonly emailService: EmailService,
|
|
164
165
|
* ) {
|
|
165
166
|
* super(betterAuthService, userMapper, configService);
|
|
166
167
|
* }
|
|
167
168
|
*
|
|
168
|
-
* override async signUp(res: Response, input:
|
|
169
|
+
* override async signUp(res: Response, input: CoreBetterAuthSignUpInput) {
|
|
169
170
|
* const result = await super.signUp(res, input);
|
|
170
171
|
* if (result.success && result.user) {
|
|
171
172
|
* await this.emailService.sendWelcomeEmail(result.user.email);
|
|
@@ -182,8 +183,8 @@ export class CoreBetterAuthController {
|
|
|
182
183
|
protected readonly logger = new Logger(CoreBetterAuthController.name);
|
|
183
184
|
|
|
184
185
|
constructor(
|
|
185
|
-
protected readonly betterAuthService:
|
|
186
|
-
protected readonly userMapper:
|
|
186
|
+
protected readonly betterAuthService: CoreBetterAuthService,
|
|
187
|
+
protected readonly userMapper: CoreBetterAuthUserMapper,
|
|
187
188
|
protected readonly configService: ConfigService,
|
|
188
189
|
) {}
|
|
189
190
|
|
|
@@ -193,17 +194,35 @@ export class CoreBetterAuthController {
|
|
|
193
194
|
|
|
194
195
|
/**
|
|
195
196
|
* Sign in with email and password
|
|
197
|
+
*
|
|
198
|
+
* **Why Custom Implementation (not hooks):**
|
|
199
|
+
* - Hooks cannot access plaintext password for legacy migration
|
|
200
|
+
* - Hooks cannot modify response format
|
|
201
|
+
* - Hooks cannot set multi-cookie auth strategy
|
|
202
|
+
*
|
|
203
|
+
* **Flow:**
|
|
204
|
+
* 1. Try legacy user migration if the user exists in legacy system
|
|
205
|
+
* → Requires plaintext password (unavailable in after-hooks)
|
|
206
|
+
* 2. Normalize password to SHA256 format for Better Auth
|
|
207
|
+
* 3. Call Better Auth API directly for consistent response format
|
|
208
|
+
* 4. For 2FA: Use native handler to ensure cookies are set correctly
|
|
209
|
+
* → Hooks cannot set cookies, so we use authInstance.handler()
|
|
210
|
+
* 5. Return response with multiple auth cookies
|
|
211
|
+
* → Hooks cannot modify response or set cookies
|
|
212
|
+
*
|
|
213
|
+
* @see README.md "Architecture: Why Custom Controllers?"
|
|
196
214
|
*/
|
|
197
|
-
@ApiBody({ type:
|
|
198
|
-
@ApiCreatedResponse({ description: 'Signed in successfully', type:
|
|
215
|
+
@ApiBody({ type: CoreBetterAuthSignInInput })
|
|
216
|
+
@ApiCreatedResponse({ description: 'Signed in successfully', type: CoreBetterAuthResponse })
|
|
199
217
|
@ApiOperation({ description: 'Sign in via Better-Auth with email and password', summary: 'Sign In' })
|
|
200
218
|
@HttpCode(HttpStatus.OK)
|
|
201
219
|
@Post('sign-in/email')
|
|
202
220
|
@Roles(RoleEnum.S_EVERYONE)
|
|
203
221
|
async signIn(
|
|
222
|
+
@Req() req: Request,
|
|
204
223
|
@Res({ passthrough: true }) res: Response,
|
|
205
|
-
@Body() input:
|
|
206
|
-
): Promise<
|
|
224
|
+
@Body() input: CoreBetterAuthSignInInput,
|
|
225
|
+
): Promise<CoreBetterAuthResponse> {
|
|
207
226
|
this.ensureEnabled();
|
|
208
227
|
|
|
209
228
|
const api = this.betterAuthService.getApi();
|
|
@@ -211,47 +230,102 @@ export class CoreBetterAuthController {
|
|
|
211
230
|
throw new BadRequestException('Better-Auth API not available');
|
|
212
231
|
}
|
|
213
232
|
|
|
214
|
-
// Try
|
|
215
|
-
|
|
216
|
-
|
|
233
|
+
// Step 1: Try legacy user migration BEFORE Better Auth handles the request
|
|
234
|
+
// This allows users who exist in legacy system to be migrated automatically
|
|
235
|
+
try {
|
|
236
|
+
const migrated = await this.userMapper.migrateAccountToIam(input.email, input.password);
|
|
237
|
+
if (migrated) {
|
|
238
|
+
this.logger.debug(`Migrated legacy user ${input.email} to IAM`);
|
|
239
|
+
}
|
|
240
|
+
} catch (error) {
|
|
241
|
+
// Migration failure is not fatal - user might not exist in legacy or already migrated
|
|
242
|
+
this.logger.debug(`Legacy migration check: ${error instanceof Error ? error.message : 'not needed'}`);
|
|
243
|
+
}
|
|
217
244
|
|
|
218
|
-
|
|
219
|
-
* Attempt sign-in with optional legacy user migration
|
|
220
|
-
* @param res - Response object
|
|
221
|
-
* @param input - Sign-in credentials
|
|
222
|
-
* @param api - Better-Auth API instance
|
|
223
|
-
* @param allowMigration - Whether to attempt legacy migration on failure
|
|
224
|
-
*/
|
|
225
|
-
private async attemptSignIn(
|
|
226
|
-
res: Response,
|
|
227
|
-
input: BetterAuthSignInInput,
|
|
228
|
-
api: ReturnType<BetterAuthService['getApi']>,
|
|
229
|
-
allowMigration: boolean,
|
|
230
|
-
): Promise<BetterAuthResponse> {
|
|
231
|
-
// Normalize password to SHA256 format for consistency with Legacy Auth
|
|
232
|
-
// This ensures users can sign in with either plain password or SHA256 hash
|
|
245
|
+
// Step 2: Normalize password for Better Auth (SHA256 format)
|
|
233
246
|
const normalizedPassword = this.userMapper.normalizePasswordForIam(input.password);
|
|
234
247
|
|
|
248
|
+
// Step 3: Call Better Auth API to check response type
|
|
235
249
|
try {
|
|
236
|
-
const response = await api
|
|
237
|
-
body: {
|
|
238
|
-
|
|
250
|
+
const response = (await api.signInEmail({
|
|
251
|
+
body: {
|
|
252
|
+
email: input.email,
|
|
253
|
+
password: normalizedPassword,
|
|
254
|
+
},
|
|
255
|
+
})) as BetterAuthSignInResponse | null;
|
|
239
256
|
|
|
240
257
|
if (!response) {
|
|
241
258
|
throw new UnauthorizedException('Invalid credentials');
|
|
242
259
|
}
|
|
243
260
|
|
|
244
261
|
// Check for 2FA requirement
|
|
262
|
+
// When 2FA is required, we need to use the native Better Auth handler
|
|
263
|
+
// because api.signInEmail() doesn't return the session token needed for 2FA verification
|
|
245
264
|
if (requires2FA(response)) {
|
|
246
|
-
|
|
265
|
+
if (!isProduction()) {
|
|
266
|
+
this.logger.debug(`2FA required for ${input.email}, forwarding to native handler for cookie handling`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Forward to native Better Auth handler which sets the session cookie correctly
|
|
270
|
+
// We need to modify the request body to use the normalized password
|
|
271
|
+
const authInstance = this.betterAuthService.getInstance();
|
|
272
|
+
if (!authInstance) {
|
|
273
|
+
throw new InternalServerErrorException('Better-Auth not initialized');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Create a modified request body with normalized password
|
|
277
|
+
const modifiedBody = JSON.stringify({
|
|
278
|
+
email: input.email,
|
|
279
|
+
password: normalizedPassword,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Build the sign-in URL
|
|
283
|
+
const basePath = this.betterAuthService.getBasePath();
|
|
284
|
+
const baseUrl = this.betterAuthService.getBaseUrl();
|
|
285
|
+
const signInUrl = new URL(`${basePath}/sign-in/email`, baseUrl);
|
|
286
|
+
|
|
287
|
+
// Create a new Web Request for Better Auth's native handler
|
|
288
|
+
const webRequest = new Request(signInUrl.toString(), {
|
|
289
|
+
body: modifiedBody,
|
|
290
|
+
headers: new Headers({
|
|
291
|
+
'Content-Type': 'application/json',
|
|
292
|
+
'Origin': req.headers.origin || baseUrl,
|
|
293
|
+
}),
|
|
294
|
+
method: 'POST',
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Call Better Auth's native handler
|
|
298
|
+
const nativeResponse = await authInstance.handler(webRequest);
|
|
299
|
+
|
|
300
|
+
// Extract and forward Set-Cookie headers
|
|
301
|
+
const setCookieHeaders = nativeResponse.headers.getSetCookie?.() || [];
|
|
302
|
+
for (const cookie of setCookieHeaders) {
|
|
303
|
+
res.setHeader('Set-Cookie', cookie);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Return the structured response
|
|
307
|
+
return {
|
|
308
|
+
requiresTwoFactor: true,
|
|
309
|
+
success: false,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Check if response indicates an error
|
|
314
|
+
const responseAny = response as any;
|
|
315
|
+
if (responseAny?.error || responseAny?.code === 'CREDENTIAL_ACCOUNT_NOT_FOUND') {
|
|
316
|
+
throw new UnauthorizedException('Invalid credentials');
|
|
247
317
|
}
|
|
248
318
|
|
|
249
|
-
// Get user data
|
|
250
319
|
if (hasUser(response)) {
|
|
320
|
+
// Link or create user in our database (in case it doesn't exist)
|
|
321
|
+
await this.userMapper.linkOrCreateUser(response.user);
|
|
322
|
+
|
|
251
323
|
const mappedUser = await this.userMapper.mapSessionUser(response.user);
|
|
252
|
-
const token = this.betterAuthService.isJwtEnabled() ? (response as TokenResponse).token : undefined;
|
|
253
324
|
|
|
254
|
-
|
|
325
|
+
// Get token (JWT if available, session token otherwise)
|
|
326
|
+
const token = responseAny.accessToken || responseAny.token;
|
|
327
|
+
|
|
328
|
+
const result: CoreBetterAuthResponse = {
|
|
255
329
|
requiresTwoFactor: false,
|
|
256
330
|
session: hasSession(response) ? this.mapSession(response.session) : undefined,
|
|
257
331
|
success: true,
|
|
@@ -264,17 +338,11 @@ export class CoreBetterAuthController {
|
|
|
264
338
|
|
|
265
339
|
throw new UnauthorizedException('Invalid credentials');
|
|
266
340
|
} catch (error) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (
|
|
271
|
-
|
|
272
|
-
const migrated = await this.userMapper.migrateAccountToIam(input.email, input.password);
|
|
273
|
-
if (migrated) {
|
|
274
|
-
this.logger.debug(`Migrated legacy user ${input.email} to IAM, retrying sign-in`);
|
|
275
|
-
// Retry sign-in after migration (without allowing another migration to prevent loops)
|
|
276
|
-
return this.attemptSignIn(res, input, api, false);
|
|
277
|
-
}
|
|
341
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
342
|
+
this.logger.debug(`Sign-in error: ${errorMessage}`);
|
|
343
|
+
|
|
344
|
+
if (error instanceof UnauthorizedException) {
|
|
345
|
+
throw error;
|
|
278
346
|
}
|
|
279
347
|
|
|
280
348
|
throw new UnauthorizedException('Invalid credentials');
|
|
@@ -283,16 +351,33 @@ export class CoreBetterAuthController {
|
|
|
283
351
|
|
|
284
352
|
/**
|
|
285
353
|
* Sign up with email and password
|
|
354
|
+
*
|
|
355
|
+
* **Why Custom Implementation (not hooks):**
|
|
356
|
+
* - After-hooks don't have access to plaintext password
|
|
357
|
+
* → Cannot call syncPasswordToLegacy() in hooks
|
|
358
|
+
* - Hooks cannot access NestJS services
|
|
359
|
+
* → Cannot use UserMapper for user linking
|
|
360
|
+
* - Hooks cannot modify response format
|
|
361
|
+
*
|
|
362
|
+
* **Custom Logic:**
|
|
363
|
+
* 1. Normalize password to SHA256 for Better Auth storage
|
|
364
|
+
* 2. Create user via Better Auth API
|
|
365
|
+
* 3. Link user to Legacy system (requires NestJS UserMapper)
|
|
366
|
+
* 4. Sync plaintext password to Legacy Auth (bcrypt hash)
|
|
367
|
+
* → CRITICAL: This requires plaintext, unavailable in after-hooks
|
|
368
|
+
* 5. Return response with session cookies
|
|
369
|
+
*
|
|
370
|
+
* @see README.md "Architecture: Why Custom Controllers?"
|
|
286
371
|
*/
|
|
287
|
-
@ApiBody({ type:
|
|
288
|
-
@ApiCreatedResponse({ description: 'Signed up successfully', type:
|
|
372
|
+
@ApiBody({ type: CoreBetterAuthSignUpInput })
|
|
373
|
+
@ApiCreatedResponse({ description: 'Signed up successfully', type: CoreBetterAuthResponse })
|
|
289
374
|
@ApiOperation({ description: 'Sign up via Better-Auth with email and password', summary: 'Sign Up' })
|
|
290
375
|
@Post('sign-up/email')
|
|
291
376
|
@Roles(RoleEnum.S_EVERYONE)
|
|
292
377
|
async signUp(
|
|
293
378
|
@Res({ passthrough: true }) res: Response,
|
|
294
|
-
@Body() input:
|
|
295
|
-
): Promise<
|
|
379
|
+
@Body() input: CoreBetterAuthSignUpInput,
|
|
380
|
+
): Promise<CoreBetterAuthResponse> {
|
|
296
381
|
this.ensureEnabled();
|
|
297
382
|
|
|
298
383
|
const api = this.betterAuthService.getApi();
|
|
@@ -326,7 +411,7 @@ export class CoreBetterAuthController {
|
|
|
326
411
|
|
|
327
412
|
const mappedUser = await this.userMapper.mapSessionUser(response.user);
|
|
328
413
|
|
|
329
|
-
const result:
|
|
414
|
+
const result: CoreBetterAuthResponse = {
|
|
330
415
|
requiresTwoFactor: false,
|
|
331
416
|
session: hasSession(response) ? this.mapSession(response.session) : undefined,
|
|
332
417
|
success: true,
|
|
@@ -349,12 +434,20 @@ export class CoreBetterAuthController {
|
|
|
349
434
|
|
|
350
435
|
/**
|
|
351
436
|
* Sign out (logout)
|
|
437
|
+
*
|
|
438
|
+
* **Why Custom Implementation (not hooks):**
|
|
439
|
+
* - Must clear multiple cookies (token, session, better-auth.session_token, etc.)
|
|
440
|
+
* - Hooks cannot modify response or set/clear cookies
|
|
441
|
+
*
|
|
442
|
+
* NOTE: Better-Auth uses POST for sign-out (matches better-auth convention)
|
|
443
|
+
*
|
|
444
|
+
* @see README.md "Architecture: Why Custom Controllers?"
|
|
352
445
|
*/
|
|
353
|
-
@ApiOkResponse({ description: 'Signed out successfully', type:
|
|
446
|
+
@ApiOkResponse({ description: 'Signed out successfully', type: CoreBetterAuthResponse })
|
|
354
447
|
@ApiOperation({ description: 'Sign out from Better-Auth', summary: 'Sign Out' })
|
|
355
|
-
@
|
|
448
|
+
@Post('sign-out')
|
|
356
449
|
@Roles(RoleEnum.S_EVERYONE)
|
|
357
|
-
async signOut(@Req() req: Request, @Res({ passthrough: true }) res: Response): Promise<
|
|
450
|
+
async signOut(@Req() req: Request, @Res({ passthrough: true }) res: Response): Promise<CoreBetterAuthResponse> {
|
|
358
451
|
if (!this.betterAuthService.isEnabled()) {
|
|
359
452
|
return { success: true };
|
|
360
453
|
}
|
|
@@ -381,12 +474,19 @@ export class CoreBetterAuthController {
|
|
|
381
474
|
|
|
382
475
|
/**
|
|
383
476
|
* Get current session
|
|
477
|
+
*
|
|
478
|
+
* **Why Custom Implementation (not hooks):**
|
|
479
|
+
* - Must map Better Auth user to nest-server user with roles
|
|
480
|
+
* - Hooks cannot access NestJS UserMapper service
|
|
481
|
+
* - Custom response format with mapped user data
|
|
482
|
+
*
|
|
483
|
+
* @see README.md "Architecture: Why Custom Controllers?"
|
|
384
484
|
*/
|
|
385
|
-
@ApiOkResponse({ description: 'Current session', type:
|
|
485
|
+
@ApiOkResponse({ description: 'Current session', type: CoreBetterAuthResponse })
|
|
386
486
|
@ApiOperation({ description: 'Get current session from Better-Auth', summary: 'Get Session' })
|
|
387
487
|
@Get('session')
|
|
388
488
|
@Roles(RoleEnum.S_EVERYONE)
|
|
389
|
-
async getSession(@Req() req: Request): Promise<
|
|
489
|
+
async getSession(@Req() req: Request): Promise<CoreBetterAuthResponse> {
|
|
390
490
|
if (!this.betterAuthService.isEnabled()) {
|
|
391
491
|
return { error: 'Better-Auth is disabled', success: false };
|
|
392
492
|
}
|
|
@@ -412,133 +512,42 @@ export class CoreBetterAuthController {
|
|
|
412
512
|
}
|
|
413
513
|
|
|
414
514
|
// ===================================================================================================================
|
|
415
|
-
//
|
|
515
|
+
// Catch-All Route for Better Auth Plugins
|
|
416
516
|
// ===================================================================================================================
|
|
417
517
|
|
|
418
518
|
/**
|
|
419
|
-
*
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
backupCodes: response.backupCodes || [],
|
|
447
|
-
success: true,
|
|
448
|
-
totpSecret: response.totpSecret || '',
|
|
449
|
-
totpUri: response.totpURI || '',
|
|
450
|
-
};
|
|
451
|
-
} catch (error) {
|
|
452
|
-
this.logger.debug(`Enable 2FA error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
453
|
-
throw new BadRequestException('Failed to enable 2FA');
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* Verify 2FA code during sign-in
|
|
519
|
+
* Catch-all route for all other Better Auth plugin endpoints.
|
|
520
|
+
*
|
|
521
|
+
* **This route USES the native Better Auth handler** via `authInstance.handler()`.
|
|
522
|
+
* It's the best of both worlds:
|
|
523
|
+
* - Custom endpoints where we need NestJS features (sign-in, sign-up, etc.)
|
|
524
|
+
* - Native handler for plugins that work correctly out-of-the-box
|
|
525
|
+
*
|
|
526
|
+
* **Why Not Fully Native:**
|
|
527
|
+
* Even this catch-all requires custom logic:
|
|
528
|
+
* - Session token injection into request (before-hooks can't inject tokens)
|
|
529
|
+
* - Converting Express Request to Web Standard Request
|
|
530
|
+
*
|
|
531
|
+
* **Handles:**
|
|
532
|
+
* - Passkey/WebAuthn (all endpoints)
|
|
533
|
+
* - Two-Factor Authentication (all endpoints)
|
|
534
|
+
* - Social Login OAuth flows
|
|
535
|
+
* - Email verification
|
|
536
|
+
* - Magic link authentication
|
|
537
|
+
* - Any other Better Auth plugin functionality
|
|
538
|
+
*
|
|
539
|
+
* IMPORTANT: This route must be defined LAST in the controller to ensure
|
|
540
|
+
* it doesn't intercept the explicitly defined routes above.
|
|
541
|
+
*
|
|
542
|
+
* Better Auth handles authentication internally - it returns appropriate
|
|
543
|
+
* errors (401, 403) if a user is not authenticated for protected endpoints.
|
|
544
|
+
*
|
|
545
|
+
* @see README.md "Architecture: Why Custom Controllers?"
|
|
459
546
|
*/
|
|
460
|
-
@
|
|
461
|
-
@ApiOkResponse({ description: 'Verification result', type: BetterAuthResponse })
|
|
462
|
-
@ApiOperation({ description: 'Verify Two-Factor Authentication code', summary: 'Verify 2FA' })
|
|
463
|
-
@HttpCode(HttpStatus.OK)
|
|
464
|
-
@Post('two-factor/verify')
|
|
547
|
+
@All('*path')
|
|
465
548
|
@Roles(RoleEnum.S_EVERYONE)
|
|
466
|
-
async
|
|
467
|
-
|
|
468
|
-
@Res({ passthrough: true }) res: Response,
|
|
469
|
-
@Body() input: BetterAuthTwoFactorInput,
|
|
470
|
-
): Promise<BetterAuthResponse> {
|
|
471
|
-
this.ensureEnabled();
|
|
472
|
-
|
|
473
|
-
if (!this.betterAuthService.isTwoFactorEnabled()) {
|
|
474
|
-
throw new BadRequestException('Two-factor authentication is not enabled');
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
const api = this.betterAuthService.getApi();
|
|
478
|
-
if (!api || !('verifyTOTP' in api)) {
|
|
479
|
-
throw new BadRequestException('2FA API not available');
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
try {
|
|
483
|
-
const headers = this.extractHeaders(req);
|
|
484
|
-
const response = await (api as any).verifyTOTP({
|
|
485
|
-
body: { code: input.code },
|
|
486
|
-
headers,
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
if (!response) {
|
|
490
|
-
throw new UnauthorizedException('Invalid 2FA code');
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
if (hasUser(response)) {
|
|
494
|
-
const mappedUser = await this.userMapper.mapSessionUser(response.user);
|
|
495
|
-
const token = this.betterAuthService.isJwtEnabled() ? (response as TokenResponse).token : undefined;
|
|
496
|
-
|
|
497
|
-
const result: BetterAuthResponse = {
|
|
498
|
-
session: hasSession(response) ? this.mapSession(response.session) : undefined,
|
|
499
|
-
success: true,
|
|
500
|
-
token,
|
|
501
|
-
user: mappedUser ? this.mapUser(response.user, mappedUser) : undefined,
|
|
502
|
-
};
|
|
503
|
-
|
|
504
|
-
return this.processCookies(res, result);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
throw new UnauthorizedException('Invalid 2FA code');
|
|
508
|
-
} catch (error) {
|
|
509
|
-
this.logger.debug(`Verify 2FA error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
510
|
-
throw new UnauthorizedException('Invalid 2FA code');
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Disable 2FA for current user
|
|
516
|
-
*/
|
|
517
|
-
@ApiOkResponse({ description: 'Disable result', type: BetterAuthResponse })
|
|
518
|
-
@ApiOperation({ description: 'Disable Two-Factor Authentication', summary: 'Disable 2FA' })
|
|
519
|
-
@Post('two-factor/disable')
|
|
520
|
-
@Roles(RoleEnum.S_USER)
|
|
521
|
-
async disableTwoFactor(@Req() req: Request): Promise<BetterAuthResponse> {
|
|
522
|
-
this.ensureEnabled();
|
|
523
|
-
|
|
524
|
-
if (!this.betterAuthService.isTwoFactorEnabled()) {
|
|
525
|
-
throw new BadRequestException('Two-factor authentication is not enabled');
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const api = this.betterAuthService.getApi();
|
|
529
|
-
if (!api || !('disableTwoFactor' in api)) {
|
|
530
|
-
throw new BadRequestException('2FA API not available');
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
try {
|
|
534
|
-
const headers = this.extractHeaders(req);
|
|
535
|
-
await (api as any).disableTwoFactor({ headers });
|
|
536
|
-
|
|
537
|
-
return { success: true };
|
|
538
|
-
} catch (error) {
|
|
539
|
-
this.logger.debug(`Disable 2FA error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
540
|
-
throw new BadRequestException('Failed to disable 2FA');
|
|
541
|
-
}
|
|
549
|
+
async handlePluginRoutes(@Req() req: Request, @Res() res: Response): Promise<void> {
|
|
550
|
+
return this.handleBetterAuthPlugins(req, res);
|
|
542
551
|
}
|
|
543
552
|
|
|
544
553
|
// ===================================================================================================================
|
|
@@ -587,12 +596,16 @@ export class CoreBetterAuthController {
|
|
|
587
596
|
|
|
588
597
|
/**
|
|
589
598
|
* Map session to response format
|
|
599
|
+
*
|
|
600
|
+
* NOTE: The session token is intentionally NOT included in the response.
|
|
601
|
+
* It is set as an httpOnly cookie for security.
|
|
590
602
|
*/
|
|
591
|
-
protected mapSession(session: null | undefined | { expiresAt: Date; id: string }):
|
|
603
|
+
protected mapSession(session: null | undefined | { expiresAt: Date; id: string; token?: string }): CoreBetterAuthSessionInfo | undefined {
|
|
592
604
|
if (!session) return undefined;
|
|
593
605
|
return {
|
|
594
606
|
expiresAt: session.expiresAt instanceof Date ? session.expiresAt.toISOString() : String(session.expiresAt),
|
|
595
607
|
id: session.id,
|
|
608
|
+
// NOTE: token is intentionally NOT returned - it's set as httpOnly cookie
|
|
596
609
|
};
|
|
597
610
|
}
|
|
598
611
|
|
|
@@ -602,7 +615,7 @@ export class CoreBetterAuthController {
|
|
|
602
615
|
* @param _mappedUser - The synced user from legacy system (available for override customization)
|
|
603
616
|
*/
|
|
604
617
|
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
605
|
-
protected mapUser(sessionUser: BetterAuthSessionUser, _mappedUser: any):
|
|
618
|
+
protected mapUser(sessionUser: BetterAuthSessionUser, _mappedUser: any): CoreBetterAuthUserResponse {
|
|
606
619
|
return {
|
|
607
620
|
email: sessionUser.email,
|
|
608
621
|
emailVerified: sessionUser.emailVerified || false,
|
|
@@ -613,19 +626,63 @@ export class CoreBetterAuthController {
|
|
|
613
626
|
|
|
614
627
|
/**
|
|
615
628
|
* Process cookies for response
|
|
629
|
+
*
|
|
630
|
+
* Sets multiple cookies for authentication compatibility:
|
|
631
|
+
*
|
|
632
|
+
* | Cookie Name | Purpose |
|
|
633
|
+
* |-------------|---------|
|
|
634
|
+
* | `token` | Primary session token (nest-server compatibility) |
|
|
635
|
+
* | `{basePath}.session_token` | Better Auth's native cookie for plugins (e.g., `iam.session_token`) |
|
|
636
|
+
* | `better-auth.session_token` | Legacy Better Auth cookie name (backwards compatibility) |
|
|
637
|
+
* | `{configured}` | Custom cookie name if configured via `options.advanced.cookies.session_token.name` |
|
|
638
|
+
* | `session` | Session ID for reference/debugging |
|
|
639
|
+
*
|
|
640
|
+
* IMPORTANT: Better Auth's sign-in returns a session token in `result.token`.
|
|
641
|
+
* This is NOT a JWT - it's the session token stored in the database.
|
|
642
|
+
* The JWT plugin generates JWTs separately via the /token endpoint when needed.
|
|
643
|
+
*
|
|
644
|
+
* For plugins like Passkey to work, the session token must be available in a cookie
|
|
645
|
+
* that Better Auth's plugin system recognizes (default: `{basePath}.session_token`).
|
|
646
|
+
*
|
|
647
|
+
* @param res - Express Response object
|
|
648
|
+
* @param result - The CoreBetterAuthResponse to return
|
|
649
|
+
* @param sessionToken - Optional session token to set in cookies (if not provided, uses result.token)
|
|
616
650
|
*/
|
|
617
|
-
protected processCookies(res: Response, result:
|
|
651
|
+
protected processCookies(res: Response, result: CoreBetterAuthResponse, sessionToken?: string): CoreBetterAuthResponse {
|
|
618
652
|
// Check if cookie handling is activated
|
|
619
653
|
if (this.configService.getFastButReadOnly('cookies')) {
|
|
620
654
|
const cookieOptions = { httpOnly: true, sameSite: 'lax' as const, secure: process.env.NODE_ENV === 'production' };
|
|
621
655
|
|
|
622
|
-
//
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
656
|
+
// Use provided sessionToken or fall back to result.token
|
|
657
|
+
const tokenToSet = sessionToken || result.token;
|
|
658
|
+
|
|
659
|
+
if (tokenToSet) {
|
|
660
|
+
// Set the primary token cookie (for nest-server compatibility)
|
|
661
|
+
res.cookie('token', tokenToSet, cookieOptions);
|
|
662
|
+
|
|
663
|
+
// Set Better Auth's native session token cookies for plugin compatibility
|
|
664
|
+
// This is CRITICAL for Passkey/WebAuthn to work
|
|
665
|
+
const basePath = this.betterAuthService.getBasePath().replace(/^\//, '').replace(/\//g, '.');
|
|
666
|
+
const defaultCookieName = `${basePath}.session_token`;
|
|
667
|
+
res.cookie(defaultCookieName, tokenToSet, cookieOptions);
|
|
668
|
+
|
|
669
|
+
// Also set the legacy cookie name for backwards compatibility
|
|
670
|
+
res.cookie('better-auth.session_token', tokenToSet, cookieOptions);
|
|
671
|
+
|
|
672
|
+
// Get configured cookie name and set if different from defaults
|
|
673
|
+
const betterAuthConfig = this.configService.getFastButReadOnly('betterAuth');
|
|
674
|
+
const configuredCookieName = betterAuthConfig?.options?.advanced?.cookies?.session_token?.name;
|
|
675
|
+
if (configuredCookieName && configuredCookieName !== 'token' && configuredCookieName !== defaultCookieName) {
|
|
676
|
+
res.cookie(configuredCookieName, tokenToSet, cookieOptions);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Remove token from response body (it's now in cookies)
|
|
680
|
+
if (result.token) {
|
|
681
|
+
delete result.token;
|
|
682
|
+
}
|
|
626
683
|
}
|
|
627
684
|
|
|
628
|
-
// Set session cookie
|
|
685
|
+
// Set session ID cookie (for reference/debugging)
|
|
629
686
|
if (result.session) {
|
|
630
687
|
res.cookie('session', result.session.id, cookieOptions);
|
|
631
688
|
}
|
|
@@ -642,5 +699,104 @@ export class CoreBetterAuthController {
|
|
|
642
699
|
res.cookie('token', '', { ...cookieOptions, maxAge: 0 });
|
|
643
700
|
res.cookie('session', '', { ...cookieOptions, maxAge: 0 });
|
|
644
701
|
res.cookie('better-auth.session_token', '', { ...cookieOptions, maxAge: 0 });
|
|
702
|
+
|
|
703
|
+
// Clear the path-based session token cookie
|
|
704
|
+
const basePath = this.betterAuthService.getBasePath().replace(/^\//, '').replace(/\//g, '.');
|
|
705
|
+
const defaultCookieName = `${basePath}.session_token`;
|
|
706
|
+
res.cookie(defaultCookieName, '', { ...cookieOptions, maxAge: 0 });
|
|
707
|
+
|
|
708
|
+
// Clear configured session token cookie if different
|
|
709
|
+
const betterAuthConfig = this.configService.getFastButReadOnly('betterAuth');
|
|
710
|
+
const configuredCookieName = betterAuthConfig?.options?.advanced?.cookies?.session_token?.name;
|
|
711
|
+
if (configuredCookieName && configuredCookieName !== 'token' && configuredCookieName !== defaultCookieName) {
|
|
712
|
+
res.cookie(configuredCookieName, '', { ...cookieOptions, maxAge: 0 });
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// ===================================================================================================================
|
|
717
|
+
// Better Auth Plugin Handler (shared implementation)
|
|
718
|
+
// ===================================================================================================================
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Handler for Better Auth plugin endpoints (Passkey, Social Login, etc.)
|
|
722
|
+
*
|
|
723
|
+
* This method forwards requests to Better Auth's native handler. It enables:
|
|
724
|
+
* - Passkey/WebAuthn registration and authentication
|
|
725
|
+
* - Social Login OAuth flows
|
|
726
|
+
* - Email verification links
|
|
727
|
+
* - Magic link authentication
|
|
728
|
+
* - And other plugin-provided functionality
|
|
729
|
+
*
|
|
730
|
+
* IMPORTANT: This method injects the session token into both cookies AND
|
|
731
|
+
* Authorization header to ensure Better Auth can find the session via
|
|
732
|
+
* multiple lookup strategies.
|
|
733
|
+
*/
|
|
734
|
+
@ApiExcludeEndpoint() // Don't show in Swagger docs
|
|
735
|
+
protected async handleBetterAuthPlugins(req: Request, res: Response): Promise<void> {
|
|
736
|
+
this.ensureEnabled();
|
|
737
|
+
|
|
738
|
+
const authInstance = this.betterAuthService.getInstance();
|
|
739
|
+
if (!authInstance) {
|
|
740
|
+
throw new InternalServerErrorException('Better-Auth not initialized');
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (!isProduction()) {
|
|
744
|
+
this.logger.debug(`Forwarding to Better Auth: ${req.method} ${req.path}`);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
try {
|
|
748
|
+
// Extract session token from the validated middleware session or cookies
|
|
749
|
+
const sessionToken = this.getSessionTokenFromRequest(req);
|
|
750
|
+
|
|
751
|
+
if (!isProduction()) {
|
|
752
|
+
this.logger.debug(`Session token for forwarding: ${maskToken(sessionToken)}`);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Get config for signing cookies
|
|
756
|
+
const config = this.betterAuthService.getConfig();
|
|
757
|
+
|
|
758
|
+
// Convert Express request to Web Standard Request with enhanced session context
|
|
759
|
+
const webRequest = await toWebRequest(req, {
|
|
760
|
+
basePath: this.betterAuthService.getBasePath(),
|
|
761
|
+
baseUrl: this.betterAuthService.getBaseUrl(),
|
|
762
|
+
logger: this.logger,
|
|
763
|
+
secret: config.secret,
|
|
764
|
+
sessionToken,
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
// Call Better Auth's native handler
|
|
768
|
+
const response = await authInstance.handler(webRequest);
|
|
769
|
+
|
|
770
|
+
if (!isProduction()) {
|
|
771
|
+
this.logger.debug(`Better Auth handler response status: ${response.status}`);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Send the response back
|
|
775
|
+
await sendWebResponse(res, response);
|
|
776
|
+
} catch (error) {
|
|
777
|
+
this.logger.error(`Better Auth handler error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
778
|
+
|
|
779
|
+
// Re-throw NestJS exceptions
|
|
780
|
+
if (error instanceof BadRequestException || error instanceof UnauthorizedException || error instanceof InternalServerErrorException) {
|
|
781
|
+
throw error;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
throw new InternalServerErrorException('Authentication handler error');
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Gets the session token from the request.
|
|
790
|
+
* Prioritizes the middleware-validated session, then falls back to cookies.
|
|
791
|
+
*/
|
|
792
|
+
private getSessionTokenFromRequest(req: Request): null | string {
|
|
793
|
+
// First, try to get token from middleware-validated session
|
|
794
|
+
const betterAuthReq = req as any;
|
|
795
|
+
if (betterAuthReq.betterAuthSession?.session?.token) {
|
|
796
|
+
return betterAuthReq.betterAuthSession.session.token;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Fall back to extracting from cookies
|
|
800
|
+
return this.extractSessionToken(req);
|
|
645
801
|
}
|
|
646
802
|
}
|