@lenne.tech/nest-server 11.7.0 → 11.7.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 +17 -1
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +35 -15
- package/dist/core/modules/auth/core-auth.controller.d.ts +1 -0
- package/dist/core/modules/auth/core-auth.controller.js +29 -3
- package/dist/core/modules/auth/core-auth.controller.js.map +1 -1
- package/dist/core/modules/auth/core-auth.module.js +14 -1
- package/dist/core/modules/auth/core-auth.module.js.map +1 -1
- package/dist/core/modules/auth/core-auth.resolver.d.ts +1 -0
- package/dist/core/modules/auth/core-auth.resolver.js +21 -3
- package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
- package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.d.ts +4 -0
- package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.js +17 -0
- package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.js.map +1 -0
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.d.ts +9 -0
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js +74 -0
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js.map +1 -0
- package/dist/core/modules/auth/interfaces/auth-provider.interface.d.ts +7 -0
- package/dist/core/modules/auth/interfaces/auth-provider.interface.js +5 -0
- package/dist/core/modules/auth/interfaces/auth-provider.interface.js.map +1 -0
- package/dist/core/modules/auth/interfaces/core-auth-user.interface.d.ts +1 -0
- package/dist/core/modules/auth/services/core-auth.service.d.ts +10 -1
- package/dist/core/modules/auth/services/core-auth.service.js +141 -9
- package/dist/core/modules/auth/services/core-auth.service.js.map +1 -1
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.d.ts +31 -0
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +153 -0
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth-migration-status.model.d.ts +10 -0
- package/dist/core/modules/better-auth/better-auth-migration-status.model.js +57 -0
- package/dist/core/modules/better-auth/better-auth-migration-status.model.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js +1 -1
- package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth-user.mapper.d.ts +33 -0
- package/dist/core/modules/better-auth/better-auth-user.mapper.js +395 -0
- package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.config.js +29 -10
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.middleware.d.ts +1 -0
- package/dist/core/modules/better-auth/better-auth.middleware.js +55 -1
- package/dist/core/modules/better-auth/better-auth.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.module.d.ts +1 -1
- package/dist/core/modules/better-auth/better-auth.module.js +46 -18
- package/dist/core/modules/better-auth/better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.resolver.js +0 -11
- package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.service.d.ts +22 -1
- package/dist/core/modules/better-auth/better-auth.service.js +209 -8
- package/dist/core/modules/better-auth/better-auth.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.types.d.ts +2 -0
- package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +1 -0
- package/dist/core/modules/better-auth/core-better-auth.controller.js +15 -2
- package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +7 -0
- package/dist/core/modules/better-auth/core-better-auth.resolver.js +72 -12
- package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
- package/dist/core/modules/better-auth/index.d.ts +1 -0
- package/dist/core/modules/better-auth/index.js +1 -0
- package/dist/core/modules/better-auth/index.js.map +1 -1
- package/dist/core/modules/user/core-user.service.d.ts +7 -1
- package/dist/core/modules/user/core-user.service.js +57 -3
- package/dist/core/modules/user/core-user.service.js.map +1 -1
- package/dist/core/modules/user/interfaces/core-user-service-options.interface.d.ts +4 -0
- package/dist/core/modules/user/interfaces/core-user-service-options.interface.js +3 -0
- package/dist/core/modules/user/interfaces/core-user-service-options.interface.js.map +1 -0
- package/dist/core.module.d.ts +3 -0
- package/dist/core.module.js +136 -55
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/server/modules/auth/auth.resolver.js +2 -0
- package/dist/server/modules/auth/auth.resolver.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.module.d.ts +1 -1
- package/dist/server/modules/better-auth/better-auth.module.js +2 -1
- package/dist/server/modules/better-auth/better-auth.module.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.resolver.d.ts +5 -0
- package/dist/server/modules/better-auth/better-auth.resolver.js +27 -11
- package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -1
- package/dist/server/modules/user/user.controller.js +0 -8
- package/dist/server/modules/user/user.controller.js.map +1 -1
- package/dist/server/modules/user/user.service.d.ts +3 -1
- package/dist/server/modules/user/user.service.js +7 -3
- package/dist/server/modules/user/user.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/config.env.ts +32 -2
- package/src/core/common/interfaces/server-options.interface.ts +304 -58
- package/src/core/modules/auth/core-auth.controller.ts +94 -6
- package/src/core/modules/auth/core-auth.module.ts +15 -1
- package/src/core/modules/auth/core-auth.resolver.ts +71 -3
- package/src/core/modules/auth/exceptions/legacy-auth-disabled.exception.ts +35 -0
- package/src/core/modules/auth/guards/legacy-auth-rate-limit.guard.ts +109 -0
- package/src/core/modules/auth/interfaces/auth-provider.interface.ts +86 -0
- package/src/core/modules/auth/interfaces/core-auth-user.interface.ts +6 -0
- package/src/core/modules/auth/services/core-auth.service.ts +245 -6
- package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +283 -0
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +255 -0
- package/src/core/modules/better-auth/README.md +565 -208
- package/src/core/modules/better-auth/better-auth-migration-status.model.ts +73 -0
- package/src/core/modules/better-auth/better-auth-rate-limiter.service.ts +1 -1
- package/src/core/modules/better-auth/better-auth-user.mapper.ts +737 -0
- package/src/core/modules/better-auth/better-auth.config.ts +45 -15
- package/src/core/modules/better-auth/better-auth.middleware.ts +85 -2
- package/src/core/modules/better-auth/better-auth.module.ts +83 -27
- package/src/core/modules/better-auth/better-auth.resolver.ts +0 -11
- package/src/core/modules/better-auth/better-auth.service.ts +367 -12
- package/src/core/modules/better-auth/better-auth.types.ts +16 -0
- package/src/core/modules/better-auth/core-better-auth.controller.ts +44 -3
- package/src/core/modules/better-auth/core-better-auth.resolver.ts +136 -16
- package/src/core/modules/better-auth/index.ts +1 -0
- package/src/core/modules/user/core-user.service.ts +131 -4
- package/src/core/modules/user/interfaces/core-user-service-options.interface.ts +15 -0
- package/src/core.module.ts +264 -76
- package/src/index.ts +5 -0
- package/src/server/modules/auth/auth.resolver.ts +8 -0
- package/src/server/modules/better-auth/better-auth.module.ts +9 -3
- package/src/server/modules/better-auth/better-auth.resolver.ts +18 -11
- package/src/server/modules/user/user.controller.ts +1 -9
- package/src/server/modules/user/user.service.ts +4 -2
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { BadRequestException, Logger, UnauthorizedException
|
|
1
|
+
import { BadRequestException, Logger, UnauthorizedException } from '@nestjs/common';
|
|
2
2
|
import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
|
|
3
3
|
import { Request, Response } from 'express';
|
|
4
4
|
|
|
5
5
|
import { Roles } from '../../common/decorators/roles.decorator';
|
|
6
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
7
|
import { BetterAuthAuthModel } from './better-auth-auth.model';
|
|
8
|
+
import { BetterAuthMigrationStatusModel } from './better-auth-migration-status.model';
|
|
10
9
|
import {
|
|
11
10
|
BetterAuth2FASetupModel,
|
|
12
11
|
BetterAuthFeaturesModel,
|
|
@@ -82,7 +81,6 @@ export class CoreBetterAuthResolver {
|
|
|
82
81
|
nullable: true,
|
|
83
82
|
})
|
|
84
83
|
@Roles(RoleEnum.S_USER)
|
|
85
|
-
@UseGuards(AuthGuard(AuthGuardStrategy.JWT))
|
|
86
84
|
async betterAuthSession(@Context() ctx: { req: Request }): Promise<BetterAuthSessionModel | null> {
|
|
87
85
|
if (!this.betterAuthService.isEnabled()) {
|
|
88
86
|
return null;
|
|
@@ -130,6 +128,51 @@ export class CoreBetterAuthResolver {
|
|
|
130
128
|
};
|
|
131
129
|
}
|
|
132
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Get a fresh JWT token for the current session.
|
|
133
|
+
*
|
|
134
|
+
* Use this when your JWT has expired but your session is still valid.
|
|
135
|
+
* The JWT can be used for stateless authentication with other services
|
|
136
|
+
* that verify tokens via JWKS (`/iam/jwks`).
|
|
137
|
+
*
|
|
138
|
+
* Returns null if:
|
|
139
|
+
* - Better-Auth is not enabled
|
|
140
|
+
* - JWT plugin is not enabled
|
|
141
|
+
* - No valid session exists
|
|
142
|
+
*/
|
|
143
|
+
@Query(() => String, {
|
|
144
|
+
description: 'Get fresh JWT token for the current session (requires valid session)',
|
|
145
|
+
nullable: true,
|
|
146
|
+
})
|
|
147
|
+
@Roles(RoleEnum.S_USER)
|
|
148
|
+
async betterAuthToken(@Context() ctx: { req: Request }): Promise<null | string> {
|
|
149
|
+
return this.betterAuthService.getToken(ctx.req);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get migration status from Legacy Auth to Better-Auth (IAM)
|
|
154
|
+
*
|
|
155
|
+
* This query provides administrators with information about how many users
|
|
156
|
+
* have been migrated to the IAM system. This helps determine when it might
|
|
157
|
+
* be safe to consider disabling Legacy Auth endpoints.
|
|
158
|
+
*
|
|
159
|
+
* A user is considered fully migrated when:
|
|
160
|
+
* 1. They have an `iamId` set (linked to Better-Auth)
|
|
161
|
+
* 2. They have a credential account in Better-Auth
|
|
162
|
+
*
|
|
163
|
+
* Note: Even when canDisableLegacyAuth returns true, Legacy Auth cannot
|
|
164
|
+
* currently be removed because CoreModule.forRoot requires AuthService
|
|
165
|
+
* for GraphQL Subscriptions authentication.
|
|
166
|
+
*/
|
|
167
|
+
@Query(() => BetterAuthMigrationStatusModel, {
|
|
168
|
+
description: 'Get migration status from Legacy Auth to Better-Auth (IAM) - Admin only',
|
|
169
|
+
})
|
|
170
|
+
@Roles(RoleEnum.ADMIN)
|
|
171
|
+
async betterAuthMigrationStatus(): Promise<BetterAuthMigrationStatusModel> {
|
|
172
|
+
const status = await this.userMapper.getMigrationStatus();
|
|
173
|
+
return status;
|
|
174
|
+
}
|
|
175
|
+
|
|
133
176
|
// ===========================================================================
|
|
134
177
|
// Mutations
|
|
135
178
|
// ===========================================================================
|
|
@@ -140,6 +183,9 @@ export class CoreBetterAuthResolver {
|
|
|
140
183
|
* This mutation wraps Better-Auth's sign-in endpoint and returns a response
|
|
141
184
|
* compatible with the existing auth system.
|
|
142
185
|
*
|
|
186
|
+
* Features automatic legacy user migration: If a user exists in Legacy Auth
|
|
187
|
+
* but not in IAM, they will be automatically migrated on first IAM sign-in.
|
|
188
|
+
*
|
|
143
189
|
* Override this method to add custom pre/post sign-in logic.
|
|
144
190
|
*/
|
|
145
191
|
@Mutation(() => BetterAuthAuthModel, {
|
|
@@ -159,12 +205,38 @@ export class CoreBetterAuthResolver {
|
|
|
159
205
|
throw new BadRequestException('Better-Auth API not available');
|
|
160
206
|
}
|
|
161
207
|
|
|
208
|
+
// Try to sign in, with automatic legacy user migration
|
|
209
|
+
return this.attemptSignIn(email, password, api, true);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Attempt sign-in with optional legacy user migration
|
|
214
|
+
* @param email - User email
|
|
215
|
+
* @param password - User password (plain or SHA256)
|
|
216
|
+
* @param api - Better-Auth API instance
|
|
217
|
+
* @param allowMigration - Whether to attempt legacy migration on failure
|
|
218
|
+
*/
|
|
219
|
+
protected async attemptSignIn(
|
|
220
|
+
email: string,
|
|
221
|
+
password: string,
|
|
222
|
+
api: ReturnType<BetterAuthService['getApi']>,
|
|
223
|
+
allowMigration: boolean,
|
|
224
|
+
): Promise<BetterAuthAuthModel> {
|
|
162
225
|
try {
|
|
163
|
-
//
|
|
164
|
-
const response = (await api
|
|
226
|
+
// Try sign-in with original password first (for native IAM users)
|
|
227
|
+
const response = (await api!.signInEmail({
|
|
165
228
|
body: { email, password },
|
|
166
229
|
})) as BetterAuthSignInResponse | null;
|
|
167
230
|
|
|
231
|
+
this.logger.debug(`[SignIn] API response for ${email}: ${JSON.stringify(response)?.substring(0, 200)}`);
|
|
232
|
+
|
|
233
|
+
// Check if response indicates an error (Better-Auth returns error objects, not throws)
|
|
234
|
+
const responseAny = response as any;
|
|
235
|
+
if (responseAny?.error || responseAny?.code === 'CREDENTIAL_ACCOUNT_NOT_FOUND') {
|
|
236
|
+
this.logger.debug(`[SignIn] API returned error for ${email}: ${responseAny?.error || responseAny?.code}`);
|
|
237
|
+
throw new Error(responseAny?.error || responseAny?.code || 'Credential account not found');
|
|
238
|
+
}
|
|
239
|
+
|
|
168
240
|
if (!response) {
|
|
169
241
|
throw new UnauthorizedException('Invalid credentials');
|
|
170
242
|
}
|
|
@@ -183,8 +255,11 @@ export class CoreBetterAuthResolver {
|
|
|
183
255
|
const sessionUser: BetterAuthSessionUser = response.user;
|
|
184
256
|
const mappedUser = await this.userMapper.mapSessionUser(sessionUser);
|
|
185
257
|
|
|
186
|
-
//
|
|
187
|
-
|
|
258
|
+
// Return the session token for session-based authentication
|
|
259
|
+
// Note: If JWT plugin is enabled, accessToken may be in response or in set-auth-jwt header
|
|
260
|
+
// For GraphQL responses, we return the session token and let clients use it for session auth
|
|
261
|
+
const responseAny = response as any;
|
|
262
|
+
const token = responseAny.accessToken || responseAny.token;
|
|
188
263
|
|
|
189
264
|
return {
|
|
190
265
|
requiresTwoFactor: false,
|
|
@@ -197,9 +272,61 @@ export class CoreBetterAuthResolver {
|
|
|
197
272
|
|
|
198
273
|
throw new UnauthorizedException('Invalid credentials');
|
|
199
274
|
} catch (error) {
|
|
200
|
-
this.logger.debug(
|
|
275
|
+
this.logger.debug(
|
|
276
|
+
`[SignIn] Sign-in failed for ${email}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// If migration is allowed, try to migrate legacy user and retry
|
|
280
|
+
if (allowMigration) {
|
|
281
|
+
this.logger.debug(`[SignIn] Attempting migration for ${email}...`);
|
|
282
|
+
// Pass the original password for legacy verification
|
|
283
|
+
const migrated = await this.userMapper.migrateAccountToIam(email, password);
|
|
284
|
+
this.logger.debug(`[SignIn] Migration result for ${email}: ${migrated}`);
|
|
285
|
+
if (migrated) {
|
|
286
|
+
this.logger.debug(`[SignIn] Migrated legacy user ${email} to IAM, retrying sign-in`);
|
|
287
|
+
// Retry sign-in after migration with normalized password (as migrateAccountToIam stores it)
|
|
288
|
+
const normalizedPassword = this.userMapper.normalizePasswordForIam(password);
|
|
289
|
+
return this.attemptSignInDirect(email, normalizedPassword, api);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
throw new UnauthorizedException('Invalid credentials');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Direct sign-in attempt without migration logic (used after migration)
|
|
299
|
+
*/
|
|
300
|
+
private async attemptSignInDirect(
|
|
301
|
+
email: string,
|
|
302
|
+
password: string,
|
|
303
|
+
api: ReturnType<BetterAuthService['getApi']>,
|
|
304
|
+
): Promise<BetterAuthAuthModel> {
|
|
305
|
+
const response = (await api!.signInEmail({
|
|
306
|
+
body: { email, password },
|
|
307
|
+
})) as BetterAuthSignInResponse | null;
|
|
308
|
+
|
|
309
|
+
if (!response || !hasUser(response)) {
|
|
201
310
|
throw new UnauthorizedException('Invalid credentials');
|
|
202
311
|
}
|
|
312
|
+
|
|
313
|
+
if (requires2FA(response)) {
|
|
314
|
+
return { requiresTwoFactor: true, success: false, user: null };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const sessionUser: BetterAuthSessionUser = response.user;
|
|
318
|
+
const mappedUser = await this.userMapper.mapSessionUser(sessionUser);
|
|
319
|
+
// Return accessToken if available (JWT), otherwise fall back to session token
|
|
320
|
+
const responseAny = response as any;
|
|
321
|
+
const token = responseAny.accessToken || responseAny.token;
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
requiresTwoFactor: false,
|
|
325
|
+
session: hasSession(response) ? this.mapSessionInfo(response.session) : null,
|
|
326
|
+
success: true,
|
|
327
|
+
token,
|
|
328
|
+
user: mappedUser ? this.mapToUserModel(mappedUser) : null,
|
|
329
|
+
};
|
|
203
330
|
}
|
|
204
331
|
|
|
205
332
|
/**
|
|
@@ -267,7 +394,6 @@ export class CoreBetterAuthResolver {
|
|
|
267
394
|
*/
|
|
268
395
|
@Mutation(() => Boolean, { description: 'Sign out via Better-Auth' })
|
|
269
396
|
@Roles(RoleEnum.S_USER)
|
|
270
|
-
@UseGuards(AuthGuard(AuthGuardStrategy.JWT))
|
|
271
397
|
async betterAuthSignOut(@Context() ctx: { req: Request }): Promise<boolean> {
|
|
272
398
|
if (!this.betterAuthService.isEnabled()) {
|
|
273
399
|
return false;
|
|
@@ -361,7 +487,6 @@ export class CoreBetterAuthResolver {
|
|
|
361
487
|
description: 'Enable 2FA for the current user',
|
|
362
488
|
})
|
|
363
489
|
@Roles(RoleEnum.S_USER)
|
|
364
|
-
@UseGuards(AuthGuard(AuthGuardStrategy.JWT))
|
|
365
490
|
async betterAuthEnable2FA(
|
|
366
491
|
@Args('password') password: string,
|
|
367
492
|
@Context() ctx: { req: Request },
|
|
@@ -416,7 +541,6 @@ export class CoreBetterAuthResolver {
|
|
|
416
541
|
description: 'Disable 2FA for the current user',
|
|
417
542
|
})
|
|
418
543
|
@Roles(RoleEnum.S_USER)
|
|
419
|
-
@UseGuards(AuthGuard(AuthGuardStrategy.JWT))
|
|
420
544
|
async betterAuthDisable2FA(@Args('password') password: string, @Context() ctx: { req: Request }): Promise<boolean> {
|
|
421
545
|
this.ensureEnabled();
|
|
422
546
|
|
|
@@ -462,7 +586,6 @@ export class CoreBetterAuthResolver {
|
|
|
462
586
|
nullable: true,
|
|
463
587
|
})
|
|
464
588
|
@Roles(RoleEnum.S_USER)
|
|
465
|
-
@UseGuards(AuthGuard(AuthGuardStrategy.JWT))
|
|
466
589
|
async betterAuthGenerateBackupCodes(@Context() ctx: { req: Request }): Promise<null | string[]> {
|
|
467
590
|
this.ensureEnabled();
|
|
468
591
|
|
|
@@ -509,7 +632,6 @@ export class CoreBetterAuthResolver {
|
|
|
509
632
|
description: 'Get passkey registration challenge for WebAuthn',
|
|
510
633
|
})
|
|
511
634
|
@Roles(RoleEnum.S_USER)
|
|
512
|
-
@UseGuards(AuthGuard(AuthGuardStrategy.JWT))
|
|
513
635
|
async betterAuthGetPasskeyChallenge(@Context() ctx: { req: Request }): Promise<BetterAuthPasskeyChallengeModel> {
|
|
514
636
|
this.ensureEnabled();
|
|
515
637
|
|
|
@@ -555,7 +677,6 @@ export class CoreBetterAuthResolver {
|
|
|
555
677
|
nullable: true,
|
|
556
678
|
})
|
|
557
679
|
@Roles(RoleEnum.S_USER)
|
|
558
|
-
@UseGuards(AuthGuard(AuthGuardStrategy.JWT))
|
|
559
680
|
async betterAuthListPasskeys(@Context() ctx: { req: Request }): Promise<BetterAuthPasskeyModel[] | null> {
|
|
560
681
|
if (!this.betterAuthService.isEnabled() || !this.betterAuthService.isPasskeyEnabled()) {
|
|
561
682
|
return null;
|
|
@@ -602,7 +723,6 @@ export class CoreBetterAuthResolver {
|
|
|
602
723
|
description: 'Delete a passkey by ID',
|
|
603
724
|
})
|
|
604
725
|
@Roles(RoleEnum.S_USER)
|
|
605
|
-
@UseGuards(AuthGuard(AuthGuardStrategy.JWT))
|
|
606
726
|
async betterAuthDeletePasskey(
|
|
607
727
|
@Args('passkeyId') passkeyId: string,
|
|
608
728
|
@Context() ctx: { req: Request },
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
export * from './better-auth-auth.model';
|
|
21
|
+
export * from './better-auth-migration-status.model';
|
|
21
22
|
export * from './better-auth-models';
|
|
22
23
|
export * from './better-auth-rate-limit.middleware';
|
|
23
24
|
export * from './better-auth-rate-limiter.service';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BadRequestException, NotFoundException, UnprocessableEntityException } from '@nestjs/common';
|
|
1
|
+
import { BadRequestException, Logger, NotFoundException, UnprocessableEntityException } from '@nestjs/common';
|
|
2
2
|
import bcrypt = require('bcrypt');
|
|
3
3
|
import crypto = require('crypto');
|
|
4
4
|
import { sha256 } from 'js-sha256';
|
|
@@ -13,20 +13,31 @@ import { CoreModelConstructor } from '../../common/types/core-model-constructor.
|
|
|
13
13
|
import { CoreUserModel } from './core-user.model';
|
|
14
14
|
import { CoreUserCreateInput } from './inputs/core-user-create.input';
|
|
15
15
|
import { CoreUserInput } from './inputs/core-user.input';
|
|
16
|
+
import { CoreUserServiceOptions } from './interfaces/core-user-service-options.interface';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* User service
|
|
20
|
+
*
|
|
21
|
+
* Provides user management with automatic synchronization between
|
|
22
|
+
* Legacy Auth and Better-Auth (IAM) systems when both are enabled.
|
|
19
23
|
*/
|
|
20
24
|
export abstract class CoreUserService<
|
|
21
25
|
TUser extends CoreUserModel,
|
|
22
26
|
TUserInput extends CoreUserInput,
|
|
23
27
|
TUserCreateInput extends CoreUserCreateInput,
|
|
24
28
|
> extends CrudService<TUser, TUserCreateInput, TUserInput> {
|
|
29
|
+
protected readonly userServiceLogger = new Logger(CoreUserService.name);
|
|
30
|
+
|
|
25
31
|
protected constructor(
|
|
26
32
|
protected override readonly configService: ConfigService,
|
|
27
33
|
protected readonly emailService: EmailService,
|
|
28
34
|
protected override readonly mainDbModel: Model<Document & TUser>,
|
|
29
35
|
protected override readonly mainModelConstructor: CoreModelConstructor<TUser>,
|
|
36
|
+
/**
|
|
37
|
+
* Optional configuration for additional features like IAM sync.
|
|
38
|
+
* Using options object pattern for extensibility without breaking changes.
|
|
39
|
+
*/
|
|
40
|
+
protected readonly options?: CoreUserServiceOptions,
|
|
30
41
|
) {
|
|
31
42
|
super();
|
|
32
43
|
}
|
|
@@ -125,6 +136,10 @@ export abstract class CoreUserService<
|
|
|
125
136
|
|
|
126
137
|
/**
|
|
127
138
|
* Set new password for user with token
|
|
139
|
+
*
|
|
140
|
+
* This method also syncs the password change to Better-Auth (IAM) if:
|
|
141
|
+
* - BetterAuthUserMapper is configured via options
|
|
142
|
+
* - User has an existing IAM credential account
|
|
128
143
|
*/
|
|
129
144
|
async resetPassword(token: string, newPassword: string, serviceOptions?: ServiceOptions): Promise<TUser> {
|
|
130
145
|
// Get user
|
|
@@ -133,6 +148,10 @@ export abstract class CoreUserService<
|
|
|
133
148
|
throw new NotFoundException(`No user found with password reset token: ${token}`);
|
|
134
149
|
}
|
|
135
150
|
|
|
151
|
+
// Store the original plain password for IAM sync before any hashing
|
|
152
|
+
// We need the plain password because IAM uses scrypt, not bcrypt+sha256
|
|
153
|
+
const plainPasswordForIamSync = /^[a-f0-9]{64}$/i.test(newPassword) ? undefined : newPassword;
|
|
154
|
+
|
|
136
155
|
return this.process(
|
|
137
156
|
async () => {
|
|
138
157
|
// Check if the password was transmitted encrypted
|
|
@@ -141,11 +160,26 @@ export abstract class CoreUserService<
|
|
|
141
160
|
newPassword = sha256(newPassword);
|
|
142
161
|
}
|
|
143
162
|
|
|
144
|
-
// Update
|
|
145
|
-
|
|
163
|
+
// Update Legacy Auth password
|
|
164
|
+
const updatedUser = await assignPlain(dbObject, {
|
|
146
165
|
password: await bcrypt.hash(newPassword, 10),
|
|
147
166
|
passwordResetToken: null,
|
|
148
167
|
}).save();
|
|
168
|
+
|
|
169
|
+
// Sync password to Better-Auth (IAM) if mapper is available
|
|
170
|
+
// This ensures users can sign in via IAM after password reset
|
|
171
|
+
if (this.options?.betterAuthUserMapper && plainPasswordForIamSync && dbObject.email) {
|
|
172
|
+
try {
|
|
173
|
+
await this.options.betterAuthUserMapper.syncPasswordChangeToIam(dbObject.email, plainPasswordForIamSync);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
// Log but don't fail - Legacy Auth password was updated successfully
|
|
176
|
+
this.userServiceLogger.warn(
|
|
177
|
+
`Failed to sync password reset to IAM for ${dbObject.email}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return updatedUser;
|
|
149
183
|
},
|
|
150
184
|
{ dbObject, serviceOptions },
|
|
151
185
|
);
|
|
@@ -186,7 +220,7 @@ export abstract class CoreUserService<
|
|
|
186
220
|
}
|
|
187
221
|
|
|
188
222
|
// Check roles values
|
|
189
|
-
if (roles.some(role => typeof role !== 'string')) {
|
|
223
|
+
if (roles.some((role) => typeof role !== 'string')) {
|
|
190
224
|
throw new BadRequestException('Roles contains invalid values');
|
|
191
225
|
}
|
|
192
226
|
|
|
@@ -198,4 +232,97 @@ export abstract class CoreUserService<
|
|
|
198
232
|
{ serviceOptions },
|
|
199
233
|
);
|
|
200
234
|
}
|
|
235
|
+
|
|
236
|
+
// ===================================================================================================================
|
|
237
|
+
// Auth System Sync Methods
|
|
238
|
+
// ===================================================================================================================
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Update user with automatic email and password sync between Legacy and IAM auth systems
|
|
242
|
+
*
|
|
243
|
+
* When the email changes and BetterAuthUserMapper is available, this method:
|
|
244
|
+
* - Invalidates all Better-Auth sessions (forces re-authentication)
|
|
245
|
+
* - The shared users collection is automatically updated
|
|
246
|
+
*
|
|
247
|
+
* When the password changes:
|
|
248
|
+
* - Updates the Legacy Auth password (bcrypt hash)
|
|
249
|
+
* - Syncs to Better-Auth (IAM) if the user has a credential account
|
|
250
|
+
*/
|
|
251
|
+
override async update(id: string, input: TUserInput, serviceOptions?: ServiceOptions): Promise<TUser> {
|
|
252
|
+
// Get the current user before update to detect email changes
|
|
253
|
+
const oldUser = (await this.mainDbModel.findById(id).lean().exec()) as null | TUser;
|
|
254
|
+
const oldEmail = oldUser?.email;
|
|
255
|
+
|
|
256
|
+
// Store plain password for IAM sync before any hashing occurs
|
|
257
|
+
// We need to capture this before super.update() which may hash it
|
|
258
|
+
const inputPassword = (input as any).password;
|
|
259
|
+
const plainPasswordForIamSync = inputPassword && !/^[a-f0-9]{64}$/i.test(inputPassword) ? inputPassword : undefined;
|
|
260
|
+
|
|
261
|
+
// Perform the update
|
|
262
|
+
const updatedUser = await super.update(id, input, serviceOptions);
|
|
263
|
+
|
|
264
|
+
// Sync email change to IAM if email was changed and mapper is available
|
|
265
|
+
if (this.options?.betterAuthUserMapper && oldEmail && input.email && oldEmail !== input.email) {
|
|
266
|
+
try {
|
|
267
|
+
await this.options.betterAuthUserMapper.syncEmailChangeFromLegacy(oldEmail, input.email);
|
|
268
|
+
this.userServiceLogger.debug(`Synced email change from Legacy to IAM: ${oldEmail} → ${input.email}`);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
this.userServiceLogger.error(
|
|
271
|
+
`Failed to sync email change to IAM: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
272
|
+
);
|
|
273
|
+
// Don't throw - email sync failure shouldn't block the update
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Sync password change to IAM if password was changed and mapper is available
|
|
278
|
+
if (this.options?.betterAuthUserMapper && plainPasswordForIamSync && oldUser?.email) {
|
|
279
|
+
try {
|
|
280
|
+
await this.options.betterAuthUserMapper.syncPasswordChangeToIam(oldUser.email, plainPasswordForIamSync);
|
|
281
|
+
this.userServiceLogger.debug(`Synced password change to IAM for user ${oldUser.email}`);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
this.userServiceLogger.warn(
|
|
284
|
+
`Failed to sync password change to IAM for ${oldUser.email}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
285
|
+
);
|
|
286
|
+
// Don't throw - password sync failure shouldn't block the update
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return updatedUser;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Delete user with automatic cleanup of IAM auth data
|
|
295
|
+
*
|
|
296
|
+
* When BetterAuthUserMapper is available, this method also:
|
|
297
|
+
* - Deletes all Better-Auth accounts for this user
|
|
298
|
+
* - Deletes all Better-Auth sessions for this user
|
|
299
|
+
*
|
|
300
|
+
* This ensures no orphaned auth data remains after user deletion.
|
|
301
|
+
*/
|
|
302
|
+
override async delete(id: string, serviceOptions?: ServiceOptions): Promise<TUser> {
|
|
303
|
+
// Get the user before deletion to cleanup IAM data
|
|
304
|
+
const user = (await this.mainDbModel.findById(id).lean().exec()) as null | (TUser & { _id: any });
|
|
305
|
+
|
|
306
|
+
// Perform the deletion
|
|
307
|
+
const deletedUser = await super.delete(id, serviceOptions);
|
|
308
|
+
|
|
309
|
+
// Cleanup IAM data if mapper is available
|
|
310
|
+
if (this.options?.betterAuthUserMapper && user?._id) {
|
|
311
|
+
try {
|
|
312
|
+
const result = await this.options.betterAuthUserMapper.cleanupIamDataForDeletedUser(user._id);
|
|
313
|
+
if (result.accountsDeleted > 0 || result.sessionsDeleted > 0) {
|
|
314
|
+
this.userServiceLogger.debug(
|
|
315
|
+
`Cleaned up IAM data for deleted user ${id}: accounts=${result.accountsDeleted}, sessions=${result.sessionsDeleted}`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
} catch (error) {
|
|
319
|
+
this.userServiceLogger.error(
|
|
320
|
+
`Failed to cleanup IAM data for deleted user: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
321
|
+
);
|
|
322
|
+
// Don't throw - cleanup failure shouldn't block the delete response
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return deletedUser;
|
|
327
|
+
}
|
|
201
328
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BetterAuthUserMapper } from '../../better-auth/better-auth-user.mapper';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Optional configuration for CoreUserService
|
|
5
|
+
*
|
|
6
|
+
* Use this interface for optional dependencies that may not be available in all projects.
|
|
7
|
+
* This pattern allows adding new optional parameters without breaking existing implementations.
|
|
8
|
+
*/
|
|
9
|
+
export interface CoreUserServiceOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Optional BetterAuthUserMapper for syncing between Legacy and IAM auth systems.
|
|
12
|
+
* When provided, email changes and user deletions are automatically synced.
|
|
13
|
+
*/
|
|
14
|
+
betterAuthUserMapper?: BetterAuthUserMapper;
|
|
15
|
+
}
|