@lenne.tech/nest-server 11.6.2 → 11.7.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.
Files changed (74) hide show
  1. package/dist/config.env.js +2 -11
  2. package/dist/config.env.js.map +1 -1
  3. package/dist/core/common/helpers/filter.helper.d.ts +9 -9
  4. package/dist/core/common/helpers/filter.helper.js +2 -4
  5. package/dist/core/common/helpers/filter.helper.js.map +1 -1
  6. package/dist/core/common/helpers/gridfs.helper.js +3 -3
  7. package/dist/core/common/helpers/gridfs.helper.js.map +1 -1
  8. package/dist/core/common/interfaces/server-options.interface.d.ts +4 -3
  9. package/dist/core/common/services/crud.service.d.ts +16 -16
  10. package/dist/core/common/services/crud.service.js +1 -1
  11. package/dist/core/common/services/crud.service.js.map +1 -1
  12. package/dist/core/modules/better-auth/better-auth-models.d.ts +0 -1
  13. package/dist/core/modules/better-auth/better-auth-models.js +0 -4
  14. package/dist/core/modules/better-auth/better-auth-models.js.map +1 -1
  15. package/dist/core/modules/better-auth/better-auth.config.js +3 -0
  16. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  17. package/dist/core/modules/better-auth/better-auth.module.d.ts +10 -2
  18. package/dist/core/modules/better-auth/better-auth.module.js +40 -52
  19. package/dist/core/modules/better-auth/better-auth.module.js.map +1 -1
  20. package/dist/core/modules/better-auth/better-auth.resolver.d.ts +8 -12
  21. package/dist/core/modules/better-auth/better-auth.resolver.js +33 -351
  22. package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
  23. package/dist/core/modules/better-auth/better-auth.service.d.ts +0 -1
  24. package/dist/core/modules/better-auth/better-auth.service.js +0 -3
  25. package/dist/core/modules/better-auth/better-auth.service.js.map +1 -1
  26. package/dist/core/modules/better-auth/better-auth.types.d.ts +9 -8
  27. package/dist/core/modules/better-auth/better-auth.types.js +14 -3
  28. package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
  29. package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +66 -0
  30. package/dist/core/modules/better-auth/core-better-auth.controller.js +491 -0
  31. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -0
  32. package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +59 -0
  33. package/dist/core/modules/better-auth/core-better-auth.resolver.js +538 -0
  34. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -0
  35. package/dist/core/modules/better-auth/index.d.ts +2 -0
  36. package/dist/core/modules/better-auth/index.js +2 -0
  37. package/dist/core/modules/better-auth/index.js.map +1 -1
  38. package/dist/core.module.js +1 -1
  39. package/dist/core.module.js.map +1 -1
  40. package/dist/server/modules/better-auth/better-auth.controller.d.ts +10 -0
  41. package/dist/server/modules/better-auth/better-auth.controller.js +36 -0
  42. package/dist/server/modules/better-auth/better-auth.controller.js.map +1 -0
  43. package/dist/server/modules/better-auth/better-auth.module.d.ts +9 -0
  44. package/dist/server/modules/better-auth/better-auth.module.js +44 -0
  45. package/dist/server/modules/better-auth/better-auth.module.js.map +1 -0
  46. package/dist/server/modules/better-auth/better-auth.resolver.d.ts +45 -0
  47. package/dist/server/modules/better-auth/better-auth.resolver.js +221 -0
  48. package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -0
  49. package/dist/server/modules/file/file-info.model.d.ts +71 -3
  50. package/dist/server/modules/user/user.model.d.ts +169 -3
  51. package/dist/server/server.module.js +6 -1
  52. package/dist/server/server.module.js.map +1 -1
  53. package/dist/tsconfig.build.tsbuildinfo +1 -1
  54. package/package.json +20 -29
  55. package/src/config.env.ts +2 -11
  56. package/src/core/common/helpers/filter.helper.ts +15 -17
  57. package/src/core/common/helpers/gridfs.helper.ts +5 -5
  58. package/src/core/common/interfaces/server-options.interface.ts +47 -14
  59. package/src/core/common/services/crud.service.ts +22 -22
  60. package/src/core/modules/better-auth/README.md +365 -39
  61. package/src/core/modules/better-auth/better-auth-models.ts +0 -3
  62. package/src/core/modules/better-auth/better-auth.config.ts +5 -0
  63. package/src/core/modules/better-auth/better-auth.module.ts +107 -66
  64. package/src/core/modules/better-auth/better-auth.resolver.ts +88 -553
  65. package/src/core/modules/better-auth/better-auth.service.ts +0 -9
  66. package/src/core/modules/better-auth/better-auth.types.ts +25 -10
  67. package/src/core/modules/better-auth/core-better-auth.controller.ts +605 -0
  68. package/src/core/modules/better-auth/core-better-auth.resolver.ts +705 -0
  69. package/src/core/modules/better-auth/index.ts +8 -1
  70. package/src/core.module.ts +3 -2
  71. package/src/server/modules/better-auth/better-auth.controller.ts +41 -0
  72. package/src/server/modules/better-auth/better-auth.module.ts +88 -0
  73. package/src/server/modules/better-auth/better-auth.resolver.ts +201 -0
  74. package/src/server/server.module.ts +10 -1
@@ -123,15 +123,6 @@ export class BetterAuthService {
123
123
  return this.isEnabled() && !!this.config.passkey && this.config.passkey.enabled !== false;
124
124
  }
125
125
 
126
- /**
127
- * Checks if legacy password handling is enabled.
128
- * Legacy password is enabled by default when the legacyPassword config block is present,
129
- * unless explicitly disabled with enabled: false.
130
- */
131
- isLegacyPasswordEnabled(): boolean {
132
- return this.isEnabled() && !!this.config.legacyPassword && this.config.legacyPassword.enabled !== false;
133
- }
134
-
135
126
  /**
136
127
  * Gets the list of enabled social providers
137
128
  * Dynamically iterates over all configured providers.
@@ -51,25 +51,40 @@ export interface BetterAuthSignUpResponse {
51
51
 
52
52
  /**
53
53
  * Type guard to check if response has session
54
+ * Preserves the original type while asserting session is defined
54
55
  */
55
- export function hasSession<T extends { session?: BetterAuthSessionResponse['session'] }>(
56
- response: T,
57
- ): response is T & { session: BetterAuthSessionResponse['session'] } {
58
- return response?.session !== undefined && response.session !== null;
56
+ export function hasSession<T>(response: T): response is T & { session: { expiresAt: Date; id: string } } {
57
+ return (
58
+ response !== null &&
59
+ typeof response === 'object' &&
60
+ 'session' in response &&
61
+ (response as { session?: unknown }).session !== null &&
62
+ (response as { session?: unknown }).session !== undefined
63
+ );
59
64
  }
60
65
 
61
66
  /**
62
67
  * Type guard to check if response has user
68
+ * Preserves the original type while asserting user is defined
63
69
  */
64
- export function hasUser<T extends { user?: BetterAuthSessionUser }>(
65
- response: T,
66
- ): response is T & { user: BetterAuthSessionUser } {
67
- return response?.user !== undefined && response.user !== null;
70
+ export function hasUser<T>(response: T): response is T & { user: BetterAuthSessionUser } {
71
+ return (
72
+ response !== null &&
73
+ typeof response === 'object' &&
74
+ 'user' in response &&
75
+ (response as { user?: unknown }).user !== null &&
76
+ (response as { user?: unknown }).user !== undefined
77
+ );
68
78
  }
69
79
 
70
80
  /**
71
81
  * Type guard to check if response requires 2FA
72
82
  */
73
- export function requires2FA(response: BetterAuthSignInResponse): boolean {
74
- return response?.twoFactorRedirect === true;
83
+ export function requires2FA<T>(response: T): response is T & { twoFactorRedirect: true } {
84
+ return (
85
+ response !== null &&
86
+ typeof response === 'object' &&
87
+ 'twoFactorRedirect' in response &&
88
+ (response as { twoFactorRedirect?: boolean }).twoFactorRedirect === true
89
+ );
75
90
  }
@@ -0,0 +1,605 @@
1
+ import {
2
+ BadRequestException,
3
+ Body,
4
+ Controller,
5
+ Get,
6
+ HttpCode,
7
+ HttpStatus,
8
+ Logger,
9
+ Post,
10
+ Req,
11
+ Res,
12
+ UnauthorizedException,
13
+ } from '@nestjs/common';
14
+ import { ApiBody, ApiCreatedResponse, ApiOkResponse, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
15
+ import { Request, Response } from 'express';
16
+
17
+ import { Roles } from '../../common/decorators/roles.decorator';
18
+ import { RoleEnum } from '../../common/enums/role.enum';
19
+ import { ConfigService } from '../../common/services/config.service';
20
+ import { BetterAuthSessionUser, BetterAuthUserMapper } from './better-auth-user.mapper';
21
+ import { BetterAuthService } from './better-auth.service';
22
+ import { hasSession, hasUser, requires2FA } from './better-auth.types';
23
+
24
+ // ===================================================================================================================
25
+ // Response Models
26
+ // ===================================================================================================================
27
+
28
+ /**
29
+ * Token response interface for JWT tokens
30
+ */
31
+ interface TokenResponse {
32
+ token?: string;
33
+ }
34
+
35
+ /**
36
+ * Session info for REST responses
37
+ */
38
+ export class BetterAuthSessionInfo {
39
+ @ApiProperty({ description: 'Session expiration time' })
40
+ expiresAt: string;
41
+
42
+ @ApiProperty({ description: 'Session ID' })
43
+ id: string;
44
+ }
45
+
46
+ /**
47
+ * User model for REST responses
48
+ */
49
+ export class BetterAuthUserResponse {
50
+ @ApiProperty({ description: 'User email address' })
51
+ email: string;
52
+
53
+ @ApiProperty({ description: 'Whether email is verified' })
54
+ emailVerified: boolean;
55
+
56
+ @ApiProperty({ description: 'User ID from Better-Auth' })
57
+ id: string;
58
+
59
+ @ApiProperty({ description: 'User display name' })
60
+ name: string;
61
+
62
+ @ApiProperty({ description: 'Whether 2FA is enabled', required: false })
63
+ twoFactorEnabled?: boolean;
64
+ }
65
+
66
+ /**
67
+ * Standard auth response
68
+ */
69
+ export class BetterAuthResponse {
70
+ @ApiProperty({ description: 'Error message if failed', required: false })
71
+ error?: string;
72
+
73
+ @ApiProperty({ description: 'Whether 2FA is required', required: false })
74
+ requiresTwoFactor?: boolean;
75
+
76
+ @ApiProperty({ description: 'Session information', required: false, type: BetterAuthSessionInfo })
77
+ session?: BetterAuthSessionInfo;
78
+
79
+ @ApiProperty({ description: 'Whether operation succeeded' })
80
+ success: boolean;
81
+
82
+ @ApiProperty({ description: 'JWT token (if JWT plugin enabled)', required: false })
83
+ token?: string;
84
+
85
+ @ApiProperty({ description: 'User information', required: false, type: BetterAuthUserResponse })
86
+ user?: BetterAuthUserResponse;
87
+ }
88
+
89
+ // ===================================================================================================================
90
+ // Type Guards
91
+ // ===================================================================================================================
92
+
93
+ /**
94
+ * Sign-in input DTO
95
+ */
96
+ export class BetterAuthSignInInput {
97
+ @ApiProperty({ description: 'User email address', example: 'user@example.com' })
98
+ email: string;
99
+
100
+ @ApiProperty({ description: 'User password' })
101
+ password: string;
102
+ }
103
+
104
+ /**
105
+ * Sign-up input DTO
106
+ */
107
+ export class BetterAuthSignUpInput {
108
+ @ApiProperty({ description: 'User email address', example: 'user@example.com' })
109
+ email: string;
110
+
111
+ @ApiProperty({ description: 'Display name', example: 'John Doe', required: false })
112
+ name?: string;
113
+
114
+ @ApiProperty({ description: 'User password (min 8 characters)' })
115
+ password: string;
116
+ }
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
+ // ===================================================================================================================
144
+ // Controller
145
+ // ===================================================================================================================
146
+
147
+ /**
148
+ * Core Better-Auth REST Controller
149
+ *
150
+ * Provides REST endpoints for Better-Auth authentication operations.
151
+ * This controller follows the same pattern as CoreAuthController and can be
152
+ * extended by project-specific implementations.
153
+ *
154
+ * @example
155
+ * ```typescript
156
+ * // In your project - src/server/modules/better-auth/better-auth.controller.ts
157
+ * @Controller('iam')
158
+ * export class BetterAuthController extends CoreBetterAuthController {
159
+ * constructor(
160
+ * betterAuthService: BetterAuthService,
161
+ * userMapper: BetterAuthUserMapper,
162
+ * configService: ConfigService,
163
+ * private readonly emailService: EmailService,
164
+ * ) {
165
+ * super(betterAuthService, userMapper, configService);
166
+ * }
167
+ *
168
+ * override async signUp(res: Response, input: BetterAuthSignUpInput) {
169
+ * const result = await super.signUp(res, input);
170
+ * if (result.success && result.user) {
171
+ * await this.emailService.sendWelcomeEmail(result.user.email);
172
+ * }
173
+ * return result;
174
+ * }
175
+ * }
176
+ * ```
177
+ */
178
+ @ApiTags('Better-Auth')
179
+ @Controller('iam')
180
+ @Roles(RoleEnum.ADMIN)
181
+ export class CoreBetterAuthController {
182
+ protected readonly logger = new Logger(CoreBetterAuthController.name);
183
+
184
+ constructor(
185
+ protected readonly betterAuthService: BetterAuthService,
186
+ protected readonly userMapper: BetterAuthUserMapper,
187
+ protected readonly configService: ConfigService,
188
+ ) {}
189
+
190
+ // ===================================================================================================================
191
+ // Authentication Endpoints
192
+ // ===================================================================================================================
193
+
194
+ /**
195
+ * Sign in with email and password
196
+ */
197
+ @ApiBody({ type: BetterAuthSignInInput })
198
+ @ApiCreatedResponse({ description: 'Signed in successfully', type: BetterAuthResponse })
199
+ @ApiOperation({ description: 'Sign in via Better-Auth with email and password', summary: 'Sign In' })
200
+ @HttpCode(HttpStatus.OK)
201
+ @Post('sign-in/email')
202
+ @Roles(RoleEnum.S_EVERYONE)
203
+ async signIn(
204
+ @Res({ passthrough: true }) res: Response,
205
+ @Body() input: BetterAuthSignInInput,
206
+ ): Promise<BetterAuthResponse> {
207
+ this.ensureEnabled();
208
+
209
+ const api = this.betterAuthService.getApi();
210
+ if (!api) {
211
+ throw new BadRequestException('Better-Auth API not available');
212
+ }
213
+
214
+ try {
215
+ const response = await api.signInEmail({
216
+ body: { email: input.email, password: input.password },
217
+ });
218
+
219
+ if (!response) {
220
+ throw new UnauthorizedException('Invalid credentials');
221
+ }
222
+
223
+ // Check for 2FA requirement
224
+ if (requires2FA(response)) {
225
+ return { requiresTwoFactor: true, success: false };
226
+ }
227
+
228
+ // Get user data
229
+ if (hasUser(response)) {
230
+ const mappedUser = await this.userMapper.mapSessionUser(response.user);
231
+ const token = this.betterAuthService.isJwtEnabled() ? (response as TokenResponse).token : undefined;
232
+
233
+ const result: BetterAuthResponse = {
234
+ requiresTwoFactor: false,
235
+ session: hasSession(response) ? this.mapSession(response.session) : undefined,
236
+ success: true,
237
+ token,
238
+ user: mappedUser ? this.mapUser(response.user, mappedUser) : undefined,
239
+ };
240
+
241
+ return this.processCookies(res, result);
242
+ }
243
+
244
+ throw new UnauthorizedException('Invalid credentials');
245
+ } catch (error) {
246
+ this.logger.debug(`Sign-in error: ${error instanceof Error ? error.message : 'Unknown error'}`);
247
+ throw new UnauthorizedException('Invalid credentials');
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Sign up with email and password
253
+ */
254
+ @ApiBody({ type: BetterAuthSignUpInput })
255
+ @ApiCreatedResponse({ description: 'Signed up successfully', type: BetterAuthResponse })
256
+ @ApiOperation({ description: 'Sign up via Better-Auth with email and password', summary: 'Sign Up' })
257
+ @Post('sign-up/email')
258
+ @Roles(RoleEnum.S_EVERYONE)
259
+ async signUp(
260
+ @Res({ passthrough: true }) res: Response,
261
+ @Body() input: BetterAuthSignUpInput,
262
+ ): Promise<BetterAuthResponse> {
263
+ this.ensureEnabled();
264
+
265
+ const api = this.betterAuthService.getApi();
266
+ if (!api) {
267
+ throw new BadRequestException('Better-Auth API not available');
268
+ }
269
+
270
+ try {
271
+ const response = await api.signUpEmail({
272
+ body: {
273
+ email: input.email,
274
+ name: input.name || input.email.split('@')[0],
275
+ password: input.password,
276
+ },
277
+ });
278
+
279
+ if (!response) {
280
+ throw new BadRequestException('Sign-up failed');
281
+ }
282
+
283
+ if (hasUser(response)) {
284
+ // Link or create user in our database
285
+ await this.userMapper.linkOrCreateUser(response.user);
286
+ const mappedUser = await this.userMapper.mapSessionUser(response.user);
287
+
288
+ const result: BetterAuthResponse = {
289
+ requiresTwoFactor: false,
290
+ session: hasSession(response) ? this.mapSession(response.session) : undefined,
291
+ success: true,
292
+ user: mappedUser ? this.mapUser(response.user, mappedUser) : undefined,
293
+ };
294
+
295
+ return this.processCookies(res, result);
296
+ }
297
+
298
+ throw new BadRequestException('Sign-up failed');
299
+ } catch (error) {
300
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
301
+ this.logger.debug(`Sign-up error: ${errorMessage}`);
302
+ if (errorMessage.includes('already exists')) {
303
+ throw new BadRequestException('User with this email already exists');
304
+ }
305
+ throw new BadRequestException('Sign-up failed');
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Sign out (logout)
311
+ */
312
+ @ApiOkResponse({ description: 'Signed out successfully', type: BetterAuthResponse })
313
+ @ApiOperation({ description: 'Sign out from Better-Auth', summary: 'Sign Out' })
314
+ @Get('sign-out')
315
+ @Roles(RoleEnum.S_EVERYONE)
316
+ async signOut(@Req() req: Request, @Res({ passthrough: true }) res: Response): Promise<BetterAuthResponse> {
317
+ if (!this.betterAuthService.isEnabled()) {
318
+ return { success: true };
319
+ }
320
+
321
+ try {
322
+ // Get session token from cookies or authorization header
323
+ const sessionToken = this.extractSessionToken(req);
324
+
325
+ if (sessionToken) {
326
+ await this.betterAuthService.revokeSession(sessionToken);
327
+ }
328
+
329
+ // Clear cookies
330
+ this.clearAuthCookies(res);
331
+
332
+ return { success: true };
333
+ } catch (error) {
334
+ this.logger.debug(`Sign-out error: ${error instanceof Error ? error.message : 'Unknown error'}`);
335
+ // Still return success - user is logged out from our perspective
336
+ this.clearAuthCookies(res);
337
+ return { success: true };
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Get current session
343
+ */
344
+ @ApiOkResponse({ description: 'Current session', type: BetterAuthResponse })
345
+ @ApiOperation({ description: 'Get current session from Better-Auth', summary: 'Get Session' })
346
+ @Get('session')
347
+ @Roles(RoleEnum.S_EVERYONE)
348
+ async getSession(@Req() req: Request): Promise<BetterAuthResponse> {
349
+ if (!this.betterAuthService.isEnabled()) {
350
+ return { error: 'Better-Auth is disabled', success: false };
351
+ }
352
+
353
+ try {
354
+ const { session, user } = await this.betterAuthService.getSession(req);
355
+
356
+ if (!session || !user) {
357
+ return { success: false };
358
+ }
359
+
360
+ const mappedUser = await this.userMapper.mapSessionUser(user);
361
+
362
+ return {
363
+ session: this.mapSession(session),
364
+ success: true,
365
+ user: mappedUser ? this.mapUser(user, mappedUser) : undefined,
366
+ };
367
+ } catch (error) {
368
+ this.logger.debug(`Get session error: ${error instanceof Error ? error.message : 'Unknown error'}`);
369
+ return { success: false };
370
+ }
371
+ }
372
+
373
+ // ===================================================================================================================
374
+ // Two-Factor Authentication Endpoints
375
+ // ===================================================================================================================
376
+
377
+ /**
378
+ * Enable 2FA for current user
379
+ */
380
+ @ApiOkResponse({ description: '2FA setup information', type: BetterAuthTwoFactorSetupResponse })
381
+ @ApiOperation({ description: 'Enable Two-Factor Authentication', summary: 'Enable 2FA' })
382
+ @Post('two-factor/enable')
383
+ @Roles(RoleEnum.S_USER)
384
+ async enableTwoFactor(@Req() req: Request): Promise<BetterAuthResponse | BetterAuthTwoFactorSetupResponse> {
385
+ this.ensureEnabled();
386
+
387
+ if (!this.betterAuthService.isTwoFactorEnabled()) {
388
+ throw new BadRequestException('Two-factor authentication is not enabled');
389
+ }
390
+
391
+ const api = this.betterAuthService.getApi();
392
+ if (!api || !('enableTwoFactor' in api)) {
393
+ throw new BadRequestException('2FA API not available');
394
+ }
395
+
396
+ try {
397
+ const headers = this.extractHeaders(req);
398
+ const response = await (api as any).enableTwoFactor({ headers });
399
+
400
+ if (!response) {
401
+ throw new BadRequestException('Failed to enable 2FA');
402
+ }
403
+
404
+ return {
405
+ backupCodes: response.backupCodes || [],
406
+ success: true,
407
+ totpSecret: response.totpSecret || '',
408
+ totpUri: response.totpURI || '',
409
+ };
410
+ } catch (error) {
411
+ this.logger.debug(`Enable 2FA error: ${error instanceof Error ? error.message : 'Unknown error'}`);
412
+ throw new BadRequestException('Failed to enable 2FA');
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Verify 2FA code during sign-in
418
+ */
419
+ @ApiBody({ type: BetterAuthTwoFactorInput })
420
+ @ApiOkResponse({ description: 'Verification result', type: BetterAuthResponse })
421
+ @ApiOperation({ description: 'Verify Two-Factor Authentication code', summary: 'Verify 2FA' })
422
+ @HttpCode(HttpStatus.OK)
423
+ @Post('two-factor/verify')
424
+ @Roles(RoleEnum.S_EVERYONE)
425
+ async verifyTwoFactor(
426
+ @Req() req: Request,
427
+ @Res({ passthrough: true }) res: Response,
428
+ @Body() input: BetterAuthTwoFactorInput,
429
+ ): Promise<BetterAuthResponse> {
430
+ this.ensureEnabled();
431
+
432
+ if (!this.betterAuthService.isTwoFactorEnabled()) {
433
+ throw new BadRequestException('Two-factor authentication is not enabled');
434
+ }
435
+
436
+ const api = this.betterAuthService.getApi();
437
+ if (!api || !('verifyTOTP' in api)) {
438
+ throw new BadRequestException('2FA API not available');
439
+ }
440
+
441
+ try {
442
+ const headers = this.extractHeaders(req);
443
+ const response = await (api as any).verifyTOTP({
444
+ body: { code: input.code },
445
+ headers,
446
+ });
447
+
448
+ if (!response) {
449
+ throw new UnauthorizedException('Invalid 2FA code');
450
+ }
451
+
452
+ if (hasUser(response)) {
453
+ const mappedUser = await this.userMapper.mapSessionUser(response.user);
454
+ const token = this.betterAuthService.isJwtEnabled() ? (response as TokenResponse).token : undefined;
455
+
456
+ const result: BetterAuthResponse = {
457
+ session: hasSession(response) ? this.mapSession(response.session) : undefined,
458
+ success: true,
459
+ token,
460
+ user: mappedUser ? this.mapUser(response.user, mappedUser) : undefined,
461
+ };
462
+
463
+ return this.processCookies(res, result);
464
+ }
465
+
466
+ throw new UnauthorizedException('Invalid 2FA code');
467
+ } catch (error) {
468
+ this.logger.debug(`Verify 2FA error: ${error instanceof Error ? error.message : 'Unknown error'}`);
469
+ throw new UnauthorizedException('Invalid 2FA code');
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Disable 2FA for current user
475
+ */
476
+ @ApiOkResponse({ description: 'Disable result', type: BetterAuthResponse })
477
+ @ApiOperation({ description: 'Disable Two-Factor Authentication', summary: 'Disable 2FA' })
478
+ @Post('two-factor/disable')
479
+ @Roles(RoleEnum.S_USER)
480
+ async disableTwoFactor(@Req() req: Request): Promise<BetterAuthResponse> {
481
+ this.ensureEnabled();
482
+
483
+ if (!this.betterAuthService.isTwoFactorEnabled()) {
484
+ throw new BadRequestException('Two-factor authentication is not enabled');
485
+ }
486
+
487
+ const api = this.betterAuthService.getApi();
488
+ if (!api || !('disableTwoFactor' in api)) {
489
+ throw new BadRequestException('2FA API not available');
490
+ }
491
+
492
+ try {
493
+ const headers = this.extractHeaders(req);
494
+ await (api as any).disableTwoFactor({ headers });
495
+
496
+ return { success: true };
497
+ } catch (error) {
498
+ this.logger.debug(`Disable 2FA error: ${error instanceof Error ? error.message : 'Unknown error'}`);
499
+ throw new BadRequestException('Failed to disable 2FA');
500
+ }
501
+ }
502
+
503
+ // ===================================================================================================================
504
+ // Helper Methods (protected for extension)
505
+ // ===================================================================================================================
506
+
507
+ /**
508
+ * Ensure Better-Auth is enabled
509
+ */
510
+ protected ensureEnabled(): void {
511
+ if (!this.betterAuthService.isEnabled()) {
512
+ throw new BadRequestException('Better-Auth is not enabled');
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Extract session token from request
518
+ */
519
+ protected extractSessionToken(req: Request): null | string {
520
+ // Check Authorization header
521
+ const authHeader = req.headers.authorization;
522
+ if (authHeader?.startsWith('Bearer ')) {
523
+ return authHeader.substring(7);
524
+ }
525
+
526
+ // Check cookies
527
+ const basePath = this.betterAuthService.getBasePath().replace(/^\//, '').replace(/\//g, '.');
528
+ const cookieName = `${basePath}.session_token`;
529
+ return req.cookies?.[cookieName] || req.cookies?.['better-auth.session_token'] || null;
530
+ }
531
+
532
+ /**
533
+ * Extract headers for Better-Auth API calls
534
+ */
535
+ protected extractHeaders(req: Request): Headers {
536
+ const headers = new Headers();
537
+ for (const [key, value] of Object.entries(req.headers)) {
538
+ if (typeof value === 'string') {
539
+ headers.set(key, value);
540
+ } else if (Array.isArray(value)) {
541
+ headers.set(key, value.join(', '));
542
+ }
543
+ }
544
+ return headers;
545
+ }
546
+
547
+ /**
548
+ * Map session to response format
549
+ */
550
+ protected mapSession(session: null | undefined | { expiresAt: Date; id: string }): BetterAuthSessionInfo | undefined {
551
+ if (!session) return undefined;
552
+ return {
553
+ expiresAt: session.expiresAt instanceof Date ? session.expiresAt.toISOString() : String(session.expiresAt),
554
+ id: session.id,
555
+ };
556
+ }
557
+
558
+ /**
559
+ * Map user to response format
560
+ * @param sessionUser - The user from Better-Auth session
561
+ * @param _mappedUser - The synced user from legacy system (available for override customization)
562
+ */
563
+ // eslint-disable-next-line unused-imports/no-unused-vars
564
+ protected mapUser(sessionUser: BetterAuthSessionUser, _mappedUser: any): BetterAuthUserResponse {
565
+ return {
566
+ email: sessionUser.email,
567
+ emailVerified: sessionUser.emailVerified || false,
568
+ id: sessionUser.id,
569
+ name: sessionUser.name || sessionUser.email.split('@')[0],
570
+ };
571
+ }
572
+
573
+ /**
574
+ * Process cookies for response
575
+ */
576
+ protected processCookies(res: Response, result: BetterAuthResponse): BetterAuthResponse {
577
+ // Check if cookie handling is activated
578
+ if (this.configService.getFastButReadOnly('cookies')) {
579
+ const cookieOptions = { httpOnly: true, sameSite: 'lax' as const, secure: process.env.NODE_ENV === 'production' };
580
+
581
+ // Set or clear token cookie
582
+ if (result.token) {
583
+ res.cookie('token', result.token, cookieOptions);
584
+ delete result.token; // Remove from response body
585
+ }
586
+
587
+ // Set session cookie if we have a session
588
+ if (result.session) {
589
+ res.cookie('session', result.session.id, cookieOptions);
590
+ }
591
+ }
592
+
593
+ return result;
594
+ }
595
+
596
+ /**
597
+ * Clear authentication cookies
598
+ */
599
+ protected clearAuthCookies(res: Response): void {
600
+ const cookieOptions = { httpOnly: true, sameSite: 'lax' as const };
601
+ res.cookie('token', '', { ...cookieOptions, maxAge: 0 });
602
+ res.cookie('session', '', { ...cookieOptions, maxAge: 0 });
603
+ res.cookie('better-auth.session_token', '', { ...cookieOptions, maxAge: 0 });
604
+ }
605
+ }