@lenne.tech/nest-server 11.6.1 → 11.6.2
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 +141 -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/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 +50 -0
- 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 +44 -0
- package/dist/core/modules/better-auth/better-auth-models.js +185 -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 +251 -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 +30 -0
- package/dist/core/modules/better-auth/better-auth.module.js +265 -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 +49 -0
- package/dist/core/modules/better-auth/better-auth.resolver.js +539 -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 +38 -0
- package/dist/core/modules/better-auth/better-auth.service.js +151 -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 +38 -0
- package/dist/core/modules/better-auth/better-auth.types.js +15 -0
- package/dist/core/modules/better-auth/better-auth.types.js.map +1 -0
- package/dist/core/modules/better-auth/index.d.ts +11 -0
- package/dist/core/modules/better-auth/index.js +28 -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/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +9 -1
- package/src/config.env.ts +148 -1
- package/src/core/common/decorators/restricted.decorator.ts +2 -2
- package/src/core/common/helpers/input.helper.ts +2 -2
- package/src/core/common/interfaces/server-options.interface.ts +344 -20
- 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 +1096 -0
- package/src/core/modules/better-auth/better-auth-auth.model.ts +69 -0
- package/src/core/modules/better-auth/better-auth-models.ts +143 -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 +483 -0
- package/src/core/modules/better-auth/better-auth.middleware.ts +111 -0
- package/src/core/modules/better-auth/better-auth.module.ts +433 -0
- package/src/core/modules/better-auth/better-auth.resolver.ts +678 -0
- package/src/core/modules/better-auth/better-auth.service.ts +323 -0
- package/src/core/modules/better-auth/better-auth.types.ts +75 -0
- package/src/core/modules/better-auth/index.ts +25 -0
- package/src/core/modules/user/core-user.model.ts +29 -0
- package/src/core.module.ts +12 -0
- package/src/index.ts +6 -0
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
import { BadRequestException, Logger, UnauthorizedException, UseGuards } from '@nestjs/common';
|
|
2
|
+
import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
|
|
3
|
+
import { Request, Response } from 'express';
|
|
4
|
+
|
|
5
|
+
import { Roles } from '../../common/decorators/roles.decorator';
|
|
6
|
+
import { RoleEnum } from '../../common/enums/role.enum';
|
|
7
|
+
import { AuthGuardStrategy } from '../auth/auth-guard-strategy.enum';
|
|
8
|
+
import { AuthGuard } from '../auth/guards/auth.guard';
|
|
9
|
+
import { BetterAuthAuthModel } from './better-auth-auth.model';
|
|
10
|
+
import {
|
|
11
|
+
BetterAuth2FASetupModel,
|
|
12
|
+
BetterAuthFeaturesModel,
|
|
13
|
+
BetterAuthPasskeyChallengeModel,
|
|
14
|
+
BetterAuthPasskeyModel,
|
|
15
|
+
BetterAuthSessionModel,
|
|
16
|
+
BetterAuthUserModel,
|
|
17
|
+
} from './better-auth-models';
|
|
18
|
+
import { BetterAuthSessionUser, BetterAuthUserMapper, MappedUser } from './better-auth-user.mapper';
|
|
19
|
+
import { BetterAuthService } from './better-auth.service';
|
|
20
|
+
import {
|
|
21
|
+
BetterAuth2FAResponse,
|
|
22
|
+
BetterAuthSignInResponse,
|
|
23
|
+
BetterAuthSignUpResponse,
|
|
24
|
+
hasSession,
|
|
25
|
+
hasUser,
|
|
26
|
+
requires2FA,
|
|
27
|
+
} from './better-auth.types';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* GraphQL Resolver for Better-Auth operations
|
|
31
|
+
*
|
|
32
|
+
* This resolver provides GraphQL mutations that wrap the Better-Auth REST API,
|
|
33
|
+
* making it compatible with existing GraphQL clients while using Better-Auth
|
|
34
|
+
* for authentication.
|
|
35
|
+
*
|
|
36
|
+
* Note: This resolver only activates when Better-Auth is enabled.
|
|
37
|
+
* When disabled, these mutations will throw an error indicating Better-Auth is not enabled.
|
|
38
|
+
*/
|
|
39
|
+
@Resolver()
|
|
40
|
+
@Roles(RoleEnum.ADMIN)
|
|
41
|
+
export class BetterAuthResolver {
|
|
42
|
+
private readonly logger = new Logger(BetterAuthResolver.name);
|
|
43
|
+
|
|
44
|
+
constructor(
|
|
45
|
+
private readonly betterAuthService: BetterAuthService,
|
|
46
|
+
private readonly userMapper: BetterAuthUserMapper,
|
|
47
|
+
) {}
|
|
48
|
+
|
|
49
|
+
// ===========================================================================
|
|
50
|
+
// Queries
|
|
51
|
+
// ===========================================================================
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get current Better-Auth session
|
|
55
|
+
*/
|
|
56
|
+
@Query(() => BetterAuthSessionModel, {
|
|
57
|
+
description: 'Get current Better-Auth session',
|
|
58
|
+
nullable: true,
|
|
59
|
+
})
|
|
60
|
+
@Roles(RoleEnum.S_USER)
|
|
61
|
+
@UseGuards(AuthGuard(AuthGuardStrategy.JWT))
|
|
62
|
+
async betterAuthSession(@Context() ctx: { req: Request }): Promise<BetterAuthSessionModel | null> {
|
|
63
|
+
if (!this.betterAuthService.isEnabled()) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { session, user } = await this.betterAuthService.getSession(ctx.req);
|
|
68
|
+
|
|
69
|
+
if (!session || !user) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
expiresAt: session.expiresAt,
|
|
75
|
+
id: session.id,
|
|
76
|
+
user: {
|
|
77
|
+
email: user.email,
|
|
78
|
+
emailVerified: user.emailVerified,
|
|
79
|
+
id: user.id,
|
|
80
|
+
name: user.name,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if Better-Auth is enabled
|
|
87
|
+
*/
|
|
88
|
+
@Query(() => Boolean, { description: 'Check if Better-Auth is enabled' })
|
|
89
|
+
@Roles(RoleEnum.S_EVERYONE)
|
|
90
|
+
betterAuthEnabled(): boolean {
|
|
91
|
+
return this.betterAuthService.isEnabled();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get enabled Better-Auth features
|
|
96
|
+
*/
|
|
97
|
+
@Query(() => BetterAuthFeaturesModel, { description: 'Get enabled Better-Auth features' })
|
|
98
|
+
@Roles(RoleEnum.S_EVERYONE)
|
|
99
|
+
betterAuthFeatures(): BetterAuthFeaturesModel {
|
|
100
|
+
return {
|
|
101
|
+
enabled: this.betterAuthService.isEnabled(),
|
|
102
|
+
jwt: this.betterAuthService.isJwtEnabled(),
|
|
103
|
+
legacyPassword: this.betterAuthService.isLegacyPasswordEnabled(),
|
|
104
|
+
passkey: this.betterAuthService.isPasskeyEnabled(),
|
|
105
|
+
socialProviders: this.betterAuthService.getEnabledSocialProviders(),
|
|
106
|
+
twoFactor: this.betterAuthService.isTwoFactorEnabled(),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ===========================================================================
|
|
111
|
+
// Mutations
|
|
112
|
+
// ===========================================================================
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Sign in via Better-Auth
|
|
116
|
+
*
|
|
117
|
+
* This mutation wraps Better-Auth's sign-in endpoint and returns a response
|
|
118
|
+
* compatible with the existing auth system.
|
|
119
|
+
*/
|
|
120
|
+
@Mutation(() => BetterAuthAuthModel, {
|
|
121
|
+
description: 'Sign in via Better-Auth (email/password)',
|
|
122
|
+
})
|
|
123
|
+
@Roles(RoleEnum.S_EVERYONE)
|
|
124
|
+
async betterAuthSignIn(
|
|
125
|
+
@Args('email') email: string,
|
|
126
|
+
@Args('password') password: string,
|
|
127
|
+
// eslint-disable-next-line unused-imports/no-unused-vars -- Reserved for future cookie/session handling
|
|
128
|
+
@Context() _ctx: { req: Request; res: Response },
|
|
129
|
+
): Promise<BetterAuthAuthModel> {
|
|
130
|
+
this.ensureEnabled();
|
|
131
|
+
|
|
132
|
+
const api = this.betterAuthService.getApi();
|
|
133
|
+
if (!api) {
|
|
134
|
+
throw new BadRequestException('Better-Auth API not available');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
// Call Better-Auth's sign-in endpoint
|
|
139
|
+
const response = (await api.signInEmail({
|
|
140
|
+
body: { email, password },
|
|
141
|
+
})) as BetterAuthSignInResponse | null;
|
|
142
|
+
|
|
143
|
+
if (!response) {
|
|
144
|
+
throw new UnauthorizedException('Invalid credentials');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check for 2FA requirement
|
|
148
|
+
if (requires2FA(response)) {
|
|
149
|
+
return {
|
|
150
|
+
requiresTwoFactor: true,
|
|
151
|
+
success: false,
|
|
152
|
+
user: null,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Get user data
|
|
157
|
+
if (hasUser(response)) {
|
|
158
|
+
const sessionUser: BetterAuthSessionUser = response.user;
|
|
159
|
+
const mappedUser = await this.userMapper.mapSessionUser(sessionUser);
|
|
160
|
+
|
|
161
|
+
// Get token if JWT plugin is enabled
|
|
162
|
+
const token = this.betterAuthService.isJwtEnabled() ? response.token : undefined;
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
requiresTwoFactor: false,
|
|
166
|
+
session: hasSession(response) ? this.mapSessionInfo(response.session) : null,
|
|
167
|
+
success: true,
|
|
168
|
+
token,
|
|
169
|
+
user: mappedUser ? this.mapToUserModel(mappedUser) : null,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
throw new UnauthorizedException('Invalid credentials');
|
|
174
|
+
} catch (error) {
|
|
175
|
+
this.logger.debug(`Sign-in error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
176
|
+
throw new UnauthorizedException('Invalid credentials');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Sign up via Better-Auth
|
|
182
|
+
*/
|
|
183
|
+
@Mutation(() => BetterAuthAuthModel, {
|
|
184
|
+
description: 'Sign up via Better-Auth (email/password)',
|
|
185
|
+
})
|
|
186
|
+
@Roles(RoleEnum.S_EVERYONE)
|
|
187
|
+
async betterAuthSignUp(
|
|
188
|
+
@Args('email') email: string,
|
|
189
|
+
@Args('password') password: string,
|
|
190
|
+
@Args('name', { nullable: true }) name?: string,
|
|
191
|
+
): Promise<BetterAuthAuthModel> {
|
|
192
|
+
this.ensureEnabled();
|
|
193
|
+
|
|
194
|
+
const api = this.betterAuthService.getApi();
|
|
195
|
+
if (!api) {
|
|
196
|
+
throw new BadRequestException('Better-Auth API not available');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const response = (await api.signUpEmail({
|
|
201
|
+
body: {
|
|
202
|
+
email,
|
|
203
|
+
name: name || email.split('@')[0],
|
|
204
|
+
password,
|
|
205
|
+
},
|
|
206
|
+
})) as BetterAuthSignUpResponse | null;
|
|
207
|
+
|
|
208
|
+
if (!response) {
|
|
209
|
+
throw new BadRequestException('Sign-up failed');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (hasUser(response)) {
|
|
213
|
+
const sessionUser: BetterAuthSessionUser = response.user;
|
|
214
|
+
|
|
215
|
+
// Link or create user in our database
|
|
216
|
+
await this.userMapper.linkOrCreateUser(sessionUser);
|
|
217
|
+
const mappedUser = await this.userMapper.mapSessionUser(sessionUser);
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
requiresTwoFactor: false,
|
|
221
|
+
session: hasSession(response) ? this.mapSessionInfo(response.session) : null,
|
|
222
|
+
success: true,
|
|
223
|
+
user: mappedUser ? this.mapToUserModel(mappedUser) : null,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
throw new BadRequestException('Sign-up failed');
|
|
228
|
+
} catch (error) {
|
|
229
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
230
|
+
this.logger.debug(`Sign-up error: ${errorMessage}`);
|
|
231
|
+
if (errorMessage.includes('already exists')) {
|
|
232
|
+
throw new BadRequestException('User with this email already exists');
|
|
233
|
+
}
|
|
234
|
+
throw new BadRequestException('Sign-up failed');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Sign out via Better-Auth
|
|
240
|
+
*/
|
|
241
|
+
@Mutation(() => Boolean, { description: 'Sign out via Better-Auth' })
|
|
242
|
+
@Roles(RoleEnum.S_USER)
|
|
243
|
+
@UseGuards(AuthGuard(AuthGuardStrategy.JWT))
|
|
244
|
+
async betterAuthSignOut(@Context() ctx: { req: Request }): Promise<boolean> {
|
|
245
|
+
if (!this.betterAuthService.isEnabled()) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const api = this.betterAuthService.getApi();
|
|
250
|
+
if (!api) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const headers = this.convertHeaders(ctx.req.headers);
|
|
256
|
+
await api.signOut({ headers });
|
|
257
|
+
return true;
|
|
258
|
+
} catch (error) {
|
|
259
|
+
this.logger.debug(`Sign-out error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Verify 2FA code
|
|
266
|
+
*/
|
|
267
|
+
@Mutation(() => BetterAuthAuthModel, {
|
|
268
|
+
description: 'Verify 2FA code during sign-in',
|
|
269
|
+
})
|
|
270
|
+
@Roles(RoleEnum.S_EVERYONE)
|
|
271
|
+
async betterAuthVerify2FA(
|
|
272
|
+
@Args('code') code: string,
|
|
273
|
+
@Context() ctx: { req: Request },
|
|
274
|
+
): Promise<BetterAuthAuthModel> {
|
|
275
|
+
this.ensureEnabled();
|
|
276
|
+
|
|
277
|
+
if (!this.betterAuthService.isTwoFactorEnabled()) {
|
|
278
|
+
throw new BadRequestException('Two-factor authentication is not enabled');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const api = this.betterAuthService.getApi();
|
|
282
|
+
if (!api) {
|
|
283
|
+
throw new BadRequestException('Better-Auth API not available');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
// Convert headers
|
|
288
|
+
const headers = this.convertHeaders(ctx.req.headers);
|
|
289
|
+
|
|
290
|
+
// Better-Auth's 2FA plugin adds twoFactor methods dynamically
|
|
291
|
+
|
|
292
|
+
const twoFactorApi = (api as Record<string, unknown>).twoFactor as
|
|
293
|
+
| undefined
|
|
294
|
+
| {
|
|
295
|
+
verifyTotp?: (params: { body: { code: string }; headers: Headers }) => Promise<BetterAuth2FAResponse>;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
if (!twoFactorApi?.verifyTotp) {
|
|
299
|
+
throw new BadRequestException('2FA verification method not available');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const response = await twoFactorApi.verifyTotp({
|
|
303
|
+
body: { code },
|
|
304
|
+
headers,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
if (response && hasUser(response)) {
|
|
308
|
+
const sessionUser: BetterAuthSessionUser = response.user;
|
|
309
|
+
const mappedUser = await this.userMapper.mapSessionUser(sessionUser);
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
requiresTwoFactor: false,
|
|
313
|
+
success: true,
|
|
314
|
+
user: mappedUser ? this.mapToUserModel(mappedUser) : null,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
throw new UnauthorizedException('Invalid 2FA code');
|
|
319
|
+
} catch (error) {
|
|
320
|
+
this.logger.debug(`2FA verification error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
321
|
+
throw new UnauthorizedException('Invalid 2FA code');
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ===========================================================================
|
|
326
|
+
// 2FA Management Mutations
|
|
327
|
+
// ===========================================================================
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Enable 2FA for the current user
|
|
331
|
+
* Returns TOTP URI for QR code generation and backup codes
|
|
332
|
+
*/
|
|
333
|
+
@Mutation(() => BetterAuth2FASetupModel, {
|
|
334
|
+
description: 'Enable 2FA for the current user',
|
|
335
|
+
})
|
|
336
|
+
@Roles(RoleEnum.S_USER)
|
|
337
|
+
@UseGuards(AuthGuard(AuthGuardStrategy.JWT))
|
|
338
|
+
async betterAuthEnable2FA(
|
|
339
|
+
@Args('password') password: string,
|
|
340
|
+
@Context() ctx: { req: Request },
|
|
341
|
+
): Promise<BetterAuth2FASetupModel> {
|
|
342
|
+
this.ensureEnabled();
|
|
343
|
+
|
|
344
|
+
if (!this.betterAuthService.isTwoFactorEnabled()) {
|
|
345
|
+
return { error: 'Two-factor authentication is not enabled on this server', success: false };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const api = this.betterAuthService.getApi();
|
|
349
|
+
if (!api) {
|
|
350
|
+
return { error: 'Better-Auth API not available', success: false };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const headers = this.convertHeaders(ctx.req.headers);
|
|
355
|
+
|
|
356
|
+
const twoFactorApi = (api as Record<string, unknown>).twoFactor as
|
|
357
|
+
| undefined
|
|
358
|
+
| {
|
|
359
|
+
enable?: (params: { body: { password: string }; headers: Headers }) => Promise<{
|
|
360
|
+
backupCodes?: string[];
|
|
361
|
+
totpURI?: string;
|
|
362
|
+
}>;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
if (!twoFactorApi?.enable) {
|
|
366
|
+
return { error: '2FA enable method not available', success: false };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const response = await twoFactorApi.enable({
|
|
370
|
+
body: { password },
|
|
371
|
+
headers,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
backupCodes: response.backupCodes,
|
|
376
|
+
success: true,
|
|
377
|
+
totpUri: response.totpURI,
|
|
378
|
+
};
|
|
379
|
+
} catch (error) {
|
|
380
|
+
this.logger.debug(`2FA enable error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
381
|
+
return { error: error instanceof Error ? error.message : 'Failed to enable 2FA', success: false };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Disable 2FA for the current user
|
|
387
|
+
*/
|
|
388
|
+
@Mutation(() => Boolean, {
|
|
389
|
+
description: 'Disable 2FA for the current user',
|
|
390
|
+
})
|
|
391
|
+
@Roles(RoleEnum.S_USER)
|
|
392
|
+
@UseGuards(AuthGuard(AuthGuardStrategy.JWT))
|
|
393
|
+
async betterAuthDisable2FA(@Args('password') password: string, @Context() ctx: { req: Request }): Promise<boolean> {
|
|
394
|
+
this.ensureEnabled();
|
|
395
|
+
|
|
396
|
+
if (!this.betterAuthService.isTwoFactorEnabled()) {
|
|
397
|
+
throw new BadRequestException('Two-factor authentication is not enabled on this server');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const api = this.betterAuthService.getApi();
|
|
401
|
+
if (!api) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
const headers = this.convertHeaders(ctx.req.headers);
|
|
407
|
+
|
|
408
|
+
const twoFactorApi = (api as Record<string, unknown>).twoFactor as
|
|
409
|
+
| undefined
|
|
410
|
+
| {
|
|
411
|
+
disable?: (params: { body: { password: string }; headers: Headers }) => Promise<{ status: boolean }>;
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
if (!twoFactorApi?.disable) {
|
|
415
|
+
throw new BadRequestException('2FA disable method not available');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const response = await twoFactorApi.disable({
|
|
419
|
+
body: { password },
|
|
420
|
+
headers,
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
return response?.status === true;
|
|
424
|
+
} catch (error) {
|
|
425
|
+
this.logger.debug(`2FA disable error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Generate new backup codes for 2FA
|
|
432
|
+
*/
|
|
433
|
+
@Mutation(() => [String], {
|
|
434
|
+
description: 'Generate new backup codes for 2FA',
|
|
435
|
+
nullable: true,
|
|
436
|
+
})
|
|
437
|
+
@Roles(RoleEnum.S_USER)
|
|
438
|
+
@UseGuards(AuthGuard(AuthGuardStrategy.JWT))
|
|
439
|
+
async betterAuthGenerateBackupCodes(@Context() ctx: { req: Request }): Promise<null | string[]> {
|
|
440
|
+
this.ensureEnabled();
|
|
441
|
+
|
|
442
|
+
if (!this.betterAuthService.isTwoFactorEnabled()) {
|
|
443
|
+
throw new BadRequestException('Two-factor authentication is not enabled on this server');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const api = this.betterAuthService.getApi();
|
|
447
|
+
if (!api) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
const headers = this.convertHeaders(ctx.req.headers);
|
|
453
|
+
|
|
454
|
+
const twoFactorApi = (api as Record<string, unknown>).twoFactor as
|
|
455
|
+
| undefined
|
|
456
|
+
| {
|
|
457
|
+
generateBackupCodes?: (params: { headers: Headers }) => Promise<{ backupCodes?: string[] }>;
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
if (!twoFactorApi?.generateBackupCodes) {
|
|
461
|
+
throw new BadRequestException('Generate backup codes method not available');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const response = await twoFactorApi.generateBackupCodes({ headers });
|
|
465
|
+
|
|
466
|
+
return response?.backupCodes || null;
|
|
467
|
+
} catch (error) {
|
|
468
|
+
this.logger.debug(`Generate backup codes error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ===========================================================================
|
|
474
|
+
// Passkey Management Mutations
|
|
475
|
+
// ===========================================================================
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Get passkey registration challenge
|
|
479
|
+
* Returns the challenge data needed for WebAuthn registration
|
|
480
|
+
*/
|
|
481
|
+
@Mutation(() => BetterAuthPasskeyChallengeModel, {
|
|
482
|
+
description: 'Get passkey registration challenge for WebAuthn',
|
|
483
|
+
})
|
|
484
|
+
@Roles(RoleEnum.S_USER)
|
|
485
|
+
@UseGuards(AuthGuard(AuthGuardStrategy.JWT))
|
|
486
|
+
async betterAuthGetPasskeyChallenge(@Context() ctx: { req: Request }): Promise<BetterAuthPasskeyChallengeModel> {
|
|
487
|
+
this.ensureEnabled();
|
|
488
|
+
|
|
489
|
+
if (!this.betterAuthService.isPasskeyEnabled()) {
|
|
490
|
+
return { error: 'Passkey authentication is not enabled on this server', success: false };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const api = this.betterAuthService.getApi();
|
|
494
|
+
if (!api) {
|
|
495
|
+
return { error: 'Better-Auth API not available', success: false };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
const headers = this.convertHeaders(ctx.req.headers);
|
|
500
|
+
|
|
501
|
+
const passkeyApi = (api as Record<string, unknown>).passkey as
|
|
502
|
+
| undefined
|
|
503
|
+
| {
|
|
504
|
+
generateRegisterOptions?: (params: { headers: Headers }) => Promise<unknown>;
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
if (!passkeyApi?.generateRegisterOptions) {
|
|
508
|
+
return { error: 'Passkey registration method not available', success: false };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const challenge = await passkeyApi.generateRegisterOptions({ headers });
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
challenge: JSON.stringify(challenge),
|
|
515
|
+
success: true,
|
|
516
|
+
};
|
|
517
|
+
} catch (error) {
|
|
518
|
+
this.logger.debug(`Passkey challenge error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
519
|
+
return { error: error instanceof Error ? error.message : 'Failed to get passkey challenge', success: false };
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* List passkeys for the current user
|
|
525
|
+
*/
|
|
526
|
+
@Query(() => [BetterAuthPasskeyModel], {
|
|
527
|
+
description: 'List passkeys for the current user',
|
|
528
|
+
nullable: true,
|
|
529
|
+
})
|
|
530
|
+
@Roles(RoleEnum.S_USER)
|
|
531
|
+
@UseGuards(AuthGuard(AuthGuardStrategy.JWT))
|
|
532
|
+
async betterAuthListPasskeys(@Context() ctx: { req: Request }): Promise<BetterAuthPasskeyModel[] | null> {
|
|
533
|
+
if (!this.betterAuthService.isEnabled() || !this.betterAuthService.isPasskeyEnabled()) {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const api = this.betterAuthService.getApi();
|
|
538
|
+
if (!api) {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
const headers = this.convertHeaders(ctx.req.headers);
|
|
544
|
+
|
|
545
|
+
const passkeyApi = (api as Record<string, unknown>).passkey as
|
|
546
|
+
| undefined
|
|
547
|
+
| {
|
|
548
|
+
listUserPasskeys?: (params: {
|
|
549
|
+
headers: Headers;
|
|
550
|
+
}) => Promise<{ createdAt: Date; credentialID: string; id: string; name?: string }[]>;
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
if (!passkeyApi?.listUserPasskeys) {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const passkeys = await passkeyApi.listUserPasskeys({ headers });
|
|
558
|
+
|
|
559
|
+
return passkeys.map((pk) => ({
|
|
560
|
+
createdAt: pk.createdAt,
|
|
561
|
+
credentialId: pk.credentialID,
|
|
562
|
+
id: pk.id,
|
|
563
|
+
name: pk.name,
|
|
564
|
+
}));
|
|
565
|
+
} catch (error) {
|
|
566
|
+
this.logger.debug(`List passkeys error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Delete a passkey
|
|
573
|
+
*/
|
|
574
|
+
@Mutation(() => Boolean, {
|
|
575
|
+
description: 'Delete a passkey by ID',
|
|
576
|
+
})
|
|
577
|
+
@Roles(RoleEnum.S_USER)
|
|
578
|
+
@UseGuards(AuthGuard(AuthGuardStrategy.JWT))
|
|
579
|
+
async betterAuthDeletePasskey(
|
|
580
|
+
@Args('passkeyId') passkeyId: string,
|
|
581
|
+
@Context() ctx: { req: Request },
|
|
582
|
+
): Promise<boolean> {
|
|
583
|
+
this.ensureEnabled();
|
|
584
|
+
|
|
585
|
+
if (!this.betterAuthService.isPasskeyEnabled()) {
|
|
586
|
+
throw new BadRequestException('Passkey authentication is not enabled on this server');
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const api = this.betterAuthService.getApi();
|
|
590
|
+
if (!api) {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
try {
|
|
595
|
+
const headers = this.convertHeaders(ctx.req.headers);
|
|
596
|
+
|
|
597
|
+
const passkeyApi = (api as Record<string, unknown>).passkey as
|
|
598
|
+
| undefined
|
|
599
|
+
| {
|
|
600
|
+
deletePasskey?: (params: { body: { id: string }; headers: Headers }) => Promise<{ status: boolean }>;
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
if (!passkeyApi?.deletePasskey) {
|
|
604
|
+
throw new BadRequestException('Delete passkey method not available');
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const response = await passkeyApi.deletePasskey({
|
|
608
|
+
body: { id: passkeyId },
|
|
609
|
+
headers,
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
return response?.status === true;
|
|
613
|
+
} catch (error) {
|
|
614
|
+
this.logger.debug(`Delete passkey error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ===========================================================================
|
|
620
|
+
// Helper Methods
|
|
621
|
+
// ===========================================================================
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Ensure Better-Auth is enabled
|
|
625
|
+
*/
|
|
626
|
+
private ensureEnabled(): void {
|
|
627
|
+
if (!this.betterAuthService.isEnabled()) {
|
|
628
|
+
throw new BadRequestException(
|
|
629
|
+
'Better-Auth is not enabled. Check that betterAuth.enabled is not set to false in your environment.',
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Convert Express headers to Web API Headers
|
|
636
|
+
*/
|
|
637
|
+
private convertHeaders(headers: Record<string, string | string[] | undefined>): Headers {
|
|
638
|
+
const result = new Headers();
|
|
639
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
640
|
+
if (typeof value === 'string') {
|
|
641
|
+
result.set(key, value);
|
|
642
|
+
} else if (Array.isArray(value)) {
|
|
643
|
+
result.set(key, value.join(', '));
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return result;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Map session response to session info model
|
|
651
|
+
*/
|
|
652
|
+
private mapSessionInfo(session: { createdAt?: Date; expiresAt?: Date; id?: string; token?: string }): {
|
|
653
|
+
expiresAt?: Date;
|
|
654
|
+
id?: string;
|
|
655
|
+
token?: string;
|
|
656
|
+
} {
|
|
657
|
+
return {
|
|
658
|
+
expiresAt: session.expiresAt,
|
|
659
|
+
id: session.id,
|
|
660
|
+
token: session.token,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Map MappedUser to BetterAuthUserModel
|
|
666
|
+
*/
|
|
667
|
+
private mapToUserModel(user: MappedUser): BetterAuthUserModel {
|
|
668
|
+
return {
|
|
669
|
+
email: user.email,
|
|
670
|
+
emailVerified: user.emailVerified,
|
|
671
|
+
iamId: user.iamId,
|
|
672
|
+
id: user.id,
|
|
673
|
+
name: user.name,
|
|
674
|
+
roles: user.roles,
|
|
675
|
+
verified: user.verified,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
}
|