@lenne.tech/nest-server 11.6.1 → 11.7.0

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 +132 -0
  2. package/dist/config.env.js.map +1 -1
  3. package/dist/core/common/decorators/graphql-populate.decorator.d.ts +2 -2
  4. package/dist/core/common/decorators/restricted.decorator.d.ts +1 -0
  5. package/dist/core/common/decorators/restricted.decorator.js +1 -1
  6. package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
  7. package/dist/core/common/helpers/filter.helper.d.ts +9 -9
  8. package/dist/core/common/helpers/filter.helper.js +2 -4
  9. package/dist/core/common/helpers/filter.helper.js.map +1 -1
  10. package/dist/core/common/helpers/gridfs.helper.js +3 -3
  11. package/dist/core/common/helpers/gridfs.helper.js.map +1 -1
  12. package/dist/core/common/helpers/input.helper.d.ts +1 -0
  13. package/dist/core/common/helpers/input.helper.js +1 -1
  14. package/dist/core/common/helpers/input.helper.js.map +1 -1
  15. package/dist/core/common/interfaces/server-options.interface.d.ts +51 -0
  16. package/dist/core/common/services/crud.service.d.ts +16 -16
  17. package/dist/core/common/services/crud.service.js +1 -1
  18. package/dist/core/common/services/crud.service.js.map +1 -1
  19. package/dist/core/modules/auth/auth-guard-strategy.enum.d.ts +1 -0
  20. package/dist/core/modules/auth/auth-guard-strategy.enum.js +1 -0
  21. package/dist/core/modules/auth/auth-guard-strategy.enum.js.map +1 -1
  22. package/dist/core/modules/auth/guards/auth.guard.js +11 -5
  23. package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
  24. package/dist/core/modules/auth/tokens.decorator.d.ts +1 -1
  25. package/dist/core/modules/better-auth/better-auth-auth.model.d.ts +9 -0
  26. package/dist/core/modules/better-auth/better-auth-auth.model.js +63 -0
  27. package/dist/core/modules/better-auth/better-auth-auth.model.js.map +1 -0
  28. package/dist/core/modules/better-auth/better-auth-models.d.ts +43 -0
  29. package/dist/core/modules/better-auth/better-auth-models.js +181 -0
  30. package/dist/core/modules/better-auth/better-auth-models.js.map +1 -0
  31. package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.d.ts +12 -0
  32. package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.js +70 -0
  33. package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.js.map +1 -0
  34. package/dist/core/modules/better-auth/better-auth-rate-limiter.service.d.ts +32 -0
  35. package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js +173 -0
  36. package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js.map +1 -0
  37. package/dist/core/modules/better-auth/better-auth-user.mapper.d.ts +43 -0
  38. package/dist/core/modules/better-auth/better-auth-user.mapper.js +159 -0
  39. package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +1 -0
  40. package/dist/core/modules/better-auth/better-auth.config.d.ts +9 -0
  41. package/dist/core/modules/better-auth/better-auth.config.js +254 -0
  42. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -0
  43. package/dist/core/modules/better-auth/better-auth.middleware.d.ts +20 -0
  44. package/dist/core/modules/better-auth/better-auth.middleware.js +79 -0
  45. package/dist/core/modules/better-auth/better-auth.middleware.js.map +1 -0
  46. package/dist/core/modules/better-auth/better-auth.module.d.ts +38 -0
  47. package/dist/core/modules/better-auth/better-auth.module.js +253 -0
  48. package/dist/core/modules/better-auth/better-auth.module.js.map +1 -0
  49. package/dist/core/modules/better-auth/better-auth.resolver.d.ts +45 -0
  50. package/dist/core/modules/better-auth/better-auth.resolver.js +221 -0
  51. package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -0
  52. package/dist/core/modules/better-auth/better-auth.service.d.ts +37 -0
  53. package/dist/core/modules/better-auth/better-auth.service.js +148 -0
  54. package/dist/core/modules/better-auth/better-auth.service.js.map +1 -0
  55. package/dist/core/modules/better-auth/better-auth.types.d.ts +39 -0
  56. package/dist/core/modules/better-auth/better-auth.types.js +26 -0
  57. package/dist/core/modules/better-auth/better-auth.types.js.map +1 -0
  58. package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +66 -0
  59. package/dist/core/modules/better-auth/core-better-auth.controller.js +491 -0
  60. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -0
  61. package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +59 -0
  62. package/dist/core/modules/better-auth/core-better-auth.resolver.js +538 -0
  63. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -0
  64. package/dist/core/modules/better-auth/index.d.ts +13 -0
  65. package/dist/core/modules/better-auth/index.js +30 -0
  66. package/dist/core/modules/better-auth/index.js.map +1 -0
  67. package/dist/core/modules/user/core-user.model.d.ts +2 -0
  68. package/dist/core/modules/user/core-user.model.js +21 -0
  69. package/dist/core/modules/user/core-user.model.js.map +1 -1
  70. package/dist/core.module.js +7 -0
  71. package/dist/core.module.js.map +1 -1
  72. package/dist/index.d.ts +1 -0
  73. package/dist/index.js +1 -0
  74. package/dist/index.js.map +1 -1
  75. package/dist/server/modules/better-auth/better-auth.controller.d.ts +10 -0
  76. package/dist/server/modules/better-auth/better-auth.controller.js +36 -0
  77. package/dist/server/modules/better-auth/better-auth.controller.js.map +1 -0
  78. package/dist/server/modules/better-auth/better-auth.module.d.ts +9 -0
  79. package/dist/server/modules/better-auth/better-auth.module.js +44 -0
  80. package/dist/server/modules/better-auth/better-auth.module.js.map +1 -0
  81. package/dist/server/modules/better-auth/better-auth.resolver.d.ts +45 -0
  82. package/dist/server/modules/better-auth/better-auth.resolver.js +221 -0
  83. package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -0
  84. package/dist/server/modules/file/file-info.model.d.ts +71 -3
  85. package/dist/server/modules/user/user.model.d.ts +169 -3
  86. package/dist/server/server.module.js +6 -1
  87. package/dist/server/server.module.js.map +1 -1
  88. package/dist/tsconfig.build.tsbuildinfo +1 -1
  89. package/package.json +21 -22
  90. package/src/config.env.ts +139 -1
  91. package/src/core/common/decorators/restricted.decorator.ts +2 -2
  92. package/src/core/common/helpers/filter.helper.ts +15 -17
  93. package/src/core/common/helpers/gridfs.helper.ts +5 -5
  94. package/src/core/common/helpers/input.helper.ts +2 -2
  95. package/src/core/common/interfaces/server-options.interface.ts +377 -20
  96. package/src/core/common/services/crud.service.ts +22 -22
  97. package/src/core/modules/auth/auth-guard-strategy.enum.ts +1 -0
  98. package/src/core/modules/auth/guards/auth.guard.ts +20 -6
  99. package/src/core/modules/better-auth/README.md +1422 -0
  100. package/src/core/modules/better-auth/better-auth-auth.model.ts +69 -0
  101. package/src/core/modules/better-auth/better-auth-models.ts +140 -0
  102. package/src/core/modules/better-auth/better-auth-rate-limit.middleware.ts +113 -0
  103. package/src/core/modules/better-auth/better-auth-rate-limiter.service.ts +326 -0
  104. package/src/core/modules/better-auth/better-auth-user.mapper.ts +269 -0
  105. package/src/core/modules/better-auth/better-auth.config.ts +488 -0
  106. package/src/core/modules/better-auth/better-auth.middleware.ts +111 -0
  107. package/src/core/modules/better-auth/better-auth.module.ts +474 -0
  108. package/src/core/modules/better-auth/better-auth.resolver.ts +213 -0
  109. package/src/core/modules/better-auth/better-auth.service.ts +314 -0
  110. package/src/core/modules/better-auth/better-auth.types.ts +90 -0
  111. package/src/core/modules/better-auth/core-better-auth.controller.ts +605 -0
  112. package/src/core/modules/better-auth/core-better-auth.resolver.ts +705 -0
  113. package/src/core/modules/better-auth/index.ts +32 -0
  114. package/src/core/modules/user/core-user.model.ts +29 -0
  115. package/src/core.module.ts +13 -0
  116. package/src/index.ts +6 -0
  117. package/src/server/modules/better-auth/better-auth.controller.ts +41 -0
  118. package/src/server/modules/better-auth/better-auth.module.ts +88 -0
  119. package/src/server/modules/better-auth/better-auth.resolver.ts +201 -0
  120. package/src/server/server.module.ts +10 -1
@@ -0,0 +1,474 @@
1
+ import {
2
+ DynamicModule,
3
+ Global,
4
+ Logger,
5
+ MiddlewareConsumer,
6
+ Module,
7
+ NestModule,
8
+ OnModuleInit,
9
+ Optional,
10
+ Type,
11
+ } from '@nestjs/common';
12
+ import { getConnectionToken } from '@nestjs/mongoose';
13
+ import mongoose, { Connection } from 'mongoose';
14
+
15
+ import { IBetterAuth } from '../../common/interfaces/server-options.interface';
16
+ import { ConfigService } from '../../common/services/config.service';
17
+ import { BetterAuthRateLimitMiddleware } from './better-auth-rate-limit.middleware';
18
+ import { BetterAuthRateLimiter } from './better-auth-rate-limiter.service';
19
+ import { BetterAuthUserMapper } from './better-auth-user.mapper';
20
+ import { BetterAuthInstance, createBetterAuthInstance } from './better-auth.config';
21
+ import { BetterAuthMiddleware } from './better-auth.middleware';
22
+ import { BetterAuthResolver } from './better-auth.resolver';
23
+ import { BetterAuthService } from './better-auth.service';
24
+ import { CoreBetterAuthController } from './core-better-auth.controller';
25
+ import { CoreBetterAuthResolver } from './core-better-auth.resolver';
26
+
27
+ /**
28
+ * Token for injecting the better-auth instance
29
+ */
30
+ export const BETTER_AUTH_INSTANCE = 'BETTER_AUTH_INSTANCE';
31
+
32
+ /**
33
+ * Options for BetterAuthModule.forRoot()
34
+ */
35
+ export interface BetterAuthModuleOptions {
36
+ /**
37
+ * Better-auth configuration
38
+ */
39
+ config: IBetterAuth;
40
+
41
+ /**
42
+ * Custom controller class to use instead of the default CoreBetterAuthController.
43
+ * The class must extend CoreBetterAuthController.
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * // Your custom controller
48
+ * @Controller('iam')
49
+ * export class MyBetterAuthController extends CoreBetterAuthController {
50
+ * override async signUp(res: Response, input: BetterAuthSignUpInput) {
51
+ * const result = await super.signUp(res, input);
52
+ * await this.emailService.sendWelcomeEmail(result.user?.email);
53
+ * return result;
54
+ * }
55
+ * }
56
+ *
57
+ * // In your module
58
+ * BetterAuthModule.forRoot({
59
+ * config: environment.betterAuth,
60
+ * controller: MyBetterAuthController,
61
+ * })
62
+ * ```
63
+ */
64
+ controller?: Type<CoreBetterAuthController>;
65
+
66
+ /**
67
+ * Fallback secrets to try if no betterAuth.secret is configured.
68
+ * The array is iterated and the first valid secret (≥32 chars) is used.
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * fallbackSecrets: [config.jwt?.secret, config.jwt?.refresh?.secret]
73
+ * ```
74
+ */
75
+ fallbackSecrets?: (string | undefined)[];
76
+
77
+ /**
78
+ * Custom resolver class to use instead of the default BetterAuthResolver.
79
+ * The class must extend CoreBetterAuthResolver.
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * // Your custom resolver
84
+ * @Resolver(() => BetterAuthAuthModel)
85
+ * export class MyBetterAuthResolver extends CoreBetterAuthResolver {
86
+ * override async betterAuthSignUp(...) {
87
+ * const result = await super.betterAuthSignUp(...);
88
+ * await this.sendWelcomeEmail(result.user);
89
+ * return result;
90
+ * }
91
+ * }
92
+ *
93
+ * // In your module
94
+ * BetterAuthModule.forRoot({
95
+ * config: environment.betterAuth,
96
+ * resolver: MyBetterAuthResolver,
97
+ * })
98
+ * ```
99
+ */
100
+ resolver?: Type<CoreBetterAuthResolver>;
101
+ }
102
+
103
+ /**
104
+ * BetterAuthModule provides integration with the better-auth authentication framework.
105
+ *
106
+ * This module:
107
+ * - Creates and configures a better-auth instance based on server configuration
108
+ * - Provides REST controller (CoreBetterAuthController) and GraphQL resolver (CoreBetterAuthResolver)
109
+ * - Supports JWT, 2FA, Passkey, and Social Login based on configuration
110
+ * - Enabled by default (zero-config) - set `enabled: false` to disable explicitly
111
+ * - Uses the global mongoose connection for MongoDB access
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * // In your AppModule - import after CoreModule so mongoose is connected
116
+ * @Module({
117
+ * imports: [
118
+ * CoreModule.forRoot(...),
119
+ * BetterAuthModule.forRoot({ config: environment.betterAuth }),
120
+ * ],
121
+ * })
122
+ * export class AppModule {}
123
+ * ```
124
+ */
125
+ @Global()
126
+ @Module({})
127
+ export class BetterAuthModule implements NestModule, OnModuleInit {
128
+ private static logger = new Logger(BetterAuthModule.name);
129
+ private static authInstance: BetterAuthInstance | null = null;
130
+ private static initialized = false;
131
+ private static initLogged = false;
132
+ private static betterAuthEnabled = false;
133
+ private static currentConfig: IBetterAuth | null = null;
134
+ private static customController: null | Type<CoreBetterAuthController> = null;
135
+ private static customResolver: null | Type<CoreBetterAuthResolver> = null;
136
+
137
+ /**
138
+ * Gets the controller class to use (custom or default)
139
+ */
140
+ private static getControllerClass(): Type<CoreBetterAuthController> {
141
+ return this.customController || CoreBetterAuthController;
142
+ }
143
+
144
+ /**
145
+ * Gets the resolver class to use (custom or default)
146
+ */
147
+ private static getResolverClass(): Type<CoreBetterAuthResolver> {
148
+ return this.customResolver || BetterAuthResolver;
149
+ }
150
+
151
+ constructor(
152
+ @Optional() private readonly betterAuthService?: BetterAuthService,
153
+ @Optional() private readonly rateLimiter?: BetterAuthRateLimiter,
154
+ ) {}
155
+
156
+ onModuleInit() {
157
+ if (BetterAuthModule.authInstance && !BetterAuthModule.initialized) {
158
+ BetterAuthModule.initialized = true;
159
+ BetterAuthModule.logger.log('BetterAuthModule ready');
160
+ }
161
+
162
+ // Configure rate limiter with stored config
163
+ if (this.rateLimiter && BetterAuthModule.currentConfig?.rateLimit) {
164
+ this.rateLimiter.configure(BetterAuthModule.currentConfig.rateLimit);
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Configure middleware for Better-Auth session handling and rate limiting
170
+ * The session middleware runs on all routes and maps Better-Auth sessions to users
171
+ * The rate limit middleware runs only on Better-Auth endpoints
172
+ */
173
+ configure(consumer: MiddlewareConsumer) {
174
+ // Only apply middleware if Better-Auth is enabled
175
+ if (BetterAuthModule.betterAuthEnabled && this.betterAuthService?.isEnabled()) {
176
+ const basePath = BetterAuthModule.currentConfig?.basePath || '/iam';
177
+
178
+ // Apply rate limiting to Better-Auth endpoints only
179
+ if (BetterAuthModule.currentConfig?.rateLimit?.enabled) {
180
+ consumer.apply(BetterAuthRateLimitMiddleware).forRoutes(`${basePath}/*path`);
181
+ BetterAuthModule.logger.log(`Rate limiting enabled for ${basePath}/*path endpoints`);
182
+ }
183
+
184
+ // Apply session middleware to all routes
185
+ consumer.apply(BetterAuthMiddleware).forRoutes('(.*)'); // New path-to-regexp syntax for wildcard
186
+ BetterAuthModule.logger.log('BetterAuthMiddleware registered for all routes');
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Waits for mongoose connection to be ready using polling
192
+ * This is more reliable than event-based waiting in test environments
193
+ * @throws Error if connection times out or fails
194
+ */
195
+ private static async waitForMongoConnection(): Promise<void> {
196
+ const maxAttempts = 60; // 60 attempts * 500ms = 30 seconds max
197
+ const pollInterval = 500;
198
+
199
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
200
+ // Check if already connected
201
+ if (mongoose.connection.readyState === 1 && mongoose.connection.db) {
202
+ return;
203
+ }
204
+
205
+ // Check for error state
206
+ if (mongoose.connection.readyState === 0) {
207
+ // Disconnected - wait for reconnection
208
+ this.logger.debug(`MongoDB not connected (attempt ${attempt + 1}/${maxAttempts})`);
209
+ }
210
+
211
+ // Wait before next check
212
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
213
+ }
214
+
215
+ throw new Error('MongoDB connection timeout - ensure MongoDB is running and accessible');
216
+ }
217
+
218
+ /**
219
+ * Creates a dynamic module for BetterAuth (synchronous)
220
+ * Note: This requires mongoose connection to be already established.
221
+ * For async initialization, use forRootAsync.
222
+ *
223
+ * @param options - Configuration options
224
+ * @returns Dynamic module configuration
225
+ */
226
+ static forRoot(options: BetterAuthModuleOptions): DynamicModule {
227
+ const { config, controller, fallbackSecrets, resolver } = options;
228
+
229
+ // Store config for middleware configuration
230
+ this.currentConfig = config;
231
+ // Store custom controller if provided
232
+ this.customController = controller || null;
233
+ // Store custom resolver if provided
234
+ this.customResolver = resolver || null;
235
+
236
+ // If better-auth is explicitly disabled, return minimal module
237
+ // Note: We don't provide middleware classes when disabled because they depend on BetterAuthService
238
+ // and won't be used anyway (middleware is only applied when enabled)
239
+ // BetterAuth is enabled by default unless explicitly set to false
240
+ if (config?.enabled === false) {
241
+ this.logger.debug('BetterAuth is explicitly disabled - skipping initialization');
242
+ this.betterAuthEnabled = false;
243
+ return {
244
+ exports: [BETTER_AUTH_INSTANCE, BetterAuthService, BetterAuthUserMapper, BetterAuthRateLimiter],
245
+ module: BetterAuthModule,
246
+ providers: [
247
+ {
248
+ provide: BETTER_AUTH_INSTANCE,
249
+ useValue: null,
250
+ },
251
+ // Note: ConfigService is provided globally by CoreModule
252
+ // Tests need to provide their own ConfigService
253
+ BetterAuthService,
254
+ BetterAuthUserMapper,
255
+ BetterAuthRateLimiter,
256
+ ],
257
+ };
258
+ }
259
+
260
+ // Enable middleware registration
261
+ this.betterAuthEnabled = true;
262
+
263
+ // Note: Secret validation is now handled in createBetterAuthInstance
264
+ // with fallback to jwt.secret, jwt.refresh.secret, or auto-generation
265
+
266
+ // Always use deferred initialization to ensure MongoDB is ready
267
+ // This prevents timing issues during application startup
268
+ return this.createDeferredModule(config, fallbackSecrets);
269
+ }
270
+
271
+ /**
272
+ * Creates an async dynamic module for BetterAuth
273
+ * This is the preferred method as it properly waits for mongoose connection.
274
+ *
275
+ * @returns Dynamic module configuration
276
+ */
277
+ static forRootAsync(): DynamicModule {
278
+ return {
279
+ controllers: [this.getControllerClass()],
280
+ exports: [BETTER_AUTH_INSTANCE, BetterAuthService, BetterAuthUserMapper, BetterAuthRateLimiter],
281
+ imports: [],
282
+ module: BetterAuthModule,
283
+ providers: [
284
+ {
285
+ inject: [ConfigService],
286
+ provide: BETTER_AUTH_INSTANCE,
287
+ useFactory: async (configService: ConfigService) => {
288
+ const config = configService.get<IBetterAuth>('betterAuth');
289
+
290
+ // Store config for middleware configuration
291
+ this.currentConfig = config || null;
292
+
293
+ // BetterAuth is enabled by default unless explicitly set to false
294
+ if (config?.enabled === false) {
295
+ this.logger.debug('BetterAuth is explicitly disabled');
296
+ this.betterAuthEnabled = false;
297
+ return null;
298
+ }
299
+
300
+ // Enable middleware registration
301
+ this.betterAuthEnabled = true;
302
+
303
+ await this.waitForMongoConnection();
304
+
305
+ const db = mongoose.connection.db;
306
+ if (!db) {
307
+ throw new Error('MongoDB database not available');
308
+ }
309
+
310
+ // Get JWT secrets from config for backwards compatibility fallback
311
+ const jwtConfig = configService.get<{ refresh?: { secret?: string }; secret?: string }>('jwt');
312
+ const fallbackSecrets = [jwtConfig?.secret, jwtConfig?.refresh?.secret];
313
+
314
+ // Note: Secret validation is now handled in createBetterAuthInstance
315
+ // with fallback to jwt.secret, jwt.refresh.secret, or auto-generation
316
+ this.authInstance = createBetterAuthInstance({ config, db, fallbackSecrets });
317
+
318
+ if (this.authInstance) {
319
+ this.logger.log('BetterAuth initialized successfully');
320
+ this.logEnabledFeatures(config);
321
+ }
322
+
323
+ return this.authInstance;
324
+ },
325
+ },
326
+ // BetterAuthService needs to be a factory that explicitly depends on BETTER_AUTH_INSTANCE
327
+ // to ensure proper initialization order
328
+ {
329
+ inject: [BETTER_AUTH_INSTANCE, ConfigService],
330
+ provide: BetterAuthService,
331
+ useFactory: (authInstance: BetterAuthInstance | null, configService: ConfigService) => {
332
+ return new BetterAuthService(authInstance, configService);
333
+ },
334
+ },
335
+ BetterAuthUserMapper,
336
+ BetterAuthMiddleware,
337
+ BetterAuthRateLimiter,
338
+ BetterAuthRateLimitMiddleware,
339
+ this.getResolverClass(),
340
+ ],
341
+ };
342
+ }
343
+
344
+ /**
345
+ * Gets the current better-auth instance
346
+ * Useful for accessing the auth API directly
347
+ */
348
+ static getInstance(): BetterAuthInstance | null {
349
+ return this.authInstance;
350
+ }
351
+
352
+ /**
353
+ * Resets the static state of BetterAuthModule
354
+ * This is primarily useful for testing to ensure clean state between tests
355
+ *
356
+ * @example
357
+ * ```typescript
358
+ * afterEach(() => {
359
+ * BetterAuthModule.reset();
360
+ * });
361
+ * ```
362
+ */
363
+ static reset(): void {
364
+ this.authInstance = null;
365
+ this.initialized = false;
366
+ this.initLogged = false;
367
+ this.betterAuthEnabled = false;
368
+ this.currentConfig = null;
369
+ this.customController = null;
370
+ this.customResolver = null;
371
+ }
372
+
373
+ /**
374
+ * Creates a deferred initialization module that waits for mongoose connection
375
+ * By injecting the Connection token, NestJS ensures Mongoose is ready first
376
+ */
377
+ private static createDeferredModule(config: IBetterAuth, fallbackSecrets?: (string | undefined)[]): DynamicModule {
378
+ return {
379
+ controllers: [this.getControllerClass()],
380
+ exports: [BETTER_AUTH_INSTANCE, BetterAuthService, BetterAuthUserMapper, BetterAuthRateLimiter],
381
+ module: BetterAuthModule,
382
+ providers: [
383
+ {
384
+ // Inject Mongoose Connection to ensure NestJS waits for it to be ready
385
+ inject: [getConnectionToken()],
386
+ provide: BETTER_AUTH_INSTANCE,
387
+ useFactory: async (connection: Connection) => {
388
+ // Connection is now guaranteed to be established
389
+ const db = connection.db;
390
+ if (!db) {
391
+ // Fallback to global mongoose if connection.db is not yet available
392
+ await this.waitForMongoConnection();
393
+ const globalDb = mongoose.connection.db;
394
+ if (!globalDb) {
395
+ throw new Error('MongoDB database not available');
396
+ }
397
+ this.authInstance = createBetterAuthInstance({ config, db: globalDb, fallbackSecrets });
398
+ } else {
399
+ this.authInstance = createBetterAuthInstance({ config, db, fallbackSecrets });
400
+ }
401
+
402
+ if (this.authInstance && !this.initLogged) {
403
+ this.initLogged = true;
404
+ this.logger.log('BetterAuth initialized');
405
+ this.logEnabledFeatures(config);
406
+ }
407
+
408
+ return this.authInstance;
409
+ },
410
+ },
411
+ // BetterAuthService needs to be a factory that explicitly depends on BETTER_AUTH_INSTANCE
412
+ // to ensure proper initialization order
413
+ {
414
+ inject: [BETTER_AUTH_INSTANCE, ConfigService],
415
+ provide: BetterAuthService,
416
+ useFactory: (authInstance: BetterAuthInstance | null, configService: ConfigService) => {
417
+ return new BetterAuthService(authInstance, configService);
418
+ },
419
+ },
420
+ BetterAuthUserMapper,
421
+ BetterAuthMiddleware,
422
+ BetterAuthRateLimiter,
423
+ BetterAuthRateLimitMiddleware,
424
+ this.getResolverClass(),
425
+ ],
426
+ };
427
+ }
428
+
429
+ /**
430
+ * Logs which features are enabled.
431
+ * Features are enabled by default when their config block is present,
432
+ * unless explicitly disabled with enabled: false.
433
+ */
434
+ private static logEnabledFeatures(config: IBetterAuth): void {
435
+ const features: string[] = [];
436
+
437
+ // Plugins are enabled by default when config block is present
438
+ if (config.jwt && config.jwt.enabled !== false) {
439
+ features.push('JWT');
440
+ }
441
+ if (config.twoFactor && config.twoFactor.enabled !== false) {
442
+ features.push('2FA/TOTP');
443
+ }
444
+ if (config.passkey && config.passkey.enabled !== false) {
445
+ features.push('Passkey/WebAuthn');
446
+ }
447
+
448
+ // Dynamically collect enabled social providers
449
+ // Providers are enabled by default if they have credentials configured
450
+ // Only disabled when explicitly set to enabled: false
451
+ const socialProviders: string[] = [];
452
+ if (config.socialProviders) {
453
+ for (const [name, provider] of Object.entries(config.socialProviders)) {
454
+ if (provider?.clientId && provider?.clientSecret && provider?.enabled !== false) {
455
+ // Capitalize first letter for display
456
+ socialProviders.push(name.charAt(0).toUpperCase() + name.slice(1));
457
+ }
458
+ }
459
+ }
460
+
461
+ if (socialProviders.length > 0) {
462
+ features.push(`Social Login (${socialProviders.join(', ')})`);
463
+ }
464
+
465
+ // Rate limiting still requires explicit enabled: true
466
+ if (config.rateLimit?.enabled) {
467
+ features.push(`Rate Limiting (${config.rateLimit.max || 10}/${config.rateLimit.windowSeconds || 60}s)`);
468
+ }
469
+
470
+ if (features.length > 0) {
471
+ this.logger.log(`Enabled features: ${features.join(', ')}`);
472
+ }
473
+ }
474
+ }
@@ -0,0 +1,213 @@
1
+ import { UseGuards } from '@nestjs/common';
2
+ import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
3
+ import { Request, Response } from 'express';
4
+
5
+ import { Roles } from '../../common/decorators/roles.decorator';
6
+ import { RoleEnum } from '../../common/enums/role.enum';
7
+ import { AuthGuardStrategy } from '../auth/auth-guard-strategy.enum';
8
+ import { AuthGuard } from '../auth/guards/auth.guard';
9
+ import { BetterAuthAuthModel } from './better-auth-auth.model';
10
+ import {
11
+ BetterAuth2FASetupModel,
12
+ BetterAuthFeaturesModel,
13
+ BetterAuthPasskeyChallengeModel,
14
+ BetterAuthPasskeyModel,
15
+ BetterAuthSessionModel,
16
+ } from './better-auth-models';
17
+ import { BetterAuthUserMapper } from './better-auth-user.mapper';
18
+ import { BetterAuthService } from './better-auth.service';
19
+ import { CoreBetterAuthResolver } from './core-better-auth.resolver';
20
+
21
+ /**
22
+ * Default BetterAuth GraphQL Resolver
23
+ *
24
+ * This resolver extends CoreBetterAuthResolver and provides the default
25
+ * Better-Auth GraphQL operations. It re-declares all methods with decorators
26
+ * because CoreBetterAuthResolver uses `isAbstract: true`, which means its
27
+ * methods are not registered in the GraphQL schema.
28
+ *
29
+ * Override in your project if you need custom behavior (e.g., sending emails after sign-up).
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * // In your project - src/server/modules/better-auth/better-auth.resolver.ts
34
+ * @Resolver(() => BetterAuthAuthModel)
35
+ * export class BetterAuthResolver extends CoreBetterAuthResolver {
36
+ * constructor(
37
+ * betterAuthService: BetterAuthService,
38
+ * userMapper: BetterAuthUserMapper,
39
+ * private readonly emailService: EmailService,
40
+ * ) {
41
+ * super(betterAuthService, userMapper);
42
+ * }
43
+ *
44
+ * override async betterAuthSignUp(email: string, password: string, name?: string) {
45
+ * const result = await super.betterAuthSignUp(email, password, name);
46
+ *
47
+ * // Send welcome email after successful sign-up
48
+ * if (result.success && result.user) {
49
+ * await this.emailService.sendWelcomeEmail(result.user.email);
50
+ * }
51
+ *
52
+ * return result;
53
+ * }
54
+ * }
55
+ * ```
56
+ */
57
+ @Resolver(() => BetterAuthAuthModel)
58
+ @Roles(RoleEnum.ADMIN)
59
+ export class BetterAuthResolver extends CoreBetterAuthResolver {
60
+ constructor(
61
+ protected override readonly betterAuthService: BetterAuthService,
62
+ protected override readonly userMapper: BetterAuthUserMapper,
63
+ ) {
64
+ super(betterAuthService, userMapper);
65
+ }
66
+
67
+ // ===========================================================================
68
+ // Queries
69
+ // ===========================================================================
70
+
71
+ @Query(() => BetterAuthSessionModel, {
72
+ description: 'Get current Better-Auth session',
73
+ nullable: true,
74
+ })
75
+ @Roles(RoleEnum.S_USER)
76
+ @UseGuards(AuthGuard(AuthGuardStrategy.JWT))
77
+ override async betterAuthSession(@Context() ctx: { req: Request }): Promise<BetterAuthSessionModel | null> {
78
+ return super.betterAuthSession(ctx);
79
+ }
80
+
81
+ @Query(() => Boolean, { description: 'Check if Better-Auth is enabled' })
82
+ @Roles(RoleEnum.S_EVERYONE)
83
+ override betterAuthEnabled(): boolean {
84
+ return super.betterAuthEnabled();
85
+ }
86
+
87
+ @Query(() => BetterAuthFeaturesModel, { description: 'Get enabled Better-Auth features' })
88
+ @Roles(RoleEnum.S_EVERYONE)
89
+ override betterAuthFeatures(): BetterAuthFeaturesModel {
90
+ return super.betterAuthFeatures();
91
+ }
92
+
93
+ @Query(() => [BetterAuthPasskeyModel], {
94
+ description: 'List passkeys for the current user',
95
+ nullable: true,
96
+ })
97
+ @Roles(RoleEnum.S_USER)
98
+ @UseGuards(AuthGuard(AuthGuardStrategy.JWT))
99
+ override async betterAuthListPasskeys(@Context() ctx: { req: Request }): Promise<BetterAuthPasskeyModel[] | null> {
100
+ return super.betterAuthListPasskeys(ctx);
101
+ }
102
+
103
+ // ===========================================================================
104
+ // Authentication Mutations
105
+ // ===========================================================================
106
+
107
+ @Mutation(() => BetterAuthAuthModel, {
108
+ description: 'Sign in via Better-Auth (email/password)',
109
+ })
110
+ @Roles(RoleEnum.S_EVERYONE)
111
+ override async betterAuthSignIn(
112
+ @Args('email') email: string,
113
+ @Args('password') password: string,
114
+ @Context() ctx: { req: Request; res: Response },
115
+ ): Promise<BetterAuthAuthModel> {
116
+ return super.betterAuthSignIn(email, password, ctx);
117
+ }
118
+
119
+ @Mutation(() => BetterAuthAuthModel, {
120
+ description: 'Sign up via Better-Auth (email/password)',
121
+ })
122
+ @Roles(RoleEnum.S_EVERYONE)
123
+ override async betterAuthSignUp(
124
+ @Args('email') email: string,
125
+ @Args('password') password: string,
126
+ @Args('name', { nullable: true }) name?: string,
127
+ ): Promise<BetterAuthAuthModel> {
128
+ return super.betterAuthSignUp(email, password, name);
129
+ }
130
+
131
+ @Mutation(() => Boolean, { description: 'Sign out via Better-Auth' })
132
+ @Roles(RoleEnum.S_USER)
133
+ @UseGuards(AuthGuard(AuthGuardStrategy.JWT))
134
+ override async betterAuthSignOut(@Context() ctx: { req: Request }): Promise<boolean> {
135
+ return super.betterAuthSignOut(ctx);
136
+ }
137
+
138
+ // ===========================================================================
139
+ // 2FA Mutations
140
+ // ===========================================================================
141
+
142
+ @Mutation(() => BetterAuthAuthModel, {
143
+ description: 'Verify 2FA code during sign-in',
144
+ })
145
+ @Roles(RoleEnum.S_EVERYONE)
146
+ override async betterAuthVerify2FA(
147
+ @Args('code') code: string,
148
+ @Context() ctx: { req: Request },
149
+ ): Promise<BetterAuthAuthModel> {
150
+ return super.betterAuthVerify2FA(code, ctx);
151
+ }
152
+
153
+ @Mutation(() => BetterAuth2FASetupModel, {
154
+ description: 'Enable 2FA for the current user',
155
+ })
156
+ @Roles(RoleEnum.S_USER)
157
+ @UseGuards(AuthGuard(AuthGuardStrategy.JWT))
158
+ override async betterAuthEnable2FA(
159
+ @Args('password') password: string,
160
+ @Context() ctx: { req: Request },
161
+ ): Promise<BetterAuth2FASetupModel> {
162
+ return super.betterAuthEnable2FA(password, ctx);
163
+ }
164
+
165
+ @Mutation(() => Boolean, {
166
+ description: 'Disable 2FA for the current user',
167
+ })
168
+ @Roles(RoleEnum.S_USER)
169
+ @UseGuards(AuthGuard(AuthGuardStrategy.JWT))
170
+ override async betterAuthDisable2FA(
171
+ @Args('password') password: string,
172
+ @Context() ctx: { req: Request },
173
+ ): Promise<boolean> {
174
+ return super.betterAuthDisable2FA(password, ctx);
175
+ }
176
+
177
+ @Mutation(() => [String], {
178
+ description: 'Generate new backup codes for 2FA',
179
+ nullable: true,
180
+ })
181
+ @Roles(RoleEnum.S_USER)
182
+ @UseGuards(AuthGuard(AuthGuardStrategy.JWT))
183
+ override async betterAuthGenerateBackupCodes(@Context() ctx: { req: Request }): Promise<null | string[]> {
184
+ return super.betterAuthGenerateBackupCodes(ctx);
185
+ }
186
+
187
+ // ===========================================================================
188
+ // Passkey Mutations
189
+ // ===========================================================================
190
+
191
+ @Mutation(() => BetterAuthPasskeyChallengeModel, {
192
+ description: 'Get passkey registration challenge for WebAuthn',
193
+ })
194
+ @Roles(RoleEnum.S_USER)
195
+ @UseGuards(AuthGuard(AuthGuardStrategy.JWT))
196
+ override async betterAuthGetPasskeyChallenge(
197
+ @Context() ctx: { req: Request },
198
+ ): Promise<BetterAuthPasskeyChallengeModel> {
199
+ return super.betterAuthGetPasskeyChallenge(ctx);
200
+ }
201
+
202
+ @Mutation(() => Boolean, {
203
+ description: 'Delete a passkey by ID',
204
+ })
205
+ @Roles(RoleEnum.S_USER)
206
+ @UseGuards(AuthGuard(AuthGuardStrategy.JWT))
207
+ override async betterAuthDeletePasskey(
208
+ @Args('passkeyId') passkeyId: string,
209
+ @Context() ctx: { req: Request },
210
+ ): Promise<boolean> {
211
+ return super.betterAuthDeletePasskey(passkeyId, ctx);
212
+ }
213
+ }