@lenne.tech/nest-server 11.13.3 → 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/core/common/interfaces/server-options.interface.d.ts +1 -0
- package/dist/core/modules/better-auth/better-auth.config.js +6 -1
- package/dist/core/modules/better-auth/better-auth.config.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.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 +1 -1
- package/src/core/common/interfaces/server-options.interface.ts +10 -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 +12 -2
- package/src/core/modules/better-auth/core-better-auth-models.ts +3 -0
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "11.13.
|
|
3
|
+
"version": "11.13.4",
|
|
4
4
|
"description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node",
|
|
@@ -1693,6 +1693,14 @@ interface IBetterAuthBase {
|
|
|
1693
1693
|
* Set `enabled: false` to explicitly disable email/password auth.
|
|
1694
1694
|
*/
|
|
1695
1695
|
emailAndPassword?: {
|
|
1696
|
+
/**
|
|
1697
|
+
* Disable user registration (sign-up) via BetterAuth.
|
|
1698
|
+
* Passed through to better-auth's native emailAndPassword.disableSignUp.
|
|
1699
|
+
* Custom endpoints (GraphQL + REST) also check this flag early.
|
|
1700
|
+
* @default false
|
|
1701
|
+
*/
|
|
1702
|
+
disableSignUp?: boolean;
|
|
1703
|
+
|
|
1696
1704
|
/**
|
|
1697
1705
|
* Whether email/password authentication is enabled.
|
|
1698
1706
|
* @default true
|
|
@@ -1993,10 +2001,7 @@ interface IBetterAuthBase {
|
|
|
1993
2001
|
* };
|
|
1994
2002
|
* ```
|
|
1995
2003
|
*/
|
|
1996
|
-
type IBetterAuthPasskeyDisabled =
|
|
1997
|
-
| false
|
|
1998
|
-
| (Omit<IBetterAuthPasskeyConfig, 'enabled'> & { enabled: false })
|
|
1999
|
-
| undefined;
|
|
2004
|
+
type IBetterAuthPasskeyDisabled = false | (Omit<IBetterAuthPasskeyConfig, 'enabled'> & { enabled: false }) | undefined;
|
|
2000
2005
|
|
|
2001
2006
|
/**
|
|
2002
2007
|
* Passkey configuration that is considered "enabled".
|
|
@@ -2006,9 +2011,7 @@ type IBetterAuthPasskeyDisabled =
|
|
|
2006
2011
|
* - `{ enabled: true, ... }` (explicit enabled)
|
|
2007
2012
|
* - `{ rpName: 'My App', ... }` (config without explicit enabled = defaults to true)
|
|
2008
2013
|
*/
|
|
2009
|
-
type IBetterAuthPasskeyEnabled =
|
|
2010
|
-
| (Omit<IBetterAuthPasskeyConfig, 'enabled'> & { enabled?: true })
|
|
2011
|
-
| true;
|
|
2014
|
+
type IBetterAuthPasskeyEnabled = (Omit<IBetterAuthPasskeyConfig, 'enabled'> & { enabled?: true }) | true;
|
|
2012
2015
|
|
|
2013
2016
|
/**
|
|
2014
2017
|
* BetterAuth configuration WITHOUT Passkey (or Passkey disabled).
|
|
@@ -313,6 +313,12 @@ After integration, verify:
|
|
|
313
313
|
- [ ] Passkey login redirects to dashboard after successful authentication
|
|
314
314
|
- [ ] Passkey can be registered, listed, and deleted from security settings
|
|
315
315
|
|
|
316
|
+
### Optional: Disable Sign-Up (`emailAndPassword.disableSignUp: true`)
|
|
317
|
+
- [ ] REST `POST /iam/sign-up/email` returns `400` with error `LTNS_0026`
|
|
318
|
+
- [ ] GraphQL `betterAuthSignUp` returns error `LTNS_0026`
|
|
319
|
+
- [ ] `GET /iam/features` reports `signUpEnabled: false`
|
|
320
|
+
- [ ] Sign-in still works for existing users
|
|
321
|
+
|
|
316
322
|
### Additional checks for Migration scenario:
|
|
317
323
|
- [ ] Sign-in via Legacy Auth works for BetterAuth-created users
|
|
318
324
|
- [ ] Sign-in via BetterAuth works for Legacy-created users
|
|
@@ -574,6 +574,32 @@ const config = {
|
|
|
574
574
|
};
|
|
575
575
|
```
|
|
576
576
|
|
|
577
|
+
### Disable Sign-Up
|
|
578
|
+
|
|
579
|
+
Disable user registration while keeping sign-in active (e.g., invite-only apps, admin-created accounts):
|
|
580
|
+
|
|
581
|
+
```typescript
|
|
582
|
+
const config = {
|
|
583
|
+
betterAuth: {
|
|
584
|
+
emailAndPassword: {
|
|
585
|
+
disableSignUp: true, // Block new registrations (REST + GraphQL)
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
When disabled:
|
|
592
|
+
- REST `POST /iam/sign-up/email` returns `400 Bad Request` with error `LTNS_0026`
|
|
593
|
+
- GraphQL `betterAuthSignUp` mutation returns error `LTNS_0026`
|
|
594
|
+
- `betterAuthFeatures` reports `signUpEnabled: false`
|
|
595
|
+
- Sign-in continues to work for existing users
|
|
596
|
+
|
|
597
|
+
**Defense in Depth:** The flag is enforced at two layers:
|
|
598
|
+
1. **Custom check** (`CoreBetterAuthService.ensureSignUpEnabled()`) runs in Controller/Resolver *before* any BetterAuth API call and returns a structured `LTNS_0026` error.
|
|
599
|
+
2. **Native BetterAuth** `emailAndPassword.disableSignUp` acts as a safety net for any direct API access that bypasses the custom check.
|
|
600
|
+
|
|
601
|
+
**Default:** `false` (sign-up enabled) - fully backward compatible.
|
|
602
|
+
|
|
577
603
|
### Additional User Fields
|
|
578
604
|
|
|
579
605
|
Add custom fields to the Better-Auth user schema:
|
|
@@ -1049,6 +1075,7 @@ type CoreBetterAuthFeaturesModel {
|
|
|
1049
1075
|
jwt: Boolean!
|
|
1050
1076
|
twoFactor: Boolean!
|
|
1051
1077
|
passkey: Boolean!
|
|
1078
|
+
signUpEnabled: Boolean!
|
|
1052
1079
|
socialProviders: [String!]!
|
|
1053
1080
|
}
|
|
1054
1081
|
```
|
|
@@ -1100,6 +1127,7 @@ query {
|
|
|
1100
1127
|
jwt
|
|
1101
1128
|
twoFactor
|
|
1102
1129
|
passkey
|
|
1130
|
+
signUpEnabled
|
|
1103
1131
|
socialProviders
|
|
1104
1132
|
}
|
|
1105
1133
|
}
|
|
@@ -1247,6 +1275,7 @@ export class MyService {
|
|
|
1247
1275
|
| `isJwtEnabled()` | Check if JWT plugin is enabled |
|
|
1248
1276
|
| `isTwoFactorEnabled()` | Check if 2FA is enabled |
|
|
1249
1277
|
| `isPasskeyEnabled()` | Check if Passkey is enabled |
|
|
1278
|
+
| `isSignUpEnabled()` | Check if sign-up is enabled |
|
|
1250
1279
|
| `getEnabledSocialProviders()` | Get list of enabled social providers |
|
|
1251
1280
|
| `getBasePath()` | Get the base path for endpoints |
|
|
1252
1281
|
| `getBaseUrl()` | Get the base URL |
|
|
@@ -1970,6 +1999,9 @@ These protected methods are available for use in your custom resolver:
|
|
|
1970
1999
|
// Check if Better-Auth is enabled (throws if not)
|
|
1971
2000
|
this.ensureEnabled();
|
|
1972
2001
|
|
|
2002
|
+
// Check if sign-up is enabled (throws if not) - delegated to service
|
|
2003
|
+
this.betterAuthService.ensureSignUpEnabled();
|
|
2004
|
+
|
|
1973
2005
|
// Convert Express headers to Web API Headers
|
|
1974
2006
|
const headers = this.convertHeaders(ctx.req.headers);
|
|
1975
2007
|
|
|
@@ -314,6 +314,12 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Bett
|
|
|
314
314
|
// Enable email/password authentication by default (required by Better-Auth 1.x)
|
|
315
315
|
// Can be disabled by setting config.emailAndPassword.enabled = false
|
|
316
316
|
emailAndPassword: {
|
|
317
|
+
// Defense in Depth: This native Better-Auth flag is the second layer.
|
|
318
|
+
// The first layer is CoreBetterAuthService.ensureSignUpEnabled() which
|
|
319
|
+
// runs in Controller/Resolver BEFORE the BetterAuth API is called and
|
|
320
|
+
// returns a structured LTNS_0026 error. The native flag acts as a safety
|
|
321
|
+
// net in case the custom check is bypassed (e.g., direct API calls).
|
|
322
|
+
disableSignUp: config.emailAndPassword?.disableSignUp === true,
|
|
317
323
|
enabled: config.emailAndPassword?.enabled !== false,
|
|
318
324
|
password: {
|
|
319
325
|
hash: nativeScryptHash,
|
|
@@ -426,7 +432,7 @@ function buildEmailVerificationConfig(
|
|
|
426
432
|
_request?: Request,
|
|
427
433
|
) => {
|
|
428
434
|
// Don't await to prevent timing attacks (as recommended by Better-Auth docs)
|
|
429
|
-
|
|
435
|
+
|
|
430
436
|
sendVerificationEmail(data);
|
|
431
437
|
};
|
|
432
438
|
}
|
|
@@ -918,7 +924,11 @@ function normalizePasskeyConfig(
|
|
|
918
924
|
// Resolve values: explicit config > resolved URLs
|
|
919
925
|
const finalRpId = rawConfig.rpId || resolvedUrls.rpId;
|
|
920
926
|
const finalOrigin = rawConfig.origin || resolvedUrls.appUrl;
|
|
921
|
-
const finalTrustedOrigins = config.trustedOrigins?.length
|
|
927
|
+
const finalTrustedOrigins = config.trustedOrigins?.length
|
|
928
|
+
? config.trustedOrigins
|
|
929
|
+
: resolvedUrls.appUrl
|
|
930
|
+
? [resolvedUrls.appUrl]
|
|
931
|
+
: undefined;
|
|
922
932
|
|
|
923
933
|
// Check if we have all required values for Passkey
|
|
924
934
|
const hasRequiredConfig = finalRpId && finalOrigin && finalTrustedOrigins?.length;
|
|
@@ -135,6 +135,9 @@ export class CoreBetterAuthFeaturesModel {
|
|
|
135
135
|
@Field(() => Boolean, { description: 'Whether Passkey is enabled' })
|
|
136
136
|
passkey: boolean;
|
|
137
137
|
|
|
138
|
+
@Field(() => Boolean, { description: 'Whether sign-up is enabled' })
|
|
139
|
+
signUpEnabled: boolean;
|
|
140
|
+
|
|
138
141
|
@Field(() => [String], { description: 'List of enabled social providers' })
|
|
139
142
|
socialProviders: string[];
|
|
140
143
|
|
|
@@ -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,
|