@lenne.tech/nest-server 11.13.2 → 11.13.4
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 +230 -1
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +2 -0
- package/dist/core/modules/better-auth/better-auth.config.js +40 -1
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.d.ts +1 -0
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +7 -0
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-models.d.ts +1 -0
- package/dist/core/modules/better-auth/core-better-auth-models.js +4 -0
- package/dist/core/modules/better-auth/core-better-auth-models.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +1 -0
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +27 -0
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +14 -16
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.controller.js +14 -6
- package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.module.js +44 -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 +3 -2
- 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 +2 -0
- package/dist/core/modules/better-auth/core-better-auth.service.js +12 -4
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/core/modules/error-code/error-codes.d.ts +9 -0
- package/dist/core/modules/error-code/error-codes.js +8 -0
- package/dist/core/modules/error-code/error-codes.js.map +1 -1
- package/dist/server/modules/error-code/error-codes.d.ts +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/config.env.ts +259 -2
- package/src/core/common/interfaces/server-options.interface.ts +17 -7
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +6 -0
- package/src/core/modules/better-auth/README.md +32 -0
- package/src/core/modules/better-auth/better-auth.config.ts +63 -2
- package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +16 -0
- package/src/core/modules/better-auth/core-better-auth-models.ts +3 -0
- package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +40 -0
- package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +24 -24
- package/src/core/modules/better-auth/core-better-auth.controller.ts +44 -16
- package/src/core/modules/better-auth/core-better-auth.module.ts +72 -37
- package/src/core/modules/better-auth/core-better-auth.resolver.ts +16 -9
- package/src/core/modules/better-auth/core-better-auth.service.ts +30 -7
- package/src/core/modules/error-code/error-codes.ts +9 -0
|
@@ -370,27 +370,38 @@ export class CoreBetterAuthUserMapper {
|
|
|
370
370
|
return false;
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
+
// Better-Auth stores account.userId as ObjectId that references users._id
|
|
374
|
+
const userMongoId = legacyUser._id as ObjectId;
|
|
375
|
+
|
|
376
|
+
// FAST PATH: Check if credential account already exists BEFORE expensive bcrypt
|
|
377
|
+
// For already-migrated users this avoids ~130ms of bcrypt.compare() per sign-in
|
|
378
|
+
const existingAccount = await accountsCollection.findOne({
|
|
379
|
+
providerId: 'credential',
|
|
380
|
+
userId: userMongoId,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
if (existingAccount) {
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// No password provided - cannot verify, cannot migrate
|
|
388
|
+
if (!plainPassword) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
|
|
373
392
|
// IMPORTANT: Verify the provided password matches the legacy hash
|
|
374
393
|
// This prevents migration with a wrong password
|
|
375
394
|
// Legacy Auth uses two formats for backwards compatibility:
|
|
376
395
|
// 1. bcrypt(password) - direct hash
|
|
377
396
|
// 2. bcrypt(sha256(password)) - sha256 then bcrypt
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
this.logger.warn(`Migration password verification failed for ${maskEmail(userEmail)}`);
|
|
384
|
-
return false;
|
|
385
|
-
}
|
|
386
|
-
} else {
|
|
387
|
-
// No password provided - cannot verify, cannot migrate
|
|
397
|
+
const directMatch = await bcrypt.compare(plainPassword, legacyUser.password);
|
|
398
|
+
const sha256Match = !directMatch ? await bcrypt.compare(sha256(plainPassword), legacyUser.password) : false;
|
|
399
|
+
if (!directMatch && !sha256Match) {
|
|
400
|
+
// Security: Wrong password provided for migration - reject
|
|
401
|
+
this.logger.warn(`Migration password verification failed for ${maskEmail(userEmail)}`);
|
|
388
402
|
return false;
|
|
389
403
|
}
|
|
390
404
|
|
|
391
|
-
// Better-Auth stores account.userId as ObjectId that references users._id
|
|
392
|
-
// The id field is a secondary string identifier used in API responses
|
|
393
|
-
const userMongoId = legacyUser._id as ObjectId;
|
|
394
405
|
const userIdHex = userMongoId.toHexString();
|
|
395
406
|
|
|
396
407
|
// Update user with Better-Auth fields if not already present
|
|
@@ -413,17 +424,6 @@ export class CoreBetterAuthUserMapper {
|
|
|
413
424
|
);
|
|
414
425
|
}
|
|
415
426
|
|
|
416
|
-
// Check if credential account already exists
|
|
417
|
-
// Better-Auth stores userId as ObjectId referencing users._id
|
|
418
|
-
const existingAccount = await accountsCollection.findOne({
|
|
419
|
-
providerId: 'credential',
|
|
420
|
-
userId: userMongoId,
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
if (existingAccount) {
|
|
424
|
-
return true;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
427
|
// Create the credential account with Better-Auth compatible scrypt hash
|
|
428
428
|
const passwordHash = await this.hashPasswordForBetterAuth(plainPassword);
|
|
429
429
|
|
|
@@ -14,7 +14,15 @@ import {
|
|
|
14
14
|
Res,
|
|
15
15
|
UnauthorizedException,
|
|
16
16
|
} from '@nestjs/common';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
ApiBody,
|
|
19
|
+
ApiCreatedResponse,
|
|
20
|
+
ApiExcludeEndpoint,
|
|
21
|
+
ApiOkResponse,
|
|
22
|
+
ApiOperation,
|
|
23
|
+
ApiProperty,
|
|
24
|
+
ApiTags,
|
|
25
|
+
} from '@nestjs/swagger';
|
|
18
26
|
import { Request, Response } from 'express';
|
|
19
27
|
|
|
20
28
|
import { Roles } from '../../common/decorators/roles.decorator';
|
|
@@ -251,7 +259,10 @@ export class CoreBetterAuthController {
|
|
|
251
259
|
* @since 11.13.0
|
|
252
260
|
*/
|
|
253
261
|
@ApiOkResponse({ description: 'Better-Auth feature flags' })
|
|
254
|
-
@ApiOperation({
|
|
262
|
+
@ApiOperation({
|
|
263
|
+
description: 'Get enabled Better-Auth features for client-side feature detection',
|
|
264
|
+
summary: 'Get Features',
|
|
265
|
+
})
|
|
255
266
|
@Get('features')
|
|
256
267
|
@Roles(RoleEnum.S_EVERYONE)
|
|
257
268
|
getFeatures(): Record<string, boolean | number | string[]> {
|
|
@@ -262,6 +273,7 @@ export class CoreBetterAuthController {
|
|
|
262
273
|
passkey: this.betterAuthService.isPasskeyEnabled(),
|
|
263
274
|
resendCooldownSeconds: this.emailVerificationService?.getConfig()?.resendCooldownSeconds ?? 60,
|
|
264
275
|
signUpChecks: this.signUpValidator?.isEnabled() ?? false,
|
|
276
|
+
signUpEnabled: this.betterAuthService.isSignUpEnabled(),
|
|
265
277
|
socialProviders: this.betterAuthService.getEnabledSocialProviders(),
|
|
266
278
|
twoFactor: this.betterAuthService.isTwoFactorEnabled(),
|
|
267
279
|
};
|
|
@@ -345,7 +357,9 @@ export class CoreBetterAuthController {
|
|
|
345
357
|
// Without this, users with 2FA enabled but unverified email could bypass verification
|
|
346
358
|
await this.checkEmailVerificationByEmail(input.email);
|
|
347
359
|
|
|
348
|
-
this.logger.debug(
|
|
360
|
+
this.logger.debug(
|
|
361
|
+
`2FA required for ${maskEmail(input.email)}, forwarding to native handler for cookie handling`,
|
|
362
|
+
);
|
|
349
363
|
|
|
350
364
|
// Forward to native Better Auth handler which sets the session cookie correctly
|
|
351
365
|
// We need to modify the request body to use the normalized password
|
|
@@ -370,7 +384,7 @@ export class CoreBetterAuthController {
|
|
|
370
384
|
body: modifiedBody,
|
|
371
385
|
headers: new Headers({
|
|
372
386
|
'Content-Type': 'application/json',
|
|
373
|
-
|
|
387
|
+
Origin: req.headers.origin || baseUrl,
|
|
374
388
|
}),
|
|
375
389
|
method: 'POST',
|
|
376
390
|
});
|
|
@@ -407,7 +421,8 @@ export class CoreBetterAuthController {
|
|
|
407
421
|
const mappedUser = await this.userMapper.mapSessionUser(response.user);
|
|
408
422
|
|
|
409
423
|
// Get token: JWT accessToken > top-level token > session.token
|
|
410
|
-
const rawToken =
|
|
424
|
+
const rawToken =
|
|
425
|
+
responseAny.accessToken || responseAny.token || (hasSession(response) ? response.session.token : undefined);
|
|
411
426
|
const token = await this.resolveJwtToken(rawToken);
|
|
412
427
|
|
|
413
428
|
const result: CoreBetterAuthResponse = {
|
|
@@ -464,6 +479,7 @@ export class CoreBetterAuthController {
|
|
|
464
479
|
@Body() input: CoreBetterAuthSignUpInput,
|
|
465
480
|
): Promise<CoreBetterAuthResponse> {
|
|
466
481
|
this.ensureEnabled();
|
|
482
|
+
this.betterAuthService.ensureSignUpEnabled();
|
|
467
483
|
|
|
468
484
|
// Validate sign-up input (termsAndPrivacyAccepted is required by default)
|
|
469
485
|
if (this.signUpValidator) {
|
|
@@ -494,7 +510,9 @@ export class CoreBetterAuthController {
|
|
|
494
510
|
if (hasUser(response)) {
|
|
495
511
|
// Link or create user in our database
|
|
496
512
|
// Pass termsAndPrivacyAccepted to store the acceptance timestamp
|
|
497
|
-
await this.userMapper.linkOrCreateUser(response.user, {
|
|
513
|
+
await this.userMapper.linkOrCreateUser(response.user, {
|
|
514
|
+
termsAndPrivacyAccepted: input.termsAndPrivacyAccepted,
|
|
515
|
+
});
|
|
498
516
|
|
|
499
517
|
// Sync password to legacy (enables IAM Sign-Up → Legacy Sign-In)
|
|
500
518
|
// Pass the plain password so it can be hashed with bcrypt for Legacy Auth
|
|
@@ -506,7 +524,8 @@ export class CoreBetterAuthController {
|
|
|
506
524
|
// Without this, no session cookies are set after sign-up, causing 401 on
|
|
507
525
|
// subsequent authenticated requests (e.g., Passkey, 2FA, /token)
|
|
508
526
|
const responseAny = response as any;
|
|
509
|
-
const rawToken =
|
|
527
|
+
const rawToken =
|
|
528
|
+
responseAny.accessToken || responseAny.token || (hasSession(response) ? response.session.token : undefined);
|
|
510
529
|
const token = await this.resolveJwtToken(rawToken);
|
|
511
530
|
|
|
512
531
|
// If email verification is enabled, revoke the session and don't return session data
|
|
@@ -518,7 +537,9 @@ export class CoreBetterAuthController {
|
|
|
518
537
|
await this.betterAuthService.revokeSession(sessionToken);
|
|
519
538
|
}
|
|
520
539
|
this.clearAuthCookies(res);
|
|
521
|
-
this.logger.debug(
|
|
540
|
+
this.logger.debug(
|
|
541
|
+
`[SignUp] Email verification required for ${maskEmail(response.user.email)}, session revoked`,
|
|
542
|
+
);
|
|
522
543
|
return {
|
|
523
544
|
emailVerificationRequired: true,
|
|
524
545
|
requiresTwoFactor: false,
|
|
@@ -692,10 +713,7 @@ export class CoreBetterAuthController {
|
|
|
692
713
|
* @throws UnauthorizedException if email is not verified and verification is required
|
|
693
714
|
*/
|
|
694
715
|
protected checkEmailVerification(sessionUser: BetterAuthSessionUser): void {
|
|
695
|
-
if (
|
|
696
|
-
this.emailVerificationService?.isEnabled()
|
|
697
|
-
&& !sessionUser.emailVerified
|
|
698
|
-
) {
|
|
716
|
+
if (this.emailVerificationService?.isEnabled() && !sessionUser.emailVerified) {
|
|
699
717
|
this.logger.debug(`[SignIn] Email not verified for ${maskEmail(sessionUser.email)}, blocking login`);
|
|
700
718
|
throw new UnauthorizedException(ErrorCode.EMAIL_VERIFICATION_REQUIRED);
|
|
701
719
|
}
|
|
@@ -764,7 +782,9 @@ export class CoreBetterAuthController {
|
|
|
764
782
|
* NOTE: The session token is intentionally NOT included in the response.
|
|
765
783
|
* It is set as an httpOnly cookie for security.
|
|
766
784
|
*/
|
|
767
|
-
protected mapSession(
|
|
785
|
+
protected mapSession(
|
|
786
|
+
session: null | undefined | { expiresAt: Date; id: string; token?: string },
|
|
787
|
+
): CoreBetterAuthSessionInfo | undefined {
|
|
768
788
|
if (!session) return undefined;
|
|
769
789
|
return {
|
|
770
790
|
expiresAt: session.expiresAt instanceof Date ? session.expiresAt.toISOString() : String(session.expiresAt),
|
|
@@ -778,7 +798,7 @@ export class CoreBetterAuthController {
|
|
|
778
798
|
* @param sessionUser - The user from Better-Auth session
|
|
779
799
|
* @param _mappedUser - The synced user from legacy system (available for override customization)
|
|
780
800
|
*/
|
|
781
|
-
|
|
801
|
+
|
|
782
802
|
protected mapUser(sessionUser: BetterAuthSessionUser, _mappedUser: any): CoreBetterAuthUserResponse {
|
|
783
803
|
return {
|
|
784
804
|
email: sessionUser.email,
|
|
@@ -802,7 +822,11 @@ export class CoreBetterAuthController {
|
|
|
802
822
|
* @param result - The CoreBetterAuthResponse to return
|
|
803
823
|
* @param sessionToken - Optional session token to set in cookies (if not provided, uses result.token)
|
|
804
824
|
*/
|
|
805
|
-
protected processCookies(
|
|
825
|
+
protected processCookies(
|
|
826
|
+
res: Response,
|
|
827
|
+
result: CoreBetterAuthResponse,
|
|
828
|
+
sessionToken?: string,
|
|
829
|
+
): CoreBetterAuthResponse {
|
|
806
830
|
const cookiesEnabled = this.configService.getFastButReadOnly('cookies') !== false;
|
|
807
831
|
|
|
808
832
|
// If a specific session token is provided, use it directly
|
|
@@ -883,7 +907,11 @@ export class CoreBetterAuthController {
|
|
|
883
907
|
this.logger.error(`Better Auth handler error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
884
908
|
|
|
885
909
|
// Re-throw NestJS exceptions
|
|
886
|
-
if (
|
|
910
|
+
if (
|
|
911
|
+
error instanceof BadRequestException ||
|
|
912
|
+
error instanceof UnauthorizedException ||
|
|
913
|
+
error instanceof InternalServerErrorException
|
|
914
|
+
) {
|
|
887
915
|
throw error;
|
|
888
916
|
}
|
|
889
917
|
|
|
@@ -313,15 +313,17 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
313
313
|
if (CoreBetterAuthModule.currentConfig) {
|
|
314
314
|
const globalConfig = ConfigService.configFastButReadOnly;
|
|
315
315
|
const cookiesDisabled = globalConfig?.cookies === false;
|
|
316
|
-
const jwtExplicitlyDisabled =
|
|
317
|
-
|
|
316
|
+
const jwtExplicitlyDisabled =
|
|
317
|
+
CoreBetterAuthModule.currentConfig.jwt === false ||
|
|
318
|
+
(typeof CoreBetterAuthModule.currentConfig.jwt === 'object' &&
|
|
319
|
+
CoreBetterAuthModule.currentConfig.jwt?.enabled === false);
|
|
318
320
|
|
|
319
321
|
if (cookiesDisabled && jwtExplicitlyDisabled) {
|
|
320
322
|
CoreBetterAuthModule.logger.warn(
|
|
321
323
|
'CONFIGURATION WARNING: cookies is set to false, but betterAuth.jwt is not enabled. ' +
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
324
|
+
'Without cookies, BetterAuth cannot establish sessions via Set-Cookie headers. ' +
|
|
325
|
+
'Enable betterAuth.jwt (set jwt: true in betterAuth config) to use Bearer token authentication, ' +
|
|
326
|
+
'or set cookies: true to use cookie-based sessions.',
|
|
325
327
|
);
|
|
326
328
|
}
|
|
327
329
|
}
|
|
@@ -331,8 +333,8 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
331
333
|
if (CoreBetterAuthModule.rolesGuardExplicitlyDisabled && !RolesGuardRegistry.isRegistered()) {
|
|
332
334
|
CoreBetterAuthModule.logger.warn(
|
|
333
335
|
'⚠️ SECURITY WARNING: registerRolesGuardGlobally is explicitly set to false, ' +
|
|
334
|
-
|
|
335
|
-
|
|
336
|
+
'but no RolesGuard is registered globally. @Roles() decorators will NOT enforce access control! ' +
|
|
337
|
+
'Either set registerRolesGuardGlobally: true, or ensure CoreAuthModule (Legacy) is imported.',
|
|
336
338
|
);
|
|
337
339
|
}
|
|
338
340
|
}
|
|
@@ -422,10 +424,10 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
422
424
|
if (this.forRootCalled && !process.env.VITEST) {
|
|
423
425
|
this.logger.warn(
|
|
424
426
|
'CoreBetterAuthModule.forRoot() was called more than once. ' +
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
427
|
+
'The second call is ignored by NestJS (DynamicModule deduplication). ' +
|
|
428
|
+
'Custom controller/resolver from the second call will NOT be registered. ' +
|
|
429
|
+
'Solutions: (1) Use betterAuth.controller/resolver in config, or ' +
|
|
430
|
+
'(2) Set betterAuth.autoRegister: false and import your module separately.',
|
|
429
431
|
);
|
|
430
432
|
if (this.cachedDynamicModule) {
|
|
431
433
|
return this.cachedDynamicModule;
|
|
@@ -453,11 +455,9 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
453
455
|
const effectiveRawConfig = rawConfig ?? globalConfig?.betterAuth;
|
|
454
456
|
|
|
455
457
|
// Auto-detect fallbackSecrets from ConfigService if not explicitly provided
|
|
456
|
-
const effectiveFallbackSecrets =
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
: undefined
|
|
460
|
-
);
|
|
458
|
+
const effectiveFallbackSecrets =
|
|
459
|
+
fallbackSecrets ??
|
|
460
|
+
(globalConfig?.jwt ? [globalConfig.jwt.secret, globalConfig.jwt.refresh?.secret].filter(Boolean) : undefined);
|
|
461
461
|
|
|
462
462
|
// Auto-detect server URLs from ConfigService if not explicitly provided
|
|
463
463
|
const effectiveServerAppUrl = serverAppUrl ?? globalConfig?.appUrl;
|
|
@@ -489,7 +489,14 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
489
489
|
this.logger.debug('BetterAuth is disabled - skipping initialization');
|
|
490
490
|
this.betterAuthEnabled = false;
|
|
491
491
|
this.cachedDynamicModule = {
|
|
492
|
-
exports: [
|
|
492
|
+
exports: [
|
|
493
|
+
BETTER_AUTH_INSTANCE,
|
|
494
|
+
CoreBetterAuthService,
|
|
495
|
+
CoreBetterAuthUserMapper,
|
|
496
|
+
CoreBetterAuthRateLimiter,
|
|
497
|
+
BetterAuthTokenService,
|
|
498
|
+
CoreBetterAuthChallengeService,
|
|
499
|
+
],
|
|
493
500
|
module: CoreBetterAuthModule,
|
|
494
501
|
providers: [
|
|
495
502
|
{
|
|
@@ -537,7 +544,16 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
537
544
|
static forRootAsync(): DynamicModule {
|
|
538
545
|
return {
|
|
539
546
|
controllers: [this.getControllerClass()],
|
|
540
|
-
exports: [
|
|
547
|
+
exports: [
|
|
548
|
+
BETTER_AUTH_INSTANCE,
|
|
549
|
+
CoreBetterAuthService,
|
|
550
|
+
CoreBetterAuthUserMapper,
|
|
551
|
+
CoreBetterAuthRateLimiter,
|
|
552
|
+
BetterAuthTokenService,
|
|
553
|
+
CoreBetterAuthChallengeService,
|
|
554
|
+
CoreBetterAuthEmailVerificationService,
|
|
555
|
+
CoreBetterAuthSignUpValidatorService,
|
|
556
|
+
],
|
|
541
557
|
imports: [],
|
|
542
558
|
module: CoreBetterAuthModule,
|
|
543
559
|
providers: [
|
|
@@ -559,7 +575,10 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
559
575
|
{
|
|
560
576
|
inject: [ConfigService, CoreBetterAuthEmailVerificationService],
|
|
561
577
|
provide: BETTER_AUTH_INSTANCE,
|
|
562
|
-
useFactory: async (
|
|
578
|
+
useFactory: async (
|
|
579
|
+
configService: ConfigService,
|
|
580
|
+
emailVerificationService: CoreBetterAuthEmailVerificationService,
|
|
581
|
+
) => {
|
|
563
582
|
// Set static reference for callbacks BEFORE creating Better-Auth instance
|
|
564
583
|
this.setEmailVerificationService(emailVerificationService);
|
|
565
584
|
|
|
@@ -608,9 +627,8 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
608
627
|
// The original config object may be frozen (from ConfigService), so we
|
|
609
628
|
// create a shallow copy with the resolved fallback secret applied.
|
|
610
629
|
const resolvedSecret = config.secret || fallbackSecrets?.find((s) => s && s.length >= 32);
|
|
611
|
-
this.currentConfig =
|
|
612
|
-
? { ...config, secret: resolvedSecret }
|
|
613
|
-
: config;
|
|
630
|
+
this.currentConfig =
|
|
631
|
+
resolvedSecret && resolvedSecret !== config.secret ? { ...config, secret: resolvedSecret } : config;
|
|
614
632
|
|
|
615
633
|
if (this.authInstance) {
|
|
616
634
|
this.logger.log('BetterAuth initialized successfully');
|
|
@@ -725,7 +743,11 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
725
743
|
*/
|
|
726
744
|
private static createEmailVerificationCallbacks(): {
|
|
727
745
|
onEmailVerified: (userId: string) => Promise<void>;
|
|
728
|
-
sendVerificationEmail: (options: {
|
|
746
|
+
sendVerificationEmail: (options: {
|
|
747
|
+
token: string;
|
|
748
|
+
url: string;
|
|
749
|
+
user: { email: string; id: string; name?: null | string };
|
|
750
|
+
}) => Promise<void>;
|
|
729
751
|
} {
|
|
730
752
|
return {
|
|
731
753
|
onEmailVerified: async (userId: string) => {
|
|
@@ -735,16 +757,17 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
735
757
|
const db = this.mongoConnection?.db;
|
|
736
758
|
if (db) {
|
|
737
759
|
const { ObjectId } = await import('mongodb');
|
|
738
|
-
await db
|
|
739
|
-
|
|
740
|
-
{ $set: { verified: true, verifiedAt: new Date() } }
|
|
741
|
-
);
|
|
760
|
+
await db
|
|
761
|
+
.collection('users')
|
|
762
|
+
.updateOne({ _id: new ObjectId(userId) }, { $set: { verified: true, verifiedAt: new Date() } });
|
|
742
763
|
this.logger.debug(`Email verified for user ${userId} - synced verified/verifiedAt`);
|
|
743
764
|
} else {
|
|
744
765
|
this.logger.warn(`Cannot sync verifiedAt for user ${userId} - no database connection`);
|
|
745
766
|
}
|
|
746
767
|
} catch (error) {
|
|
747
|
-
this.logger.error(
|
|
768
|
+
this.logger.error(
|
|
769
|
+
`Failed to sync verifiedAt for user ${userId}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
770
|
+
);
|
|
748
771
|
}
|
|
749
772
|
},
|
|
750
773
|
sendVerificationEmail: async (options) => {
|
|
@@ -778,7 +801,16 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
778
801
|
): DynamicModule {
|
|
779
802
|
return {
|
|
780
803
|
controllers: [this.getControllerClass()],
|
|
781
|
-
exports: [
|
|
804
|
+
exports: [
|
|
805
|
+
BETTER_AUTH_INSTANCE,
|
|
806
|
+
CoreBetterAuthService,
|
|
807
|
+
CoreBetterAuthUserMapper,
|
|
808
|
+
CoreBetterAuthRateLimiter,
|
|
809
|
+
BetterAuthTokenService,
|
|
810
|
+
CoreBetterAuthChallengeService,
|
|
811
|
+
CoreBetterAuthEmailVerificationService,
|
|
812
|
+
CoreBetterAuthSignUpValidatorService,
|
|
813
|
+
],
|
|
782
814
|
module: CoreBetterAuthModule,
|
|
783
815
|
providers: [
|
|
784
816
|
// Optional BrevoService: uses factory to avoid constructor error when brevo config is missing
|
|
@@ -801,7 +833,10 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
801
833
|
// Also inject EmailVerificationService to set static reference before Better-Auth init
|
|
802
834
|
inject: [getConnectionToken(), CoreBetterAuthEmailVerificationService],
|
|
803
835
|
provide: BETTER_AUTH_INSTANCE,
|
|
804
|
-
useFactory: async (
|
|
836
|
+
useFactory: async (
|
|
837
|
+
connection: Connection,
|
|
838
|
+
emailVerificationService: CoreBetterAuthEmailVerificationService,
|
|
839
|
+
) => {
|
|
805
840
|
// Set static references for callbacks BEFORE creating Better-Auth instance
|
|
806
841
|
this.setEmailVerificationService(emailVerificationService);
|
|
807
842
|
this.mongoConnection = connection;
|
|
@@ -843,9 +878,8 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
843
878
|
// Store a config copy with the resolved secret (same as first forRoot variant)
|
|
844
879
|
const fallbacks = options?.fallbackSecrets;
|
|
845
880
|
const resolvedSecret2 = config.secret || fallbacks?.find((s) => s && s.length >= 32);
|
|
846
|
-
this.currentConfig =
|
|
847
|
-
? { ...config, secret: resolvedSecret2 }
|
|
848
|
-
: config;
|
|
881
|
+
this.currentConfig =
|
|
882
|
+
resolvedSecret2 && resolvedSecret2 !== config.secret ? { ...config, secret: resolvedSecret2 } : config;
|
|
849
883
|
|
|
850
884
|
// Keep static betterAuthEnabled in sync with the authInstance state.
|
|
851
885
|
// This is important because forRoot() sets it synchronously, but reset()
|
|
@@ -907,10 +941,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
907
941
|
...(this.shouldRegisterRolesGuardGlobally && !RolesGuardRegistry.isRegistered()
|
|
908
942
|
? (() => {
|
|
909
943
|
RolesGuardRegistry.markRegistered('CoreBetterAuthModule');
|
|
910
|
-
return [
|
|
911
|
-
BetterAuthRolesGuard,
|
|
912
|
-
{ provide: APP_GUARD, useExisting: BetterAuthRolesGuard },
|
|
913
|
-
];
|
|
944
|
+
return [BetterAuthRolesGuard, { provide: APP_GUARD, useExisting: BetterAuthRolesGuard }];
|
|
914
945
|
})()
|
|
915
946
|
: []),
|
|
916
947
|
],
|
|
@@ -979,6 +1010,10 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
979
1010
|
features.push('Sign-Up Checks');
|
|
980
1011
|
}
|
|
981
1012
|
|
|
1013
|
+
if (config.emailAndPassword?.disableSignUp) {
|
|
1014
|
+
features.push('Sign-Up Disabled');
|
|
1015
|
+
}
|
|
1016
|
+
|
|
982
1017
|
if (features.length > 0) {
|
|
983
1018
|
this.logger.log(`Enabled features: ${features.join(', ')}`);
|
|
984
1019
|
}
|
|
@@ -107,10 +107,7 @@ export class CoreBetterAuthResolver {
|
|
|
107
107
|
* @throws UnauthorizedException if email is not verified and verification is required
|
|
108
108
|
*/
|
|
109
109
|
protected checkEmailVerification(sessionUser: BetterAuthSessionUser): void {
|
|
110
|
-
if (
|
|
111
|
-
this.emailVerificationService?.isEnabled()
|
|
112
|
-
&& !sessionUser.emailVerified
|
|
113
|
-
) {
|
|
110
|
+
if (this.emailVerificationService?.isEnabled() && !sessionUser.emailVerified) {
|
|
114
111
|
this.logger.debug(`[SignIn] Email not verified for ${maskEmail(sessionUser.email)}, blocking login`);
|
|
115
112
|
throw new UnauthorizedException(ErrorCode.EMAIL_VERIFICATION_REQUIRED);
|
|
116
113
|
}
|
|
@@ -193,6 +190,7 @@ export class CoreBetterAuthResolver {
|
|
|
193
190
|
enabled: this.betterAuthService.isEnabled(),
|
|
194
191
|
jwt: this.betterAuthService.isJwtEnabled(),
|
|
195
192
|
passkey: this.betterAuthService.isPasskeyEnabled(),
|
|
193
|
+
signUpEnabled: this.betterAuthService.isSignUpEnabled(),
|
|
196
194
|
socialProviders: this.betterAuthService.getEnabledSocialProviders(),
|
|
197
195
|
twoFactor: this.betterAuthService.isTwoFactorEnabled(),
|
|
198
196
|
};
|
|
@@ -303,12 +301,16 @@ export class CoreBetterAuthResolver {
|
|
|
303
301
|
body: { email, password },
|
|
304
302
|
})) as BetterAuthSignInResponse | null;
|
|
305
303
|
|
|
306
|
-
this.logger.debug(
|
|
304
|
+
this.logger.debug(
|
|
305
|
+
`[SignIn] API response for ${maskEmail(email)}: ${JSON.stringify(response)?.substring(0, 200)}`,
|
|
306
|
+
);
|
|
307
307
|
|
|
308
308
|
// Check if response indicates an error (Better-Auth returns error objects, not throws)
|
|
309
309
|
const responseAny = response as any;
|
|
310
310
|
if (responseAny?.error || responseAny?.code === 'CREDENTIAL_ACCOUNT_NOT_FOUND') {
|
|
311
|
-
this.logger.debug(
|
|
311
|
+
this.logger.debug(
|
|
312
|
+
`[SignIn] API returned error for ${maskEmail(email)}: ${responseAny?.error || responseAny?.code}`,
|
|
313
|
+
);
|
|
312
314
|
throw new Error(responseAny?.error || responseAny?.code || 'Credential account not found');
|
|
313
315
|
}
|
|
314
316
|
|
|
@@ -341,7 +343,8 @@ export class CoreBetterAuthResolver {
|
|
|
341
343
|
// 2. token (top-level, some BetterAuth versions)
|
|
342
344
|
// 3. session.token (session-based fallback)
|
|
343
345
|
const responseAny = response as any;
|
|
344
|
-
const rawToken =
|
|
346
|
+
const rawToken =
|
|
347
|
+
responseAny.accessToken || responseAny.token || (hasSession(response) ? response.session.token : undefined);
|
|
345
348
|
const token = await this.resolveJwtToken(rawToken);
|
|
346
349
|
|
|
347
350
|
return {
|
|
@@ -410,7 +413,8 @@ export class CoreBetterAuthResolver {
|
|
|
410
413
|
// 2. token (top-level, some BetterAuth versions)
|
|
411
414
|
// 3. session.token (session-based fallback)
|
|
412
415
|
const responseAny = response as any;
|
|
413
|
-
const rawToken =
|
|
416
|
+
const rawToken =
|
|
417
|
+
responseAny.accessToken || responseAny.token || (hasSession(response) ? response.session.token : undefined);
|
|
414
418
|
const token = await this.resolveJwtToken(rawToken);
|
|
415
419
|
|
|
416
420
|
return {
|
|
@@ -448,6 +452,7 @@ export class CoreBetterAuthResolver {
|
|
|
448
452
|
@Args('termsAndPrivacyAccepted', { nullable: true }) termsAndPrivacyAccepted?: boolean,
|
|
449
453
|
): Promise<CoreBetterAuthAuthModel> {
|
|
450
454
|
this.ensureEnabled();
|
|
455
|
+
this.betterAuthService.ensureSignUpEnabled();
|
|
451
456
|
|
|
452
457
|
// Validate sign-up input (termsAndPrivacyAccepted is required by default)
|
|
453
458
|
if (this.signUpValidator) {
|
|
@@ -487,7 +492,9 @@ export class CoreBetterAuthResolver {
|
|
|
487
492
|
if (sessionToken) {
|
|
488
493
|
await this.betterAuthService.revokeSession(sessionToken);
|
|
489
494
|
}
|
|
490
|
-
this.logger.debug(
|
|
495
|
+
this.logger.debug(
|
|
496
|
+
`[SignUp] Email verification required for ${maskEmail(sessionUser.email)}, session revoked`,
|
|
497
|
+
);
|
|
491
498
|
return {
|
|
492
499
|
emailVerificationRequired: true,
|
|
493
500
|
requiresTwoFactor: false,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Inject, Injectable, Logger, Optional } from '@nestjs/common';
|
|
1
|
+
import { BadRequestException, Inject, Injectable, Logger, Optional } from '@nestjs/common';
|
|
2
2
|
import { InjectConnection } from '@nestjs/mongoose';
|
|
3
3
|
import { Request } from 'express';
|
|
4
4
|
import { importJWK, jwtVerify } from 'jose';
|
|
@@ -7,6 +7,7 @@ import { Connection } from 'mongoose';
|
|
|
7
7
|
import { 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
|
+
import { ErrorCode } from '../error-code/error-codes';
|
|
10
11
|
import { BetterAuthInstance } from './better-auth.config';
|
|
11
12
|
import { BetterAuthSessionUser } from './core-better-auth-user.mapper';
|
|
12
13
|
import { convertExpressHeaders, parseCookieHeader, signCookieValueIfNeeded } from './core-better-auth-web.helper';
|
|
@@ -155,6 +156,26 @@ export class CoreBetterAuthService {
|
|
|
155
156
|
return true;
|
|
156
157
|
}
|
|
157
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Checks if sign-up is enabled.
|
|
161
|
+
* Sign-up is enabled by default unless explicitly disabled via
|
|
162
|
+
* emailAndPassword.disableSignUp: true
|
|
163
|
+
*/
|
|
164
|
+
isSignUpEnabled(): boolean {
|
|
165
|
+
if (!this.isEnabled()) return false;
|
|
166
|
+
return this.config.emailAndPassword?.disableSignUp !== true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Throws BadRequestException if sign-up is disabled.
|
|
171
|
+
* Used by Controller and Resolver as a guard before sign-up logic.
|
|
172
|
+
*/
|
|
173
|
+
ensureSignUpEnabled(): void {
|
|
174
|
+
if (!this.isSignUpEnabled()) {
|
|
175
|
+
throw new BadRequestException(ErrorCode.SIGNUP_DISABLED);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
158
179
|
/**
|
|
159
180
|
* Gets the list of enabled social providers
|
|
160
181
|
* Dynamically iterates over all configured providers.
|
|
@@ -593,10 +614,7 @@ export class CoreBetterAuthService {
|
|
|
593
614
|
{
|
|
594
615
|
$match: {
|
|
595
616
|
$expr: {
|
|
596
|
-
$or: [
|
|
597
|
-
{ $eq: ['$userId', userId] },
|
|
598
|
-
{ $eq: [{ $toString: '$userId' }, userId] },
|
|
599
|
-
],
|
|
617
|
+
$or: [{ $eq: ['$userId', userId] }, { $eq: [{ $toString: '$userId' }, userId] }],
|
|
600
618
|
},
|
|
601
619
|
expiresAt: { $gt: new Date() },
|
|
602
620
|
},
|
|
@@ -690,7 +708,9 @@ export class CoreBetterAuthService {
|
|
|
690
708
|
}
|
|
691
709
|
return !!result.user.emailVerified;
|
|
692
710
|
} catch (error) {
|
|
693
|
-
this.logger.debug(
|
|
711
|
+
this.logger.debug(
|
|
712
|
+
`isUserEmailVerified error for ${maskEmail(email)}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
713
|
+
);
|
|
694
714
|
return null;
|
|
695
715
|
}
|
|
696
716
|
}
|
|
@@ -867,7 +887,10 @@ export class CoreBetterAuthService {
|
|
|
867
887
|
* @param token - Optional JWT token to verify directly (bypasses header extraction)
|
|
868
888
|
* @returns The JWT payload with user info, or null if no valid token
|
|
869
889
|
*/
|
|
870
|
-
async verifyJwtFromRequest(
|
|
890
|
+
async verifyJwtFromRequest(
|
|
891
|
+
req: Request,
|
|
892
|
+
token?: string,
|
|
893
|
+
): Promise<null | {
|
|
871
894
|
[key: string]: any;
|
|
872
895
|
email?: string;
|
|
873
896
|
sub: string;
|
|
@@ -278,6 +278,15 @@ export const LtnsErrors = {
|
|
|
278
278
|
},
|
|
279
279
|
},
|
|
280
280
|
|
|
281
|
+
SIGNUP_DISABLED: {
|
|
282
|
+
code: 'LTNS_0026',
|
|
283
|
+
message: 'Sign-up is currently disabled',
|
|
284
|
+
translations: {
|
|
285
|
+
de: 'Die Registrierung ist derzeit deaktiviert.',
|
|
286
|
+
en: 'Sign-up is currently disabled.',
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
|
|
281
290
|
// =====================================================
|
|
282
291
|
// Authorization Errors (LTNS_0100-LTNS_0199)
|
|
283
292
|
// =====================================================
|