@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
@@ -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,16 +230,25 @@ export class CoreModule implements NestModule {
229
230
  imports.push(CoreHealthCheckModule);
230
231
  }
231
232
 
232
- // Add BetterAuthModule only if autoRegister is explicitly true
233
- // By default (autoRegister: false), projects integrate via an extended module in their project
234
- if (config.betterAuth?.enabled !== false && config.betterAuth?.autoRegister === true) {
235
- imports.push(
236
- BetterAuthModule.forRoot({
237
- config: config.betterAuth || {},
238
- // Pass JWT secrets for backwards compatibility fallback
239
- fallbackSecrets: [config.jwt?.secret, config.jwt?.refresh?.secret],
240
- }),
241
- );
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
236
+ // betterAuth can be: boolean | IBetterAuth | undefined
237
+ const betterAuthConfig = config.betterAuth;
238
+ const isBetterAuthEnabled =
239
+ betterAuthConfig === true || (typeof betterAuthConfig === 'object' && betterAuthConfig?.enabled !== false);
240
+ const isAutoRegister = typeof betterAuthConfig === 'object' && betterAuthConfig?.autoRegister === true;
241
+
242
+ if (isBetterAuthEnabled) {
243
+ if (isIamOnlyMode || isAutoRegister) {
244
+ imports.push(
245
+ BetterAuthModule.forRoot({
246
+ config: betterAuthConfig === true ? {} : betterAuthConfig || {},
247
+ // Pass JWT secrets for backwards compatibility fallback
248
+ fallbackSecrets: [config.jwt?.secret, config.jwt?.refresh?.secret],
249
+ }),
250
+ );
251
+ }
242
252
  }
243
253
 
244
254
  // Set exports
@@ -255,4 +265,182 @@ export class CoreModule implements NestModule {
255
265
  providers,
256
266
  };
257
267
  }
268
+
269
+ // =============================================================================
270
+ // GraphQL Driver Configuration Helpers
271
+ // =============================================================================
272
+
273
+ /**
274
+ * Build GraphQL driver configuration for IAM-only mode
275
+ *
276
+ * Uses BetterAuthService for subscription authentication via JWT tokens.
277
+ * This is the recommended mode for new projects.
278
+ */
279
+ private static buildIamOnlyGraphQlDriver(cors: object, options: Partial<IServerOptions>) {
280
+ return {
281
+ imports: [BetterAuthModule],
282
+ inject: [BetterAuthService, BetterAuthUserMapper],
283
+ useFactory: async (betterAuthService: BetterAuthService, userMapper: BetterAuthUserMapper) =>
284
+ Object.assign(
285
+ {
286
+ autoSchemaFile: 'schema.gql',
287
+ context: ({ req, res }) => ({ req, res }),
288
+ cors,
289
+ installSubscriptionHandlers: true,
290
+ subscriptions: {
291
+ 'graphql-ws': {
292
+ context: ({ extra }) => extra,
293
+ onConnect: async (context: Context<any>) => {
294
+ const { connectionParams, extra } = context;
295
+ const enableAuth = options?.graphQl?.enableSubscriptionAuth ?? true;
296
+
297
+ if (enableAuth) {
298
+ // Get headers from raw headers or connection params
299
+ const headers = CoreModule.getHeaderFromArray(extra.request?.rawHeaders);
300
+ const authToken: string =
301
+ connectionParams?.Authorization?.split(' ')[1] ?? headers.Authorization?.split(' ')[1];
302
+
303
+ if (authToken) {
304
+ // Validate via BetterAuth session
305
+ const { session, user: sessionUser } = await betterAuthService.getSession({
306
+ headers: { authorization: `Bearer ${authToken}` },
307
+ });
308
+
309
+ if (!session || !sessionUser) {
310
+ throw new UnauthorizedException('Invalid or expired session');
311
+ }
312
+
313
+ // Map to full user with roles
314
+ const user = await userMapper.mapSessionUser(sessionUser);
315
+ if (!user) {
316
+ throw new UnauthorizedException('User not found');
317
+ }
318
+
319
+ extra.user = user;
320
+ extra.headers = connectionParams ?? headers;
321
+ return extra;
322
+ }
323
+
324
+ throw new UnauthorizedException('Missing authentication token');
325
+ }
326
+ },
327
+ },
328
+ 'subscriptions-transport-ws': {
329
+ onConnect: async (connectionParams) => {
330
+ const enableAuth = options?.graphQl?.enableSubscriptionAuth ?? true;
331
+
332
+ if (enableAuth) {
333
+ const authToken: string = connectionParams?.Authorization?.split(' ')[1];
334
+
335
+ if (authToken) {
336
+ // Validate via BetterAuth session
337
+ const { session, user: sessionUser } = await betterAuthService.getSession({
338
+ headers: { authorization: `Bearer ${authToken}` },
339
+ });
340
+
341
+ if (!session || !sessionUser) {
342
+ throw new UnauthorizedException('Invalid or expired session');
343
+ }
344
+
345
+ // Map to full user with roles
346
+ const user = await userMapper.mapSessionUser(sessionUser);
347
+ if (!user) {
348
+ throw new UnauthorizedException('User not found');
349
+ }
350
+
351
+ return { headers: connectionParams, user };
352
+ }
353
+
354
+ throw new UnauthorizedException('Missing authentication token');
355
+ }
356
+ },
357
+ },
358
+ },
359
+ },
360
+ options?.graphQl?.driver,
361
+ ),
362
+ };
363
+ }
364
+
365
+ /**
366
+ * Build GraphQL driver configuration for Legacy Auth mode
367
+ *
368
+ * Uses the provided AuthService for subscription authentication via JWT tokens.
369
+ * This is for existing projects that need backwards compatibility.
370
+ *
371
+ * @deprecated Use IAM-only mode (single-parameter forRoot) for new projects
372
+ */
373
+ private static buildLegacyGraphQlDriver(
374
+ AuthService: any,
375
+ AuthModule: any,
376
+ cors: object,
377
+ options: Partial<IServerOptions>,
378
+ ) {
379
+ // Store config reference for use in callbacks
380
+ const enableSubscriptionAuth = options?.graphQl?.enableSubscriptionAuth ?? true;
381
+
382
+ return {
383
+ imports: [AuthModule],
384
+ inject: [AuthService],
385
+ useFactory: async (authService: any) =>
386
+ Object.assign(
387
+ {
388
+ autoSchemaFile: 'schema.gql',
389
+ context: ({ req, res }) => ({ req, res }),
390
+ cors,
391
+ installSubscriptionHandlers: true,
392
+ subscriptions: {
393
+ 'graphql-ws': {
394
+ context: ({ extra }) => extra,
395
+ onConnect: async (context: Context<any>) => {
396
+ const { connectionParams, extra } = context;
397
+ if (enableSubscriptionAuth) {
398
+ // get authToken from authorization header
399
+ const headers = CoreModule.getHeaderFromArray(extra.request?.rawHeaders);
400
+ const authToken: string =
401
+ connectionParams?.Authorization?.split(' ')[1] ?? headers.Authorization?.split(' ')[1];
402
+ if (authToken) {
403
+ // verify authToken/getJwtPayLoad
404
+ const payload = authService.decodeJwt(authToken);
405
+ const user = await authService.validateUser(payload);
406
+ if (!user) {
407
+ throw new UnauthorizedException('No user found for token');
408
+ }
409
+ // the user/jwtPayload object found will be available as context.currentUser/jwtPayload in your GraphQL resolvers
410
+ extra.user = user;
411
+ extra.headers = connectionParams ?? headers;
412
+ return extra;
413
+ }
414
+
415
+ throw new UnauthorizedException('Missing authentication token');
416
+ }
417
+ },
418
+ },
419
+ 'subscriptions-transport-ws': {
420
+ onConnect: async (connectionParams) => {
421
+ if (enableSubscriptionAuth) {
422
+ // get authToken from authorization header
423
+ const authToken: string = connectionParams?.Authorization?.split(' ')[1];
424
+
425
+ if (authToken) {
426
+ // verify authToken/getJwtPayLoad
427
+ const payload = authService.decodeJwt(authToken);
428
+ const user = await authService.validateUser(payload);
429
+ if (!user) {
430
+ throw new UnauthorizedException('No user found for token');
431
+ }
432
+ // the user/jwtPayload object found will be available as context.currentUser/jwtPayload in your GraphQL resolvers
433
+ return { headers: connectionParams, user };
434
+ }
435
+
436
+ throw new UnauthorizedException('Missing authentication token');
437
+ }
438
+ },
439
+ },
440
+ },
441
+ },
442
+ options?.graphQl?.driver,
443
+ ),
444
+ };
445
+ }
258
446
  }
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
  }
@@ -10,9 +10,13 @@ import { BetterAuthResolver } from './better-auth.resolver';
10
10
  */
11
11
  export interface ServerBetterAuthModuleOptions {
12
12
  /**
13
- * Better-auth configuration from environment
13
+ * Better-auth configuration.
14
+ * Accepts:
15
+ * - `true`: Enable with all defaults (including JWT)
16
+ * - `false`: Disable BetterAuth
17
+ * - `{ ... }`: Enable with custom configuration
14
18
  */
15
- config: IBetterAuth;
19
+ config: boolean | IBetterAuth;
16
20
 
17
21
  /**
18
22
  * Fallback secrets for backwards compatibility with JWT config.
@@ -63,7 +67,9 @@ export class BetterAuthModule {
63
67
  const { config, fallbackSecrets } = options;
64
68
 
65
69
  // If better-auth is explicitly disabled, return minimal module
66
- if (config?.enabled === false) {
70
+ // Supports: false, { enabled: false }, or undefined/null
71
+ const isDisabled = config === false || (typeof config === 'object' && config?.enabled === false);
72
+ if (isDisabled) {
67
73
  return {
68
74
  exports: [],
69
75
  module: BetterAuthModule,
@@ -1,12 +1,10 @@
1
- import { UseGuards } from '@nestjs/common';
2
1
  import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
3
2
  import { Request, Response } from 'express';
4
3
 
5
4
  import { Roles } from '../../../core/common/decorators/roles.decorator';
6
5
  import { RoleEnum } from '../../../core/common/enums/role.enum';
7
- import { AuthGuardStrategy } from '../../../core/modules/auth/auth-guard-strategy.enum';
8
- import { AuthGuard } from '../../../core/modules/auth/guards/auth.guard';
9
6
  import { BetterAuthAuthModel } from '../../../core/modules/better-auth/better-auth-auth.model';
7
+ import { BetterAuthMigrationStatusModel } from '../../../core/modules/better-auth/better-auth-migration-status.model';
10
8
  import {
11
9
  BetterAuth2FASetupModel,
12
10
  BetterAuthFeaturesModel,
@@ -61,7 +59,6 @@ export class BetterAuthResolver extends CoreBetterAuthResolver {
61
59
  nullable: true,
62
60
  })
63
61
  @Roles(RoleEnum.S_USER)
64
- @UseGuards(AuthGuard(AuthGuardStrategy.JWT))
65
62
  override async betterAuthSession(@Context() ctx: { req: Request }): Promise<BetterAuthSessionModel | null> {
66
63
  return super.betterAuthSession(ctx);
67
64
  }
@@ -78,12 +75,28 @@ export class BetterAuthResolver extends CoreBetterAuthResolver {
78
75
  return super.betterAuthFeatures();
79
76
  }
80
77
 
78
+ @Query(() => BetterAuthMigrationStatusModel, {
79
+ description: 'Get migration status from Legacy Auth to Better-Auth (IAM) - Admin only',
80
+ })
81
+ @Roles(RoleEnum.ADMIN)
82
+ override async betterAuthMigrationStatus(): Promise<BetterAuthMigrationStatusModel> {
83
+ return super.betterAuthMigrationStatus();
84
+ }
85
+
86
+ @Query(() => String, {
87
+ description: 'Get fresh JWT token for the current session (requires valid session)',
88
+ nullable: true,
89
+ })
90
+ @Roles(RoleEnum.S_USER)
91
+ override async betterAuthToken(@Context() ctx: { req: Request }): Promise<null | string> {
92
+ return super.betterAuthToken(ctx);
93
+ }
94
+
81
95
  @Query(() => [BetterAuthPasskeyModel], {
82
96
  description: 'List passkeys for the current user',
83
97
  nullable: true,
84
98
  })
85
99
  @Roles(RoleEnum.S_USER)
86
- @UseGuards(AuthGuard(AuthGuardStrategy.JWT))
87
100
  override async betterAuthListPasskeys(@Context() ctx: { req: Request }): Promise<BetterAuthPasskeyModel[] | null> {
88
101
  return super.betterAuthListPasskeys(ctx);
89
102
  }
@@ -118,7 +131,6 @@ export class BetterAuthResolver extends CoreBetterAuthResolver {
118
131
 
119
132
  @Mutation(() => Boolean, { description: 'Sign out via Better-Auth' })
120
133
  @Roles(RoleEnum.S_USER)
121
- @UseGuards(AuthGuard(AuthGuardStrategy.JWT))
122
134
  override async betterAuthSignOut(@Context() ctx: { req: Request }): Promise<boolean> {
123
135
  return super.betterAuthSignOut(ctx);
124
136
  }
@@ -142,7 +154,6 @@ export class BetterAuthResolver extends CoreBetterAuthResolver {
142
154
  description: 'Enable 2FA for the current user',
143
155
  })
144
156
  @Roles(RoleEnum.S_USER)
145
- @UseGuards(AuthGuard(AuthGuardStrategy.JWT))
146
157
  override async betterAuthEnable2FA(
147
158
  @Args('password') password: string,
148
159
  @Context() ctx: { req: Request },
@@ -154,7 +165,6 @@ export class BetterAuthResolver extends CoreBetterAuthResolver {
154
165
  description: 'Disable 2FA for the current user',
155
166
  })
156
167
  @Roles(RoleEnum.S_USER)
157
- @UseGuards(AuthGuard(AuthGuardStrategy.JWT))
158
168
  override async betterAuthDisable2FA(
159
169
  @Args('password') password: string,
160
170
  @Context() ctx: { req: Request },
@@ -167,7 +177,6 @@ export class BetterAuthResolver extends CoreBetterAuthResolver {
167
177
  nullable: true,
168
178
  })
169
179
  @Roles(RoleEnum.S_USER)
170
- @UseGuards(AuthGuard(AuthGuardStrategy.JWT))
171
180
  override async betterAuthGenerateBackupCodes(@Context() ctx: { req: Request }): Promise<null | string[]> {
172
181
  return super.betterAuthGenerateBackupCodes(ctx);
173
182
  }
@@ -180,7 +189,6 @@ export class BetterAuthResolver extends CoreBetterAuthResolver {
180
189
  description: 'Get passkey registration challenge for WebAuthn',
181
190
  })
182
191
  @Roles(RoleEnum.S_USER)
183
- @UseGuards(AuthGuard(AuthGuardStrategy.JWT))
184
192
  override async betterAuthGetPasskeyChallenge(
185
193
  @Context() ctx: { req: Request },
186
194
  ): Promise<BetterAuthPasskeyChallengeModel> {
@@ -191,7 +199,6 @@ export class BetterAuthResolver extends CoreBetterAuthResolver {
191
199
  description: 'Delete a passkey by ID',
192
200
  })
193
201
  @Roles(RoleEnum.S_USER)
194
- @UseGuards(AuthGuard(AuthGuardStrategy.JWT))
195
202
  override async betterAuthDeletePasskey(
196
203
  @Args('passkeyId') passkeyId: string,
197
204
  @Context() ctx: { req: Request },
@@ -1,4 +1,4 @@
1
- import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
1
+ import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
2
2
  import {
3
3
  ApiBearerAuth,
4
4
  ApiBody,
@@ -15,8 +15,6 @@ import { CurrentUser } from '../../../core/common/decorators/current-user.decora
15
15
  import { Roles } from '../../../core/common/decorators/roles.decorator';
16
16
  import { RoleEnum } from '../../../core/common/enums/role.enum';
17
17
  import { ServiceOptions } from '../../../core/common/interfaces/service-options.interface';
18
- import { AuthGuardStrategy } from '../../../core/modules/auth/auth-guard-strategy.enum';
19
- import { AuthGuard } from '../../../core/modules/auth/guards/auth.guard';
20
18
  import { UserCreateInput } from './inputs/user-create.input';
21
19
  import { UserInput } from './inputs/user.input';
22
20
  import { FindAndCountUsersResult } from './outputs/find-and-count-users-result.output';
@@ -48,7 +46,6 @@ export class UserController {
48
46
  @ApiOperation({ description: 'Find users (via filter)', summary: 'Get all users' })
49
47
  @Get()
50
48
  @Roles(RoleEnum.ADMIN)
51
- @UseGuards(AuthGuard(AuthGuardStrategy.JWT))
52
49
  async findUsers(@CurrentUser() currentUser: User): Promise<User[]> {
53
50
  const serviceOptions: ServiceOptions = {
54
51
  currentUser,
@@ -67,7 +64,6 @@ export class UserController {
67
64
  })
68
65
  @Get('count')
69
66
  @Roles(RoleEnum.ADMIN)
70
- @UseGuards(AuthGuard(AuthGuardStrategy.JWT))
71
67
  async findAndCountUsers(@CurrentUser() currentUser: User): Promise<FindAndCountUsersResult> {
72
68
  const serviceOptions: ServiceOptions = {
73
69
  currentUser,
@@ -99,7 +95,6 @@ export class UserController {
99
95
  @ApiParam({ description: 'User ID', name: 'id', type: String })
100
96
  @Get(':id')
101
97
  @Roles(RoleEnum.S_USER)
102
- @UseGuards(AuthGuard(AuthGuardStrategy.JWT))
103
98
  async getUser(@CurrentUser() currentUser: User, @Param('id') id: string): Promise<User> {
104
99
  const serviceOptions: ServiceOptions = {
105
100
  currentUser,
@@ -121,7 +116,6 @@ export class UserController {
121
116
  @ApiOperation({ description: 'Create a new user', summary: 'Create user' })
122
117
  @Post()
123
118
  @Roles(RoleEnum.ADMIN)
124
- @UseGuards(AuthGuard(AuthGuardStrategy.JWT))
125
119
  async createUser(@CurrentUser() currentUser: User, @Body() input: UserCreateInput): Promise<User> {
126
120
  const serviceOptions: ServiceOptions = {
127
121
  currentUser,
@@ -208,7 +202,6 @@ export class UserController {
208
202
  @ApiParam({ description: 'User ID', name: 'id', type: String })
209
203
  @Patch(':id')
210
204
  @Roles(RoleEnum.S_USER)
211
- @UseGuards(AuthGuard(AuthGuardStrategy.JWT))
212
205
  async updateUser(@CurrentUser() currentUser: User, @Param('id') id: string, @Body() input: UserInput): Promise<User> {
213
206
  const serviceOptions: ServiceOptions = {
214
207
  currentUser,
@@ -231,7 +224,6 @@ export class UserController {
231
224
  @ApiParam({ description: 'User ID', name: 'id', type: String })
232
225
  @Delete(':id')
233
226
  @Roles(RoleEnum.S_USER)
234
- @UseGuards(AuthGuard(AuthGuardStrategy.JWT))
235
227
  async deleteUser(@CurrentUser() currentUser: User, @Param('id') id: string): Promise<User> {
236
228
  const serviceOptions: ServiceOptions = {
237
229
  currentUser,
@@ -1,4 +1,4 @@
1
- import { Inject, Injectable, UnauthorizedException, UnprocessableEntityException } from '@nestjs/common';
1
+ import { Inject, Injectable, Optional, UnauthorizedException, UnprocessableEntityException } from '@nestjs/common';
2
2
  import { InjectModel } from '@nestjs/mongoose';
3
3
  import fs = require('fs');
4
4
  import { PubSub } from 'graphql-subscriptions';
@@ -9,6 +9,7 @@ import { ServiceOptions } from '../../../core/common/interfaces/service-options.
9
9
  import { ConfigService } from '../../../core/common/services/config.service';
10
10
  import { EmailService } from '../../../core/common/services/email.service';
11
11
  import { CoreModelConstructor } from '../../../core/common/types/core-model-constructor.type';
12
+ import { BetterAuthUserMapper } from '../../../core/modules/better-auth/better-auth-user.mapper';
12
13
  import { CoreUserService } from '../../../core/modules/user/core-user.service';
13
14
  import { UserCreateInput } from './inputs/user-create.input';
14
15
  import { UserInput } from './inputs/user.input';
@@ -32,8 +33,9 @@ export class UserService extends CoreUserService<User, UserInput, UserCreateInpu
32
33
  @Inject('USER_CLASS') protected override readonly mainModelConstructor: CoreModelConstructor<User>,
33
34
  @InjectModel('User') protected override readonly mainDbModel: Model<UserDocument>,
34
35
  @Inject('PUB_SUB') protected readonly pubSub: PubSub,
36
+ @Optional() private readonly betterAuthUserMapper?: BetterAuthUserMapper,
35
37
  ) {
36
- super(configService, emailService, mainDbModel, mainModelConstructor);
38
+ super(configService, emailService, mainDbModel, mainModelConstructor, { betterAuthUserMapper });
37
39
  }
38
40
 
39
41
  // ===================================================================================================================