@lenne.tech/nest-server 11.10.2 → 11.10.3
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.
- package/dist/config.env.js +16 -133
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +4 -0
- package/dist/core/modules/better-auth/better-auth.config.d.ts +3 -0
- package/dist/core/modules/better-auth/better-auth.config.js +176 -47
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.d.ts +5 -1
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +101 -8
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-challenge.service.d.ts +20 -0
- package/dist/core/modules/better-auth/core-better-auth-challenge.service.js +142 -0
- package/dist/core/modules/better-auth/core-better-auth-challenge.service.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +1 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-web.helper.d.ts +2 -0
- package/dist/core/modules/better-auth/core-better-auth-web.helper.js +29 -1
- package/dist/core/modules/better-auth/core-better-auth-web.helper.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.controller.js +5 -13
- package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.middleware.d.ts +0 -1
- package/dist/core/modules/better-auth/core-better-auth.middleware.js +6 -19
- package/dist/core/modules/better-auth/core-better-auth.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.module.d.ts +4 -1
- package/dist/core/modules/better-auth/core-better-auth.module.js +53 -19
- package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.resolver.js +7 -6
- package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.service.d.ts +0 -2
- package/dist/core/modules/better-auth/core-better-auth.service.js +23 -37
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/core.module.js +3 -0
- package/dist/core.module.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.module.d.ts +4 -1
- package/dist/server/modules/better-auth/better-auth.module.js +4 -1
- package/dist/server/modules/better-auth/better-auth.module.js.map +1 -1
- package/dist/server/server.module.js +1 -4
- package/dist/server/server.module.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/config.env.ts +24 -174
- package/src/core/common/interfaces/server-options.interface.ts +288 -35
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +82 -56
- package/src/core/modules/better-auth/README.md +132 -35
- package/src/core/modules/better-auth/better-auth.config.ts +402 -70
- package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +158 -18
- package/src/core/modules/better-auth/core-better-auth-challenge.service.ts +254 -0
- package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +1 -1
- package/src/core/modules/better-auth/core-better-auth-web.helper.ts +64 -1
- package/src/core/modules/better-auth/core-better-auth.controller.ts +6 -14
- package/src/core/modules/better-auth/core-better-auth.middleware.ts +7 -20
- package/src/core/modules/better-auth/core-better-auth.module.ts +135 -25
- package/src/core/modules/better-auth/core-better-auth.resolver.ts +7 -6
- package/src/core/modules/better-auth/core-better-auth.service.ts +27 -48
- package/src/core.module.ts +5 -0
- package/src/server/modules/better-auth/better-auth.module.ts +40 -10
- package/src/server/server.module.ts +2 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
|
|
2
2
|
import { NextFunction, Request, Response } from 'express';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { maskEmail, maskToken } from '../../common/helpers/logging.helper';
|
|
5
5
|
import { BetterAuthSessionUser, CoreBetterAuthUserMapper, MappedUser } from './core-better-auth-user.mapper';
|
|
6
6
|
import { extractSessionToken } from './core-better-auth-web.helper';
|
|
7
7
|
import { CoreBetterAuthService } from './core-better-auth.service';
|
|
@@ -33,7 +33,6 @@ export interface CoreBetterAuthRequest extends Request {
|
|
|
33
33
|
@Injectable()
|
|
34
34
|
export class CoreBetterAuthMiddleware implements NestMiddleware {
|
|
35
35
|
private readonly logger = new Logger(CoreBetterAuthMiddleware.name);
|
|
36
|
-
private readonly isProd = isProduction();
|
|
37
36
|
|
|
38
37
|
constructor(
|
|
39
38
|
private readonly betterAuthService: CoreBetterAuthService,
|
|
@@ -134,9 +133,7 @@ export class CoreBetterAuthMiddleware implements NestMiddleware {
|
|
|
134
133
|
} catch (error) {
|
|
135
134
|
// Don't block the request on auth errors
|
|
136
135
|
// The guards will handle unauthorized access
|
|
137
|
-
|
|
138
|
-
this.logger.debug(`Session validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
139
|
-
}
|
|
136
|
+
this.logger.debug(`Session validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
140
137
|
}
|
|
141
138
|
|
|
142
139
|
next();
|
|
@@ -176,26 +173,18 @@ export class CoreBetterAuthMiddleware implements NestMiddleware {
|
|
|
176
173
|
const basePath = this.betterAuthService.getBasePath();
|
|
177
174
|
const sessionToken = extractSessionToken(req, basePath);
|
|
178
175
|
|
|
179
|
-
|
|
180
|
-
this.logger.debug(`[MIDDLEWARE] getSession called, token found: ${sessionToken ? 'yes' : 'no'}`);
|
|
181
|
-
}
|
|
176
|
+
this.logger.debug(`[MIDDLEWARE] getSession called, token found: ${sessionToken ? 'yes' : 'no'}`);
|
|
182
177
|
|
|
183
178
|
if (sessionToken) {
|
|
184
|
-
|
|
185
|
-
this.logger.debug(`[MIDDLEWARE] Found session token in cookies: ${maskToken(sessionToken)}`);
|
|
186
|
-
}
|
|
179
|
+
this.logger.debug(`[MIDDLEWARE] Found session token in cookies: ${maskToken(sessionToken)}`);
|
|
187
180
|
|
|
188
181
|
// Use getSessionByToken to validate session directly from database
|
|
189
182
|
const sessionResult = await this.betterAuthService.getSessionByToken(sessionToken);
|
|
190
183
|
|
|
191
|
-
|
|
192
|
-
this.logger.debug(`[MIDDLEWARE] getSessionByToken result: user=${maskEmail(sessionResult?.user?.email)}, session=${!!sessionResult?.session}`);
|
|
193
|
-
}
|
|
184
|
+
this.logger.debug(`[MIDDLEWARE] getSessionByToken result: user=${maskEmail(sessionResult?.user?.email)}, session=${!!sessionResult?.session}`);
|
|
194
185
|
|
|
195
186
|
if (sessionResult?.user && sessionResult?.session) {
|
|
196
|
-
|
|
197
|
-
this.logger.debug(`[MIDDLEWARE] Session validated for user: ${maskEmail(sessionResult.user.email)}`);
|
|
198
|
-
}
|
|
187
|
+
this.logger.debug(`[MIDDLEWARE] Session validated for user: ${maskEmail(sessionResult.user.email)}`);
|
|
199
188
|
return sessionResult as { session: any; user: BetterAuthSessionUser };
|
|
200
189
|
}
|
|
201
190
|
}
|
|
@@ -225,9 +214,7 @@ export class CoreBetterAuthMiddleware implements NestMiddleware {
|
|
|
225
214
|
|
|
226
215
|
return null;
|
|
227
216
|
} catch (error) {
|
|
228
|
-
|
|
229
|
-
this.logger.debug(`getSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
230
|
-
}
|
|
217
|
+
this.logger.debug(`getSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
231
218
|
return null;
|
|
232
219
|
}
|
|
233
220
|
}
|
|
@@ -20,6 +20,7 @@ import { BetterAuthTokenService } from './better-auth-token.service';
|
|
|
20
20
|
import { BetterAuthInstance, createBetterAuthInstance } from './better-auth.config';
|
|
21
21
|
import { DefaultBetterAuthResolver } from './better-auth.resolver';
|
|
22
22
|
import { CoreBetterAuthApiMiddleware } from './core-better-auth-api.middleware';
|
|
23
|
+
import { CoreBetterAuthChallengeService } from './core-better-auth-challenge.service';
|
|
23
24
|
import { CoreBetterAuthRateLimitMiddleware } from './core-better-auth-rate-limit.middleware';
|
|
24
25
|
import { CoreBetterAuthRateLimiter } from './core-better-auth-rate-limiter.service';
|
|
25
26
|
import { CoreBetterAuthUserMapper } from './core-better-auth-user.mapper';
|
|
@@ -38,13 +39,14 @@ export const BETTER_AUTH_INSTANCE = 'BETTER_AUTH_INSTANCE';
|
|
|
38
39
|
*/
|
|
39
40
|
export interface CoreBetterAuthModuleOptions {
|
|
40
41
|
/**
|
|
41
|
-
* Better-auth configuration.
|
|
42
|
+
* Better-auth configuration (optional - auto-read from ConfigService).
|
|
42
43
|
* Accepts:
|
|
43
44
|
* - `true`: Enable with all defaults (including JWT)
|
|
44
45
|
* - `false`: Disable BetterAuth
|
|
45
46
|
* - `{ ... }`: Enable with custom configuration
|
|
47
|
+
* - `undefined`: Auto-read from ConfigService (Zero-Config)
|
|
46
48
|
*/
|
|
47
|
-
config
|
|
49
|
+
config?: boolean | IBetterAuth;
|
|
48
50
|
|
|
49
51
|
/**
|
|
50
52
|
* Custom controller class to use instead of the default CoreBetterAuthController.
|
|
@@ -119,19 +121,66 @@ export interface CoreBetterAuthModuleOptions {
|
|
|
119
121
|
* ```
|
|
120
122
|
*/
|
|
121
123
|
resolver?: Type<CoreBetterAuthResolver>;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Server-level app/frontend URL (from IServerOptions.appUrl).
|
|
127
|
+
* This is the frontend application URL where the browser runs.
|
|
128
|
+
*
|
|
129
|
+
* Used for:
|
|
130
|
+
* - CORS trustedOrigins
|
|
131
|
+
* - Passkey/WebAuthn origin
|
|
132
|
+
*
|
|
133
|
+
* Auto-Detection:
|
|
134
|
+
* - If not set, derived from `serverBaseUrl`:
|
|
135
|
+
* - 'https://api.example.com' → 'https://example.com'
|
|
136
|
+
* - 'https://example.com' → 'https://example.com'
|
|
137
|
+
* - When `serverEnv: 'local'` and not set: defaults to 'http://localhost:3001'
|
|
138
|
+
*
|
|
139
|
+
* @example 'https://example.com'
|
|
140
|
+
*/
|
|
141
|
+
serverAppUrl?: string;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Server-level base URL (from IServerOptions.baseUrl).
|
|
145
|
+
* This is the API server URL.
|
|
146
|
+
*
|
|
147
|
+
* Used for:
|
|
148
|
+
* - Email links (password reset, verification)
|
|
149
|
+
* - OAuth callback URLs
|
|
150
|
+
* - As fallback for betterAuth.baseUrl
|
|
151
|
+
*
|
|
152
|
+
* Auto-Detection:
|
|
153
|
+
* - When `serverEnv: 'local'` and not set: defaults to 'http://localhost:3000'
|
|
154
|
+
*
|
|
155
|
+
* @example 'https://api.example.com'
|
|
156
|
+
*/
|
|
157
|
+
serverBaseUrl?: string;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Server environment (from IServerOptions.env).
|
|
161
|
+
* Used for local environment defaults:
|
|
162
|
+
* - When `env: 'local'` and no URLs are set:
|
|
163
|
+
* - `serverBaseUrl` defaults to 'http://localhost:3000'
|
|
164
|
+
* - `serverAppUrl` defaults to 'http://localhost:3001'
|
|
165
|
+
*/
|
|
166
|
+
serverEnv?: string;
|
|
122
167
|
}
|
|
123
168
|
|
|
124
169
|
/**
|
|
125
170
|
* Normalizes betterAuth config from boolean | IBetterAuth to IBetterAuth | null
|
|
126
171
|
* - `true` → `{}` (enabled with defaults)
|
|
127
172
|
* - `false` → `null` (disabled)
|
|
128
|
-
* - `undefined` → `
|
|
173
|
+
* - `undefined` → `{}` (enabled by default - zero-config)
|
|
174
|
+
* - `{ enabled: false }` → `null` (disabled)
|
|
129
175
|
* - `{ ... }` → `{ ... }` (pass through)
|
|
130
176
|
*/
|
|
131
177
|
function normalizeBetterAuthConfig(config: boolean | IBetterAuth | undefined): IBetterAuth | null {
|
|
132
|
-
|
|
178
|
+
// BetterAuth is enabled by default (zero-config)
|
|
179
|
+
if (config === undefined || config === null) return {};
|
|
133
180
|
if (config === true) return {};
|
|
134
181
|
if (config === false) return null;
|
|
182
|
+
// Check for explicit { enabled: false }
|
|
183
|
+
if (typeof config === 'object' && config.enabled === false) return null;
|
|
135
184
|
return config;
|
|
136
185
|
}
|
|
137
186
|
|
|
@@ -268,10 +317,39 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
268
317
|
* @returns Dynamic module configuration
|
|
269
318
|
*/
|
|
270
319
|
static forRoot(options: CoreBetterAuthModuleOptions): DynamicModule {
|
|
271
|
-
const {
|
|
320
|
+
const {
|
|
321
|
+
config: rawConfig,
|
|
322
|
+
controller,
|
|
323
|
+
fallbackSecrets,
|
|
324
|
+
registerRolesGuardGlobally,
|
|
325
|
+
resolver,
|
|
326
|
+
serverAppUrl,
|
|
327
|
+
serverBaseUrl,
|
|
328
|
+
serverEnv,
|
|
329
|
+
} = options;
|
|
330
|
+
|
|
331
|
+
// Auto-read from global ConfigService if not explicitly provided
|
|
332
|
+
// This allows projects to use BetterAuthModule.forRoot({}) for true Zero-Config
|
|
333
|
+
// as all values are already available from CoreModule.forRoot(envConfig)
|
|
334
|
+
const globalConfig = ConfigService.configFastButReadOnly;
|
|
335
|
+
|
|
336
|
+
// Auto-detect config from ConfigService if not explicitly provided
|
|
337
|
+
const effectiveRawConfig = rawConfig ?? globalConfig?.betterAuth;
|
|
338
|
+
|
|
339
|
+
// Auto-detect fallbackSecrets from ConfigService if not explicitly provided
|
|
340
|
+
const effectiveFallbackSecrets = fallbackSecrets ?? (
|
|
341
|
+
globalConfig?.jwt
|
|
342
|
+
? [globalConfig.jwt.secret, globalConfig.jwt.refresh?.secret].filter(Boolean)
|
|
343
|
+
: undefined
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// Auto-detect server URLs from ConfigService if not explicitly provided
|
|
347
|
+
const effectiveServerAppUrl = serverAppUrl ?? globalConfig?.appUrl;
|
|
348
|
+
const effectiveServerBaseUrl = serverBaseUrl ?? globalConfig?.baseUrl;
|
|
349
|
+
const effectiveServerEnv = serverEnv ?? globalConfig?.env;
|
|
272
350
|
|
|
273
351
|
// Normalize config: true → {}, false/undefined → null
|
|
274
|
-
const config = normalizeBetterAuthConfig(
|
|
352
|
+
const config = normalizeBetterAuthConfig(effectiveRawConfig);
|
|
275
353
|
|
|
276
354
|
// Store config for middleware configuration
|
|
277
355
|
this.currentConfig = config;
|
|
@@ -289,7 +367,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
289
367
|
this.logger.debug('BetterAuth is disabled - skipping initialization');
|
|
290
368
|
this.betterAuthEnabled = false;
|
|
291
369
|
return {
|
|
292
|
-
exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService],
|
|
370
|
+
exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService, CoreBetterAuthChallengeService],
|
|
293
371
|
module: CoreBetterAuthModule,
|
|
294
372
|
providers: [
|
|
295
373
|
{
|
|
@@ -302,6 +380,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
302
380
|
CoreBetterAuthUserMapper,
|
|
303
381
|
CoreBetterAuthRateLimiter,
|
|
304
382
|
BetterAuthTokenService,
|
|
383
|
+
CoreBetterAuthChallengeService,
|
|
305
384
|
],
|
|
306
385
|
};
|
|
307
386
|
}
|
|
@@ -314,7 +393,12 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
314
393
|
|
|
315
394
|
// Always use deferred initialization to ensure MongoDB is ready
|
|
316
395
|
// This prevents timing issues during application startup
|
|
317
|
-
|
|
396
|
+
// Pass server-level URLs for Passkey auto-detection (using effective values from ConfigService fallback)
|
|
397
|
+
return this.createDeferredModule(config, effectiveFallbackSecrets, {
|
|
398
|
+
serverAppUrl: effectiveServerAppUrl,
|
|
399
|
+
serverBaseUrl: effectiveServerBaseUrl,
|
|
400
|
+
serverEnv: effectiveServerEnv,
|
|
401
|
+
});
|
|
318
402
|
}
|
|
319
403
|
|
|
320
404
|
/**
|
|
@@ -326,7 +410,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
326
410
|
static forRootAsync(): DynamicModule {
|
|
327
411
|
return {
|
|
328
412
|
controllers: [this.getControllerClass()],
|
|
329
|
-
exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService],
|
|
413
|
+
exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService, CoreBetterAuthChallengeService],
|
|
330
414
|
imports: [],
|
|
331
415
|
module: CoreBetterAuthModule,
|
|
332
416
|
providers: [
|
|
@@ -408,6 +492,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
408
492
|
return new BetterAuthTokenService(betterAuthService, connection);
|
|
409
493
|
},
|
|
410
494
|
},
|
|
495
|
+
CoreBetterAuthChallengeService,
|
|
411
496
|
this.getResolverClass(),
|
|
412
497
|
],
|
|
413
498
|
};
|
|
@@ -446,11 +531,19 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
446
531
|
/**
|
|
447
532
|
* Creates a deferred initialization module that waits for mongoose connection
|
|
448
533
|
* By injecting the Connection token, NestJS ensures Mongoose is ready first
|
|
534
|
+
*
|
|
535
|
+
* @param config - BetterAuth configuration
|
|
536
|
+
* @param fallbackSecrets - Fallback secrets for backwards compatibility
|
|
537
|
+
* @param serverUrls - Server-level URLs for Passkey auto-detection
|
|
449
538
|
*/
|
|
450
|
-
private static createDeferredModule(
|
|
539
|
+
private static createDeferredModule(
|
|
540
|
+
config: IBetterAuth,
|
|
541
|
+
fallbackSecrets?: (string | undefined)[],
|
|
542
|
+
serverUrls?: { serverAppUrl?: string; serverBaseUrl?: string; serverEnv?: string },
|
|
543
|
+
): DynamicModule {
|
|
451
544
|
return {
|
|
452
545
|
controllers: [this.getControllerClass()],
|
|
453
|
-
exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService],
|
|
546
|
+
exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService, CoreBetterAuthChallengeService],
|
|
454
547
|
module: CoreBetterAuthModule,
|
|
455
548
|
providers: [
|
|
456
549
|
{
|
|
@@ -467,9 +560,23 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
467
560
|
if (!globalDb) {
|
|
468
561
|
throw new Error('MongoDB database not available');
|
|
469
562
|
}
|
|
470
|
-
this.authInstance = createBetterAuthInstance({
|
|
563
|
+
this.authInstance = createBetterAuthInstance({
|
|
564
|
+
config,
|
|
565
|
+
db: globalDb,
|
|
566
|
+
fallbackSecrets,
|
|
567
|
+
serverAppUrl: serverUrls?.serverAppUrl,
|
|
568
|
+
serverBaseUrl: serverUrls?.serverBaseUrl,
|
|
569
|
+
serverEnv: serverUrls?.serverEnv,
|
|
570
|
+
});
|
|
471
571
|
} else {
|
|
472
|
-
this.authInstance = createBetterAuthInstance({
|
|
572
|
+
this.authInstance = createBetterAuthInstance({
|
|
573
|
+
config,
|
|
574
|
+
db,
|
|
575
|
+
fallbackSecrets,
|
|
576
|
+
serverAppUrl: serverUrls?.serverAppUrl,
|
|
577
|
+
serverBaseUrl: serverUrls?.serverBaseUrl,
|
|
578
|
+
serverEnv: serverUrls?.serverEnv,
|
|
579
|
+
});
|
|
473
580
|
}
|
|
474
581
|
|
|
475
582
|
// IMPORTANT: Store the config AFTER createBetterAuthInstance mutates it
|
|
@@ -516,6 +623,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
516
623
|
return new BetterAuthTokenService(betterAuthService, connection);
|
|
517
624
|
},
|
|
518
625
|
},
|
|
626
|
+
CoreBetterAuthChallengeService,
|
|
519
627
|
this.getResolverClass(),
|
|
520
628
|
// Conditionally register RolesGuard globally for IAM-only setups
|
|
521
629
|
// In Legacy mode, RolesGuard is already registered globally via CoreAuthModule
|
|
@@ -539,24 +647,26 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
539
647
|
private static logEnabledFeatures(config: IBetterAuth): void {
|
|
540
648
|
const features: string[] = [];
|
|
541
649
|
|
|
542
|
-
// Helper to check if a plugin is
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
if (value === true) return true;
|
|
548
|
-
return value.enabled !== false;
|
|
650
|
+
// Helper to check if a plugin is explicitly disabled
|
|
651
|
+
const isExplicitlyDisabled = <T extends { enabled?: boolean }>(value: boolean | T | undefined): boolean => {
|
|
652
|
+
if (value === false) return true;
|
|
653
|
+
if (typeof value === 'object' && value?.enabled === false) return true;
|
|
654
|
+
return false;
|
|
549
655
|
};
|
|
550
656
|
|
|
551
|
-
//
|
|
552
|
-
if (
|
|
657
|
+
// JWT and 2FA are enabled by default unless explicitly disabled
|
|
658
|
+
if (!isExplicitlyDisabled(config.jwt)) {
|
|
553
659
|
features.push('JWT');
|
|
554
660
|
}
|
|
555
|
-
if (
|
|
661
|
+
if (!isExplicitlyDisabled(config.twoFactor)) {
|
|
556
662
|
features.push('2FA/TOTP');
|
|
557
663
|
}
|
|
558
|
-
|
|
559
|
-
|
|
664
|
+
// Passkey is enabled by default, unless explicitly set to false
|
|
665
|
+
if (config.passkey !== false && !(typeof config.passkey === 'object' && config.passkey?.enabled === false)) {
|
|
666
|
+
const passkeyConfig = typeof config.passkey === 'object' ? config.passkey : null;
|
|
667
|
+
// Challenge storage is 'database' by default, can be overridden via config
|
|
668
|
+
const challengeStorage = passkeyConfig?.challengeStorage || 'database';
|
|
669
|
+
features.push(`Passkey/WebAuthn (challenges: ${challengeStorage})`);
|
|
560
670
|
}
|
|
561
671
|
|
|
562
672
|
// Dynamically collect enabled social providers
|
|
@@ -4,6 +4,7 @@ import { Request, Response } from 'express';
|
|
|
4
4
|
|
|
5
5
|
import { Roles } from '../../common/decorators/roles.decorator';
|
|
6
6
|
import { RoleEnum } from '../../common/enums/role.enum';
|
|
7
|
+
import { maskEmail } from '../../common/helpers/logging.helper';
|
|
7
8
|
import {
|
|
8
9
|
BetterAuth2FAResponse,
|
|
9
10
|
BetterAuthSignInResponse,
|
|
@@ -228,12 +229,12 @@ export class CoreBetterAuthResolver {
|
|
|
228
229
|
body: { email, password },
|
|
229
230
|
})) as BetterAuthSignInResponse | null;
|
|
230
231
|
|
|
231
|
-
this.logger.debug(`[SignIn] API response for ${email}: ${JSON.stringify(response)?.substring(0, 200)}`);
|
|
232
|
+
this.logger.debug(`[SignIn] API response for ${maskEmail(email)}: ${JSON.stringify(response)?.substring(0, 200)}`);
|
|
232
233
|
|
|
233
234
|
// Check if response indicates an error (Better-Auth returns error objects, not throws)
|
|
234
235
|
const responseAny = response as any;
|
|
235
236
|
if (responseAny?.error || responseAny?.code === 'CREDENTIAL_ACCOUNT_NOT_FOUND') {
|
|
236
|
-
this.logger.debug(`[SignIn] API returned error for ${email}: ${responseAny?.error || responseAny?.code}`);
|
|
237
|
+
this.logger.debug(`[SignIn] API returned error for ${maskEmail(email)}: ${responseAny?.error || responseAny?.code}`);
|
|
237
238
|
throw new Error(responseAny?.error || responseAny?.code || 'Credential account not found');
|
|
238
239
|
}
|
|
239
240
|
|
|
@@ -273,17 +274,17 @@ export class CoreBetterAuthResolver {
|
|
|
273
274
|
throw new UnauthorizedException('Invalid credentials');
|
|
274
275
|
} catch (error) {
|
|
275
276
|
this.logger.debug(
|
|
276
|
-
`[SignIn] Sign-in failed for ${email}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
277
|
+
`[SignIn] Sign-in failed for ${maskEmail(email)}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
277
278
|
);
|
|
278
279
|
|
|
279
280
|
// If migration is allowed, try to migrate legacy user and retry
|
|
280
281
|
if (allowMigration) {
|
|
281
|
-
this.logger.debug(`[SignIn] Attempting migration for ${email}...`);
|
|
282
|
+
this.logger.debug(`[SignIn] Attempting migration for ${maskEmail(email)}...`);
|
|
282
283
|
// Pass the original password for legacy verification
|
|
283
284
|
const migrated = await this.userMapper.migrateAccountToIam(email, password);
|
|
284
|
-
this.logger.debug(`[SignIn] Migration result for ${email}: ${migrated}`);
|
|
285
|
+
this.logger.debug(`[SignIn] Migration result for ${maskEmail(email)}: ${migrated}`);
|
|
285
286
|
if (migrated) {
|
|
286
|
-
this.logger.debug(`[SignIn] Migrated legacy user ${email} to IAM, retrying sign-in`);
|
|
287
|
+
this.logger.debug(`[SignIn] Migrated legacy user ${maskEmail(email)} to IAM, retrying sign-in`);
|
|
287
288
|
// Retry sign-in after migration with normalized password (as migrateAccountToIam stores it)
|
|
288
289
|
const normalizedPassword = this.userMapper.normalizePasswordForIam(password);
|
|
289
290
|
return this.attemptSignInDirect(email, normalizedPassword, api);
|
|
@@ -4,7 +4,7 @@ import { Request } from 'express';
|
|
|
4
4
|
import { importJWK, jwtVerify } from 'jose';
|
|
5
5
|
import { Connection } from 'mongoose';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { maskCookieHeader, maskEmail, maskToken } from '../../common/helpers/logging.helper';
|
|
8
8
|
import { IBetterAuth } from '../../common/interfaces/server-options.interface';
|
|
9
9
|
import { ConfigService } from '../../common/services/config.service';
|
|
10
10
|
import { BetterAuthInstance } from './better-auth.config';
|
|
@@ -55,7 +55,6 @@ export const BETTER_AUTH_CONFIG = 'BETTER_AUTH_CONFIG';
|
|
|
55
55
|
@Injectable()
|
|
56
56
|
export class CoreBetterAuthService {
|
|
57
57
|
private readonly logger = new Logger(CoreBetterAuthService.name);
|
|
58
|
-
private readonly isProd = isProduction();
|
|
59
58
|
private readonly config: IBetterAuth;
|
|
60
59
|
|
|
61
60
|
constructor(
|
|
@@ -129,32 +128,30 @@ export class CoreBetterAuthService {
|
|
|
129
128
|
|
|
130
129
|
/**
|
|
131
130
|
* Checks if 2FA is enabled.
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* - `false`
|
|
131
|
+
* 2FA is enabled by default when BetterAuth is enabled.
|
|
132
|
+
* Only disabled when explicitly set to:
|
|
133
|
+
* - `false`
|
|
134
|
+
* - `{ enabled: false }`
|
|
135
135
|
*/
|
|
136
136
|
isTwoFactorEnabled(): boolean {
|
|
137
|
-
|
|
137
|
+
if (!this.isEnabled()) return false;
|
|
138
|
+
if (this.config.twoFactor === false) return false;
|
|
139
|
+
if (typeof this.config.twoFactor === 'object' && this.config.twoFactor?.enabled === false) return false;
|
|
140
|
+
return true;
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
/**
|
|
141
144
|
* Checks if Passkey/WebAuthn is enabled.
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* - `false`
|
|
145
|
+
* Passkey is enabled by default when BetterAuth is enabled.
|
|
146
|
+
* Only disabled when explicitly set to:
|
|
147
|
+
* - `false`
|
|
148
|
+
* - `{ enabled: false }`
|
|
145
149
|
*/
|
|
146
150
|
isPasskeyEnabled(): boolean {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
* Helper to check if a plugin configuration is enabled.
|
|
152
|
-
* Supports both boolean and object configuration.
|
|
153
|
-
*/
|
|
154
|
-
private isPluginEnabled<T extends { enabled?: boolean }>(config: boolean | T | undefined): boolean {
|
|
155
|
-
if (config === undefined) return false;
|
|
156
|
-
if (typeof config === 'boolean') return config;
|
|
157
|
-
return config.enabled !== false;
|
|
151
|
+
if (!this.isEnabled()) return false;
|
|
152
|
+
if (this.config.passkey === false) return false;
|
|
153
|
+
if (typeof this.config.passkey === 'object' && this.config.passkey?.enabled === false) return false;
|
|
154
|
+
return true;
|
|
158
155
|
}
|
|
159
156
|
|
|
160
157
|
/**
|
|
@@ -320,17 +317,13 @@ export class CoreBetterAuthService {
|
|
|
320
317
|
}
|
|
321
318
|
|
|
322
319
|
// Debug: Log the cookie header being sent to api.getSession (masked for security)
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
this.logger.debug(`getSession called with cookies: ${maskCookieHeader(cookieHeader)}`);
|
|
326
|
-
}
|
|
320
|
+
const cookieHeader = headers.get('cookie');
|
|
321
|
+
this.logger.debug(`getSession called with cookies: ${maskCookieHeader(cookieHeader)}`);
|
|
327
322
|
|
|
328
323
|
const response = await api.getSession({ headers });
|
|
329
324
|
|
|
330
325
|
// Debug: Log the response from api.getSession
|
|
331
|
-
|
|
332
|
-
this.logger.debug(`getSession response: ${JSON.stringify(response)?.substring(0, 200)}`);
|
|
333
|
-
}
|
|
326
|
+
this.logger.debug(`getSession response: ${JSON.stringify(response)?.substring(0, 200)}`);
|
|
334
327
|
|
|
335
328
|
if (response && typeof response === 'object' && 'user' in response) {
|
|
336
329
|
return response as SessionResult;
|
|
@@ -338,9 +331,7 @@ export class CoreBetterAuthService {
|
|
|
338
331
|
|
|
339
332
|
return { session: null, user: null };
|
|
340
333
|
} catch (error) {
|
|
341
|
-
|
|
342
|
-
this.logger.debug(`getSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
343
|
-
}
|
|
334
|
+
this.logger.debug(`getSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
344
335
|
return { session: null, user: null };
|
|
345
336
|
}
|
|
346
337
|
}
|
|
@@ -383,9 +374,7 @@ export class CoreBetterAuthService {
|
|
|
383
374
|
await api.signOut({ headers });
|
|
384
375
|
return true;
|
|
385
376
|
} catch (error) {
|
|
386
|
-
|
|
387
|
-
this.logger.debug(`revokeSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
388
|
-
}
|
|
377
|
+
this.logger.debug(`revokeSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
389
378
|
return false;
|
|
390
379
|
}
|
|
391
380
|
}
|
|
@@ -494,31 +483,23 @@ export class CoreBetterAuthService {
|
|
|
494
483
|
const result = results[0];
|
|
495
484
|
|
|
496
485
|
if (!result) {
|
|
497
|
-
|
|
498
|
-
this.logger.debug(`getSessionByToken: session not found for token ${maskToken(token)}`);
|
|
499
|
-
}
|
|
486
|
+
this.logger.debug(`getSessionByToken: session not found for token ${maskToken(token)}`);
|
|
500
487
|
return { session: null, user: null };
|
|
501
488
|
}
|
|
502
489
|
|
|
503
490
|
// Check if session is expired
|
|
504
491
|
if (result.expiresAt && new Date(result.expiresAt) < new Date()) {
|
|
505
|
-
|
|
506
|
-
this.logger.debug(`getSessionByToken: session expired`);
|
|
507
|
-
}
|
|
492
|
+
this.logger.debug(`getSessionByToken: session expired`);
|
|
508
493
|
return { session: null, user: null };
|
|
509
494
|
}
|
|
510
495
|
|
|
511
496
|
const user = result.userDoc;
|
|
512
497
|
if (!user) {
|
|
513
|
-
|
|
514
|
-
this.logger.debug(`getSessionByToken: user not found for session`);
|
|
515
|
-
}
|
|
498
|
+
this.logger.debug(`getSessionByToken: user not found for session`);
|
|
516
499
|
return { session: null, user: null };
|
|
517
500
|
}
|
|
518
501
|
|
|
519
|
-
|
|
520
|
-
this.logger.debug(`getSessionByToken: found session for user ${maskEmail(user.email)}`);
|
|
521
|
-
}
|
|
502
|
+
this.logger.debug(`getSessionByToken: found session for user ${maskEmail(user.email)}`);
|
|
522
503
|
|
|
523
504
|
return {
|
|
524
505
|
session: {
|
|
@@ -535,9 +516,7 @@ export class CoreBetterAuthService {
|
|
|
535
516
|
},
|
|
536
517
|
};
|
|
537
518
|
} catch (error) {
|
|
538
|
-
|
|
539
|
-
this.logger.debug(`getSessionByToken error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
540
|
-
}
|
|
519
|
+
this.logger.debug(`getSessionByToken error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
541
520
|
return { session: null, user: null };
|
|
542
521
|
}
|
|
543
522
|
}
|
package/src/core.module.ts
CHANGED
|
@@ -265,6 +265,11 @@ export class CoreModule implements NestModule {
|
|
|
265
265
|
// In IAM-only mode, register RolesGuard globally to enforce @Roles() decorators
|
|
266
266
|
// In Legacy mode (autoRegister), RolesGuard is already registered via CoreAuthModule
|
|
267
267
|
registerRolesGuardGlobally: isIamOnlyMode,
|
|
268
|
+
// Pass server-level URLs for Passkey auto-detection
|
|
269
|
+
// When env: 'local', defaults are: baseUrl=localhost:3000, appUrl=localhost:3001
|
|
270
|
+
serverAppUrl: config.appUrl,
|
|
271
|
+
serverBaseUrl: config.baseUrl,
|
|
272
|
+
serverEnv: config.env,
|
|
268
273
|
}),
|
|
269
274
|
);
|
|
270
275
|
}
|
|
@@ -7,6 +7,19 @@ import { BetterAuthResolver } from './better-auth.resolver';
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Options for BetterAuthModule.forRoot()
|
|
10
|
+
*
|
|
11
|
+
* All options are optional when using Zero-Config:
|
|
12
|
+
* All values are auto-read from ConfigService (set by CoreModule.forRoot)
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // Zero-Config - all values auto-detected from ConfigService
|
|
16
|
+
* BetterAuthModule.forRoot({})
|
|
17
|
+
*
|
|
18
|
+
* // Or with explicit overrides
|
|
19
|
+
* BetterAuthModule.forRoot({
|
|
20
|
+
* config: { secret: 'custom-secret' },
|
|
21
|
+
* serverAppUrl: 'https://custom-app.com',
|
|
22
|
+
* })
|
|
10
23
|
*/
|
|
11
24
|
export interface ServerBetterAuthModuleOptions {
|
|
12
25
|
/**
|
|
@@ -15,14 +28,33 @@ export interface ServerBetterAuthModuleOptions {
|
|
|
15
28
|
* - `true`: Enable with all defaults (including JWT)
|
|
16
29
|
* - `false`: Disable BetterAuth
|
|
17
30
|
* - `{ ... }`: Enable with custom configuration
|
|
31
|
+
* - `undefined`: Auto-read from ConfigService (Zero-Config)
|
|
18
32
|
*/
|
|
19
|
-
config
|
|
33
|
+
config?: boolean | IBetterAuth;
|
|
20
34
|
|
|
21
35
|
/**
|
|
22
36
|
* Fallback secrets for backwards compatibility with JWT config.
|
|
23
37
|
* If no betterAuth.secret is configured, these secrets are tried in order.
|
|
24
38
|
*/
|
|
25
39
|
fallbackSecrets?: (string | undefined)[];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Server-level app URL for Passkey auto-detection.
|
|
43
|
+
* @see IServerOptions.appUrl
|
|
44
|
+
*/
|
|
45
|
+
serverAppUrl?: string;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Server-level base URL for Passkey auto-detection.
|
|
49
|
+
* @see IServerOptions.baseUrl
|
|
50
|
+
*/
|
|
51
|
+
serverBaseUrl?: string;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Server environment for localhost defaults (local, ci, e2e).
|
|
55
|
+
* @see IServerOptions.env
|
|
56
|
+
*/
|
|
57
|
+
serverEnv?: string;
|
|
26
58
|
}
|
|
27
59
|
|
|
28
60
|
/**
|
|
@@ -42,14 +74,9 @@ export interface ServerBetterAuthModuleOptions {
|
|
|
42
74
|
*
|
|
43
75
|
* @Module({
|
|
44
76
|
* imports: [
|
|
45
|
-
* CoreModule.forRoot(CoreAuthService, AuthModule.forRoot(envConfig.jwt),
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* }),
|
|
49
|
-
* BetterAuthModule.forRoot({
|
|
50
|
-
* config: envConfig.betterAuth,
|
|
51
|
-
* fallbackSecrets: [envConfig.jwt?.secret, envConfig.jwt?.refresh?.secret],
|
|
52
|
-
* }),
|
|
77
|
+
* CoreModule.forRoot(CoreAuthService, AuthModule.forRoot(envConfig.jwt), envConfig),
|
|
78
|
+
* // Zero-Config: All values auto-read from ConfigService
|
|
79
|
+
* BetterAuthModule.forRoot({}),
|
|
53
80
|
* ],
|
|
54
81
|
* })
|
|
55
82
|
* export class ServerModule {}
|
|
@@ -64,7 +91,7 @@ export class BetterAuthModule {
|
|
|
64
91
|
* @returns Dynamic module configuration
|
|
65
92
|
*/
|
|
66
93
|
static forRoot(options: ServerBetterAuthModuleOptions): DynamicModule {
|
|
67
|
-
const { config, fallbackSecrets } = options;
|
|
94
|
+
const { config, fallbackSecrets, serverAppUrl, serverBaseUrl, serverEnv } = options;
|
|
68
95
|
|
|
69
96
|
// If better-auth is explicitly disabled, return minimal module
|
|
70
97
|
// Supports: false, { enabled: false }, or undefined/null
|
|
@@ -85,6 +112,9 @@ export class BetterAuthModule {
|
|
|
85
112
|
controller: BetterAuthController,
|
|
86
113
|
fallbackSecrets,
|
|
87
114
|
resolver: BetterAuthResolver,
|
|
115
|
+
serverAppUrl,
|
|
116
|
+
serverBaseUrl,
|
|
117
|
+
serverEnv,
|
|
88
118
|
}),
|
|
89
119
|
],
|
|
90
120
|
module: BetterAuthModule,
|
|
@@ -45,11 +45,9 @@ import { ServerController } from './server.controller';
|
|
|
45
45
|
AuthModule.forRoot(envConfig.jwt),
|
|
46
46
|
|
|
47
47
|
// Include BetterAuthModule for better-auth integration
|
|
48
|
+
// Zero-Config: All values are auto-read from ConfigService (set by CoreModule.forRoot)
|
|
48
49
|
// This allows project-specific customization via BetterAuthResolver
|
|
49
|
-
BetterAuthModule.forRoot({
|
|
50
|
-
config: envConfig.betterAuth,
|
|
51
|
-
fallbackSecrets: [envConfig.jwt?.secret, envConfig.jwt?.refresh?.secret],
|
|
52
|
-
}),
|
|
50
|
+
BetterAuthModule.forRoot({}),
|
|
53
51
|
|
|
54
52
|
// Include ErrorCodeModule with project-specific error codes
|
|
55
53
|
// Uses Core ErrorCodeModule.forRoot() with custom service and controller
|