@lenne.tech/nest-server 11.6.2 → 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 (136) hide show
  1. package/dist/config.env.js +19 -12
  2. package/dist/config.env.js.map +1 -1
  3. package/dist/core/common/helpers/filter.helper.d.ts +9 -9
  4. package/dist/core/common/helpers/filter.helper.js +2 -4
  5. package/dist/core/common/helpers/filter.helper.js.map +1 -1
  6. package/dist/core/common/helpers/gridfs.helper.js +3 -3
  7. package/dist/core/common/helpers/gridfs.helper.js.map +1 -1
  8. package/dist/core/common/interfaces/server-options.interface.d.ts +21 -3
  9. package/dist/core/common/services/crud.service.d.ts +16 -16
  10. package/dist/core/common/services/crud.service.js +1 -1
  11. package/dist/core/common/services/crud.service.js.map +1 -1
  12. package/dist/core/modules/auth/core-auth.controller.d.ts +1 -0
  13. package/dist/core/modules/auth/core-auth.controller.js +28 -2
  14. package/dist/core/modules/auth/core-auth.controller.js.map +1 -1
  15. package/dist/core/modules/auth/core-auth.module.js +14 -1
  16. package/dist/core/modules/auth/core-auth.module.js.map +1 -1
  17. package/dist/core/modules/auth/core-auth.resolver.d.ts +1 -0
  18. package/dist/core/modules/auth/core-auth.resolver.js +20 -2
  19. package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
  20. package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.d.ts +4 -0
  21. package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.js +17 -0
  22. package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.js.map +1 -0
  23. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.d.ts +9 -0
  24. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js +74 -0
  25. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js.map +1 -0
  26. package/dist/core/modules/auth/interfaces/auth-provider.interface.d.ts +7 -0
  27. package/dist/core/modules/auth/interfaces/auth-provider.interface.js +5 -0
  28. package/dist/core/modules/auth/interfaces/auth-provider.interface.js.map +1 -0
  29. package/dist/core/modules/auth/interfaces/core-auth-user.interface.d.ts +1 -0
  30. package/dist/core/modules/auth/services/core-auth.service.d.ts +10 -1
  31. package/dist/core/modules/auth/services/core-auth.service.js +141 -9
  32. package/dist/core/modules/auth/services/core-auth.service.js.map +1 -1
  33. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.d.ts +31 -0
  34. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +153 -0
  35. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -0
  36. package/dist/core/modules/better-auth/better-auth-migration-status.model.d.ts +10 -0
  37. package/dist/core/modules/better-auth/better-auth-migration-status.model.js +57 -0
  38. package/dist/core/modules/better-auth/better-auth-migration-status.model.js.map +1 -0
  39. package/dist/core/modules/better-auth/better-auth-models.d.ts +0 -1
  40. package/dist/core/modules/better-auth/better-auth-models.js +0 -4
  41. package/dist/core/modules/better-auth/better-auth-models.js.map +1 -1
  42. package/dist/core/modules/better-auth/better-auth-user.mapper.d.ts +33 -0
  43. package/dist/core/modules/better-auth/better-auth-user.mapper.js +443 -0
  44. package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +1 -1
  45. package/dist/core/modules/better-auth/better-auth.config.js +3 -0
  46. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  47. package/dist/core/modules/better-auth/better-auth.module.d.ts +10 -2
  48. package/dist/core/modules/better-auth/better-auth.module.js +40 -52
  49. package/dist/core/modules/better-auth/better-auth.module.js.map +1 -1
  50. package/dist/core/modules/better-auth/better-auth.resolver.d.ts +8 -12
  51. package/dist/core/modules/better-auth/better-auth.resolver.js +33 -351
  52. package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
  53. package/dist/core/modules/better-auth/better-auth.service.d.ts +0 -1
  54. package/dist/core/modules/better-auth/better-auth.service.js +0 -3
  55. package/dist/core/modules/better-auth/better-auth.service.js.map +1 -1
  56. package/dist/core/modules/better-auth/better-auth.types.d.ts +9 -8
  57. package/dist/core/modules/better-auth/better-auth.types.js +14 -3
  58. package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
  59. package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +67 -0
  60. package/dist/core/modules/better-auth/core-better-auth.controller.js +504 -0
  61. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -0
  62. package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +61 -0
  63. package/dist/core/modules/better-auth/core-better-auth.resolver.js +552 -0
  64. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -0
  65. package/dist/core/modules/better-auth/index.d.ts +3 -0
  66. package/dist/core/modules/better-auth/index.js +3 -0
  67. package/dist/core/modules/better-auth/index.js.map +1 -1
  68. package/dist/core/modules/user/core-user.service.d.ts +7 -1
  69. package/dist/core/modules/user/core-user.service.js +57 -3
  70. package/dist/core/modules/user/core-user.service.js.map +1 -1
  71. package/dist/core/modules/user/interfaces/core-user-service-options.interface.d.ts +4 -0
  72. package/dist/core/modules/user/interfaces/core-user-service-options.interface.js +3 -0
  73. package/dist/core/modules/user/interfaces/core-user-service-options.interface.js.map +1 -0
  74. package/dist/core.module.d.ts +3 -0
  75. package/dist/core.module.js +132 -54
  76. package/dist/core.module.js.map +1 -1
  77. package/dist/index.d.ts +5 -0
  78. package/dist/index.js +5 -0
  79. package/dist/index.js.map +1 -1
  80. package/dist/server/modules/auth/auth.resolver.js +2 -0
  81. package/dist/server/modules/auth/auth.resolver.js.map +1 -1
  82. package/dist/server/modules/better-auth/better-auth.controller.d.ts +10 -0
  83. package/dist/server/modules/better-auth/better-auth.controller.js +36 -0
  84. package/dist/server/modules/better-auth/better-auth.controller.js.map +1 -0
  85. package/dist/server/modules/better-auth/better-auth.module.d.ts +9 -0
  86. package/dist/server/modules/better-auth/better-auth.module.js +44 -0
  87. package/dist/server/modules/better-auth/better-auth.module.js.map +1 -0
  88. package/dist/server/modules/better-auth/better-auth.resolver.d.ts +47 -0
  89. package/dist/server/modules/better-auth/better-auth.resolver.js +234 -0
  90. package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -0
  91. package/dist/server/modules/file/file-info.model.d.ts +71 -3
  92. package/dist/server/modules/user/user.model.d.ts +169 -3
  93. package/dist/server/modules/user/user.service.d.ts +3 -1
  94. package/dist/server/modules/user/user.service.js +7 -3
  95. package/dist/server/modules/user/user.service.js.map +1 -1
  96. package/dist/server/server.module.js +6 -1
  97. package/dist/server/server.module.js.map +1 -1
  98. package/dist/tsconfig.build.tsbuildinfo +1 -1
  99. package/package.json +20 -29
  100. package/src/config.env.ts +34 -13
  101. package/src/core/common/helpers/filter.helper.ts +15 -17
  102. package/src/core/common/helpers/gridfs.helper.ts +5 -5
  103. package/src/core/common/interfaces/server-options.interface.ts +222 -14
  104. package/src/core/common/services/crud.service.ts +22 -22
  105. package/src/core/modules/auth/core-auth.controller.ts +93 -5
  106. package/src/core/modules/auth/core-auth.module.ts +15 -1
  107. package/src/core/modules/auth/core-auth.resolver.ts +70 -2
  108. package/src/core/modules/auth/exceptions/legacy-auth-disabled.exception.ts +35 -0
  109. package/src/core/modules/auth/guards/legacy-auth-rate-limit.guard.ts +109 -0
  110. package/src/core/modules/auth/interfaces/auth-provider.interface.ts +86 -0
  111. package/src/core/modules/auth/interfaces/core-auth-user.interface.ts +6 -0
  112. package/src/core/modules/auth/services/core-auth.service.ts +245 -6
  113. package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +283 -0
  114. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +254 -0
  115. package/src/core/modules/better-auth/README.md +698 -54
  116. package/src/core/modules/better-auth/better-auth-migration-status.model.ts +73 -0
  117. package/src/core/modules/better-auth/better-auth-models.ts +0 -3
  118. package/src/core/modules/better-auth/better-auth-user.mapper.ts +805 -0
  119. package/src/core/modules/better-auth/better-auth.config.ts +5 -0
  120. package/src/core/modules/better-auth/better-auth.module.ts +107 -66
  121. package/src/core/modules/better-auth/better-auth.resolver.ts +88 -553
  122. package/src/core/modules/better-auth/better-auth.service.ts +0 -9
  123. package/src/core/modules/better-auth/better-auth.types.ts +25 -10
  124. package/src/core/modules/better-auth/core-better-auth.controller.ts +646 -0
  125. package/src/core/modules/better-auth/core-better-auth.resolver.ts +730 -0
  126. package/src/core/modules/better-auth/index.ts +9 -1
  127. package/src/core/modules/user/core-user.service.ts +131 -4
  128. package/src/core/modules/user/interfaces/core-user-service-options.interface.ts +15 -0
  129. package/src/core.module.ts +257 -74
  130. package/src/index.ts +5 -0
  131. package/src/server/modules/auth/auth.resolver.ts +8 -0
  132. package/src/server/modules/better-auth/better-auth.controller.ts +41 -0
  133. package/src/server/modules/better-auth/better-auth.module.ts +88 -0
  134. package/src/server/modules/better-auth/better-auth.resolver.ts +210 -0
  135. package/src/server/modules/user/user.service.ts +4 -2
  136. package/src/server/server.module.ts +10 -1
@@ -8,11 +8,17 @@
8
8
  * - Two-Factor Authentication (TOTP)
9
9
  * - Passkey/WebAuthn authentication
10
10
  * - Social Login (Google, GitHub, Apple)
11
- * - Legacy password handling for migration
12
11
  * - Rate limiting for brute-force protection
12
+ * - Parallel operation with Legacy Auth (bcrypt compatible)
13
+ *
14
+ * Extension points:
15
+ * - CoreBetterAuthController: Abstract controller for REST extension
16
+ * - CoreBetterAuthResolver: Abstract resolver for GraphQL extension (isAbstract: true)
17
+ * - BetterAuthController/BetterAuthResolver: Default implementations
13
18
  */
14
19
 
15
20
  export * from './better-auth-auth.model';
21
+ export * from './better-auth-migration-status.model';
16
22
  export * from './better-auth-models';
17
23
  export * from './better-auth-rate-limit.middleware';
18
24
  export * from './better-auth-rate-limiter.service';
@@ -23,3 +29,5 @@ export * from './better-auth.module';
23
29
  export * from './better-auth.resolver';
24
30
  export * from './better-auth.service';
25
31
  export * from './better-auth.types';
32
+ export * from './core-better-auth.controller';
33
+ export * from './core-better-auth.resolver';
@@ -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
+ }
@@ -19,7 +19,9 @@ import { EmailService } from './core/common/services/email.service';
19
19
  import { MailjetService } from './core/common/services/mailjet.service';
20
20
  import { ModelDocService } from './core/common/services/model-doc.service';
21
21
  import { TemplateService } from './core/common/services/template.service';
22
+ import { BetterAuthUserMapper } from './core/modules/better-auth/better-auth-user.mapper';
22
23
  import { BetterAuthModule } from './core/modules/better-auth/better-auth.module';
24
+ import { BetterAuthService } from './core/modules/better-auth/better-auth.service';
23
25
  import { CoreHealthCheckModule } from './core/modules/health-check/core-health-check.module';
24
26
 
25
27
  /**
@@ -66,10 +68,65 @@ export class CoreModule implements NestModule {
66
68
  }
67
69
 
68
70
  /**
69
- * Dynamic module
70
- * see https://docs.nestjs.com/modules#dynamic-modules
71
+ * Dynamic module initialization
72
+ *
73
+ * @see https://docs.nestjs.com/modules#dynamic-modules
74
+ *
75
+ * ## Signatures
76
+ *
77
+ * ### IAM-Only Signature (Recommended for new projects)
78
+ *
79
+ * ```typescript
80
+ * CoreModule.forRoot(envConfig)
81
+ * ```
82
+ *
83
+ * Use this for new projects that only use BetterAuth (IAM) for authentication.
84
+ * GraphQL Subscription authentication uses BetterAuth JWT tokens.
85
+ *
86
+ * **Requirements:**
87
+ * - Configure `betterAuth` in your config (enabled by default)
88
+ * - Create BetterAuthModule, Resolver, and Controller in your project
89
+ * - Inject BetterAuthUserMapper in UserService
90
+ *
91
+ * ### Legacy + IAM Signature (For existing projects)
92
+ *
93
+ * ```typescript
94
+ * CoreModule.forRoot(CoreAuthService, AuthModule.forRoot(envConfig.jwt), envConfig)
95
+ * ```
96
+ *
97
+ * @deprecated This 3-parameter signature is deprecated for new projects.
98
+ * Use the single-parameter signature `CoreModule.forRoot(envConfig)` instead.
99
+ * Existing projects can continue using this signature during migration.
100
+ *
101
+ * Use this for existing projects that need Legacy Auth for backwards compatibility.
102
+ * Both Legacy Auth and BetterAuth (IAM) can run in parallel.
103
+ *
104
+ * ## Migration Path
105
+ *
106
+ * 1. **Existing projects**: Use the 3-parameter signature, run Legacy + IAM in parallel
107
+ * 2. **Monitor**: Use `betterAuthMigrationStatus` query to track user migration
108
+ * 3. **Disable Legacy**: Set `auth.legacyEndpoints.enabled: false` after all users migrated
109
+ * 4. **New projects**: Use the single-parameter signature with IAM only
110
+ *
111
+ * @see https://github.com/lenneTech/nest-server/blob/develop/.claude/rules/module-deprecation.md
71
112
  */
72
- static forRoot(AuthService: any, AuthModule: any, options: Partial<IServerOptions>): DynamicModule {
113
+ static forRoot(options: Partial<IServerOptions>): DynamicModule;
114
+ /**
115
+ * @deprecated Use the single-parameter signature `CoreModule.forRoot(envConfig)` for new projects.
116
+ * This 3-parameter signature is for existing projects during migration to IAM.
117
+ */
118
+ static forRoot(AuthService: any, AuthModule: any, options: Partial<IServerOptions>): DynamicModule;
119
+ static forRoot(
120
+ authServiceOrOptions: any,
121
+ authModuleOrUndefined?: any,
122
+ optionsOrUndefined?: Partial<IServerOptions>,
123
+ ): DynamicModule {
124
+ // Detect which signature was used
125
+ const isIamOnlyMode = authModuleOrUndefined === undefined && optionsOrUndefined === undefined;
126
+ const AuthService = isIamOnlyMode ? null : authServiceOrOptions;
127
+ const AuthModule = isIamOnlyMode ? null : authModuleOrUndefined;
128
+ const options: Partial<IServerOptions> = isIamOnlyMode ? authServiceOrOptions : optionsOrUndefined;
129
+
73
130
  // Process config
74
131
  let cors = {};
75
132
  if (options?.cookies) {
@@ -78,73 +135,17 @@ export class CoreModule implements NestModule {
78
135
  origin: true,
79
136
  };
80
137
  }
138
+
139
+ // Build GraphQL driver configuration based on auth mode
140
+ const graphQlDriverConfig = isIamOnlyMode
141
+ ? this.buildIamOnlyGraphQlDriver(cors, options)
142
+ : this.buildLegacyGraphQlDriver(AuthService, AuthModule, cors, options);
143
+
81
144
  const config: IServerOptions = merge(
82
145
  {
83
146
  env: 'develop',
84
147
  graphQl: {
85
- driver: {
86
- imports: [AuthModule],
87
- inject: [AuthService],
88
- useFactory: async (authService: any) =>
89
- Object.assign(
90
- {
91
- autoSchemaFile: 'schema.gql',
92
- context: ({ req, res }) => ({ req, res }),
93
- cors,
94
- installSubscriptionHandlers: true,
95
- subscriptions: {
96
- 'graphql-ws': {
97
- context: ({ extra }) => extra,
98
- onConnect: async (context: Context<any>) => {
99
- const { connectionParams, extra } = context;
100
- if (config.graphQl.enableSubscriptionAuth) {
101
- // get authToken from authorization header
102
- const headers = this.getHeaderFromArray(extra.request?.rawHeaders);
103
- const authToken: string =
104
- connectionParams?.Authorization?.split(' ')[1] ?? headers.Authorization?.split(' ')[1];
105
- if (authToken) {
106
- // verify authToken/getJwtPayLoad
107
- const payload = authService.decodeJwt(authToken);
108
- const user = await authService.validateUser(payload);
109
- if (!user) {
110
- throw new UnauthorizedException('No user found for token');
111
- }
112
- // the user/jwtPayload object found will be available as context.currentUser/jwtPayload in your GraphQL resolvers
113
- extra.user = user;
114
- extra.headers = connectionParams ?? headers;
115
- return extra;
116
- }
117
-
118
- throw new UnauthorizedException('Missing authentication token');
119
- }
120
- },
121
- },
122
- 'subscriptions-transport-ws': {
123
- onConnect: async (connectionParams) => {
124
- if (config.graphQl.enableSubscriptionAuth) {
125
- // get authToken from authorization header
126
- const authToken: string = connectionParams?.Authorization?.split(' ')[1];
127
-
128
- if (authToken) {
129
- // verify authToken/getJwtPayLoad
130
- const payload = authService.decodeJwt(authToken);
131
- const user = await authService.validateUser(payload);
132
- if (!user) {
133
- throw new UnauthorizedException('No user found for token');
134
- }
135
- // the user/jwtPayload object found will be available as context.currentUser/jwtPayload in your GraphQL resolvers
136
- return { headers: connectionParams, user };
137
- }
138
-
139
- throw new UnauthorizedException('Missing authentication token');
140
- }
141
- },
142
- },
143
- },
144
- },
145
- options?.graphQl?.driver,
146
- ),
147
- },
148
+ driver: graphQlDriverConfig,
148
149
  enableSubscriptionAuth: true,
149
150
  },
150
151
  mongoose: {
@@ -229,15 +230,19 @@ export class CoreModule implements NestModule {
229
230
  imports.push(CoreHealthCheckModule);
230
231
  }
231
232
 
232
- // Add BetterAuthModule - enabled by default unless explicitly disabled
233
+ // Add BetterAuthModule based on mode
234
+ // IAM-only mode: Always register BetterAuthModule (required for subscription auth)
235
+ // Legacy mode: Only register if autoRegister is explicitly true
233
236
  if (config.betterAuth?.enabled !== false) {
234
- imports.push(
235
- BetterAuthModule.forRoot({
236
- config: config.betterAuth || {},
237
- // Pass JWT secrets for backwards compatibility fallback
238
- fallbackSecrets: [config.jwt?.secret, config.jwt?.refresh?.secret],
239
- }),
240
- );
237
+ if (isIamOnlyMode || config.betterAuth?.autoRegister === true) {
238
+ imports.push(
239
+ BetterAuthModule.forRoot({
240
+ config: config.betterAuth || {},
241
+ // Pass JWT secrets for backwards compatibility fallback
242
+ fallbackSecrets: [config.jwt?.secret, config.jwt?.refresh?.secret],
243
+ }),
244
+ );
245
+ }
241
246
  }
242
247
 
243
248
  // Set exports
@@ -254,4 +259,182 @@ export class CoreModule implements NestModule {
254
259
  providers,
255
260
  };
256
261
  }
262
+
263
+ // =============================================================================
264
+ // GraphQL Driver Configuration Helpers
265
+ // =============================================================================
266
+
267
+ /**
268
+ * Build GraphQL driver configuration for IAM-only mode
269
+ *
270
+ * Uses BetterAuthService for subscription authentication via JWT tokens.
271
+ * This is the recommended mode for new projects.
272
+ */
273
+ private static buildIamOnlyGraphQlDriver(cors: object, options: Partial<IServerOptions>) {
274
+ return {
275
+ imports: [BetterAuthModule],
276
+ inject: [BetterAuthService, BetterAuthUserMapper],
277
+ useFactory: async (betterAuthService: BetterAuthService, userMapper: BetterAuthUserMapper) =>
278
+ Object.assign(
279
+ {
280
+ autoSchemaFile: 'schema.gql',
281
+ context: ({ req, res }) => ({ req, res }),
282
+ cors,
283
+ installSubscriptionHandlers: true,
284
+ subscriptions: {
285
+ 'graphql-ws': {
286
+ context: ({ extra }) => extra,
287
+ onConnect: async (context: Context<any>) => {
288
+ const { connectionParams, extra } = context;
289
+ const enableAuth = options?.graphQl?.enableSubscriptionAuth ?? true;
290
+
291
+ if (enableAuth) {
292
+ // Get headers from raw headers or connection params
293
+ const headers = CoreModule.getHeaderFromArray(extra.request?.rawHeaders);
294
+ const authToken: string =
295
+ connectionParams?.Authorization?.split(' ')[1] ?? headers.Authorization?.split(' ')[1];
296
+
297
+ if (authToken) {
298
+ // Validate via BetterAuth session
299
+ const { session, user: sessionUser } = await betterAuthService.getSession({
300
+ headers: { authorization: `Bearer ${authToken}` },
301
+ });
302
+
303
+ if (!session || !sessionUser) {
304
+ throw new UnauthorizedException('Invalid or expired session');
305
+ }
306
+
307
+ // Map to full user with roles
308
+ const user = await userMapper.mapSessionUser(sessionUser);
309
+ if (!user) {
310
+ throw new UnauthorizedException('User not found');
311
+ }
312
+
313
+ extra.user = user;
314
+ extra.headers = connectionParams ?? headers;
315
+ return extra;
316
+ }
317
+
318
+ throw new UnauthorizedException('Missing authentication token');
319
+ }
320
+ },
321
+ },
322
+ 'subscriptions-transport-ws': {
323
+ onConnect: async (connectionParams) => {
324
+ const enableAuth = options?.graphQl?.enableSubscriptionAuth ?? true;
325
+
326
+ if (enableAuth) {
327
+ const authToken: string = connectionParams?.Authorization?.split(' ')[1];
328
+
329
+ if (authToken) {
330
+ // Validate via BetterAuth session
331
+ const { session, user: sessionUser } = await betterAuthService.getSession({
332
+ headers: { authorization: `Bearer ${authToken}` },
333
+ });
334
+
335
+ if (!session || !sessionUser) {
336
+ throw new UnauthorizedException('Invalid or expired session');
337
+ }
338
+
339
+ // Map to full user with roles
340
+ const user = await userMapper.mapSessionUser(sessionUser);
341
+ if (!user) {
342
+ throw new UnauthorizedException('User not found');
343
+ }
344
+
345
+ return { headers: connectionParams, user };
346
+ }
347
+
348
+ throw new UnauthorizedException('Missing authentication token');
349
+ }
350
+ },
351
+ },
352
+ },
353
+ },
354
+ options?.graphQl?.driver,
355
+ ),
356
+ };
357
+ }
358
+
359
+ /**
360
+ * Build GraphQL driver configuration for Legacy Auth mode
361
+ *
362
+ * Uses the provided AuthService for subscription authentication via JWT tokens.
363
+ * This is for existing projects that need backwards compatibility.
364
+ *
365
+ * @deprecated Use IAM-only mode (single-parameter forRoot) for new projects
366
+ */
367
+ private static buildLegacyGraphQlDriver(
368
+ AuthService: any,
369
+ AuthModule: any,
370
+ cors: object,
371
+ options: Partial<IServerOptions>,
372
+ ) {
373
+ // Store config reference for use in callbacks
374
+ const enableSubscriptionAuth = options?.graphQl?.enableSubscriptionAuth ?? true;
375
+
376
+ return {
377
+ imports: [AuthModule],
378
+ inject: [AuthService],
379
+ useFactory: async (authService: any) =>
380
+ Object.assign(
381
+ {
382
+ autoSchemaFile: 'schema.gql',
383
+ context: ({ req, res }) => ({ req, res }),
384
+ cors,
385
+ installSubscriptionHandlers: true,
386
+ subscriptions: {
387
+ 'graphql-ws': {
388
+ context: ({ extra }) => extra,
389
+ onConnect: async (context: Context<any>) => {
390
+ const { connectionParams, extra } = context;
391
+ if (enableSubscriptionAuth) {
392
+ // get authToken from authorization header
393
+ const headers = CoreModule.getHeaderFromArray(extra.request?.rawHeaders);
394
+ const authToken: string =
395
+ connectionParams?.Authorization?.split(' ')[1] ?? headers.Authorization?.split(' ')[1];
396
+ if (authToken) {
397
+ // verify authToken/getJwtPayLoad
398
+ const payload = authService.decodeJwt(authToken);
399
+ const user = await authService.validateUser(payload);
400
+ if (!user) {
401
+ throw new UnauthorizedException('No user found for token');
402
+ }
403
+ // the user/jwtPayload object found will be available as context.currentUser/jwtPayload in your GraphQL resolvers
404
+ extra.user = user;
405
+ extra.headers = connectionParams ?? headers;
406
+ return extra;
407
+ }
408
+
409
+ throw new UnauthorizedException('Missing authentication token');
410
+ }
411
+ },
412
+ },
413
+ 'subscriptions-transport-ws': {
414
+ onConnect: async (connectionParams) => {
415
+ if (enableSubscriptionAuth) {
416
+ // get authToken from authorization header
417
+ const authToken: string = connectionParams?.Authorization?.split(' ')[1];
418
+
419
+ if (authToken) {
420
+ // verify authToken/getJwtPayLoad
421
+ const payload = authService.decodeJwt(authToken);
422
+ const user = await authService.validateUser(payload);
423
+ if (!user) {
424
+ throw new UnauthorizedException('No user found for token');
425
+ }
426
+ // the user/jwtPayload object found will be available as context.currentUser/jwtPayload in your GraphQL resolvers
427
+ return { headers: connectionParams, user };
428
+ }
429
+
430
+ throw new UnauthorizedException('Missing authentication token');
431
+ }
432
+ },
433
+ },
434
+ },
435
+ },
436
+ options?.graphQl?.driver,
437
+ ),
438
+ };
439
+ }
257
440
  }
package/src/index.ts CHANGED
@@ -108,15 +108,19 @@ export * from './core/modules/auth/core-auth.resolver';
108
108
  export * from './core/modules/auth/exceptions/expired-refresh-token.exception';
109
109
  export * from './core/modules/auth/exceptions/expired-token.exception';
110
110
  export * from './core/modules/auth/exceptions/invalid-token.exception';
111
+ export * from './core/modules/auth/exceptions/legacy-auth-disabled.exception';
111
112
  export * from './core/modules/auth/guards/auth.guard';
113
+ export * from './core/modules/auth/guards/legacy-auth-rate-limit.guard';
112
114
  export * from './core/modules/auth/guards/roles.guard';
113
115
  export * from './core/modules/auth/inputs/core-auth-sign-in.input';
114
116
  export * from './core/modules/auth/inputs/core-auth-sign-up.input';
117
+ export * from './core/modules/auth/interfaces/auth-provider.interface';
115
118
  export * from './core/modules/auth/interfaces/core-auth-user.interface';
116
119
  export * from './core/modules/auth/interfaces/core-token-data.interface';
117
120
  export * from './core/modules/auth/interfaces/jwt-payload.interface';
118
121
  export * from './core/modules/auth/services/core-auth-user.service';
119
122
  export * from './core/modules/auth/services/core-auth.service';
123
+ export * from './core/modules/auth/services/legacy-auth-rate-limiter.service';
120
124
  export * from './core/modules/auth/strategies/jwt-refresh.strategy';
121
125
  export * from './core/modules/auth/strategies/jwt.strategy';
122
126
  export * from './core/modules/auth/tokens.decorator';
@@ -162,6 +166,7 @@ export * from './core/modules/user/core-user.model';
162
166
  export * from './core/modules/user/core-user.service';
163
167
  export * from './core/modules/user/inputs/core-user-create.input';
164
168
  export * from './core/modules/user/inputs/core-user.input';
169
+ export * from './core/modules/user/interfaces/core-user-service-options.interface';
165
170
 
166
171
  // =====================================================================================================================
167
172
  // Tests
@@ -30,6 +30,8 @@ export class AuthResolver extends CoreAuthResolver {
30
30
 
31
31
  /**
32
32
  * SignIn for User
33
+ *
34
+ * @throws LegacyAuthDisabledException if legacy endpoints are disabled
33
35
  */
34
36
  @Mutation(() => Auth, { description: 'Sign in and get JWT token' })
35
37
  @Roles(RoleEnum.S_EVERYONE)
@@ -38,6 +40,8 @@ export class AuthResolver extends CoreAuthResolver {
38
40
  @Context() ctx: { res: ResponseType },
39
41
  @Args('input') input: AuthSignInInput,
40
42
  ): Promise<Auth> {
43
+ // Check if legacy endpoints are enabled before proceeding
44
+ this.checkLegacyGraphQLEnabled('signIn');
41
45
  const result = await this.authService.signIn(input, {
42
46
  ...serviceOptions,
43
47
  inputType: AuthSignInInput,
@@ -47,6 +51,8 @@ export class AuthResolver extends CoreAuthResolver {
47
51
 
48
52
  /**
49
53
  * Sign up for user
54
+ *
55
+ * @throws LegacyAuthDisabledException if legacy endpoints are disabled
50
56
  */
51
57
  @Mutation(() => Auth, {
52
58
  description: 'Sign up user and get JWT token',
@@ -57,6 +63,8 @@ export class AuthResolver extends CoreAuthResolver {
57
63
  @Context() ctx: { res: ResponseType },
58
64
  @Args('input') input: AuthSignUpInput,
59
65
  ): Promise<Auth> {
66
+ // Check if legacy endpoints are enabled before proceeding
67
+ this.checkLegacyGraphQLEnabled('signUp');
60
68
  const result = await this.authService.signUp(input, serviceOptions);
61
69
  return this.processCookies(ctx, result);
62
70
  }