@lenne.tech/nest-server 11.13.3 → 11.13.5
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 +11 -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 +36 -0
- package/dist/core/modules/error-code/error-codes.js +32 -0
- package/dist/core/modules/error-code/error-codes.js.map +1 -1
- package/dist/core/modules/system-setup/core-system-setup.controller.d.ts +12 -0
- package/dist/core/modules/system-setup/core-system-setup.controller.js +86 -0
- package/dist/core/modules/system-setup/core-system-setup.controller.js.map +1 -0
- package/dist/core/modules/system-setup/core-system-setup.module.d.ts +2 -0
- package/dist/core/modules/system-setup/core-system-setup.module.js +22 -0
- package/dist/core/modules/system-setup/core-system-setup.module.js.map +1 -0
- package/dist/core/modules/system-setup/core-system-setup.service.d.ts +30 -0
- package/dist/core/modules/system-setup/core-system-setup.service.js +157 -0
- package/dist/core/modules/system-setup/core-system-setup.service.js.map +1 -0
- package/dist/core.module.js +9 -9
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/server/modules/error-code/error-codes.d.ts +4 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/core/common/interfaces/server-options.interface.ts +86 -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 +40 -0
- package/src/core/modules/system-setup/INTEGRATION-CHECKLIST.md +107 -0
- package/src/core/modules/system-setup/README.md +267 -0
- package/src/core/modules/system-setup/core-system-setup.controller.ts +69 -0
- package/src/core/modules/system-setup/core-system-setup.module.ts +19 -0
- package/src/core/modules/system-setup/core-system-setup.service.ts +226 -0
- package/src/core.module.ts +14 -9
- package/src/index.ts +8 -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.5",
|
|
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",
|
|
@@ -1429,6 +1429,39 @@ export interface IServerOptions {
|
|
|
1429
1429
|
path?: string;
|
|
1430
1430
|
};
|
|
1431
1431
|
|
|
1432
|
+
/**
|
|
1433
|
+
* System setup configuration for initial admin creation.
|
|
1434
|
+
*
|
|
1435
|
+
* When enabled, provides REST endpoints for creating the first admin user
|
|
1436
|
+
* on a fresh deployment (zero users in the database).
|
|
1437
|
+
*
|
|
1438
|
+
* Enabled by default when BetterAuth is active. Explicitly disable with:
|
|
1439
|
+
* - `{ enabled: false }`: Disabled
|
|
1440
|
+
*
|
|
1441
|
+
* Auto-creation via config/ENV (no REST call needed):
|
|
1442
|
+
* - `{ initialAdmin: { email: '...', password: '...' } }`
|
|
1443
|
+
* - `NSC__systemSetup__initialAdmin__email` + `NSC__systemSetup__initialAdmin__password`
|
|
1444
|
+
*
|
|
1445
|
+
* @since 11.14.0
|
|
1446
|
+
*
|
|
1447
|
+
* @example
|
|
1448
|
+
* ```typescript
|
|
1449
|
+
* // Enabled by default (no config needed when BetterAuth is active)
|
|
1450
|
+
*
|
|
1451
|
+
* // Disable explicitly
|
|
1452
|
+
* systemSetup: { enabled: false },
|
|
1453
|
+
*
|
|
1454
|
+
* // Auto-create admin on server start (useful for Docker/CI)
|
|
1455
|
+
* systemSetup: {
|
|
1456
|
+
* initialAdmin: {
|
|
1457
|
+
* email: process.env.INITIAL_ADMIN_EMAIL,
|
|
1458
|
+
* password: process.env.INITIAL_ADMIN_PASSWORD,
|
|
1459
|
+
* },
|
|
1460
|
+
* },
|
|
1461
|
+
* ```
|
|
1462
|
+
*/
|
|
1463
|
+
systemSetup?: ISystemSetup;
|
|
1464
|
+
|
|
1432
1465
|
/**
|
|
1433
1466
|
* Templates
|
|
1434
1467
|
*/
|
|
@@ -1476,6 +1509,49 @@ export interface IServerOptions {
|
|
|
1476
1509
|
tus?: boolean | ITusConfig;
|
|
1477
1510
|
}
|
|
1478
1511
|
|
|
1512
|
+
export interface ISystemSetup {
|
|
1513
|
+
/**
|
|
1514
|
+
* Whether system setup is enabled.
|
|
1515
|
+
* @default true (when BetterAuth is enabled)
|
|
1516
|
+
*/
|
|
1517
|
+
enabled?: boolean;
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* Pre-configured initial admin credentials for automatic creation on server start.
|
|
1521
|
+
*
|
|
1522
|
+
* When set, the service will automatically create the initial admin user
|
|
1523
|
+
* during application bootstrap if zero users exist. This is useful for
|
|
1524
|
+
* automated deployments (Docker, CI/CD) where no manual REST call is possible.
|
|
1525
|
+
*
|
|
1526
|
+
* Can be provided via environment variables:
|
|
1527
|
+
* - `NSC__systemSetup__initialAdmin__email`
|
|
1528
|
+
* - `NSC__systemSetup__initialAdmin__password`
|
|
1529
|
+
* - `NSC__systemSetup__initialAdmin__name` (optional)
|
|
1530
|
+
*
|
|
1531
|
+
* Security: Same zero-user guard applies - only works when no users exist.
|
|
1532
|
+
*/
|
|
1533
|
+
initialAdmin?: ISystemSetupInitialAdmin;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* System setup configuration interface
|
|
1538
|
+
*
|
|
1539
|
+
* Follows the "presence implies enabled" pattern:
|
|
1540
|
+
* - `undefined`: Disabled (default, backward compatible)
|
|
1541
|
+
* - `{}`: Enabled with defaults
|
|
1542
|
+
* - `{ enabled: false }`: Disabled explicitly
|
|
1543
|
+
*
|
|
1544
|
+
* @since 11.14.0
|
|
1545
|
+
*/
|
|
1546
|
+
export interface ISystemSetupInitialAdmin {
|
|
1547
|
+
/** Email address for the initial admin user */
|
|
1548
|
+
email: string;
|
|
1549
|
+
/** Name of the initial admin user (defaults to email prefix) */
|
|
1550
|
+
name?: string;
|
|
1551
|
+
/** Password for the initial admin user (minimum 8 characters) */
|
|
1552
|
+
password: string;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1479
1555
|
/**
|
|
1480
1556
|
* TUS Upload Configuration Interface
|
|
1481
1557
|
*
|
|
@@ -1693,6 +1769,14 @@ interface IBetterAuthBase {
|
|
|
1693
1769
|
* Set `enabled: false` to explicitly disable email/password auth.
|
|
1694
1770
|
*/
|
|
1695
1771
|
emailAndPassword?: {
|
|
1772
|
+
/**
|
|
1773
|
+
* Disable user registration (sign-up) via BetterAuth.
|
|
1774
|
+
* Passed through to better-auth's native emailAndPassword.disableSignUp.
|
|
1775
|
+
* Custom endpoints (GraphQL + REST) also check this flag early.
|
|
1776
|
+
* @default false
|
|
1777
|
+
*/
|
|
1778
|
+
disableSignUp?: boolean;
|
|
1779
|
+
|
|
1696
1780
|
/**
|
|
1697
1781
|
* Whether email/password authentication is enabled.
|
|
1698
1782
|
* @default true
|
|
@@ -1993,10 +2077,7 @@ interface IBetterAuthBase {
|
|
|
1993
2077
|
* };
|
|
1994
2078
|
* ```
|
|
1995
2079
|
*/
|
|
1996
|
-
type IBetterAuthPasskeyDisabled =
|
|
1997
|
-
| false
|
|
1998
|
-
| (Omit<IBetterAuthPasskeyConfig, 'enabled'> & { enabled: false })
|
|
1999
|
-
| undefined;
|
|
2080
|
+
type IBetterAuthPasskeyDisabled = false | (Omit<IBetterAuthPasskeyConfig, 'enabled'> & { enabled: false }) | undefined;
|
|
2000
2081
|
|
|
2001
2082
|
/**
|
|
2002
2083
|
* Passkey configuration that is considered "enabled".
|
|
@@ -2006,9 +2087,7 @@ type IBetterAuthPasskeyDisabled =
|
|
|
2006
2087
|
* - `{ enabled: true, ... }` (explicit enabled)
|
|
2007
2088
|
* - `{ rpName: 'My App', ... }` (config without explicit enabled = defaults to true)
|
|
2008
2089
|
*/
|
|
2009
|
-
type IBetterAuthPasskeyEnabled =
|
|
2010
|
-
| (Omit<IBetterAuthPasskeyConfig, 'enabled'> & { enabled?: true })
|
|
2011
|
-
| true;
|
|
2090
|
+
type IBetterAuthPasskeyEnabled = (Omit<IBetterAuthPasskeyConfig, 'enabled'> & { enabled?: true }) | true;
|
|
2012
2091
|
|
|
2013
2092
|
/**
|
|
2014
2093
|
* 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
|
}
|