@lenne.tech/nest-server 11.6.1 → 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.
- package/dist/config.env.js +132 -0
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/decorators/graphql-populate.decorator.d.ts +2 -2
- package/dist/core/common/decorators/restricted.decorator.d.ts +1 -0
- package/dist/core/common/decorators/restricted.decorator.js +1 -1
- package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
- package/dist/core/common/helpers/filter.helper.d.ts +9 -9
- package/dist/core/common/helpers/filter.helper.js +2 -4
- package/dist/core/common/helpers/filter.helper.js.map +1 -1
- package/dist/core/common/helpers/gridfs.helper.js +3 -3
- package/dist/core/common/helpers/gridfs.helper.js.map +1 -1
- package/dist/core/common/helpers/input.helper.d.ts +1 -0
- package/dist/core/common/helpers/input.helper.js +1 -1
- package/dist/core/common/helpers/input.helper.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +51 -0
- package/dist/core/common/services/crud.service.d.ts +16 -16
- package/dist/core/common/services/crud.service.js +1 -1
- package/dist/core/common/services/crud.service.js.map +1 -1
- package/dist/core/modules/auth/auth-guard-strategy.enum.d.ts +1 -0
- package/dist/core/modules/auth/auth-guard-strategy.enum.js +1 -0
- package/dist/core/modules/auth/auth-guard-strategy.enum.js.map +1 -1
- package/dist/core/modules/auth/guards/auth.guard.js +11 -5
- package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
- package/dist/core/modules/auth/tokens.decorator.d.ts +1 -1
- package/dist/core/modules/better-auth/better-auth-auth.model.d.ts +9 -0
- package/dist/core/modules/better-auth/better-auth-auth.model.js +63 -0
- package/dist/core/modules/better-auth/better-auth-auth.model.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth-models.d.ts +43 -0
- package/dist/core/modules/better-auth/better-auth-models.js +181 -0
- package/dist/core/modules/better-auth/better-auth-models.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.d.ts +12 -0
- package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.js +70 -0
- package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth-rate-limiter.service.d.ts +32 -0
- package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js +173 -0
- package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth-user.mapper.d.ts +43 -0
- package/dist/core/modules/better-auth/better-auth-user.mapper.js +159 -0
- package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth.config.d.ts +9 -0
- package/dist/core/modules/better-auth/better-auth.config.js +254 -0
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth.middleware.d.ts +20 -0
- package/dist/core/modules/better-auth/better-auth.middleware.js +79 -0
- package/dist/core/modules/better-auth/better-auth.middleware.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth.module.d.ts +38 -0
- package/dist/core/modules/better-auth/better-auth.module.js +253 -0
- package/dist/core/modules/better-auth/better-auth.module.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth.resolver.d.ts +45 -0
- package/dist/core/modules/better-auth/better-auth.resolver.js +221 -0
- package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth.service.d.ts +37 -0
- package/dist/core/modules/better-auth/better-auth.service.js +148 -0
- package/dist/core/modules/better-auth/better-auth.service.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth.types.d.ts +39 -0
- package/dist/core/modules/better-auth/better-auth.types.js +26 -0
- package/dist/core/modules/better-auth/better-auth.types.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +66 -0
- package/dist/core/modules/better-auth/core-better-auth.controller.js +491 -0
- package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +59 -0
- package/dist/core/modules/better-auth/core-better-auth.resolver.js +538 -0
- package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -0
- package/dist/core/modules/better-auth/index.d.ts +13 -0
- package/dist/core/modules/better-auth/index.js +30 -0
- package/dist/core/modules/better-auth/index.js.map +1 -0
- package/dist/core/modules/user/core-user.model.d.ts +2 -0
- package/dist/core/modules/user/core-user.model.js +21 -0
- package/dist/core/modules/user/core-user.model.js.map +1 -1
- package/dist/core.module.js +7 -0
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.controller.d.ts +10 -0
- package/dist/server/modules/better-auth/better-auth.controller.js +36 -0
- package/dist/server/modules/better-auth/better-auth.controller.js.map +1 -0
- package/dist/server/modules/better-auth/better-auth.module.d.ts +9 -0
- package/dist/server/modules/better-auth/better-auth.module.js +44 -0
- package/dist/server/modules/better-auth/better-auth.module.js.map +1 -0
- package/dist/server/modules/better-auth/better-auth.resolver.d.ts +45 -0
- package/dist/server/modules/better-auth/better-auth.resolver.js +221 -0
- package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -0
- package/dist/server/modules/file/file-info.model.d.ts +71 -3
- package/dist/server/modules/user/user.model.d.ts +169 -3
- package/dist/server/server.module.js +6 -1
- package/dist/server/server.module.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +21 -22
- package/src/config.env.ts +139 -1
- package/src/core/common/decorators/restricted.decorator.ts +2 -2
- package/src/core/common/helpers/filter.helper.ts +15 -17
- package/src/core/common/helpers/gridfs.helper.ts +5 -5
- package/src/core/common/helpers/input.helper.ts +2 -2
- package/src/core/common/interfaces/server-options.interface.ts +377 -20
- package/src/core/common/services/crud.service.ts +22 -22
- package/src/core/modules/auth/auth-guard-strategy.enum.ts +1 -0
- package/src/core/modules/auth/guards/auth.guard.ts +20 -6
- package/src/core/modules/better-auth/README.md +1422 -0
- package/src/core/modules/better-auth/better-auth-auth.model.ts +69 -0
- package/src/core/modules/better-auth/better-auth-models.ts +140 -0
- package/src/core/modules/better-auth/better-auth-rate-limit.middleware.ts +113 -0
- package/src/core/modules/better-auth/better-auth-rate-limiter.service.ts +326 -0
- package/src/core/modules/better-auth/better-auth-user.mapper.ts +269 -0
- package/src/core/modules/better-auth/better-auth.config.ts +488 -0
- package/src/core/modules/better-auth/better-auth.middleware.ts +111 -0
- package/src/core/modules/better-auth/better-auth.module.ts +474 -0
- package/src/core/modules/better-auth/better-auth.resolver.ts +213 -0
- package/src/core/modules/better-auth/better-auth.service.ts +314 -0
- package/src/core/modules/better-auth/better-auth.types.ts +90 -0
- package/src/core/modules/better-auth/core-better-auth.controller.ts +605 -0
- package/src/core/modules/better-auth/core-better-auth.resolver.ts +705 -0
- package/src/core/modules/better-auth/index.ts +32 -0
- package/src/core/modules/user/core-user.model.ts +29 -0
- package/src/core.module.ts +13 -0
- package/src/index.ts +6 -0
- package/src/server/modules/better-auth/better-auth.controller.ts +41 -0
- package/src/server/modules/better-auth/better-auth.module.ts +88 -0
- package/src/server/modules/better-auth/better-auth.resolver.ts +201 -0
- package/src/server/server.module.ts +10 -1
|
@@ -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
|
+
}
|