@lenne.tech/nest-server 11.6.0 → 11.6.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 (87) hide show
  1. package/dist/config.env.js +141 -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/input.helper.d.ts +1 -0
  8. package/dist/core/common/helpers/input.helper.js +1 -1
  9. package/dist/core/common/helpers/input.helper.js.map +1 -1
  10. package/dist/core/common/interceptors/check-security.interceptor.js +4 -3
  11. package/dist/core/common/interceptors/check-security.interceptor.js.map +1 -1
  12. package/dist/core/common/interfaces/server-options.interface.d.ts +50 -0
  13. package/dist/core/modules/auth/auth-guard-strategy.enum.d.ts +1 -0
  14. package/dist/core/modules/auth/auth-guard-strategy.enum.js +1 -0
  15. package/dist/core/modules/auth/auth-guard-strategy.enum.js.map +1 -1
  16. package/dist/core/modules/auth/guards/auth.guard.js +11 -5
  17. package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
  18. package/dist/core/modules/auth/tokens.decorator.d.ts +1 -1
  19. package/dist/core/modules/better-auth/better-auth-auth.model.d.ts +9 -0
  20. package/dist/core/modules/better-auth/better-auth-auth.model.js +63 -0
  21. package/dist/core/modules/better-auth/better-auth-auth.model.js.map +1 -0
  22. package/dist/core/modules/better-auth/better-auth-models.d.ts +44 -0
  23. package/dist/core/modules/better-auth/better-auth-models.js +185 -0
  24. package/dist/core/modules/better-auth/better-auth-models.js.map +1 -0
  25. package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.d.ts +12 -0
  26. package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.js +70 -0
  27. package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.js.map +1 -0
  28. package/dist/core/modules/better-auth/better-auth-rate-limiter.service.d.ts +32 -0
  29. package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js +173 -0
  30. package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js.map +1 -0
  31. package/dist/core/modules/better-auth/better-auth-user.mapper.d.ts +43 -0
  32. package/dist/core/modules/better-auth/better-auth-user.mapper.js +159 -0
  33. package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +1 -0
  34. package/dist/core/modules/better-auth/better-auth.config.d.ts +9 -0
  35. package/dist/core/modules/better-auth/better-auth.config.js +251 -0
  36. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -0
  37. package/dist/core/modules/better-auth/better-auth.middleware.d.ts +20 -0
  38. package/dist/core/modules/better-auth/better-auth.middleware.js +79 -0
  39. package/dist/core/modules/better-auth/better-auth.middleware.js.map +1 -0
  40. package/dist/core/modules/better-auth/better-auth.module.d.ts +30 -0
  41. package/dist/core/modules/better-auth/better-auth.module.js +265 -0
  42. package/dist/core/modules/better-auth/better-auth.module.js.map +1 -0
  43. package/dist/core/modules/better-auth/better-auth.resolver.d.ts +49 -0
  44. package/dist/core/modules/better-auth/better-auth.resolver.js +539 -0
  45. package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -0
  46. package/dist/core/modules/better-auth/better-auth.service.d.ts +38 -0
  47. package/dist/core/modules/better-auth/better-auth.service.js +151 -0
  48. package/dist/core/modules/better-auth/better-auth.service.js.map +1 -0
  49. package/dist/core/modules/better-auth/better-auth.types.d.ts +38 -0
  50. package/dist/core/modules/better-auth/better-auth.types.js +15 -0
  51. package/dist/core/modules/better-auth/better-auth.types.js.map +1 -0
  52. package/dist/core/modules/better-auth/index.d.ts +11 -0
  53. package/dist/core/modules/better-auth/index.js +28 -0
  54. package/dist/core/modules/better-auth/index.js.map +1 -0
  55. package/dist/core/modules/user/core-user.model.d.ts +2 -0
  56. package/dist/core/modules/user/core-user.model.js +21 -0
  57. package/dist/core/modules/user/core-user.model.js.map +1 -1
  58. package/dist/core.module.js +7 -0
  59. package/dist/core.module.js.map +1 -1
  60. package/dist/index.d.ts +1 -0
  61. package/dist/index.js +1 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/tsconfig.build.tsbuildinfo +1 -1
  64. package/package.json +9 -1
  65. package/src/config.env.ts +148 -1
  66. package/src/core/common/decorators/restricted.decorator.ts +2 -2
  67. package/src/core/common/helpers/input.helper.ts +2 -2
  68. package/src/core/common/interceptors/check-security.interceptor.ts +6 -5
  69. package/src/core/common/interfaces/server-options.interface.ts +344 -20
  70. package/src/core/modules/auth/auth-guard-strategy.enum.ts +1 -0
  71. package/src/core/modules/auth/guards/auth.guard.ts +20 -6
  72. package/src/core/modules/better-auth/README.md +1096 -0
  73. package/src/core/modules/better-auth/better-auth-auth.model.ts +69 -0
  74. package/src/core/modules/better-auth/better-auth-models.ts +143 -0
  75. package/src/core/modules/better-auth/better-auth-rate-limit.middleware.ts +113 -0
  76. package/src/core/modules/better-auth/better-auth-rate-limiter.service.ts +326 -0
  77. package/src/core/modules/better-auth/better-auth-user.mapper.ts +269 -0
  78. package/src/core/modules/better-auth/better-auth.config.ts +483 -0
  79. package/src/core/modules/better-auth/better-auth.middleware.ts +111 -0
  80. package/src/core/modules/better-auth/better-auth.module.ts +433 -0
  81. package/src/core/modules/better-auth/better-auth.resolver.ts +678 -0
  82. package/src/core/modules/better-auth/better-auth.service.ts +323 -0
  83. package/src/core/modules/better-auth/better-auth.types.ts +75 -0
  84. package/src/core/modules/better-auth/index.ts +25 -0
  85. package/src/core/modules/user/core-user.model.ts +29 -0
  86. package/src/core.module.ts +12 -0
  87. package/src/index.ts +6 -0
@@ -0,0 +1,483 @@
1
+ import { passkey } from '@better-auth/passkey';
2
+ import { Logger } from '@nestjs/common';
3
+ import { betterAuth, BetterAuthPlugin } from 'better-auth';
4
+ import { mongodbAdapter } from 'better-auth/adapters/mongodb';
5
+ import { jwt, twoFactor } from 'better-auth/plugins';
6
+ import * as crypto from 'crypto';
7
+
8
+ import { IBetterAuth } from '../../common/interfaces/server-options.interface';
9
+
10
+ /**
11
+ * Type for better-auth instance with plugins
12
+ */
13
+ export type BetterAuthInstance = ReturnType<typeof betterAuth>;
14
+
15
+ /**
16
+ * Generates a cryptographically secure random secret.
17
+ * Used as fallback when no BETTER_AUTH_SECRET is configured.
18
+ *
19
+ * NOTE: This secret is generated at server startup, meaning:
20
+ * - All existing sessions become invalid on server restart
21
+ * - This is acceptable for development environments
22
+ * - For production, ALWAYS set BETTER_AUTH_SECRET to maintain sessions across restarts
23
+ */
24
+ function generateSecureSecret(): string {
25
+ return crypto.randomBytes(32).toString('base64');
26
+ }
27
+
28
+ /**
29
+ * Cached auto-generated secret for the current server instance.
30
+ * Generated once at module load to ensure consistency within a single run.
31
+ */
32
+ let cachedAutoGeneratedSecret: null | string = null;
33
+
34
+ /**
35
+ * Options for creating a better-auth instance
36
+ */
37
+ export interface CreateBetterAuthOptions {
38
+ /**
39
+ * Better-auth configuration from server options
40
+ */
41
+ config: IBetterAuth;
42
+
43
+ /**
44
+ * MongoDB database instance
45
+ * Note: Uses 'any' type to handle version incompatibilities between
46
+ * mongoose's bundled mongodb types and the project's mongodb package.
47
+ * At runtime, this is a mongodb.Db instance.
48
+ */
49
+ db: any;
50
+
51
+ /**
52
+ * Fallback secrets to try if no betterAuth.secret is configured.
53
+ * The array is iterated and the first valid secret (≥32 chars) is used.
54
+ * If no valid secret is found, an auto-generated secret is used.
55
+ *
56
+ * Typical usage: Pass existing secrets from your config (e.g., jwt.secret)
57
+ * for backwards compatibility.
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * fallbackSecrets: [config.jwt?.secret, config.jwt?.refresh?.secret]
62
+ * ```
63
+ */
64
+ fallbackSecrets?: (string | undefined)[];
65
+ }
66
+
67
+ /**
68
+ * Better-Auth field type definition
69
+ * Matches the DBFieldType from better-auth
70
+ */
71
+ type BetterAuthFieldType = 'boolean' | 'date' | 'json' | 'number' | 'number[]' | 'string' | 'string[]';
72
+
73
+ /**
74
+ * Social provider configuration for better-auth
75
+ */
76
+ interface SocialProviderConfig {
77
+ clientId: string;
78
+ clientSecret: string;
79
+ }
80
+
81
+ /**
82
+ * User field configuration type for Better-Auth
83
+ * Matches the DBFieldAttribute structure from better-auth
84
+ */
85
+ interface UserFieldConfig {
86
+ defaultValue?: unknown;
87
+ fieldName?: string;
88
+ required?: boolean;
89
+ type: BetterAuthFieldType;
90
+ }
91
+
92
+ /**
93
+ * Validation result for configuration
94
+ */
95
+ interface ValidationResult {
96
+ errors: string[];
97
+ valid: boolean;
98
+ warnings: string[];
99
+ }
100
+
101
+ /**
102
+ * Creates a better-auth instance based on configuration
103
+ *
104
+ * @param options - Configuration options including betterAuth config and MongoDB connection
105
+ * @returns Configured better-auth instance or null if not enabled
106
+ * @throws Error if configuration validation fails
107
+ */
108
+ export function createBetterAuthInstance(options: CreateBetterAuthOptions): BetterAuthInstance | null {
109
+ const logger = new Logger('BetterAuthConfig');
110
+ const { config, db, fallbackSecrets } = options;
111
+
112
+ // Return null only if better-auth is explicitly disabled
113
+ // BetterAuth is enabled by default (zero-config)
114
+ if (config?.enabled === false) {
115
+ return null;
116
+ }
117
+
118
+ // Validate configuration (with fallback secrets for backwards compatibility)
119
+ const validation = validateConfig(config, fallbackSecrets);
120
+
121
+ // Log warnings
122
+ for (const warning of validation.warnings) {
123
+ logger.warn(warning);
124
+ }
125
+
126
+ // Throw on validation errors
127
+ if (!validation.valid) {
128
+ throw new Error(`BetterAuth configuration invalid: ${validation.errors.join('; ')}`);
129
+ }
130
+
131
+ // Build configuration components
132
+ const plugins = buildPlugins(config);
133
+ const socialProviders = buildSocialProviders(config);
134
+ const trustedOrigins = buildTrustedOrigins(config);
135
+ const additionalFields = buildUserFields(config);
136
+
137
+ // Build the base Better-Auth configuration
138
+ const betterAuthConfig = {
139
+ basePath: config.basePath || '/iam',
140
+ baseURL: config.baseUrl || 'http://localhost:3000',
141
+ database: mongodbAdapter(db),
142
+ plugins,
143
+ secret: config.secret,
144
+ socialProviders,
145
+ trustedOrigins,
146
+ user: {
147
+ additionalFields,
148
+ modelName: 'users',
149
+ },
150
+ };
151
+
152
+ // Merge with custom options passthrough
153
+ // This allows projects to configure any Better-Auth option not explicitly defined
154
+ const finalConfig = config.options ? { ...betterAuthConfig, ...config.options } : betterAuthConfig;
155
+
156
+ // Create and return the better-auth instance
157
+ // Type assertion needed for maximum flexibility - allows projects to use any Better-Auth option
158
+
159
+ return betterAuth(finalConfig as any);
160
+ }
161
+
162
+ /**
163
+ * Builds the plugins array based on configuration.
164
+ * Merges built-in plugins (jwt, twoFactor, passkey) with custom plugins from config.
165
+ *
166
+ * Plugins are enabled by default when their configuration block is present.
167
+ * Set `enabled: false` to explicitly disable a configured plugin.
168
+ */
169
+ function buildPlugins(config: IBetterAuth): BetterAuthPlugin[] {
170
+ const plugins: BetterAuthPlugin[] = [];
171
+
172
+ // JWT Plugin for API client compatibility
173
+ // Enabled by default when jwt config is present, unless explicitly disabled
174
+ if (config.jwt && config.jwt.enabled !== false) {
175
+ plugins.push(
176
+ jwt({
177
+ jwt: {
178
+ expirationTime: config.jwt.expiresIn || '15m',
179
+ },
180
+ }),
181
+ );
182
+ }
183
+
184
+ // Two-Factor Authentication Plugin
185
+ // Enabled by default when twoFactor config is present, unless explicitly disabled
186
+ if (config.twoFactor && config.twoFactor.enabled !== false) {
187
+ plugins.push(
188
+ twoFactor({
189
+ issuer: config.twoFactor.appName || 'Nest Server',
190
+ }),
191
+ );
192
+ }
193
+
194
+ // Passkey/WebAuthn Plugin
195
+ // Enabled by default when passkey config is present, unless explicitly disabled
196
+ if (config.passkey && config.passkey.enabled !== false) {
197
+ plugins.push(
198
+ passkey({
199
+ origin: config.passkey.origin || 'http://localhost:3000',
200
+ rpID: config.passkey.rpId || 'localhost',
201
+ rpName: config.passkey.rpName || 'Nest Server',
202
+ }),
203
+ );
204
+ }
205
+
206
+ // Merge custom plugins from configuration
207
+ // This allows projects to add any Better-Auth plugin without modifying this package
208
+ if (config.plugins?.length) {
209
+ plugins.push(...(config.plugins as BetterAuthPlugin[]));
210
+ }
211
+
212
+ return plugins;
213
+ }
214
+
215
+ /**
216
+ * Builds the social providers configuration object dynamically.
217
+ * Iterates over all configured providers and includes those that are enabled
218
+ * with valid clientId and clientSecret.
219
+ */
220
+ function buildSocialProviders(config: IBetterAuth): Record<string, SocialProviderConfig> {
221
+ const socialProvidersConfig: Record<string, SocialProviderConfig> = {};
222
+
223
+ // Iterate over all configured social providers dynamically
224
+ // A provider is enabled by default if it has credentials configured
225
+ // It can be explicitly disabled by setting enabled: false
226
+ if (config.socialProviders) {
227
+ for (const [name, provider] of Object.entries(config.socialProviders)) {
228
+ if (provider?.clientId && provider?.clientSecret && provider?.enabled !== false) {
229
+ socialProvidersConfig[name] = {
230
+ clientId: provider.clientId,
231
+ clientSecret: provider.clientSecret,
232
+ };
233
+ }
234
+ }
235
+ }
236
+
237
+ return socialProvidersConfig;
238
+ }
239
+
240
+ /**
241
+ * Builds the trusted origins array
242
+ */
243
+ function buildTrustedOrigins(config: IBetterAuth): string[] {
244
+ if (config.trustedOrigins?.length) {
245
+ return config.trustedOrigins;
246
+ }
247
+ if (config.baseUrl) {
248
+ return [config.baseUrl];
249
+ }
250
+ return ['http://localhost:3000'];
251
+ }
252
+
253
+ /**
254
+ * Builds the user additional fields configuration.
255
+ * Merges core fields (firstName, lastName, etc.) with custom fields from config.
256
+ * Custom fields override core fields if they have the same key.
257
+ */
258
+ function buildUserFields(config: IBetterAuth): Record<string, UserFieldConfig> {
259
+ // Core fields required for nest-server functionality
260
+ const coreFields: Record<string, UserFieldConfig> = {
261
+ firstName: {
262
+ defaultValue: null,
263
+ fieldName: 'firstName',
264
+ type: 'string',
265
+ },
266
+ iamId: {
267
+ defaultValue: null,
268
+ fieldName: 'iamId',
269
+ type: 'string',
270
+ },
271
+ lastName: {
272
+ defaultValue: null,
273
+ fieldName: 'lastName',
274
+ type: 'string',
275
+ },
276
+ roles: {
277
+ defaultValue: [],
278
+ fieldName: 'roles',
279
+ type: 'string[]',
280
+ },
281
+ twoFactorEnabled: {
282
+ defaultValue: false,
283
+ fieldName: 'twoFactorEnabled',
284
+ type: 'boolean',
285
+ },
286
+ verified: {
287
+ defaultValue: false,
288
+ fieldName: 'verified',
289
+ type: 'boolean',
290
+ },
291
+ };
292
+
293
+ // Merge with custom additional fields from configuration
294
+ // Custom fields can override core fields or add new ones
295
+ if (config.additionalUserFields) {
296
+ for (const [key, field] of Object.entries(config.additionalUserFields)) {
297
+ coreFields[key] = {
298
+ defaultValue: field.defaultValue,
299
+ fieldName: field.fieldName || key,
300
+ required: field.required,
301
+ type: field.type,
302
+ };
303
+ }
304
+ }
305
+
306
+ return coreFields;
307
+ }
308
+
309
+ /**
310
+ * Gets or generates the fallback secret for development.
311
+ * The secret is cached to ensure consistency during the server's lifetime.
312
+ */
313
+ function getAutoGeneratedSecret(): string {
314
+ if (!cachedAutoGeneratedSecret) {
315
+ cachedAutoGeneratedSecret = generateSecureSecret();
316
+ }
317
+ return cachedAutoGeneratedSecret;
318
+ }
319
+
320
+ /**
321
+ * Checks if a secret has valid minimum length (32 characters)
322
+ */
323
+ function isValidSecretLength(secret: string): boolean {
324
+ return secret && secret.length >= 32;
325
+ }
326
+
327
+ /**
328
+ * Validates a URL string
329
+ */
330
+ function isValidUrl(url: string): boolean {
331
+ try {
332
+ new URL(url);
333
+ return true;
334
+ } catch {
335
+ return false;
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Validates the better-auth configuration and applies fallback secret if needed.
341
+ * Mutates config.secret if fallback is applied.
342
+ *
343
+ * Secret resolution order:
344
+ * 1. betterAuth.secret (if configured and valid)
345
+ * 2. First valid secret from fallbackSecrets array (≥32 chars)
346
+ * 3. Auto-generated secure secret (with warning)
347
+ *
348
+ * @param config - Better-auth configuration
349
+ * @param fallbackSecrets - Optional array of fallback secrets to try
350
+ */
351
+ function validateConfig(config: IBetterAuth, fallbackSecrets?: (string | undefined)[]): ValidationResult {
352
+ const errors: string[] = [];
353
+ const warnings: string[] = [];
354
+
355
+ // Track secret source for appropriate messaging
356
+ let secretSource: 'auto-generated' | 'explicit' | 'fallback' = 'explicit';
357
+
358
+ // Resolve secret with fallback chain
359
+ if (!config.secret || config.secret.trim() === '') {
360
+ // Try fallback secrets in order
361
+ const validFallback = fallbackSecrets?.find((secret) => secret && isValidSecretLength(secret));
362
+
363
+ if (validFallback) {
364
+ config.secret = validFallback;
365
+ secretSource = 'fallback';
366
+ } else {
367
+ // Last resort: auto-generate
368
+ config.secret = getAutoGeneratedSecret();
369
+ secretSource = 'auto-generated';
370
+ }
371
+ }
372
+
373
+ // Validate the resolved secret
374
+ const secretValidation = validateSecret(config.secret);
375
+ if (!secretValidation.valid) {
376
+ errors.push(secretValidation.message!);
377
+ } else if (secretValidation.message) {
378
+ warnings.push(secretValidation.message);
379
+ }
380
+
381
+ // Log information about secret source
382
+ switch (secretSource) {
383
+ case 'auto-generated':
384
+ warnings.push('⚠️ BETTER_AUTH: No secret configured - using auto-generated secret.');
385
+ warnings.push('⚠️ CONSEQUENCE: All user sessions will be invalidated on server restart!');
386
+ warnings.push(
387
+ '💡 FOR PRODUCTION: Set betterAuth.secret in config or provide a valid fallback secret (min 32 chars).',
388
+ );
389
+ warnings.push("💡 Generate with: node -e \"console.log(require('crypto').randomBytes(32).toString('base64'))\"");
390
+ break;
391
+ case 'fallback':
392
+ warnings.push(
393
+ '💡 BETTER_AUTH: Using fallback secret (backwards compatible). Consider setting betterAuth.secret explicitly.',
394
+ );
395
+ break;
396
+ // 'explicit' - no warning needed, explicitly configured
397
+ }
398
+
399
+ // Validate baseUrl
400
+ if (config.baseUrl && !isValidUrl(config.baseUrl)) {
401
+ errors.push(`Invalid baseUrl format: ${config.baseUrl}`);
402
+ }
403
+
404
+ // Validate trustedOrigins
405
+ if (config.trustedOrigins) {
406
+ for (const origin of config.trustedOrigins) {
407
+ if (!isValidUrl(origin)) {
408
+ errors.push(`Invalid trustedOrigin format: ${origin}`);
409
+ }
410
+ }
411
+ }
412
+
413
+ // Validate passkey origin
414
+ if (config.passkey?.enabled && config.passkey.origin && !isValidUrl(config.passkey.origin)) {
415
+ errors.push(`Invalid passkey origin format: ${config.passkey.origin}`);
416
+ }
417
+
418
+ // Validate social providers dynamically
419
+ // Providers are enabled by default unless explicitly disabled (enabled: false)
420
+ // We only validate credentials for providers that are not explicitly disabled
421
+ if (config.socialProviders) {
422
+ for (const [name, provider] of Object.entries(config.socialProviders)) {
423
+ // Provider is considered active if: configured AND not explicitly disabled
424
+ if (provider && provider.enabled !== false) {
425
+ // Validate that credentials are provided for active providers
426
+ const hasClientId = !!provider.clientId;
427
+ const hasClientSecret = !!provider.clientSecret;
428
+
429
+ if (hasClientId && hasClientSecret) {
430
+ // Both credentials present - provider is fully configured
431
+ continue;
432
+ } else if (hasClientId || hasClientSecret) {
433
+ // Only one credential provided - this is an error
434
+ if (!hasClientId) {
435
+ errors.push(`Social provider '${name}' is missing clientId`);
436
+ }
437
+ if (!hasClientSecret) {
438
+ errors.push(`Social provider '${name}' is missing clientSecret`);
439
+ }
440
+ } else {
441
+ // No credentials provided but provider is configured and not disabled
442
+ // This is likely a configuration mistake - warn the user
443
+ warnings.push(
444
+ `Social provider '${name}' is configured but missing both clientId and clientSecret. ` +
445
+ `Set 'enabled: false' to disable it explicitly, or provide credentials to enable it.`,
446
+ );
447
+ }
448
+ }
449
+ }
450
+ }
451
+
452
+ return {
453
+ errors,
454
+ valid: errors.length === 0,
455
+ warnings,
456
+ };
457
+ }
458
+
459
+ /**
460
+ * Validates the secret strength
461
+ * Requirements: min 32 chars, should contain mixed characters
462
+ */
463
+ function validateSecret(secret: string): { message?: string; valid: boolean } {
464
+ if (!secret || secret.length < 32) {
465
+ return { message: 'Secret must be at least 32 characters long', valid: false };
466
+ }
467
+
468
+ // Check for character diversity (at least 2 of: lowercase, uppercase, numbers, special)
469
+ const hasLowercase = /[a-z]/.test(secret);
470
+ const hasUppercase = /[A-Z]/.test(secret);
471
+ const hasNumbers = /[0-9]/.test(secret);
472
+ const hasSpecial = /[^a-zA-Z0-9]/.test(secret);
473
+ const diversityCount = [hasLowercase, hasUppercase, hasNumbers, hasSpecial].filter(Boolean).length;
474
+
475
+ if (diversityCount < 2) {
476
+ return {
477
+ message: 'Secret should contain at least 2 different character types (lowercase, uppercase, numbers, special)',
478
+ valid: true, // Warning only, not an error
479
+ };
480
+ }
481
+
482
+ return { valid: true };
483
+ }
@@ -0,0 +1,111 @@
1
+ import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
2
+ import { NextFunction, Request, Response } from 'express';
3
+
4
+ import { BetterAuthSessionUser, BetterAuthUserMapper, MappedUser } from './better-auth-user.mapper';
5
+ import { BetterAuthService } from './better-auth.service';
6
+
7
+ /**
8
+ * Extended Express Request with Better-Auth session data
9
+ */
10
+ export interface BetterAuthRequest extends Request {
11
+ betterAuthSession?: {
12
+ session: any;
13
+ user: BetterAuthSessionUser;
14
+ };
15
+ betterAuthUser?: BetterAuthSessionUser;
16
+ user?: MappedUser | Request['user'];
17
+ }
18
+
19
+ /**
20
+ * Middleware that processes Better-Auth sessions and maps users
21
+ *
22
+ * This middleware:
23
+ * 1. Checks if Better-Auth is enabled
24
+ * 2. Validates the session using Better-Auth's API
25
+ * 3. Maps the Better-Auth user to our User model with hasRole() capability
26
+ * 4. Attaches the mapped user to req.user for use with our security decorators
27
+ *
28
+ * IMPORTANT: This middleware runs BEFORE guards, so the user will be available
29
+ * for RolesGuard and other security checks.
30
+ */
31
+ @Injectable()
32
+ export class BetterAuthMiddleware implements NestMiddleware {
33
+ private readonly logger = new Logger(BetterAuthMiddleware.name);
34
+
35
+ constructor(
36
+ private readonly betterAuthService: BetterAuthService,
37
+ private readonly userMapper: BetterAuthUserMapper,
38
+ ) {}
39
+
40
+ async use(req: BetterAuthRequest, _res: Response, next: NextFunction) {
41
+ // Skip if Better-Auth is not enabled
42
+ if (!this.betterAuthService.isEnabled()) {
43
+ return next();
44
+ }
45
+
46
+ // Skip if user is already set (e.g., by JWT auth)
47
+ if (req.user) {
48
+ return next();
49
+ }
50
+
51
+ try {
52
+ // Get session from Better-Auth
53
+ const session = await this.getSession(req);
54
+
55
+ if (session?.user) {
56
+ // Store the original Better-Auth session
57
+ req.betterAuthSession = session;
58
+ req.betterAuthUser = session.user;
59
+
60
+ // Map the Better-Auth user to our User model with hasRole()
61
+ const mappedUser = await this.userMapper.mapSessionUser(session.user);
62
+
63
+ if (mappedUser) {
64
+ // Attach the mapped user to the request
65
+ // This makes it compatible with @CurrentUser() and RolesGuard
66
+ req.user = mappedUser;
67
+ }
68
+ }
69
+ } catch (error) {
70
+ // Don't block the request on auth errors
71
+ // The guards will handle unauthorized access
72
+ this.logger.debug(`Session validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
73
+ }
74
+
75
+ next();
76
+ }
77
+
78
+ /**
79
+ * Gets the session from Better-Auth
80
+ */
81
+ private async getSession(req: Request): Promise<null | { session: any; user: BetterAuthSessionUser }> {
82
+ const api = this.betterAuthService.getApi();
83
+ if (!api) {
84
+ return null;
85
+ }
86
+
87
+ try {
88
+ // Convert Express headers to the format Better-Auth expects
89
+ const headers = new Headers();
90
+ for (const [key, value] of Object.entries(req.headers)) {
91
+ if (typeof value === 'string') {
92
+ headers.set(key, value);
93
+ } else if (Array.isArray(value)) {
94
+ headers.set(key, value.join(', '));
95
+ }
96
+ }
97
+
98
+ // Call Better-Auth's getSession API
99
+ const response = await api.getSession({ headers });
100
+
101
+ if (response && typeof response === 'object' && 'user' in response) {
102
+ return response as { session: any; user: BetterAuthSessionUser };
103
+ }
104
+
105
+ return null;
106
+ } catch (error) {
107
+ this.logger.debug(`getSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
108
+ return null;
109
+ }
110
+ }
111
+ }