@lenne.tech/nest-server 11.12.0 → 11.13.0
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 +1 -0
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +16 -0
- package/dist/core/modules/better-auth/better-auth.config.d.ts +13 -0
- package/dist/core/modules/better-auth/better-auth.config.js +68 -18
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.resolver.d.ts +7 -3
- package/dist/core/modules/better-auth/better-auth.resolver.js +16 -6
- package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.d.ts +2 -2
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +46 -2
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-auth.model.d.ts +1 -0
- package/dist/core/modules/better-auth/core-better-auth-auth.model.js +7 -0
- package/dist/core/modules/better-auth/core-better-auth-auth.model.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.d.ts +48 -0
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +241 -0
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-models.d.ts +2 -1
- package/dist/core/modules/better-auth/core-better-auth-models.js +8 -4
- package/dist/core/modules/better-auth/core-better-auth-models.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-signup-validator.service.d.ts +18 -0
- package/dist/core/modules/better-auth/core-better-auth-signup-validator.service.js +82 -0
- package/dist/core/modules/better-auth/core-better-auth-signup-validator.service.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.d.ts +0 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +15 -8
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-web.helper.d.ts +1 -0
- package/dist/core/modules/better-auth/core-better-auth-web.helper.js +33 -24
- package/dist/core/modules/better-auth/core-better-auth-web.helper.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +11 -1
- package/dist/core/modules/better-auth/core-better-auth.controller.js +91 -15
- package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.middleware.js +55 -19
- package/dist/core/modules/better-auth/core-better-auth.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.module.d.ts +6 -0
- package/dist/core/modules/better-auth/core-better-auth.module.js +129 -24
- package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +12 -5
- package/dist/core/modules/better-auth/core-better-auth.resolver.js +64 -17
- 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 +4 -1
- package/dist/core/modules/better-auth/core-better-auth.service.js +123 -23
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/core/modules/better-auth/index.d.ts +2 -0
- package/dist/core/modules/better-auth/index.js +2 -0
- package/dist/core/modules/better-auth/index.js.map +1 -1
- package/dist/core/modules/error-code/error-codes.d.ts +45 -0
- package/dist/core/modules/error-code/error-codes.js +40 -0
- package/dist/core/modules/error-code/error-codes.js.map +1 -1
- package/dist/core/modules/user/core-user.model.d.ts +1 -0
- package/dist/core/modules/user/core-user.model.js +11 -0
- package/dist/core/modules/user/core-user.model.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.controller.d.ts +3 -1
- package/dist/server/modules/better-auth/better-auth.controller.js +12 -3
- package/dist/server/modules/better-auth/better-auth.controller.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.resolver.d.ts +7 -3
- package/dist/server/modules/better-auth/better-auth.resolver.js +16 -6
- package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -1
- package/dist/server/modules/error-code/error-codes.d.ts +5 -0
- package/dist/server/modules/user/user.model.d.ts +5 -0
- package/dist/templates/email-verification-de.ejs +78 -0
- package/dist/templates/email-verification-en.ejs +78 -0
- package/dist/test/test.helper.d.ts +4 -0
- package/dist/test/test.helper.js +54 -1
- package/dist/test/test.helper.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/config.env.ts +2 -0
- package/src/core/common/interfaces/server-options.interface.ts +240 -0
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +113 -0
- package/src/core/modules/better-auth/README.md +72 -7
- package/src/core/modules/better-auth/better-auth.config.ts +168 -42
- package/src/core/modules/better-auth/better-auth.resolver.ts +16 -5
- package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +65 -8
- package/src/core/modules/better-auth/core-better-auth-auth.model.ts +10 -0
- package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +433 -0
- package/src/core/modules/better-auth/core-better-auth-models.ts +6 -3
- package/src/core/modules/better-auth/core-better-auth-signup-validator.service.ts +178 -0
- package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +18 -14
- package/src/core/modules/better-auth/core-better-auth-web.helper.ts +53 -40
- package/src/core/modules/better-auth/core-better-auth.controller.ts +155 -16
- package/src/core/modules/better-auth/core-better-auth.middleware.ts +89 -31
- package/src/core/modules/better-auth/core-better-auth.module.ts +215 -38
- package/src/core/modules/better-auth/core-better-auth.resolver.ts +140 -20
- package/src/core/modules/better-auth/core-better-auth.service.ts +181 -25
- package/src/core/modules/better-auth/index.ts +2 -0
- package/src/core/modules/error-code/error-codes.ts +45 -0
- package/src/core/modules/user/core-user.model.ts +15 -0
- package/src/server/modules/better-auth/better-auth.controller.ts +6 -2
- package/src/server/modules/better-auth/better-auth.resolver.ts +16 -5
- package/src/templates/email-verification-de.ejs +78 -0
- package/src/templates/email-verification-en.ejs +78 -0
- package/src/test/README.md +190 -0
- package/src/test/test.helper.ts +82 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BadRequestException, Logger, UnauthorizedException } from '@nestjs/common';
|
|
1
|
+
import { BadRequestException, Logger, Optional, UnauthorizedException } from '@nestjs/common';
|
|
2
2
|
import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
|
|
3
3
|
import { Request, Response } from 'express';
|
|
4
4
|
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
requires2FA,
|
|
16
16
|
} from './better-auth.types';
|
|
17
17
|
import { CoreBetterAuthAuthModel } from './core-better-auth-auth.model';
|
|
18
|
+
import { CoreBetterAuthEmailVerificationService } from './core-better-auth-email-verification.service';
|
|
18
19
|
import { CoreBetterAuthMigrationStatusModel } from './core-better-auth-migration-status.model';
|
|
19
20
|
import {
|
|
20
21
|
CoreBetterAuth2FASetupModel,
|
|
@@ -24,7 +25,9 @@ import {
|
|
|
24
25
|
CoreBetterAuthSessionModel,
|
|
25
26
|
CoreBetterAuthUserModel,
|
|
26
27
|
} from './core-better-auth-models';
|
|
28
|
+
import { CoreBetterAuthSignUpValidatorService } from './core-better-auth-signup-validator.service';
|
|
27
29
|
import { BetterAuthSessionUser, CoreBetterAuthUserMapper, MappedUser } from './core-better-auth-user.mapper';
|
|
30
|
+
import { convertExpressHeaders } from './core-better-auth-web.helper';
|
|
28
31
|
import { CoreBetterAuthService } from './core-better-auth.service';
|
|
29
32
|
|
|
30
33
|
/**
|
|
@@ -69,8 +72,72 @@ export class CoreBetterAuthResolver {
|
|
|
69
72
|
constructor(
|
|
70
73
|
protected readonly betterAuthService: CoreBetterAuthService,
|
|
71
74
|
protected readonly userMapper: CoreBetterAuthUserMapper,
|
|
75
|
+
@Optional() protected readonly signUpValidator?: CoreBetterAuthSignUpValidatorService,
|
|
76
|
+
@Optional() protected readonly emailVerificationService?: CoreBetterAuthEmailVerificationService,
|
|
72
77
|
) {}
|
|
73
78
|
|
|
79
|
+
// ===========================================================================
|
|
80
|
+
// Token Resolution
|
|
81
|
+
// ===========================================================================
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolves a session token to a JWT when cookies are disabled and JWT is enabled.
|
|
85
|
+
* Delegates to CoreBetterAuthService.resolveJwtToken().
|
|
86
|
+
*
|
|
87
|
+
* @param token - The token from BetterAuth response (may be session token or JWT)
|
|
88
|
+
* @returns A proper JWT token, or the original token if conversion is not needed/possible
|
|
89
|
+
*/
|
|
90
|
+
protected async resolveJwtToken(token: string | undefined): Promise<string | undefined> {
|
|
91
|
+
return this.betterAuthService.resolveJwtToken(token);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ===========================================================================
|
|
95
|
+
// Email Verification
|
|
96
|
+
// ===========================================================================
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if email verification is required and the user's email is verified.
|
|
100
|
+
* Throws UnauthorizedException with EMAIL_VERIFICATION_REQUIRED if:
|
|
101
|
+
* - emailVerificationService is available AND enabled
|
|
102
|
+
* - AND the user's email is NOT verified
|
|
103
|
+
*
|
|
104
|
+
* Override this method to customize the email verification check behavior.
|
|
105
|
+
*
|
|
106
|
+
* @param sessionUser - The user from Better-Auth sign-in response
|
|
107
|
+
* @throws UnauthorizedException if email is not verified and verification is required
|
|
108
|
+
*/
|
|
109
|
+
protected checkEmailVerification(sessionUser: BetterAuthSessionUser): void {
|
|
110
|
+
if (
|
|
111
|
+
this.emailVerificationService?.isEnabled()
|
|
112
|
+
&& !sessionUser.emailVerified
|
|
113
|
+
) {
|
|
114
|
+
this.logger.debug(`[SignIn] Email not verified for ${maskEmail(sessionUser.email)}, blocking login`);
|
|
115
|
+
throw new UnauthorizedException(ErrorCode.EMAIL_VERIFICATION_REQUIRED);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check email verification by looking up the user by email address.
|
|
121
|
+
*
|
|
122
|
+
* This is used in the 2FA path where the sign-in response does NOT include user data
|
|
123
|
+
* (only a 2FA challenge), so we cannot use checkEmailVerification(sessionUser).
|
|
124
|
+
* Instead, we look up the user via CoreBetterAuthService.isUserEmailVerified().
|
|
125
|
+
*
|
|
126
|
+
* @param email - The email address to check
|
|
127
|
+
* @throws UnauthorizedException if email is not verified and verification is required
|
|
128
|
+
*/
|
|
129
|
+
protected async checkEmailVerificationByEmail(email: string): Promise<void> {
|
|
130
|
+
if (!this.emailVerificationService?.isEnabled()) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const verified = await this.betterAuthService.isUserEmailVerified(email);
|
|
135
|
+
if (verified === false) {
|
|
136
|
+
this.logger.debug(`[SignIn/2FA] Email not verified for ${maskEmail(email)}, blocking login`);
|
|
137
|
+
throw new UnauthorizedException(ErrorCode.EMAIL_VERIFICATION_REQUIRED);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
74
141
|
// ===========================================================================
|
|
75
142
|
// Queries
|
|
76
143
|
// ===========================================================================
|
|
@@ -122,6 +189,7 @@ export class CoreBetterAuthResolver {
|
|
|
122
189
|
@Roles(RoleEnum.S_EVERYONE)
|
|
123
190
|
betterAuthFeatures(): CoreBetterAuthFeaturesModel {
|
|
124
191
|
return {
|
|
192
|
+
emailVerification: this.emailVerificationService?.isEnabled() ?? false,
|
|
125
193
|
enabled: this.betterAuthService.isEnabled(),
|
|
126
194
|
jwt: this.betterAuthService.isJwtEnabled(),
|
|
127
195
|
passkey: this.betterAuthService.isPasskeyEnabled(),
|
|
@@ -189,6 +257,11 @@ export class CoreBetterAuthResolver {
|
|
|
189
257
|
* but not in IAM, they will be automatically migrated on first IAM sign-in.
|
|
190
258
|
*
|
|
191
259
|
* Override this method to add custom pre/post sign-in logic.
|
|
260
|
+
*
|
|
261
|
+
* NOTE: The `_ctx` parameter is intentionally kept but unused in the base implementation.
|
|
262
|
+
* It provides override flexibility for consuming projects that need access to the Express
|
|
263
|
+
* Request/Response context (e.g., for IP logging, custom cookie handling, audit trails).
|
|
264
|
+
* DO NOT REMOVE this parameter - it would break existing project overrides.
|
|
192
265
|
*/
|
|
193
266
|
@Mutation(() => CoreBetterAuthAuthModel, {
|
|
194
267
|
description: 'Sign in via Better-Auth (email/password)',
|
|
@@ -197,8 +270,8 @@ export class CoreBetterAuthResolver {
|
|
|
197
270
|
async betterAuthSignIn(
|
|
198
271
|
@Args('email') email: string,
|
|
199
272
|
@Args('password') password: string,
|
|
200
|
-
|
|
201
|
-
@Context() _ctx
|
|
273
|
+
// Kept for override flexibility - projects may need req/res in their overrides
|
|
274
|
+
@Context() _ctx?: { req: Request; res: Response },
|
|
202
275
|
): Promise<CoreBetterAuthAuthModel> {
|
|
203
276
|
this.ensureEnabled();
|
|
204
277
|
|
|
@@ -245,6 +318,8 @@ export class CoreBetterAuthResolver {
|
|
|
245
318
|
|
|
246
319
|
// Check for 2FA requirement
|
|
247
320
|
if (requires2FA(response)) {
|
|
321
|
+
// Defense-in-depth: Check email verification even for 2FA users
|
|
322
|
+
await this.checkEmailVerificationByEmail(email);
|
|
248
323
|
return {
|
|
249
324
|
requiresTwoFactor: true,
|
|
250
325
|
success: false,
|
|
@@ -255,13 +330,19 @@ export class CoreBetterAuthResolver {
|
|
|
255
330
|
// Get user data
|
|
256
331
|
if (hasUser(response)) {
|
|
257
332
|
const sessionUser: BetterAuthSessionUser = response.user;
|
|
333
|
+
|
|
334
|
+
// Check email verification requirement before allowing login
|
|
335
|
+
this.checkEmailVerification(sessionUser);
|
|
336
|
+
|
|
258
337
|
const mappedUser = await this.userMapper.mapSessionUser(sessionUser);
|
|
259
338
|
|
|
260
|
-
// Return the
|
|
261
|
-
//
|
|
262
|
-
//
|
|
339
|
+
// Return the best available token:
|
|
340
|
+
// 1. accessToken (JWT plugin enriched response)
|
|
341
|
+
// 2. token (top-level, some BetterAuth versions)
|
|
342
|
+
// 3. session.token (session-based fallback)
|
|
263
343
|
const responseAny = response as any;
|
|
264
|
-
const
|
|
344
|
+
const rawToken = responseAny.accessToken || responseAny.token || (hasSession(response) ? response.session.token : undefined);
|
|
345
|
+
const token = await this.resolveJwtToken(rawToken);
|
|
265
346
|
|
|
266
347
|
return {
|
|
267
348
|
requiresTwoFactor: false,
|
|
@@ -299,7 +380,7 @@ export class CoreBetterAuthResolver {
|
|
|
299
380
|
/**
|
|
300
381
|
* Direct sign-in attempt without migration logic (used after migration)
|
|
301
382
|
*/
|
|
302
|
-
|
|
383
|
+
protected async attemptSignInDirect(
|
|
303
384
|
email: string,
|
|
304
385
|
password: string,
|
|
305
386
|
api: ReturnType<CoreBetterAuthService['getApi']>,
|
|
@@ -313,14 +394,24 @@ export class CoreBetterAuthResolver {
|
|
|
313
394
|
}
|
|
314
395
|
|
|
315
396
|
if (requires2FA(response)) {
|
|
397
|
+
// Defense-in-depth: Check email verification even for 2FA users
|
|
398
|
+
await this.checkEmailVerificationByEmail(email);
|
|
316
399
|
return { requiresTwoFactor: true, success: false, user: null };
|
|
317
400
|
}
|
|
318
401
|
|
|
319
402
|
const sessionUser: BetterAuthSessionUser = response.user;
|
|
403
|
+
|
|
404
|
+
// Check email verification requirement before allowing login
|
|
405
|
+
this.checkEmailVerification(sessionUser);
|
|
406
|
+
|
|
320
407
|
const mappedUser = await this.userMapper.mapSessionUser(sessionUser);
|
|
321
|
-
// Return
|
|
408
|
+
// Return the best available token:
|
|
409
|
+
// 1. accessToken (JWT plugin enriched response)
|
|
410
|
+
// 2. token (top-level, some BetterAuth versions)
|
|
411
|
+
// 3. session.token (session-based fallback)
|
|
322
412
|
const responseAny = response as any;
|
|
323
|
-
const
|
|
413
|
+
const rawToken = responseAny.accessToken || responseAny.token || (hasSession(response) ? response.session.token : undefined);
|
|
414
|
+
const token = await this.resolveJwtToken(rawToken);
|
|
324
415
|
|
|
325
416
|
return {
|
|
326
417
|
requiresTwoFactor: false,
|
|
@@ -335,6 +426,16 @@ export class CoreBetterAuthResolver {
|
|
|
335
426
|
* Sign up via Better-Auth
|
|
336
427
|
*
|
|
337
428
|
* Override this method to add custom pre/post sign-up logic (e.g., sending welcome emails).
|
|
429
|
+
*
|
|
430
|
+
* By default, `termsAndPrivacyAccepted` must be `true` for sign-up to succeed.
|
|
431
|
+
* This can be configured via `betterAuth.signUpChecks` in your server config:
|
|
432
|
+
* - `signUpChecks: false` - Disable all sign-up checks
|
|
433
|
+
* - `signUpChecks: { requiredFields: ['termsAndPrivacyAccepted', 'ageConfirmed'] }` - Custom fields
|
|
434
|
+
*
|
|
435
|
+
* @param email - User email address
|
|
436
|
+
* @param password - User password
|
|
437
|
+
* @param name - Optional display name
|
|
438
|
+
* @param termsAndPrivacyAccepted - Whether user accepted terms and privacy policy (required by default)
|
|
338
439
|
*/
|
|
339
440
|
@Mutation(() => CoreBetterAuthAuthModel, {
|
|
340
441
|
description: 'Sign up via Better-Auth (email/password)',
|
|
@@ -344,9 +445,15 @@ export class CoreBetterAuthResolver {
|
|
|
344
445
|
@Args('email') email: string,
|
|
345
446
|
@Args('password') password: string,
|
|
346
447
|
@Args('name', { nullable: true }) name?: string,
|
|
448
|
+
@Args('termsAndPrivacyAccepted', { nullable: true }) termsAndPrivacyAccepted?: boolean,
|
|
347
449
|
): Promise<CoreBetterAuthAuthModel> {
|
|
348
450
|
this.ensureEnabled();
|
|
349
451
|
|
|
452
|
+
// Validate sign-up input (termsAndPrivacyAccepted is required by default)
|
|
453
|
+
if (this.signUpValidator) {
|
|
454
|
+
this.signUpValidator.validateSignUpInput({ termsAndPrivacyAccepted });
|
|
455
|
+
}
|
|
456
|
+
|
|
350
457
|
const api = this.betterAuthService.getApi();
|
|
351
458
|
if (!api) {
|
|
352
459
|
throw new BadRequestException(ErrorCode.BETTERAUTH_API_NOT_AVAILABLE);
|
|
@@ -369,9 +476,26 @@ export class CoreBetterAuthResolver {
|
|
|
369
476
|
const sessionUser: BetterAuthSessionUser = response.user;
|
|
370
477
|
|
|
371
478
|
// Link or create user in our database
|
|
372
|
-
|
|
479
|
+
// Pass termsAndPrivacyAccepted to store the acceptance timestamp
|
|
480
|
+
await this.userMapper.linkOrCreateUser(sessionUser, { termsAndPrivacyAccepted });
|
|
373
481
|
const mappedUser = await this.userMapper.mapSessionUser(sessionUser);
|
|
374
482
|
|
|
483
|
+
// If email verification is enabled, revoke the session and don't return session data
|
|
484
|
+
// The user must verify their email before they can use any session
|
|
485
|
+
if (this.emailVerificationService?.isEnabled()) {
|
|
486
|
+
const sessionToken = hasSession(response) ? response.session.token : undefined;
|
|
487
|
+
if (sessionToken) {
|
|
488
|
+
await this.betterAuthService.revokeSession(sessionToken);
|
|
489
|
+
}
|
|
490
|
+
this.logger.debug(`[SignUp] Email verification required for ${maskEmail(sessionUser.email)}, session revoked`);
|
|
491
|
+
return {
|
|
492
|
+
emailVerificationRequired: true,
|
|
493
|
+
requiresTwoFactor: false,
|
|
494
|
+
success: true,
|
|
495
|
+
user: mappedUser ? this.mapToUserModel(mappedUser) : null,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
375
499
|
return {
|
|
376
500
|
requiresTwoFactor: false,
|
|
377
501
|
session: hasSession(response) ? this.mapSessionInfo(response.session) : null,
|
|
@@ -387,6 +511,10 @@ export class CoreBetterAuthResolver {
|
|
|
387
511
|
if (errorMessage.includes('already exists')) {
|
|
388
512
|
throw new BadRequestException(ErrorCode.EMAIL_ALREADY_EXISTS);
|
|
389
513
|
}
|
|
514
|
+
// Re-throw BadRequestException (e.g., from sign-up validation)
|
|
515
|
+
if (error instanceof BadRequestException) {
|
|
516
|
+
throw error;
|
|
517
|
+
}
|
|
390
518
|
throw new BadRequestException(ErrorCode.SIGNUP_FAILED);
|
|
391
519
|
}
|
|
392
520
|
}
|
|
@@ -782,15 +910,7 @@ export class CoreBetterAuthResolver {
|
|
|
782
910
|
* Convert Express headers to Web API Headers
|
|
783
911
|
*/
|
|
784
912
|
protected convertHeaders(headers: Record<string, string | string[] | undefined>): Headers {
|
|
785
|
-
|
|
786
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
787
|
-
if (typeof value === 'string') {
|
|
788
|
-
result.set(key, value);
|
|
789
|
-
} else if (Array.isArray(value)) {
|
|
790
|
-
result.set(key, value.join(', '));
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
return result;
|
|
913
|
+
return convertExpressHeaders(headers);
|
|
794
914
|
}
|
|
795
915
|
|
|
796
916
|
/**
|
|
@@ -9,7 +9,7 @@ import { IBetterAuth } from '../../common/interfaces/server-options.interface';
|
|
|
9
9
|
import { ConfigService } from '../../common/services/config.service';
|
|
10
10
|
import { BetterAuthInstance } from './better-auth.config';
|
|
11
11
|
import { BetterAuthSessionUser } from './core-better-auth-user.mapper';
|
|
12
|
-
import { parseCookieHeader, signCookieValueIfNeeded } from './core-better-auth-web.helper';
|
|
12
|
+
import { convertExpressHeaders, parseCookieHeader, signCookieValueIfNeeded } from './core-better-auth-web.helper';
|
|
13
13
|
import { BETTER_AUTH_INSTANCE } from './core-better-auth.module';
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -216,6 +216,48 @@ export class CoreBetterAuthService {
|
|
|
216
216
|
// JWT Token Methods
|
|
217
217
|
// ===================================================================================================================
|
|
218
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Resolves a session token to a JWT when cookies are disabled and JWT is enabled.
|
|
221
|
+
*
|
|
222
|
+
* When cookies are disabled, BetterAuth's internal API may return a session token
|
|
223
|
+
* (opaque string like "TVRRtiL19h9q...") instead of a JWT. This method detects
|
|
224
|
+
* non-JWT tokens and converts them to proper JWTs via the BetterAuth JWT plugin.
|
|
225
|
+
*
|
|
226
|
+
* @param token - The token from BetterAuth response (may be session token or JWT)
|
|
227
|
+
* @returns A proper JWT token, or the original token if conversion is not needed/possible
|
|
228
|
+
*/
|
|
229
|
+
async resolveJwtToken(token: string | undefined): Promise<string | undefined> {
|
|
230
|
+
if (!token) return undefined;
|
|
231
|
+
|
|
232
|
+
const cookiesEnabled = ConfigService.configFastButReadOnly?.cookies !== false;
|
|
233
|
+
if (cookiesEnabled || !this.isJwtEnabled()) {
|
|
234
|
+
return token;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Already a JWT (three base64url segments separated by dots)
|
|
238
|
+
if (token.startsWith('eyJ') && token.split('.').length === 3) {
|
|
239
|
+
return token;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Convert session token to JWT via BetterAuth JWT plugin
|
|
243
|
+
// BetterAuth identifies sessions via the session cookie, so we pass
|
|
244
|
+
// the session token as a cookie header (signed, as BetterAuth expects)
|
|
245
|
+
const cookieName = this.getSessionCookieName();
|
|
246
|
+
const secret = this.config?.secret || '';
|
|
247
|
+
const signedToken = secret ? signCookieValueIfNeeded(token, secret, true) : encodeURIComponent(token);
|
|
248
|
+
const jwt = await this.getToken({
|
|
249
|
+
headers: { cookie: `${cookieName}=${signedToken}` },
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
if (jwt) {
|
|
253
|
+
this.logger.debug('Resolved session token to JWT for cookie-less mode');
|
|
254
|
+
return jwt;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this.logger.warn('Failed to resolve session token to JWT - returning session token as fallback');
|
|
258
|
+
return token;
|
|
259
|
+
}
|
|
260
|
+
|
|
219
261
|
/**
|
|
220
262
|
* Gets a fresh JWT token for the current session.
|
|
221
263
|
*
|
|
@@ -249,16 +291,8 @@ export class CoreBetterAuthService {
|
|
|
249
291
|
|
|
250
292
|
try {
|
|
251
293
|
// Convert headers to the format Better-Auth expects
|
|
252
|
-
const headers = new Headers();
|
|
253
294
|
const reqHeaders = 'headers' in req ? req.headers : {};
|
|
254
|
-
|
|
255
|
-
for (const [key, value] of Object.entries(reqHeaders)) {
|
|
256
|
-
if (typeof value === 'string') {
|
|
257
|
-
headers.set(key, value);
|
|
258
|
-
} else if (Array.isArray(value)) {
|
|
259
|
-
headers.set(key, value.join(', '));
|
|
260
|
-
}
|
|
261
|
-
}
|
|
295
|
+
const headers = convertExpressHeaders(reqHeaders as Record<string, string | string[] | undefined>);
|
|
262
296
|
|
|
263
297
|
// Call the token endpoint via Better-Auth API
|
|
264
298
|
// The jwt plugin adds a getToken method to the API
|
|
@@ -306,16 +340,8 @@ export class CoreBetterAuthService {
|
|
|
306
340
|
|
|
307
341
|
try {
|
|
308
342
|
// Convert headers to the format Better-Auth expects
|
|
309
|
-
const headers = new Headers();
|
|
310
343
|
const reqHeaders = 'headers' in req ? req.headers : {};
|
|
311
|
-
|
|
312
|
-
for (const [key, value] of Object.entries(reqHeaders)) {
|
|
313
|
-
if (typeof value === 'string') {
|
|
314
|
-
headers.set(key, value);
|
|
315
|
-
} else if (Array.isArray(value)) {
|
|
316
|
-
headers.set(key, value.join(', '));
|
|
317
|
-
}
|
|
318
|
-
}
|
|
344
|
+
const headers = convertExpressHeaders(reqHeaders as Record<string, string | string[] | undefined>);
|
|
319
345
|
|
|
320
346
|
// Sign cookies before sending to Better-Auth API
|
|
321
347
|
// Browser clients send unsigned cookies, but Better-Auth expects signed cookies
|
|
@@ -543,6 +569,132 @@ export class CoreBetterAuthService {
|
|
|
543
569
|
}
|
|
544
570
|
}
|
|
545
571
|
|
|
572
|
+
/**
|
|
573
|
+
* Gets the most recent active (non-expired) session for a user.
|
|
574
|
+
*
|
|
575
|
+
* This is needed in JWT mode where the client only has a JWT but BetterAuth's
|
|
576
|
+
* plugin endpoints (2FA, Passkey, etc.) need a real session token for authentication.
|
|
577
|
+
*
|
|
578
|
+
* @param userId - The user ID to find an active session for
|
|
579
|
+
* @returns Session result with token, or null if no active session exists
|
|
580
|
+
*/
|
|
581
|
+
async getActiveSessionForUser(userId: string): Promise<SessionResult> {
|
|
582
|
+
if (!this.isEnabled() || !this.connection?.db) {
|
|
583
|
+
return { session: null, user: null };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
try {
|
|
587
|
+
const db = this.connection.db;
|
|
588
|
+
const sessionsCollection = db.collection('session');
|
|
589
|
+
|
|
590
|
+
// Find the most recent non-expired session for this user
|
|
591
|
+
const results = await sessionsCollection
|
|
592
|
+
.aggregate([
|
|
593
|
+
{
|
|
594
|
+
$match: {
|
|
595
|
+
$expr: {
|
|
596
|
+
$or: [
|
|
597
|
+
{ $eq: ['$userId', userId] },
|
|
598
|
+
{ $eq: [{ $toString: '$userId' }, userId] },
|
|
599
|
+
],
|
|
600
|
+
},
|
|
601
|
+
expiresAt: { $gt: new Date() },
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
{ $sort: { createdAt: -1 } },
|
|
605
|
+
{ $limit: 1 },
|
|
606
|
+
{
|
|
607
|
+
$lookup: {
|
|
608
|
+
as: 'userDoc',
|
|
609
|
+
from: 'users',
|
|
610
|
+
let: { sessionUserId: '$userId' },
|
|
611
|
+
pipeline: [
|
|
612
|
+
{
|
|
613
|
+
$match: {
|
|
614
|
+
$expr: {
|
|
615
|
+
$or: [
|
|
616
|
+
{ $eq: ['$_id', '$$sessionUserId'] },
|
|
617
|
+
{ $eq: [{ $toString: '$_id' }, { $toString: '$$sessionUserId' }] },
|
|
618
|
+
{ $eq: ['$id', { $toString: '$$sessionUserId' }] },
|
|
619
|
+
],
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
],
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
{ $unwind: { path: '$userDoc', preserveNullAndEmptyArrays: true } },
|
|
627
|
+
])
|
|
628
|
+
.toArray();
|
|
629
|
+
|
|
630
|
+
const result = results[0];
|
|
631
|
+
|
|
632
|
+
if (!result) {
|
|
633
|
+
this.logger.debug(`getActiveSessionForUser: no active session for user ${userId}`);
|
|
634
|
+
return { session: null, user: null };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const user = result.userDoc;
|
|
638
|
+
|
|
639
|
+
return {
|
|
640
|
+
session: {
|
|
641
|
+
expiresAt: result.expiresAt,
|
|
642
|
+
id: result.id || result._id?.toString(),
|
|
643
|
+
token: result.token,
|
|
644
|
+
userId: result.userId?.toString(),
|
|
645
|
+
},
|
|
646
|
+
user: user
|
|
647
|
+
? {
|
|
648
|
+
email: user.email,
|
|
649
|
+
emailVerified: user.emailVerified,
|
|
650
|
+
id: user.id || user._id?.toString(),
|
|
651
|
+
name: user.name,
|
|
652
|
+
}
|
|
653
|
+
: null,
|
|
654
|
+
};
|
|
655
|
+
} catch (error) {
|
|
656
|
+
this.logger.debug(`getActiveSessionForUser error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
657
|
+
return { session: null, user: null };
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ===================================================================================================================
|
|
662
|
+
// User Lookup Methods
|
|
663
|
+
// ===================================================================================================================
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Checks whether a user's email is verified via Better-Auth's internal adapter.
|
|
667
|
+
*
|
|
668
|
+
* Returns:
|
|
669
|
+
* - `true` if the user exists and their email is verified
|
|
670
|
+
* - `false` if the user exists and their email is NOT verified
|
|
671
|
+
* - `null` if the user could not be found or Better-Auth is not available
|
|
672
|
+
*
|
|
673
|
+
* This method is used by controller and resolver to check email verification
|
|
674
|
+
* in the 2FA path, where the sign-in response does not include user data.
|
|
675
|
+
*
|
|
676
|
+
* @param email - The email address to look up
|
|
677
|
+
* @returns `true`, `false`, or `null`
|
|
678
|
+
*/
|
|
679
|
+
async isUserEmailVerified(email: string): Promise<boolean | null> {
|
|
680
|
+
const authInstance = this.getInstance();
|
|
681
|
+
if (!authInstance) {
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
try {
|
|
686
|
+
const context = await authInstance.$context;
|
|
687
|
+
const result = await context.internalAdapter.findUserByEmail(email);
|
|
688
|
+
if (!result?.user) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
return !!result.user.emailVerified;
|
|
692
|
+
} catch (error) {
|
|
693
|
+
this.logger.debug(`isUserEmailVerified error for ${maskEmail(email)}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
546
698
|
// ===================================================================================================================
|
|
547
699
|
// JWT Token Verification (for BetterAuth JWT tokens using JWKS)
|
|
548
700
|
// ===================================================================================================================
|
|
@@ -708,22 +860,26 @@ export class CoreBetterAuthService {
|
|
|
708
860
|
}
|
|
709
861
|
|
|
710
862
|
/**
|
|
711
|
-
* Extracts and verifies a JWT token from a request's Authorization header
|
|
863
|
+
* Extracts and verifies a JWT token from a request's Authorization header,
|
|
864
|
+
* or verifies a directly provided token.
|
|
712
865
|
*
|
|
713
866
|
* @param req - Express request object
|
|
867
|
+
* @param token - Optional JWT token to verify directly (bypasses header extraction)
|
|
714
868
|
* @returns The JWT payload with user info, or null if no valid token
|
|
715
869
|
*/
|
|
716
|
-
async verifyJwtFromRequest(req: Request): Promise<null | {
|
|
870
|
+
async verifyJwtFromRequest(req: Request, token?: string): Promise<null | {
|
|
717
871
|
[key: string]: any;
|
|
718
872
|
email?: string;
|
|
719
873
|
sub: string;
|
|
720
874
|
}> {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
875
|
+
if (!token) {
|
|
876
|
+
const authHeader = req.headers.authorization;
|
|
877
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
token = authHeader.substring(7);
|
|
724
881
|
}
|
|
725
882
|
|
|
726
|
-
const token = authHeader.substring(7);
|
|
727
883
|
return this.verifyJwtToken(token);
|
|
728
884
|
}
|
|
729
885
|
}
|
|
@@ -29,10 +29,12 @@ export * from './better-auth.types';
|
|
|
29
29
|
export * from './core-better-auth-api.middleware';
|
|
30
30
|
export * from './core-better-auth-auth.model';
|
|
31
31
|
export * from './core-better-auth-cookie.helper';
|
|
32
|
+
export * from './core-better-auth-email-verification.service';
|
|
32
33
|
export * from './core-better-auth-migration-status.model';
|
|
33
34
|
export * from './core-better-auth-models';
|
|
34
35
|
export * from './core-better-auth-rate-limit.middleware';
|
|
35
36
|
export * from './core-better-auth-rate-limiter.service';
|
|
37
|
+
export * from './core-better-auth-signup-validator.service';
|
|
36
38
|
export * from './core-better-auth-token.helper';
|
|
37
39
|
export * from './core-better-auth-user.mapper';
|
|
38
40
|
export * from './core-better-auth-web.helper';
|
|
@@ -233,6 +233,51 @@ export const LtnsErrors = {
|
|
|
233
233
|
},
|
|
234
234
|
},
|
|
235
235
|
|
|
236
|
+
SIGNUP_TERMS_NOT_ACCEPTED: {
|
|
237
|
+
code: 'LTNS_0021',
|
|
238
|
+
message: 'Terms and privacy policy must be accepted',
|
|
239
|
+
translations: {
|
|
240
|
+
de: 'Die Nutzungsbedingungen und Datenschutzrichtlinie müssen akzeptiert werden.',
|
|
241
|
+
en: 'Terms and privacy policy must be accepted.',
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
SIGNUP_MISSING_REQUIRED_FIELDS: {
|
|
246
|
+
code: 'LTNS_0022',
|
|
247
|
+
message: 'Required sign-up fields are missing',
|
|
248
|
+
translations: {
|
|
249
|
+
de: 'Erforderliche Registrierungsfelder fehlen.',
|
|
250
|
+
en: 'Required sign-up fields are missing.',
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
EMAIL_VERIFICATION_REQUIRED: {
|
|
255
|
+
code: 'LTNS_0023',
|
|
256
|
+
message: 'Email verification required',
|
|
257
|
+
translations: {
|
|
258
|
+
de: 'Bitte verifizieren Sie Ihre E-Mail-Adresse.',
|
|
259
|
+
en: 'Please verify your email address.',
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
EMAIL_VERIFICATION_TOKEN_INVALID: {
|
|
264
|
+
code: 'LTNS_0024',
|
|
265
|
+
message: 'Email verification token is invalid or expired',
|
|
266
|
+
translations: {
|
|
267
|
+
de: 'Der E-Mail-Verifizierungslink ist ungültig oder abgelaufen.',
|
|
268
|
+
en: 'The email verification link is invalid or expired.',
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
EMAIL_ALREADY_VERIFIED: {
|
|
273
|
+
code: 'LTNS_0025',
|
|
274
|
+
message: 'Email is already verified',
|
|
275
|
+
translations: {
|
|
276
|
+
de: 'Die E-Mail-Adresse ist bereits verifiziert.',
|
|
277
|
+
en: 'The email address is already verified.',
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
|
|
236
281
|
// =====================================================
|
|
237
282
|
// Authorization Errors (LTNS_0100-LTNS_0199)
|
|
238
283
|
// =====================================================
|
|
@@ -256,6 +256,21 @@ export abstract class CoreUserModel extends CorePersistenceModel {
|
|
|
256
256
|
})
|
|
257
257
|
twoFactorEnabled: boolean = undefined;
|
|
258
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Date when terms and privacy policy were accepted
|
|
261
|
+
* Set during sign-up when user accepts terms and privacy policy
|
|
262
|
+
*
|
|
263
|
+
* @since 11.13.0
|
|
264
|
+
*/
|
|
265
|
+
@UnifiedField({
|
|
266
|
+
description: 'Date when terms and privacy policy were accepted',
|
|
267
|
+
isOptional: true,
|
|
268
|
+
mongoose: { type: Date },
|
|
269
|
+
roles: RoleEnum.S_EVERYONE,
|
|
270
|
+
type: () => Date,
|
|
271
|
+
})
|
|
272
|
+
termsAndPrivacyAcceptedAt: Date = undefined;
|
|
273
|
+
|
|
259
274
|
// ===================================================================================================================
|
|
260
275
|
// Methods
|
|
261
276
|
// ===================================================================================================================
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { Controller } from '@nestjs/common';
|
|
1
|
+
import { Controller, Optional } from '@nestjs/common';
|
|
2
2
|
|
|
3
3
|
import { Roles } from '../../../core/common/decorators/roles.decorator';
|
|
4
4
|
import { RoleEnum } from '../../../core/common/enums/role.enum';
|
|
5
5
|
import { ConfigService } from '../../../core/common/services/config.service';
|
|
6
|
+
import { CoreBetterAuthEmailVerificationService } from '../../../core/modules/better-auth/core-better-auth-email-verification.service';
|
|
7
|
+
import { CoreBetterAuthSignUpValidatorService } from '../../../core/modules/better-auth/core-better-auth-signup-validator.service';
|
|
6
8
|
import { CoreBetterAuthUserMapper } from '../../../core/modules/better-auth/core-better-auth-user.mapper';
|
|
7
9
|
import { CoreBetterAuthController } from '../../../core/modules/better-auth/core-better-auth.controller';
|
|
8
10
|
import { CoreBetterAuthService } from '../../../core/modules/better-auth/core-better-auth.service';
|
|
@@ -35,7 +37,9 @@ export class BetterAuthController extends CoreBetterAuthController {
|
|
|
35
37
|
protected override readonly betterAuthService: CoreBetterAuthService,
|
|
36
38
|
protected override readonly userMapper: CoreBetterAuthUserMapper,
|
|
37
39
|
protected override readonly configService: ConfigService,
|
|
40
|
+
@Optional() signUpValidator?: CoreBetterAuthSignUpValidatorService,
|
|
41
|
+
@Optional() emailVerificationService?: CoreBetterAuthEmailVerificationService,
|
|
38
42
|
) {
|
|
39
|
-
super(betterAuthService, userMapper, configService);
|
|
43
|
+
super(betterAuthService, userMapper, configService, signUpValidator, emailVerificationService);
|
|
40
44
|
}
|
|
41
45
|
}
|