@lenne.tech/nest-server 11.7.0 → 11.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) 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 +17 -0
  4. package/dist/core/modules/auth/core-auth.controller.d.ts +1 -0
  5. package/dist/core/modules/auth/core-auth.controller.js +28 -2
  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 +20 -2
  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-user.mapper.d.ts +33 -0
  32. package/dist/core/modules/better-auth/better-auth-user.mapper.js +443 -0
  33. package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +1 -1
  34. package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +1 -0
  35. package/dist/core/modules/better-auth/core-better-auth.controller.js +15 -2
  36. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  37. package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +2 -0
  38. package/dist/core/modules/better-auth/core-better-auth.resolver.js +14 -0
  39. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
  40. package/dist/core/modules/better-auth/index.d.ts +1 -0
  41. package/dist/core/modules/better-auth/index.js +1 -0
  42. package/dist/core/modules/better-auth/index.js.map +1 -1
  43. package/dist/core/modules/user/core-user.service.d.ts +7 -1
  44. package/dist/core/modules/user/core-user.service.js +57 -3
  45. package/dist/core/modules/user/core-user.service.js.map +1 -1
  46. package/dist/core/modules/user/interfaces/core-user-service-options.interface.d.ts +4 -0
  47. package/dist/core/modules/user/interfaces/core-user-service-options.interface.js +3 -0
  48. package/dist/core/modules/user/interfaces/core-user-service-options.interface.js.map +1 -0
  49. package/dist/core.module.d.ts +3 -0
  50. package/dist/core.module.js +133 -55
  51. package/dist/core.module.js.map +1 -1
  52. package/dist/index.d.ts +5 -0
  53. package/dist/index.js +5 -0
  54. package/dist/index.js.map +1 -1
  55. package/dist/server/modules/auth/auth.resolver.js +2 -0
  56. package/dist/server/modules/auth/auth.resolver.js.map +1 -1
  57. package/dist/server/modules/better-auth/better-auth.resolver.d.ts +2 -0
  58. package/dist/server/modules/better-auth/better-auth.resolver.js +13 -0
  59. package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -1
  60. package/dist/server/modules/user/user.service.d.ts +3 -1
  61. package/dist/server/modules/user/user.service.js +7 -3
  62. package/dist/server/modules/user/user.service.js.map +1 -1
  63. package/dist/tsconfig.build.tsbuildinfo +1 -1
  64. package/package.json +1 -1
  65. package/src/config.env.ts +32 -2
  66. package/src/core/common/interfaces/server-options.interface.ts +175 -0
  67. package/src/core/modules/auth/core-auth.controller.ts +93 -5
  68. package/src/core/modules/auth/core-auth.module.ts +15 -1
  69. package/src/core/modules/auth/core-auth.resolver.ts +70 -2
  70. package/src/core/modules/auth/exceptions/legacy-auth-disabled.exception.ts +35 -0
  71. package/src/core/modules/auth/guards/legacy-auth-rate-limit.guard.ts +109 -0
  72. package/src/core/modules/auth/interfaces/auth-provider.interface.ts +86 -0
  73. package/src/core/modules/auth/interfaces/core-auth-user.interface.ts +6 -0
  74. package/src/core/modules/auth/services/core-auth.service.ts +245 -6
  75. package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +283 -0
  76. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +254 -0
  77. package/src/core/modules/better-auth/README.md +487 -169
  78. package/src/core/modules/better-auth/better-auth-migration-status.model.ts +73 -0
  79. package/src/core/modules/better-auth/better-auth-user.mapper.ts +805 -0
  80. package/src/core/modules/better-auth/core-better-auth.controller.ts +44 -3
  81. package/src/core/modules/better-auth/core-better-auth.resolver.ts +25 -0
  82. package/src/core/modules/better-auth/index.ts +1 -0
  83. package/src/core/modules/user/core-user.service.ts +131 -4
  84. package/src/core/modules/user/interfaces/core-user-service-options.interface.ts +15 -0
  85. package/src/core.module.ts +258 -76
  86. package/src/index.ts +5 -0
  87. package/src/server/modules/auth/auth.resolver.ts +8 -0
  88. package/src/server/modules/better-auth/better-auth.resolver.ts +9 -0
  89. package/src/server/modules/user/user.service.ts +4 -2
@@ -211,9 +211,30 @@ export class CoreBetterAuthController {
211
211
  throw new BadRequestException('Better-Auth API not available');
212
212
  }
213
213
 
214
+ // Try to sign in, with automatic legacy user migration
215
+ return this.attemptSignIn(res, input, api, true);
216
+ }
217
+
218
+ /**
219
+ * Attempt sign-in with optional legacy user migration
220
+ * @param res - Response object
221
+ * @param input - Sign-in credentials
222
+ * @param api - Better-Auth API instance
223
+ * @param allowMigration - Whether to attempt legacy migration on failure
224
+ */
225
+ private async attemptSignIn(
226
+ res: Response,
227
+ input: BetterAuthSignInInput,
228
+ api: ReturnType<BetterAuthService['getApi']>,
229
+ allowMigration: boolean,
230
+ ): Promise<BetterAuthResponse> {
231
+ // Normalize password to SHA256 format for consistency with Legacy Auth
232
+ // This ensures users can sign in with either plain password or SHA256 hash
233
+ const normalizedPassword = this.userMapper.normalizePasswordForIam(input.password);
234
+
214
235
  try {
215
- const response = await api.signInEmail({
216
- body: { email: input.email, password: input.password },
236
+ const response = await api!.signInEmail({
237
+ body: { email: input.email, password: normalizedPassword },
217
238
  });
218
239
 
219
240
  if (!response) {
@@ -244,6 +265,18 @@ export class CoreBetterAuthController {
244
265
  throw new UnauthorizedException('Invalid credentials');
245
266
  } catch (error) {
246
267
  this.logger.debug(`Sign-in error: ${error instanceof Error ? error.message : 'Unknown error'}`);
268
+
269
+ // If migration is allowed, try to migrate legacy user and retry
270
+ if (allowMigration) {
271
+ // Pass the original password for legacy verification, but migration uses normalized password
272
+ const migrated = await this.userMapper.migrateAccountToIam(input.email, input.password);
273
+ if (migrated) {
274
+ this.logger.debug(`Migrated legacy user ${input.email} to IAM, retrying sign-in`);
275
+ // Retry sign-in after migration (without allowing another migration to prevent loops)
276
+ return this.attemptSignIn(res, input, api, false);
277
+ }
278
+ }
279
+
247
280
  throw new UnauthorizedException('Invalid credentials');
248
281
  }
249
282
  }
@@ -267,12 +300,15 @@ export class CoreBetterAuthController {
267
300
  throw new BadRequestException('Better-Auth API not available');
268
301
  }
269
302
 
303
+ // Normalize password to SHA256 format for consistency with Legacy Auth
304
+ const normalizedPassword = this.userMapper.normalizePasswordForIam(input.password);
305
+
270
306
  try {
271
307
  const response = await api.signUpEmail({
272
308
  body: {
273
309
  email: input.email,
274
310
  name: input.name || input.email.split('@')[0],
275
- password: input.password,
311
+ password: normalizedPassword,
276
312
  },
277
313
  });
278
314
 
@@ -283,6 +319,11 @@ export class CoreBetterAuthController {
283
319
  if (hasUser(response)) {
284
320
  // Link or create user in our database
285
321
  await this.userMapper.linkOrCreateUser(response.user);
322
+
323
+ // Sync password to legacy (enables IAM Sign-Up → Legacy Sign-In)
324
+ // Pass the plain password so it can be hashed with bcrypt for Legacy Auth
325
+ await this.userMapper.syncPasswordToLegacy(response.user.id, response.user.email, input.password);
326
+
286
327
  const mappedUser = await this.userMapper.mapSessionUser(response.user);
287
328
 
288
329
  const result: BetterAuthResponse = {
@@ -7,6 +7,7 @@ import { RoleEnum } from '../../common/enums/role.enum';
7
7
  import { AuthGuardStrategy } from '../auth/auth-guard-strategy.enum';
8
8
  import { AuthGuard } from '../auth/guards/auth.guard';
9
9
  import { BetterAuthAuthModel } from './better-auth-auth.model';
10
+ import { BetterAuthMigrationStatusModel } from './better-auth-migration-status.model';
10
11
  import {
11
12
  BetterAuth2FASetupModel,
12
13
  BetterAuthFeaturesModel,
@@ -130,6 +131,30 @@ export class CoreBetterAuthResolver {
130
131
  };
131
132
  }
132
133
 
134
+ /**
135
+ * Get migration status from Legacy Auth to Better-Auth (IAM)
136
+ *
137
+ * This query provides administrators with information about how many users
138
+ * have been migrated to the IAM system. This helps determine when it might
139
+ * be safe to consider disabling Legacy Auth endpoints.
140
+ *
141
+ * A user is considered fully migrated when:
142
+ * 1. They have an `iamId` set (linked to Better-Auth)
143
+ * 2. They have a credential account in Better-Auth
144
+ *
145
+ * Note: Even when canDisableLegacyAuth returns true, Legacy Auth cannot
146
+ * currently be removed because CoreModule.forRoot requires AuthService
147
+ * for GraphQL Subscriptions authentication.
148
+ */
149
+ @Query(() => BetterAuthMigrationStatusModel, {
150
+ description: 'Get migration status from Legacy Auth to Better-Auth (IAM) - Admin only',
151
+ })
152
+ @Roles(RoleEnum.ADMIN)
153
+ async betterAuthMigrationStatus(): Promise<BetterAuthMigrationStatusModel> {
154
+ const status = await this.userMapper.getMigrationStatus();
155
+ return status;
156
+ }
157
+
133
158
  // ===========================================================================
134
159
  // Mutations
135
160
  // ===========================================================================
@@ -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
+ }