@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.
Files changed (120) hide show
  1. package/dist/config.env.js +17 -1
  2. package/dist/config.env.js.map +1 -1
  3. package/dist/core/common/interfaces/server-options.interface.d.ts +35 -15
  4. package/dist/core/modules/auth/core-auth.controller.d.ts +1 -0
  5. package/dist/core/modules/auth/core-auth.controller.js +29 -3
  6. package/dist/core/modules/auth/core-auth.controller.js.map +1 -1
  7. package/dist/core/modules/auth/core-auth.module.js +14 -1
  8. package/dist/core/modules/auth/core-auth.module.js.map +1 -1
  9. package/dist/core/modules/auth/core-auth.resolver.d.ts +1 -0
  10. package/dist/core/modules/auth/core-auth.resolver.js +21 -3
  11. package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
  12. package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.d.ts +4 -0
  13. package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.js +17 -0
  14. package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.js.map +1 -0
  15. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.d.ts +9 -0
  16. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js +74 -0
  17. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js.map +1 -0
  18. package/dist/core/modules/auth/interfaces/auth-provider.interface.d.ts +7 -0
  19. package/dist/core/modules/auth/interfaces/auth-provider.interface.js +5 -0
  20. package/dist/core/modules/auth/interfaces/auth-provider.interface.js.map +1 -0
  21. package/dist/core/modules/auth/interfaces/core-auth-user.interface.d.ts +1 -0
  22. package/dist/core/modules/auth/services/core-auth.service.d.ts +10 -1
  23. package/dist/core/modules/auth/services/core-auth.service.js +141 -9
  24. package/dist/core/modules/auth/services/core-auth.service.js.map +1 -1
  25. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.d.ts +31 -0
  26. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +153 -0
  27. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -0
  28. package/dist/core/modules/better-auth/better-auth-migration-status.model.d.ts +10 -0
  29. package/dist/core/modules/better-auth/better-auth-migration-status.model.js +57 -0
  30. package/dist/core/modules/better-auth/better-auth-migration-status.model.js.map +1 -0
  31. package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js +1 -1
  32. package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js.map +1 -1
  33. package/dist/core/modules/better-auth/better-auth-user.mapper.d.ts +33 -0
  34. package/dist/core/modules/better-auth/better-auth-user.mapper.js +395 -0
  35. package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +1 -1
  36. package/dist/core/modules/better-auth/better-auth.config.js +29 -10
  37. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  38. package/dist/core/modules/better-auth/better-auth.middleware.d.ts +1 -0
  39. package/dist/core/modules/better-auth/better-auth.middleware.js +55 -1
  40. package/dist/core/modules/better-auth/better-auth.middleware.js.map +1 -1
  41. package/dist/core/modules/better-auth/better-auth.module.d.ts +1 -1
  42. package/dist/core/modules/better-auth/better-auth.module.js +46 -18
  43. package/dist/core/modules/better-auth/better-auth.module.js.map +1 -1
  44. package/dist/core/modules/better-auth/better-auth.resolver.js +0 -11
  45. package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
  46. package/dist/core/modules/better-auth/better-auth.service.d.ts +22 -1
  47. package/dist/core/modules/better-auth/better-auth.service.js +209 -8
  48. package/dist/core/modules/better-auth/better-auth.service.js.map +1 -1
  49. package/dist/core/modules/better-auth/better-auth.types.d.ts +2 -0
  50. package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
  51. package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +1 -0
  52. package/dist/core/modules/better-auth/core-better-auth.controller.js +15 -2
  53. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  54. package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +7 -0
  55. package/dist/core/modules/better-auth/core-better-auth.resolver.js +72 -12
  56. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
  57. package/dist/core/modules/better-auth/index.d.ts +1 -0
  58. package/dist/core/modules/better-auth/index.js +1 -0
  59. package/dist/core/modules/better-auth/index.js.map +1 -1
  60. package/dist/core/modules/user/core-user.service.d.ts +7 -1
  61. package/dist/core/modules/user/core-user.service.js +57 -3
  62. package/dist/core/modules/user/core-user.service.js.map +1 -1
  63. package/dist/core/modules/user/interfaces/core-user-service-options.interface.d.ts +4 -0
  64. package/dist/core/modules/user/interfaces/core-user-service-options.interface.js +3 -0
  65. package/dist/core/modules/user/interfaces/core-user-service-options.interface.js.map +1 -0
  66. package/dist/core.module.d.ts +3 -0
  67. package/dist/core.module.js +136 -55
  68. package/dist/core.module.js.map +1 -1
  69. package/dist/index.d.ts +5 -0
  70. package/dist/index.js +5 -0
  71. package/dist/index.js.map +1 -1
  72. package/dist/server/modules/auth/auth.resolver.js +2 -0
  73. package/dist/server/modules/auth/auth.resolver.js.map +1 -1
  74. package/dist/server/modules/better-auth/better-auth.module.d.ts +1 -1
  75. package/dist/server/modules/better-auth/better-auth.module.js +2 -1
  76. package/dist/server/modules/better-auth/better-auth.module.js.map +1 -1
  77. package/dist/server/modules/better-auth/better-auth.resolver.d.ts +5 -0
  78. package/dist/server/modules/better-auth/better-auth.resolver.js +27 -11
  79. package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -1
  80. package/dist/server/modules/user/user.controller.js +0 -8
  81. package/dist/server/modules/user/user.controller.js.map +1 -1
  82. package/dist/server/modules/user/user.service.d.ts +3 -1
  83. package/dist/server/modules/user/user.service.js +7 -3
  84. package/dist/server/modules/user/user.service.js.map +1 -1
  85. package/dist/tsconfig.build.tsbuildinfo +1 -1
  86. package/package.json +1 -1
  87. package/src/config.env.ts +32 -2
  88. package/src/core/common/interfaces/server-options.interface.ts +304 -58
  89. package/src/core/modules/auth/core-auth.controller.ts +94 -6
  90. package/src/core/modules/auth/core-auth.module.ts +15 -1
  91. package/src/core/modules/auth/core-auth.resolver.ts +71 -3
  92. package/src/core/modules/auth/exceptions/legacy-auth-disabled.exception.ts +35 -0
  93. package/src/core/modules/auth/guards/legacy-auth-rate-limit.guard.ts +109 -0
  94. package/src/core/modules/auth/interfaces/auth-provider.interface.ts +86 -0
  95. package/src/core/modules/auth/interfaces/core-auth-user.interface.ts +6 -0
  96. package/src/core/modules/auth/services/core-auth.service.ts +245 -6
  97. package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +283 -0
  98. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +255 -0
  99. package/src/core/modules/better-auth/README.md +565 -208
  100. package/src/core/modules/better-auth/better-auth-migration-status.model.ts +73 -0
  101. package/src/core/modules/better-auth/better-auth-rate-limiter.service.ts +1 -1
  102. package/src/core/modules/better-auth/better-auth-user.mapper.ts +737 -0
  103. package/src/core/modules/better-auth/better-auth.config.ts +45 -15
  104. package/src/core/modules/better-auth/better-auth.middleware.ts +85 -2
  105. package/src/core/modules/better-auth/better-auth.module.ts +83 -27
  106. package/src/core/modules/better-auth/better-auth.resolver.ts +0 -11
  107. package/src/core/modules/better-auth/better-auth.service.ts +367 -12
  108. package/src/core/modules/better-auth/better-auth.types.ts +16 -0
  109. package/src/core/modules/better-auth/core-better-auth.controller.ts +44 -3
  110. package/src/core/modules/better-auth/core-better-auth.resolver.ts +136 -16
  111. package/src/core/modules/better-auth/index.ts +1 -0
  112. package/src/core/modules/user/core-user.service.ts +131 -4
  113. package/src/core/modules/user/interfaces/core-user-service-options.interface.ts +15 -0
  114. package/src/core.module.ts +264 -76
  115. package/src/index.ts +5 -0
  116. package/src/server/modules/auth/auth.resolver.ts +8 -0
  117. package/src/server/modules/better-auth/better-auth.module.ts +9 -3
  118. package/src/server/modules/better-auth/better-auth.resolver.ts +18 -11
  119. package/src/server/modules/user/user.controller.ts +1 -9
  120. package/src/server/modules/user/user.service.ts +4 -2
@@ -1,12 +1,11 @@
1
- import { BadRequestException, Logger, UnauthorizedException, UseGuards } from '@nestjs/common';
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
- // Call Better-Auth's sign-in endpoint
164
- const response = (await api.signInEmail({
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
- // Get token if JWT plugin is enabled
187
- const token = this.betterAuthService.isJwtEnabled() ? response.token : undefined;
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(`Sign-in error: ${error instanceof Error ? error.message : 'Unknown error'}`);
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 and return user
145
- return await assignPlain(dbObject, {
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
+ }