@lenne.tech/nest-server 11.6.2 → 11.7.1

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 (136) hide show
  1. package/dist/config.env.js +19 -12
  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 +21 -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/auth/core-auth.controller.d.ts +1 -0
  13. package/dist/core/modules/auth/core-auth.controller.js +28 -2
  14. package/dist/core/modules/auth/core-auth.controller.js.map +1 -1
  15. package/dist/core/modules/auth/core-auth.module.js +14 -1
  16. package/dist/core/modules/auth/core-auth.module.js.map +1 -1
  17. package/dist/core/modules/auth/core-auth.resolver.d.ts +1 -0
  18. package/dist/core/modules/auth/core-auth.resolver.js +20 -2
  19. package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
  20. package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.d.ts +4 -0
  21. package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.js +17 -0
  22. package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.js.map +1 -0
  23. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.d.ts +9 -0
  24. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js +74 -0
  25. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js.map +1 -0
  26. package/dist/core/modules/auth/interfaces/auth-provider.interface.d.ts +7 -0
  27. package/dist/core/modules/auth/interfaces/auth-provider.interface.js +5 -0
  28. package/dist/core/modules/auth/interfaces/auth-provider.interface.js.map +1 -0
  29. package/dist/core/modules/auth/interfaces/core-auth-user.interface.d.ts +1 -0
  30. package/dist/core/modules/auth/services/core-auth.service.d.ts +10 -1
  31. package/dist/core/modules/auth/services/core-auth.service.js +141 -9
  32. package/dist/core/modules/auth/services/core-auth.service.js.map +1 -1
  33. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.d.ts +31 -0
  34. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +153 -0
  35. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -0
  36. package/dist/core/modules/better-auth/better-auth-migration-status.model.d.ts +10 -0
  37. package/dist/core/modules/better-auth/better-auth-migration-status.model.js +57 -0
  38. package/dist/core/modules/better-auth/better-auth-migration-status.model.js.map +1 -0
  39. package/dist/core/modules/better-auth/better-auth-models.d.ts +0 -1
  40. package/dist/core/modules/better-auth/better-auth-models.js +0 -4
  41. package/dist/core/modules/better-auth/better-auth-models.js.map +1 -1
  42. package/dist/core/modules/better-auth/better-auth-user.mapper.d.ts +33 -0
  43. package/dist/core/modules/better-auth/better-auth-user.mapper.js +443 -0
  44. package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +1 -1
  45. package/dist/core/modules/better-auth/better-auth.config.js +3 -0
  46. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  47. package/dist/core/modules/better-auth/better-auth.module.d.ts +10 -2
  48. package/dist/core/modules/better-auth/better-auth.module.js +40 -52
  49. package/dist/core/modules/better-auth/better-auth.module.js.map +1 -1
  50. package/dist/core/modules/better-auth/better-auth.resolver.d.ts +8 -12
  51. package/dist/core/modules/better-auth/better-auth.resolver.js +33 -351
  52. package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
  53. package/dist/core/modules/better-auth/better-auth.service.d.ts +0 -1
  54. package/dist/core/modules/better-auth/better-auth.service.js +0 -3
  55. package/dist/core/modules/better-auth/better-auth.service.js.map +1 -1
  56. package/dist/core/modules/better-auth/better-auth.types.d.ts +9 -8
  57. package/dist/core/modules/better-auth/better-auth.types.js +14 -3
  58. package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
  59. package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +67 -0
  60. package/dist/core/modules/better-auth/core-better-auth.controller.js +504 -0
  61. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -0
  62. package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +61 -0
  63. package/dist/core/modules/better-auth/core-better-auth.resolver.js +552 -0
  64. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -0
  65. package/dist/core/modules/better-auth/index.d.ts +3 -0
  66. package/dist/core/modules/better-auth/index.js +3 -0
  67. package/dist/core/modules/better-auth/index.js.map +1 -1
  68. package/dist/core/modules/user/core-user.service.d.ts +7 -1
  69. package/dist/core/modules/user/core-user.service.js +57 -3
  70. package/dist/core/modules/user/core-user.service.js.map +1 -1
  71. package/dist/core/modules/user/interfaces/core-user-service-options.interface.d.ts +4 -0
  72. package/dist/core/modules/user/interfaces/core-user-service-options.interface.js +3 -0
  73. package/dist/core/modules/user/interfaces/core-user-service-options.interface.js.map +1 -0
  74. package/dist/core.module.d.ts +3 -0
  75. package/dist/core.module.js +132 -54
  76. package/dist/core.module.js.map +1 -1
  77. package/dist/index.d.ts +5 -0
  78. package/dist/index.js +5 -0
  79. package/dist/index.js.map +1 -1
  80. package/dist/server/modules/auth/auth.resolver.js +2 -0
  81. package/dist/server/modules/auth/auth.resolver.js.map +1 -1
  82. package/dist/server/modules/better-auth/better-auth.controller.d.ts +10 -0
  83. package/dist/server/modules/better-auth/better-auth.controller.js +36 -0
  84. package/dist/server/modules/better-auth/better-auth.controller.js.map +1 -0
  85. package/dist/server/modules/better-auth/better-auth.module.d.ts +9 -0
  86. package/dist/server/modules/better-auth/better-auth.module.js +44 -0
  87. package/dist/server/modules/better-auth/better-auth.module.js.map +1 -0
  88. package/dist/server/modules/better-auth/better-auth.resolver.d.ts +47 -0
  89. package/dist/server/modules/better-auth/better-auth.resolver.js +234 -0
  90. package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -0
  91. package/dist/server/modules/file/file-info.model.d.ts +71 -3
  92. package/dist/server/modules/user/user.model.d.ts +169 -3
  93. package/dist/server/modules/user/user.service.d.ts +3 -1
  94. package/dist/server/modules/user/user.service.js +7 -3
  95. package/dist/server/modules/user/user.service.js.map +1 -1
  96. package/dist/server/server.module.js +6 -1
  97. package/dist/server/server.module.js.map +1 -1
  98. package/dist/tsconfig.build.tsbuildinfo +1 -1
  99. package/package.json +20 -29
  100. package/src/config.env.ts +34 -13
  101. package/src/core/common/helpers/filter.helper.ts +15 -17
  102. package/src/core/common/helpers/gridfs.helper.ts +5 -5
  103. package/src/core/common/interfaces/server-options.interface.ts +222 -14
  104. package/src/core/common/services/crud.service.ts +22 -22
  105. package/src/core/modules/auth/core-auth.controller.ts +93 -5
  106. package/src/core/modules/auth/core-auth.module.ts +15 -1
  107. package/src/core/modules/auth/core-auth.resolver.ts +70 -2
  108. package/src/core/modules/auth/exceptions/legacy-auth-disabled.exception.ts +35 -0
  109. package/src/core/modules/auth/guards/legacy-auth-rate-limit.guard.ts +109 -0
  110. package/src/core/modules/auth/interfaces/auth-provider.interface.ts +86 -0
  111. package/src/core/modules/auth/interfaces/core-auth-user.interface.ts +6 -0
  112. package/src/core/modules/auth/services/core-auth.service.ts +245 -6
  113. package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +283 -0
  114. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +254 -0
  115. package/src/core/modules/better-auth/README.md +698 -54
  116. package/src/core/modules/better-auth/better-auth-migration-status.model.ts +73 -0
  117. package/src/core/modules/better-auth/better-auth-models.ts +0 -3
  118. package/src/core/modules/better-auth/better-auth-user.mapper.ts +805 -0
  119. package/src/core/modules/better-auth/better-auth.config.ts +5 -0
  120. package/src/core/modules/better-auth/better-auth.module.ts +107 -66
  121. package/src/core/modules/better-auth/better-auth.resolver.ts +88 -553
  122. package/src/core/modules/better-auth/better-auth.service.ts +0 -9
  123. package/src/core/modules/better-auth/better-auth.types.ts +25 -10
  124. package/src/core/modules/better-auth/core-better-auth.controller.ts +646 -0
  125. package/src/core/modules/better-auth/core-better-auth.resolver.ts +730 -0
  126. package/src/core/modules/better-auth/index.ts +9 -1
  127. package/src/core/modules/user/core-user.service.ts +131 -4
  128. package/src/core/modules/user/interfaces/core-user-service-options.interface.ts +15 -0
  129. package/src/core.module.ts +257 -74
  130. package/src/index.ts +5 -0
  131. package/src/server/modules/auth/auth.resolver.ts +8 -0
  132. package/src/server/modules/better-auth/better-auth.controller.ts +41 -0
  133. package/src/server/modules/better-auth/better-auth.module.ts +88 -0
  134. package/src/server/modules/better-auth/better-auth.resolver.ts +210 -0
  135. package/src/server/modules/user/user.service.ts +4 -2
  136. package/src/server/server.module.ts +10 -1
@@ -0,0 +1,646 @@
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 to sign in, with automatic legacy user migration
215
+ return this.attemptSignIn(res, input, api, true);
216
+ }
217
+
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
233
+ const normalizedPassword = this.userMapper.normalizePasswordForIam(input.password);
234
+
235
+ try {
236
+ const response = await api!.signInEmail({
237
+ body: { email: input.email, password: normalizedPassword },
238
+ });
239
+
240
+ if (!response) {
241
+ throw new UnauthorizedException('Invalid credentials');
242
+ }
243
+
244
+ // Check for 2FA requirement
245
+ if (requires2FA(response)) {
246
+ return { requiresTwoFactor: true, success: false };
247
+ }
248
+
249
+ // Get user data
250
+ if (hasUser(response)) {
251
+ const mappedUser = await this.userMapper.mapSessionUser(response.user);
252
+ const token = this.betterAuthService.isJwtEnabled() ? (response as TokenResponse).token : undefined;
253
+
254
+ const result: BetterAuthResponse = {
255
+ requiresTwoFactor: false,
256
+ session: hasSession(response) ? this.mapSession(response.session) : undefined,
257
+ success: true,
258
+ token,
259
+ user: mappedUser ? this.mapUser(response.user, mappedUser) : undefined,
260
+ };
261
+
262
+ return this.processCookies(res, result);
263
+ }
264
+
265
+ throw new UnauthorizedException('Invalid credentials');
266
+ } catch (error) {
267
+ this.logger.debug(`Sign-in error: ${error instanceof Error ? error.message : 'Unknown error'}`);
268
+
269
+ // If migration is allowed, try to migrate legacy user and retry
270
+ if (allowMigration) {
271
+ // Pass the original password for legacy verification, but migration uses normalized password
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
+ }
278
+ }
279
+
280
+ throw new UnauthorizedException('Invalid credentials');
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Sign up with email and password
286
+ */
287
+ @ApiBody({ type: BetterAuthSignUpInput })
288
+ @ApiCreatedResponse({ description: 'Signed up successfully', type: BetterAuthResponse })
289
+ @ApiOperation({ description: 'Sign up via Better-Auth with email and password', summary: 'Sign Up' })
290
+ @Post('sign-up/email')
291
+ @Roles(RoleEnum.S_EVERYONE)
292
+ async signUp(
293
+ @Res({ passthrough: true }) res: Response,
294
+ @Body() input: BetterAuthSignUpInput,
295
+ ): Promise<BetterAuthResponse> {
296
+ this.ensureEnabled();
297
+
298
+ const api = this.betterAuthService.getApi();
299
+ if (!api) {
300
+ throw new BadRequestException('Better-Auth API not available');
301
+ }
302
+
303
+ // Normalize password to SHA256 format for consistency with Legacy Auth
304
+ const normalizedPassword = this.userMapper.normalizePasswordForIam(input.password);
305
+
306
+ try {
307
+ const response = await api.signUpEmail({
308
+ body: {
309
+ email: input.email,
310
+ name: input.name || input.email.split('@')[0],
311
+ password: normalizedPassword,
312
+ },
313
+ });
314
+
315
+ if (!response) {
316
+ throw new BadRequestException('Sign-up failed');
317
+ }
318
+
319
+ if (hasUser(response)) {
320
+ // Link or create user in our database
321
+ await this.userMapper.linkOrCreateUser(response.user);
322
+
323
+ // Sync password to legacy (enables IAM Sign-Up → Legacy Sign-In)
324
+ // Pass the plain password so it can be hashed with bcrypt for Legacy Auth
325
+ await this.userMapper.syncPasswordToLegacy(response.user.id, response.user.email, input.password);
326
+
327
+ const mappedUser = await this.userMapper.mapSessionUser(response.user);
328
+
329
+ const result: BetterAuthResponse = {
330
+ requiresTwoFactor: false,
331
+ session: hasSession(response) ? this.mapSession(response.session) : undefined,
332
+ success: true,
333
+ user: mappedUser ? this.mapUser(response.user, mappedUser) : undefined,
334
+ };
335
+
336
+ return this.processCookies(res, result);
337
+ }
338
+
339
+ throw new BadRequestException('Sign-up failed');
340
+ } catch (error) {
341
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
342
+ this.logger.debug(`Sign-up error: ${errorMessage}`);
343
+ if (errorMessage.includes('already exists')) {
344
+ throw new BadRequestException('User with this email already exists');
345
+ }
346
+ throw new BadRequestException('Sign-up failed');
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Sign out (logout)
352
+ */
353
+ @ApiOkResponse({ description: 'Signed out successfully', type: BetterAuthResponse })
354
+ @ApiOperation({ description: 'Sign out from Better-Auth', summary: 'Sign Out' })
355
+ @Get('sign-out')
356
+ @Roles(RoleEnum.S_EVERYONE)
357
+ async signOut(@Req() req: Request, @Res({ passthrough: true }) res: Response): Promise<BetterAuthResponse> {
358
+ if (!this.betterAuthService.isEnabled()) {
359
+ return { success: true };
360
+ }
361
+
362
+ try {
363
+ // Get session token from cookies or authorization header
364
+ const sessionToken = this.extractSessionToken(req);
365
+
366
+ if (sessionToken) {
367
+ await this.betterAuthService.revokeSession(sessionToken);
368
+ }
369
+
370
+ // Clear cookies
371
+ this.clearAuthCookies(res);
372
+
373
+ return { success: true };
374
+ } catch (error) {
375
+ this.logger.debug(`Sign-out error: ${error instanceof Error ? error.message : 'Unknown error'}`);
376
+ // Still return success - user is logged out from our perspective
377
+ this.clearAuthCookies(res);
378
+ return { success: true };
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Get current session
384
+ */
385
+ @ApiOkResponse({ description: 'Current session', type: BetterAuthResponse })
386
+ @ApiOperation({ description: 'Get current session from Better-Auth', summary: 'Get Session' })
387
+ @Get('session')
388
+ @Roles(RoleEnum.S_EVERYONE)
389
+ async getSession(@Req() req: Request): Promise<BetterAuthResponse> {
390
+ if (!this.betterAuthService.isEnabled()) {
391
+ return { error: 'Better-Auth is disabled', success: false };
392
+ }
393
+
394
+ try {
395
+ const { session, user } = await this.betterAuthService.getSession(req);
396
+
397
+ if (!session || !user) {
398
+ return { success: false };
399
+ }
400
+
401
+ const mappedUser = await this.userMapper.mapSessionUser(user);
402
+
403
+ return {
404
+ session: this.mapSession(session),
405
+ success: true,
406
+ user: mappedUser ? this.mapUser(user, mappedUser) : undefined,
407
+ };
408
+ } catch (error) {
409
+ this.logger.debug(`Get session error: ${error instanceof Error ? error.message : 'Unknown error'}`);
410
+ return { success: false };
411
+ }
412
+ }
413
+
414
+ // ===================================================================================================================
415
+ // Two-Factor Authentication Endpoints
416
+ // ===================================================================================================================
417
+
418
+ /**
419
+ * Enable 2FA for current user
420
+ */
421
+ @ApiOkResponse({ description: '2FA setup information', type: BetterAuthTwoFactorSetupResponse })
422
+ @ApiOperation({ description: 'Enable Two-Factor Authentication', summary: 'Enable 2FA' })
423
+ @Post('two-factor/enable')
424
+ @Roles(RoleEnum.S_USER)
425
+ async enableTwoFactor(@Req() req: Request): Promise<BetterAuthResponse | BetterAuthTwoFactorSetupResponse> {
426
+ this.ensureEnabled();
427
+
428
+ if (!this.betterAuthService.isTwoFactorEnabled()) {
429
+ throw new BadRequestException('Two-factor authentication is not enabled');
430
+ }
431
+
432
+ const api = this.betterAuthService.getApi();
433
+ if (!api || !('enableTwoFactor' in api)) {
434
+ throw new BadRequestException('2FA API not available');
435
+ }
436
+
437
+ try {
438
+ const headers = this.extractHeaders(req);
439
+ const response = await (api as any).enableTwoFactor({ headers });
440
+
441
+ if (!response) {
442
+ throw new BadRequestException('Failed to enable 2FA');
443
+ }
444
+
445
+ return {
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
459
+ */
460
+ @ApiBody({ type: BetterAuthTwoFactorInput })
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')
465
+ @Roles(RoleEnum.S_EVERYONE)
466
+ async verifyTwoFactor(
467
+ @Req() req: Request,
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
+ }
542
+ }
543
+
544
+ // ===================================================================================================================
545
+ // Helper Methods (protected for extension)
546
+ // ===================================================================================================================
547
+
548
+ /**
549
+ * Ensure Better-Auth is enabled
550
+ */
551
+ protected ensureEnabled(): void {
552
+ if (!this.betterAuthService.isEnabled()) {
553
+ throw new BadRequestException('Better-Auth is not enabled');
554
+ }
555
+ }
556
+
557
+ /**
558
+ * Extract session token from request
559
+ */
560
+ protected extractSessionToken(req: Request): null | string {
561
+ // Check Authorization header
562
+ const authHeader = req.headers.authorization;
563
+ if (authHeader?.startsWith('Bearer ')) {
564
+ return authHeader.substring(7);
565
+ }
566
+
567
+ // Check cookies
568
+ const basePath = this.betterAuthService.getBasePath().replace(/^\//, '').replace(/\//g, '.');
569
+ const cookieName = `${basePath}.session_token`;
570
+ return req.cookies?.[cookieName] || req.cookies?.['better-auth.session_token'] || null;
571
+ }
572
+
573
+ /**
574
+ * Extract headers for Better-Auth API calls
575
+ */
576
+ protected extractHeaders(req: Request): Headers {
577
+ const headers = new Headers();
578
+ for (const [key, value] of Object.entries(req.headers)) {
579
+ if (typeof value === 'string') {
580
+ headers.set(key, value);
581
+ } else if (Array.isArray(value)) {
582
+ headers.set(key, value.join(', '));
583
+ }
584
+ }
585
+ return headers;
586
+ }
587
+
588
+ /**
589
+ * Map session to response format
590
+ */
591
+ protected mapSession(session: null | undefined | { expiresAt: Date; id: string }): BetterAuthSessionInfo | undefined {
592
+ if (!session) return undefined;
593
+ return {
594
+ expiresAt: session.expiresAt instanceof Date ? session.expiresAt.toISOString() : String(session.expiresAt),
595
+ id: session.id,
596
+ };
597
+ }
598
+
599
+ /**
600
+ * Map user to response format
601
+ * @param sessionUser - The user from Better-Auth session
602
+ * @param _mappedUser - The synced user from legacy system (available for override customization)
603
+ */
604
+ // eslint-disable-next-line unused-imports/no-unused-vars
605
+ protected mapUser(sessionUser: BetterAuthSessionUser, _mappedUser: any): BetterAuthUserResponse {
606
+ return {
607
+ email: sessionUser.email,
608
+ emailVerified: sessionUser.emailVerified || false,
609
+ id: sessionUser.id,
610
+ name: sessionUser.name || sessionUser.email.split('@')[0],
611
+ };
612
+ }
613
+
614
+ /**
615
+ * Process cookies for response
616
+ */
617
+ protected processCookies(res: Response, result: BetterAuthResponse): BetterAuthResponse {
618
+ // Check if cookie handling is activated
619
+ if (this.configService.getFastButReadOnly('cookies')) {
620
+ const cookieOptions = { httpOnly: true, sameSite: 'lax' as const, secure: process.env.NODE_ENV === 'production' };
621
+
622
+ // Set or clear token cookie
623
+ if (result.token) {
624
+ res.cookie('token', result.token, cookieOptions);
625
+ delete result.token; // Remove from response body
626
+ }
627
+
628
+ // Set session cookie if we have a session
629
+ if (result.session) {
630
+ res.cookie('session', result.session.id, cookieOptions);
631
+ }
632
+ }
633
+
634
+ return result;
635
+ }
636
+
637
+ /**
638
+ * Clear authentication cookies
639
+ */
640
+ protected clearAuthCookies(res: Response): void {
641
+ const cookieOptions = { httpOnly: true, sameSite: 'lax' as const };
642
+ res.cookie('token', '', { ...cookieOptions, maxAge: 0 });
643
+ res.cookie('session', '', { ...cookieOptions, maxAge: 0 });
644
+ res.cookie('better-auth.session_token', '', { ...cookieOptions, maxAge: 0 });
645
+ }
646
+ }