@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.
Files changed (113) hide show
  1. package/dist/config.env.js +1 -0
  2. package/dist/config.env.js.map +1 -1
  3. package/dist/core/common/interfaces/server-options.interface.d.ts +16 -0
  4. package/dist/core/modules/auth/core-auth.controller.js +1 -1
  5. package/dist/core/modules/auth/core-auth.controller.js.map +1 -1
  6. package/dist/core/modules/auth/core-auth.resolver.js +1 -1
  7. package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
  8. package/dist/core/modules/better-auth/better-auth-token.service.js +1 -4
  9. package/dist/core/modules/better-auth/better-auth-token.service.js.map +1 -1
  10. package/dist/core/modules/better-auth/better-auth.config.d.ts +13 -0
  11. package/dist/core/modules/better-auth/better-auth.config.js +114 -17
  12. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  13. package/dist/core/modules/better-auth/better-auth.resolver.d.ts +7 -3
  14. package/dist/core/modules/better-auth/better-auth.resolver.js +16 -6
  15. package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
  16. package/dist/core/modules/better-auth/core-better-auth-api.middleware.d.ts +4 -2
  17. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +63 -18
  18. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -1
  19. package/dist/core/modules/better-auth/core-better-auth-auth.model.d.ts +1 -0
  20. package/dist/core/modules/better-auth/core-better-auth-auth.model.js +7 -0
  21. package/dist/core/modules/better-auth/core-better-auth-auth.model.js.map +1 -1
  22. package/dist/core/modules/better-auth/core-better-auth-cookie.helper.d.ts +41 -0
  23. package/dist/core/modules/better-auth/core-better-auth-cookie.helper.js +107 -0
  24. package/dist/core/modules/better-auth/core-better-auth-cookie.helper.js.map +1 -0
  25. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.d.ts +48 -0
  26. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +241 -0
  27. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -0
  28. package/dist/core/modules/better-auth/core-better-auth-models.d.ts +2 -1
  29. package/dist/core/modules/better-auth/core-better-auth-models.js +8 -4
  30. package/dist/core/modules/better-auth/core-better-auth-models.js.map +1 -1
  31. package/dist/core/modules/better-auth/core-better-auth-signup-validator.service.d.ts +18 -0
  32. package/dist/core/modules/better-auth/core-better-auth-signup-validator.service.js +82 -0
  33. package/dist/core/modules/better-auth/core-better-auth-signup-validator.service.js.map +1 -0
  34. package/dist/core/modules/better-auth/core-better-auth-token.helper.d.ts +16 -0
  35. package/dist/core/modules/better-auth/core-better-auth-token.helper.js +66 -0
  36. package/dist/core/modules/better-auth/core-better-auth-token.helper.js.map +1 -0
  37. package/dist/core/modules/better-auth/core-better-auth-user.mapper.d.ts +0 -1
  38. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +15 -8
  39. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
  40. package/dist/core/modules/better-auth/core-better-auth-web.helper.d.ts +3 -3
  41. package/dist/core/modules/better-auth/core-better-auth-web.helper.js +64 -44
  42. package/dist/core/modules/better-auth/core-better-auth-web.helper.js.map +1 -1
  43. package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +13 -1
  44. package/dist/core/modules/better-auth/core-better-auth.controller.js +108 -49
  45. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  46. package/dist/core/modules/better-auth/core-better-auth.middleware.d.ts +0 -1
  47. package/dist/core/modules/better-auth/core-better-auth.middleware.js +57 -39
  48. package/dist/core/modules/better-auth/core-better-auth.middleware.js.map +1 -1
  49. package/dist/core/modules/better-auth/core-better-auth.module.d.ts +6 -0
  50. package/dist/core/modules/better-auth/core-better-auth.module.js +129 -24
  51. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  52. package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +12 -5
  53. package/dist/core/modules/better-auth/core-better-auth.resolver.js +64 -17
  54. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
  55. package/dist/core/modules/better-auth/core-better-auth.service.d.ts +4 -1
  56. package/dist/core/modules/better-auth/core-better-auth.service.js +143 -23
  57. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  58. package/dist/core/modules/better-auth/index.d.ts +4 -0
  59. package/dist/core/modules/better-auth/index.js +4 -0
  60. package/dist/core/modules/better-auth/index.js.map +1 -1
  61. package/dist/core/modules/error-code/error-codes.d.ts +45 -0
  62. package/dist/core/modules/error-code/error-codes.js +40 -0
  63. package/dist/core/modules/error-code/error-codes.js.map +1 -1
  64. package/dist/core/modules/user/core-user.model.d.ts +1 -0
  65. package/dist/core/modules/user/core-user.model.js +11 -0
  66. package/dist/core/modules/user/core-user.model.js.map +1 -1
  67. package/dist/server/modules/better-auth/better-auth.controller.d.ts +3 -1
  68. package/dist/server/modules/better-auth/better-auth.controller.js +12 -3
  69. package/dist/server/modules/better-auth/better-auth.controller.js.map +1 -1
  70. package/dist/server/modules/better-auth/better-auth.resolver.d.ts +7 -3
  71. package/dist/server/modules/better-auth/better-auth.resolver.js +16 -6
  72. package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -1
  73. package/dist/server/modules/error-code/error-codes.d.ts +5 -0
  74. package/dist/server/modules/user/user.model.d.ts +5 -0
  75. package/dist/templates/email-verification-de.ejs +78 -0
  76. package/dist/templates/email-verification-en.ejs +78 -0
  77. package/dist/test/test.helper.d.ts +4 -0
  78. package/dist/test/test.helper.js +54 -1
  79. package/dist/test/test.helper.js.map +1 -1
  80. package/dist/tsconfig.build.tsbuildinfo +1 -1
  81. package/package.json +10 -10
  82. package/src/config.env.ts +2 -0
  83. package/src/core/common/interfaces/server-options.interface.ts +240 -0
  84. package/src/core/modules/auth/core-auth.controller.ts +2 -2
  85. package/src/core/modules/auth/core-auth.resolver.ts +2 -2
  86. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +113 -0
  87. package/src/core/modules/better-auth/README.md +72 -7
  88. package/src/core/modules/better-auth/better-auth-token.service.ts +5 -8
  89. package/src/core/modules/better-auth/better-auth.config.ts +282 -29
  90. package/src/core/modules/better-auth/better-auth.resolver.ts +16 -5
  91. package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +100 -22
  92. package/src/core/modules/better-auth/core-better-auth-auth.model.ts +10 -0
  93. package/src/core/modules/better-auth/core-better-auth-cookie.helper.ts +323 -0
  94. package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +433 -0
  95. package/src/core/modules/better-auth/core-better-auth-models.ts +6 -3
  96. package/src/core/modules/better-auth/core-better-auth-signup-validator.service.ts +178 -0
  97. package/src/core/modules/better-auth/core-better-auth-token.helper.ts +200 -0
  98. package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +18 -14
  99. package/src/core/modules/better-auth/core-better-auth-web.helper.ts +119 -69
  100. package/src/core/modules/better-auth/core-better-auth.controller.ts +197 -84
  101. package/src/core/modules/better-auth/core-better-auth.middleware.ts +93 -64
  102. package/src/core/modules/better-auth/core-better-auth.module.ts +215 -38
  103. package/src/core/modules/better-auth/core-better-auth.resolver.ts +140 -20
  104. package/src/core/modules/better-auth/core-better-auth.service.ts +210 -32
  105. package/src/core/modules/better-auth/index.ts +4 -0
  106. package/src/core/modules/error-code/error-codes.ts +45 -0
  107. package/src/core/modules/user/core-user.model.ts +15 -0
  108. package/src/server/modules/better-auth/better-auth.controller.ts +6 -2
  109. package/src/server/modules/better-auth/better-auth.resolver.ts +16 -5
  110. package/src/templates/email-verification-de.ejs +78 -0
  111. package/src/templates/email-verification-en.ejs +78 -0
  112. package/src/test/README.md +190 -0
  113. 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
+ }