@lenne.tech/nest-server 11.11.1 → 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/auth/core-auth.controller.js +1 -1
- package/dist/core/modules/auth/core-auth.controller.js.map +1 -1
- package/dist/core/modules/auth/core-auth.resolver.js +1 -1
- package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth-token.service.js +1 -4
- package/dist/core/modules/better-auth/better-auth-token.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.config.d.ts +13 -0
- package/dist/core/modules/better-auth/better-auth.config.js +114 -17
- 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 +4 -2
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +63 -18
- 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-cookie.helper.d.ts +41 -0
- package/dist/core/modules/better-auth/core-better-auth-cookie.helper.js +107 -0
- package/dist/core/modules/better-auth/core-better-auth-cookie.helper.js.map +1 -0
- 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-token.helper.d.ts +16 -0
- package/dist/core/modules/better-auth/core-better-auth-token.helper.js +66 -0
- package/dist/core/modules/better-auth/core-better-auth-token.helper.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 +3 -3
- package/dist/core/modules/better-auth/core-better-auth-web.helper.js +64 -44
- 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 +13 -1
- package/dist/core/modules/better-auth/core-better-auth.controller.js +108 -49
- package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.middleware.d.ts +0 -1
- package/dist/core/modules/better-auth/core-better-auth.middleware.js +57 -39
- 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 +143 -23
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/core/modules/better-auth/index.d.ts +4 -0
- package/dist/core/modules/better-auth/index.js +4 -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 +10 -10
- package/src/config.env.ts +2 -0
- package/src/core/common/interfaces/server-options.interface.ts +240 -0
- package/src/core/modules/auth/core-auth.controller.ts +2 -2
- package/src/core/modules/auth/core-auth.resolver.ts +2 -2
- 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-token.service.ts +5 -8
- package/src/core/modules/better-auth/better-auth.config.ts +282 -29
- package/src/core/modules/better-auth/better-auth.resolver.ts +16 -5
- package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +100 -22
- package/src/core/modules/better-auth/core-better-auth-auth.model.ts +10 -0
- package/src/core/modules/better-auth/core-better-auth-cookie.helper.ts +323 -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-token.helper.ts +200 -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 +119 -69
- package/src/core/modules/better-auth/core-better-auth.controller.ts +197 -84
- package/src/core/modules/better-auth/core-better-auth.middleware.ts +93 -64
- 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 +210 -32
- package/src/core/modules/better-auth/index.ts +4 -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
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { Inject, Injectable, Logger, Optional } from '@nestjs/common';
|
|
2
|
+
import * as ejs from 'ejs';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
|
|
6
|
+
import { maskEmail } from '../../common/helpers/logging.helper';
|
|
7
|
+
import { IBetterAuthEmailVerificationConfig } from '../../common/interfaces/server-options.interface';
|
|
8
|
+
import { BrevoService } from '../../common/services/brevo.service';
|
|
9
|
+
import { ConfigService } from '../../common/services/config.service';
|
|
10
|
+
import { EmailService } from '../../common/services/email.service';
|
|
11
|
+
import { TemplateService } from '../../common/services/template.service';
|
|
12
|
+
import { formatProjectName } from './better-auth.config';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolved configuration type for email verification
|
|
16
|
+
* Uses Required for mandatory fields but preserves optional nature of brevoTemplateId
|
|
17
|
+
*/
|
|
18
|
+
type ResolvedEmailVerificationConfig = Pick<IBetterAuthEmailVerificationConfig, 'brevoTemplateId' | 'callbackURL'>
|
|
19
|
+
& Required<Omit<IBetterAuthEmailVerificationConfig, 'brevoTemplateId' | 'callbackURL' | 'resendCooldownSeconds'>>
|
|
20
|
+
& { resendCooldownSeconds: number };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Default configuration for email verification
|
|
24
|
+
*/
|
|
25
|
+
const DEFAULT_CONFIG: ResolvedEmailVerificationConfig = {
|
|
26
|
+
autoSignInAfterVerification: true,
|
|
27
|
+
callbackURL: '/auth/verify-email',
|
|
28
|
+
enabled: true,
|
|
29
|
+
expiresIn: 86400, // 24 hours in seconds
|
|
30
|
+
locale: 'en',
|
|
31
|
+
resendCooldownSeconds: 60,
|
|
32
|
+
template: 'email-verification',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Options for sending verification email
|
|
37
|
+
*/
|
|
38
|
+
export interface SendVerificationEmailOptions {
|
|
39
|
+
/**
|
|
40
|
+
* The token for email verification (used to build the verification URL)
|
|
41
|
+
*/
|
|
42
|
+
token: string;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The verification URL to send to the user
|
|
46
|
+
*/
|
|
47
|
+
url: string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The user object from Better-Auth
|
|
51
|
+
*/
|
|
52
|
+
user: {
|
|
53
|
+
email: string;
|
|
54
|
+
id: string;
|
|
55
|
+
name?: null | string;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* CoreBetterAuthEmailVerificationService handles email verification for Better-Auth.
|
|
61
|
+
*
|
|
62
|
+
* This service:
|
|
63
|
+
* - Sends verification emails using nest-server's EmailService
|
|
64
|
+
* - Resolves templates with project → nest-server fallback
|
|
65
|
+
* - Syncs `verifiedAt` when email is verified
|
|
66
|
+
*
|
|
67
|
+
* **Template Resolution:**
|
|
68
|
+
* Templates are resolved in this order:
|
|
69
|
+
* 1. `<template>-<locale>.ejs` in project templates directory
|
|
70
|
+
* 2. `<template>.ejs` in project templates directory
|
|
71
|
+
* 3. `<template>-<locale>.ejs` in nest-server templates directory (fallback)
|
|
72
|
+
* 4. `<template>.ejs` in nest-server templates directory (fallback)
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* // Override to customize email sending
|
|
77
|
+
* @Injectable()
|
|
78
|
+
* export class MyEmailVerificationService extends CoreBetterAuthEmailVerificationService {
|
|
79
|
+
* override async sendVerificationEmail(options: SendVerificationEmailOptions): Promise<void> {
|
|
80
|
+
* // Custom logic before
|
|
81
|
+
* await super.sendVerificationEmail(options);
|
|
82
|
+
* // Custom logic after (e.g., analytics)
|
|
83
|
+
* }
|
|
84
|
+
* }
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* @since 11.13.0
|
|
88
|
+
*/
|
|
89
|
+
@Injectable()
|
|
90
|
+
export class CoreBetterAuthEmailVerificationService {
|
|
91
|
+
protected readonly logger = new Logger(CoreBetterAuthEmailVerificationService.name);
|
|
92
|
+
protected config: ResolvedEmailVerificationConfig = DEFAULT_CONFIG;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* In-memory tracking of last send time per email address for cooldown enforcement.
|
|
96
|
+
* Key: email address (lowercase), Value: timestamp (ms) of last send
|
|
97
|
+
*/
|
|
98
|
+
private readonly lastSendTimes = new Map<string, number>();
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Token for optional BrevoService injection.
|
|
102
|
+
* BrevoService cannot be injected directly with @Optional() because its
|
|
103
|
+
* constructor throws when no brevo config exists. Instead, a factory
|
|
104
|
+
* provider creates the instance or returns null.
|
|
105
|
+
*/
|
|
106
|
+
static readonly BREVO_SERVICE_TOKEN = 'BETTER_AUTH_BREVO_SERVICE';
|
|
107
|
+
|
|
108
|
+
constructor(
|
|
109
|
+
protected readonly configService: ConfigService,
|
|
110
|
+
@Optional() protected readonly emailService?: EmailService,
|
|
111
|
+
@Optional() protected readonly templateService?: TemplateService,
|
|
112
|
+
@Optional() @Inject(CoreBetterAuthEmailVerificationService.BREVO_SERVICE_TOKEN) protected readonly brevoService?: BrevoService | null,
|
|
113
|
+
) {
|
|
114
|
+
this.configure();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if email verification is enabled
|
|
119
|
+
*/
|
|
120
|
+
isEnabled(): boolean {
|
|
121
|
+
return this.config.enabled;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get the email verification configuration
|
|
126
|
+
*/
|
|
127
|
+
getConfig(): ResolvedEmailVerificationConfig {
|
|
128
|
+
return { ...this.config };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get the expiration time in seconds
|
|
133
|
+
*/
|
|
134
|
+
getExpiresIn(): number {
|
|
135
|
+
return this.config.expiresIn;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check if auto sign-in after verification is enabled
|
|
140
|
+
*/
|
|
141
|
+
shouldAutoSignIn(): boolean {
|
|
142
|
+
return this.config.autoSignInAfterVerification;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Send verification email to user
|
|
147
|
+
*
|
|
148
|
+
* This method is called by Better-Auth's emailVerification plugin hook.
|
|
149
|
+
* Override this method to customize email sending behavior.
|
|
150
|
+
*
|
|
151
|
+
* @param options - The verification email options from Better-Auth
|
|
152
|
+
*/
|
|
153
|
+
async sendVerificationEmail(options: SendVerificationEmailOptions): Promise<void> {
|
|
154
|
+
const { token, user } = options;
|
|
155
|
+
let { url } = options;
|
|
156
|
+
|
|
157
|
+
// Check resend cooldown per email address
|
|
158
|
+
if (this.isInCooldown(user.email)) {
|
|
159
|
+
this.logger.debug(`Resend cooldown active for ${this.maskEmail(user.email)}, skipping email send`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Override URL if callbackURL is configured (frontend-based verification)
|
|
164
|
+
if (this.config.callbackURL) {
|
|
165
|
+
url = this.buildFrontendVerificationUrl(token);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Always log verification URL for development/testing (useful for capturing in tests)
|
|
169
|
+
// Uses console.log directly to ensure reliable capture in test environments (Vitest, Jest)
|
|
170
|
+
// NestJS Logger may buffer output which makes interception unreliable in tests
|
|
171
|
+
// eslint-disable-next-line no-console
|
|
172
|
+
console.log(`[EMAIL VERIFICATION] User: ${user.email}, URL: ${url}`);
|
|
173
|
+
|
|
174
|
+
// Brevo template path: send via Brevo transactional API if configured
|
|
175
|
+
if (this.config.brevoTemplateId && this.brevoService) {
|
|
176
|
+
try {
|
|
177
|
+
const appName = this.getAppName();
|
|
178
|
+
await this.brevoService.sendMail(user.email, this.config.brevoTemplateId, {
|
|
179
|
+
appName,
|
|
180
|
+
expiresIn: this.formatExpiresIn(this.config.expiresIn),
|
|
181
|
+
link: url,
|
|
182
|
+
name: user.name || user.email.split('@')[0],
|
|
183
|
+
});
|
|
184
|
+
this.trackSend(user.email);
|
|
185
|
+
this.logger.debug(`Verification email sent via Brevo to ${this.maskEmail(user.email)}`);
|
|
186
|
+
return;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
this.logger.error(`Failed to send verification email via Brevo to ${this.maskEmail(user.email)}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!this.emailService) {
|
|
194
|
+
this.logger.warn('EmailService not available, cannot send verification email');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const resolved = await this.resolveTemplatePath(this.config.template, this.config.locale);
|
|
200
|
+
const appName = this.getAppName();
|
|
201
|
+
|
|
202
|
+
const templateData = {
|
|
203
|
+
appName,
|
|
204
|
+
expiresIn: this.formatExpiresIn(this.config.expiresIn),
|
|
205
|
+
link: url,
|
|
206
|
+
name: user.name || user.email.split('@')[0],
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (resolved.isAbsolute) {
|
|
210
|
+
// Fallback template from nest-server: render directly via EJS
|
|
211
|
+
const templateContent = fs.readFileSync(`${resolved.path}.ejs`, 'utf-8');
|
|
212
|
+
const html = ejs.render(templateContent, templateData);
|
|
213
|
+
|
|
214
|
+
await this.emailService.sendMail(
|
|
215
|
+
user.email,
|
|
216
|
+
this.getEmailSubject(appName),
|
|
217
|
+
{ html },
|
|
218
|
+
);
|
|
219
|
+
} else {
|
|
220
|
+
// Project template: use TemplateService (relative path)
|
|
221
|
+
await this.emailService.sendMail(
|
|
222
|
+
user.email,
|
|
223
|
+
this.getEmailSubject(appName),
|
|
224
|
+
{
|
|
225
|
+
htmlTemplate: resolved.path,
|
|
226
|
+
templateData,
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.trackSend(user.email);
|
|
232
|
+
this.logger.debug(`Verification email sent to ${this.maskEmail(user.email)}`);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
this.logger.error(`Failed to send verification email to ${this.maskEmail(user.email)}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Configure the service with Better-Auth settings
|
|
241
|
+
*
|
|
242
|
+
* Follows the "presence implies enabled" pattern:
|
|
243
|
+
* - If config is undefined/null: enabled with defaults
|
|
244
|
+
* - If config is `true`: enabled with defaults
|
|
245
|
+
* - If config is `false`: disabled
|
|
246
|
+
* - If config is an object: enabled with merged settings (unless `enabled: false`)
|
|
247
|
+
*/
|
|
248
|
+
protected configure(): void {
|
|
249
|
+
const rawConfig = this.configService.getFastButReadOnly<boolean | IBetterAuthEmailVerificationConfig>('betterAuth.emailVerification');
|
|
250
|
+
|
|
251
|
+
// false = explicitly disabled
|
|
252
|
+
if (rawConfig === false) {
|
|
253
|
+
this.config = { ...DEFAULT_CONFIG, enabled: false };
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// undefined/null/true = enabled with defaults (zero-config: email verification is on by default)
|
|
258
|
+
if (!rawConfig || rawConfig === true) {
|
|
259
|
+
this.config = { ...DEFAULT_CONFIG, enabled: true };
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Object config: merge with defaults, enabled unless explicitly disabled
|
|
264
|
+
this.config = {
|
|
265
|
+
...DEFAULT_CONFIG,
|
|
266
|
+
...rawConfig,
|
|
267
|
+
enabled: rawConfig.enabled !== false,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Build the frontend verification URL from the configured callbackURL and token.
|
|
273
|
+
*
|
|
274
|
+
* Resolves relative paths against `appUrl`. Appends the token as a query parameter.
|
|
275
|
+
*
|
|
276
|
+
* @param token - The verification token from Better-Auth
|
|
277
|
+
* @returns The full frontend URL with token query parameter
|
|
278
|
+
*/
|
|
279
|
+
protected buildFrontendVerificationUrl(token: string): string {
|
|
280
|
+
let baseUrl = this.config.callbackURL!;
|
|
281
|
+
|
|
282
|
+
// Resolve relative paths against appUrl
|
|
283
|
+
if (baseUrl.startsWith('/')) {
|
|
284
|
+
const appUrl = this.configService.getFastButReadOnly<string>('appUrl') || 'http://localhost:3001';
|
|
285
|
+
baseUrl = `${appUrl.replace(/\/$/, '')}${baseUrl}`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Append token as query parameter
|
|
289
|
+
const separator = baseUrl.includes('?') ? '&' : '?';
|
|
290
|
+
return `${baseUrl}${separator}token=${token}`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Resolve template path with fallback logic
|
|
295
|
+
*
|
|
296
|
+
* Resolution order:
|
|
297
|
+
* 1. `<template>-<locale>.ejs` in project templates
|
|
298
|
+
* 2. `<template>.ejs` in project templates
|
|
299
|
+
* 3. `<template>-<locale>.ejs` in nest-server templates
|
|
300
|
+
* 4. `<template>.ejs` in nest-server templates
|
|
301
|
+
*
|
|
302
|
+
* @param templateName - The template name without extension
|
|
303
|
+
* @param locale - The locale for the template
|
|
304
|
+
* @returns Object with `path` (without .ejs) and `isAbsolute` flag
|
|
305
|
+
*/
|
|
306
|
+
protected async resolveTemplatePath(templateName: string, locale: string): Promise<{ isAbsolute: boolean; path: string }> {
|
|
307
|
+
const projectTemplatesPath = this.configService.getFastButReadOnly<string>('templates.path');
|
|
308
|
+
const nestServerTemplatesPath = path.join(__dirname, '..', '..', '..', 'templates');
|
|
309
|
+
|
|
310
|
+
const candidates = [
|
|
311
|
+
// Project templates (with locale)
|
|
312
|
+
{ base: projectTemplatesPath, isNestServer: false, name: `${templateName}-${locale}` },
|
|
313
|
+
// Project templates (without locale)
|
|
314
|
+
{ base: projectTemplatesPath, isNestServer: false, name: templateName },
|
|
315
|
+
// nest-server templates (with locale)
|
|
316
|
+
{ base: nestServerTemplatesPath, isNestServer: true, name: `${templateName}-${locale}` },
|
|
317
|
+
// nest-server templates (without locale)
|
|
318
|
+
{ base: nestServerTemplatesPath, isNestServer: true, name: templateName },
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
for (const candidate of candidates) {
|
|
322
|
+
if (!candidate.base) continue;
|
|
323
|
+
|
|
324
|
+
const fullPath = path.join(candidate.base, `${candidate.name}.ejs`);
|
|
325
|
+
if (fs.existsSync(fullPath)) {
|
|
326
|
+
if (candidate.isNestServer) {
|
|
327
|
+
// nest-server template: return absolute path (rendered directly via EJS)
|
|
328
|
+
return { isAbsolute: true, path: fullPath.replace('.ejs', '') };
|
|
329
|
+
}
|
|
330
|
+
// Project template: return relative name (for TemplateService)
|
|
331
|
+
return { isAbsolute: false, path: candidate.name };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Fallback to default template name (will likely fail, but provides clear error)
|
|
336
|
+
this.logger.warn(`Template '${templateName}' not found in any location, using fallback`);
|
|
337
|
+
return { isAbsolute: false, path: templateName };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Get the app name for the email
|
|
342
|
+
*/
|
|
343
|
+
protected getAppName(): string {
|
|
344
|
+
// Try to get from package.json name
|
|
345
|
+
try {
|
|
346
|
+
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
347
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
348
|
+
if (packageJson.name) {
|
|
349
|
+
return this.formatProjectName(packageJson.name);
|
|
350
|
+
}
|
|
351
|
+
} catch {
|
|
352
|
+
// Ignore
|
|
353
|
+
}
|
|
354
|
+
return 'Nest Server';
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Format project name from package.json
|
|
359
|
+
* @deprecated Use the shared formatProjectName from better-auth.config.ts directly instead
|
|
360
|
+
*/
|
|
361
|
+
protected formatProjectName(name: string): string {
|
|
362
|
+
return formatProjectName(name);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Get the email subject
|
|
367
|
+
*/
|
|
368
|
+
protected getEmailSubject(appName: string): string {
|
|
369
|
+
const locale = this.config.locale;
|
|
370
|
+
if (locale === 'de') {
|
|
371
|
+
return `${appName} - E-Mail-Adresse bestätigen`;
|
|
372
|
+
}
|
|
373
|
+
return `${appName} - Verify your email address`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Format expires in seconds to human readable string
|
|
378
|
+
*/
|
|
379
|
+
protected formatExpiresIn(seconds: number): string {
|
|
380
|
+
const hours = Math.floor(seconds / 3600);
|
|
381
|
+
const locale = this.config.locale;
|
|
382
|
+
|
|
383
|
+
if (hours >= 24) {
|
|
384
|
+
const days = Math.floor(hours / 24);
|
|
385
|
+
if (locale === 'de') {
|
|
386
|
+
return days === 1 ? '1 Tag' : `${days} Tage`;
|
|
387
|
+
}
|
|
388
|
+
return days === 1 ? '1 day' : `${days} days`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (locale === 'de') {
|
|
392
|
+
return hours === 1 ? '1 Stunde' : `${hours} Stunden`;
|
|
393
|
+
}
|
|
394
|
+
return hours === 1 ? '1 hour' : `${hours} hours`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Check if an email address is still in the resend cooldown period
|
|
399
|
+
*/
|
|
400
|
+
protected isInCooldown(email: string): boolean {
|
|
401
|
+
const cooldown = this.config.resendCooldownSeconds;
|
|
402
|
+
if (cooldown <= 0) return false;
|
|
403
|
+
|
|
404
|
+
const key = email.toLowerCase();
|
|
405
|
+
const lastSend = this.lastSendTimes.get(key);
|
|
406
|
+
if (!lastSend) return false;
|
|
407
|
+
|
|
408
|
+
const elapsed = (Date.now() - lastSend) / 1000;
|
|
409
|
+
return elapsed < cooldown;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Track that a verification email was sent to this address
|
|
414
|
+
*/
|
|
415
|
+
protected trackSend(email: string): void {
|
|
416
|
+
const key = email.toLowerCase();
|
|
417
|
+
this.lastSendTimes.set(key, Date.now());
|
|
418
|
+
|
|
419
|
+
// Schedule cleanup to prevent memory leak
|
|
420
|
+
const cooldown = this.config.resendCooldownSeconds;
|
|
421
|
+
if (cooldown > 0) {
|
|
422
|
+
setTimeout(() => this.lastSendTimes.delete(key), cooldown * 1000);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Mask email for logging (privacy)
|
|
428
|
+
* @deprecated Use the shared maskEmail from logging.helper.ts directly instead
|
|
429
|
+
*/
|
|
430
|
+
protected maskEmail(email: string): string {
|
|
431
|
+
return maskEmail(email);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
@@ -123,18 +123,21 @@ export class CoreBetterAuthPasskeyChallengeModel {
|
|
|
123
123
|
@ObjectType({ description: 'Better-Auth features status' })
|
|
124
124
|
@Restricted(RoleEnum.S_EVERYONE)
|
|
125
125
|
export class CoreBetterAuthFeaturesModel {
|
|
126
|
+
@Field(() => Boolean, { description: 'Whether email verification is required on sign-up' })
|
|
127
|
+
emailVerification: boolean;
|
|
128
|
+
|
|
126
129
|
@Field(() => Boolean, { description: 'Whether Better-Auth is enabled' })
|
|
127
130
|
enabled: boolean;
|
|
128
131
|
|
|
129
132
|
@Field(() => Boolean, { description: 'Whether JWT plugin is enabled' })
|
|
130
133
|
jwt: boolean;
|
|
131
134
|
|
|
132
|
-
@Field(() => Boolean, { description: 'Whether 2FA is enabled' })
|
|
133
|
-
twoFactor: boolean;
|
|
134
|
-
|
|
135
135
|
@Field(() => Boolean, { description: 'Whether Passkey is enabled' })
|
|
136
136
|
passkey: boolean;
|
|
137
137
|
|
|
138
138
|
@Field(() => [String], { description: 'List of enabled social providers' })
|
|
139
139
|
socialProviders: string[];
|
|
140
|
+
|
|
141
|
+
@Field(() => Boolean, { description: 'Whether 2FA is enabled' })
|
|
142
|
+
twoFactor: boolean;
|
|
140
143
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import { IBetterAuthSignUpChecksConfig } from '../../common/interfaces/server-options.interface';
|
|
4
|
+
import { ConfigService } from '../../common/services/config.service';
|
|
5
|
+
import { ErrorCode } from '../error-code/error-codes';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Default configuration for sign-up checks
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_CONFIG: Required<IBetterAuthSignUpChecksConfig> = {
|
|
11
|
+
enabled: true,
|
|
12
|
+
requiredFields: ['termsAndPrivacyAccepted'],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Sign-up input interface for validation
|
|
17
|
+
*/
|
|
18
|
+
export interface SignUpValidationInput {
|
|
19
|
+
/**
|
|
20
|
+
* Allow additional fields for custom validation
|
|
21
|
+
*/
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether terms and privacy policy were accepted
|
|
26
|
+
*/
|
|
27
|
+
termsAndPrivacyAccepted?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* CoreBetterAuthSignUpValidatorService validates sign-up input against configured required fields.
|
|
32
|
+
*
|
|
33
|
+
* This service enforces that certain fields must be provided and truthy during sign-up.
|
|
34
|
+
* By default, `termsAndPrivacyAccepted` is required.
|
|
35
|
+
*
|
|
36
|
+
* **Enabled by Default:** Sign-up checks are enabled by default with
|
|
37
|
+
* `requiredFields: ['termsAndPrivacyAccepted']`.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* // In your resolver/controller
|
|
42
|
+
* @Mutation(() => AuthModel)
|
|
43
|
+
* async signUp(
|
|
44
|
+
* @Args('email') email: string,
|
|
45
|
+
* @Args('password') password: string,
|
|
46
|
+
* @Args('termsAndPrivacyAccepted') termsAndPrivacyAccepted: boolean,
|
|
47
|
+
* ) {
|
|
48
|
+
* // Validate required fields
|
|
49
|
+
* this.signUpValidator.validateSignUpInput({ termsAndPrivacyAccepted });
|
|
50
|
+
*
|
|
51
|
+
* // Continue with sign-up...
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* // Disable sign-up checks in config.env.ts
|
|
58
|
+
* betterAuth: {
|
|
59
|
+
* signUpChecks: false,
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* // Custom required fields in config.env.ts
|
|
66
|
+
* betterAuth: {
|
|
67
|
+
* signUpChecks: {
|
|
68
|
+
* requiredFields: ['termsAndPrivacyAccepted', 'ageConfirmed'],
|
|
69
|
+
* }
|
|
70
|
+
* }
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* @since 11.13.0
|
|
74
|
+
*/
|
|
75
|
+
@Injectable()
|
|
76
|
+
export class CoreBetterAuthSignUpValidatorService {
|
|
77
|
+
protected readonly logger = new Logger(CoreBetterAuthSignUpValidatorService.name);
|
|
78
|
+
protected config: Required<IBetterAuthSignUpChecksConfig> = DEFAULT_CONFIG;
|
|
79
|
+
|
|
80
|
+
constructor(protected readonly configService: ConfigService) {
|
|
81
|
+
this.configure();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if sign-up validation is enabled
|
|
86
|
+
*/
|
|
87
|
+
isEnabled(): boolean {
|
|
88
|
+
return this.config.enabled;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get the current configuration
|
|
93
|
+
*/
|
|
94
|
+
getConfig(): Required<IBetterAuthSignUpChecksConfig> {
|
|
95
|
+
return { ...this.config };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get the list of required fields
|
|
100
|
+
*/
|
|
101
|
+
getRequiredFields(): string[] {
|
|
102
|
+
return [...this.config.requiredFields];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Validate sign-up input against configured required fields
|
|
107
|
+
*
|
|
108
|
+
* @param input - The sign-up input to validate
|
|
109
|
+
* @throws BadRequestException if any required field is missing or falsy
|
|
110
|
+
*/
|
|
111
|
+
validateSignUpInput(input: SignUpValidationInput): void {
|
|
112
|
+
if (!this.config.enabled) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const missingFields: string[] = [];
|
|
117
|
+
|
|
118
|
+
for (const field of this.config.requiredFields) {
|
|
119
|
+
const value = input[field];
|
|
120
|
+
|
|
121
|
+
// Check if the field is missing or falsy
|
|
122
|
+
if (value === undefined || value === null || value === false || value === '') {
|
|
123
|
+
missingFields.push(field);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (missingFields.length > 0) {
|
|
128
|
+
this.logger.debug(`Sign-up validation failed: missing required fields: ${missingFields.join(', ')}`);
|
|
129
|
+
|
|
130
|
+
// Throw specific error for termsAndPrivacyAccepted (most common case)
|
|
131
|
+
if (missingFields.includes('termsAndPrivacyAccepted')) {
|
|
132
|
+
throw new BadRequestException(ErrorCode.SIGNUP_TERMS_NOT_ACCEPTED);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Generic error for other missing fields
|
|
136
|
+
throw new BadRequestException(
|
|
137
|
+
`${ErrorCode.SIGNUP_MISSING_REQUIRED_FIELDS} - Missing: ${missingFields.join(', ')}`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Configure the service with Better-Auth settings
|
|
144
|
+
*
|
|
145
|
+
* Follows the "presence implies enabled" pattern:
|
|
146
|
+
* - If config is undefined/null: enabled with defaults
|
|
147
|
+
* - If config is `true`: enabled with defaults
|
|
148
|
+
* - If config is `false`: disabled
|
|
149
|
+
* - If config is an object: enabled with merged settings (unless `enabled: false`)
|
|
150
|
+
*/
|
|
151
|
+
protected configure(): void {
|
|
152
|
+
const rawConfig = this.configService.getFastButReadOnly<boolean | IBetterAuthSignUpChecksConfig>('betterAuth.signUpChecks');
|
|
153
|
+
|
|
154
|
+
// Sign-up checks are enabled by default
|
|
155
|
+
if (rawConfig === undefined || rawConfig === null || rawConfig === true) {
|
|
156
|
+
this.config = { ...DEFAULT_CONFIG, enabled: true };
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (rawConfig === false) {
|
|
161
|
+
this.config = { ...DEFAULT_CONFIG, enabled: false };
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Object config: merge with defaults
|
|
166
|
+
const enabled = rawConfig.enabled !== false;
|
|
167
|
+
this.config = {
|
|
168
|
+
...DEFAULT_CONFIG,
|
|
169
|
+
...rawConfig,
|
|
170
|
+
enabled,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Ensure requiredFields is an array
|
|
174
|
+
if (!Array.isArray(this.config.requiredFields)) {
|
|
175
|
+
this.config.requiredFields = DEFAULT_CONFIG.requiredFields;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|