@lenne.tech/nest-server 11.32.4 → 11.33.1
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/.claude/rules/architecture.md +16 -0
- package/.claude/rules/configurable-features.md +11 -3
- package/.claude/rules/testing.md +101 -1
- package/CLAUDE.md +5 -0
- package/FRAMEWORK-API.md +58 -3
- package/dist/config.env.js +6 -0
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/content-disposition.helper.d.ts +1 -0
- package/dist/core/common/helpers/content-disposition.helper.js +14 -0
- package/dist/core/common/helpers/content-disposition.helper.js.map +1 -0
- package/dist/core/common/helpers/file.helper.d.ts +7 -0
- package/dist/core/common/helpers/file.helper.js +21 -4
- package/dist/core/common/helpers/file.helper.js.map +1 -1
- package/dist/core/common/helpers/graceful-shutdown.helper.d.ts +2 -0
- package/dist/core/common/helpers/graceful-shutdown.helper.js +55 -0
- package/dist/core/common/helpers/graceful-shutdown.helper.js.map +1 -0
- package/dist/core/common/helpers/project-name.helper.d.ts +2 -0
- package/dist/core/common/helpers/project-name.helper.js +38 -0
- package/dist/core/common/helpers/project-name.helper.js.map +1 -0
- package/dist/core/common/interfaces/cron-job-config.interface.d.ts +1 -0
- package/dist/core/common/interfaces/server-options.interface.d.ts +39 -0
- package/dist/core/common/services/core-cron-jobs.initializer.d.ts +10 -0
- package/dist/core/common/services/core-cron-jobs.initializer.js +46 -0
- package/dist/core/common/services/core-cron-jobs.initializer.js.map +1 -0
- package/dist/core/common/services/core-cron-jobs.registry.d.ts +8 -0
- package/dist/core/common/services/core-cron-jobs.registry.js +12 -0
- package/dist/core/common/services/core-cron-jobs.registry.js.map +1 -0
- package/dist/core/common/services/core-cron-jobs.service.d.ts +37 -7
- package/dist/core/common/services/core-cron-jobs.service.js +278 -27
- package/dist/core/common/services/core-cron-jobs.service.js.map +1 -1
- package/dist/core/common/services/core-redis-pubsub.d.ts +20 -0
- package/dist/core/common/services/core-redis-pubsub.js +112 -0
- package/dist/core/common/services/core-redis-pubsub.js.map +1 -0
- package/dist/core/common/services/core-redis.service.d.ts +25 -0
- package/dist/core/common/services/core-redis.service.js +154 -0
- package/dist/core/common/services/core-redis.service.js.map +1 -0
- package/dist/core/common/services/core-s3.service.d.ts +51 -0
- package/dist/core/common/services/core-s3.service.js +261 -0
- package/dist/core/common/services/core-s3.service.js.map +1 -0
- package/dist/core/common/services/core-trust-proxy.initializer.d.ts +16 -0
- package/dist/core/common/services/core-trust-proxy.initializer.js +77 -0
- package/dist/core/common/services/core-trust-proxy.initializer.js.map +1 -0
- package/dist/core/common/services/rate-limit-store.d.ts +61 -0
- package/dist/core/common/services/rate-limit-store.js +235 -0
- package/dist/core/common/services/rate-limit-store.js.map +1 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.d.ts +19 -3
- package/dist/core/modules/ai/core-ai-mcp.controller.js +148 -18
- package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai.service.d.ts +7 -2
- package/dist/core/modules/ai/services/core-ai.service.js +24 -20
- package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
- package/dist/core/modules/auth/core-auth.module.js +4 -1
- package/dist/core/modules/auth/core-auth.module.js.map +1 -1
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.d.ts +1 -1
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js +3 -14
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js.map +1 -1
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.d.ts +12 -10
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +30 -59
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.d.ts +8 -2
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +118 -45
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.d.ts +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js +2 -11
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +12 -10
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +30 -85
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +0 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/file/core-file.controller.d.ts +11 -2
- package/dist/core/modules/file/core-file.controller.js +52 -15
- package/dist/core/modules/file/core-file.controller.js.map +1 -1
- package/dist/core/modules/file/core-file.resolver.d.ts +4 -4
- package/dist/core/modules/file/core-file.resolver.js +23 -16
- package/dist/core/modules/file/core-file.resolver.js.map +1 -1
- package/dist/core/modules/file/core-file.service.d.ts +41 -7
- package/dist/core/modules/file/core-file.service.js +228 -8
- package/dist/core/modules/file/core-file.service.js.map +1 -1
- package/dist/core/modules/file/file-metadata.helper.d.ts +16 -0
- package/dist/core/modules/file/file-metadata.helper.js +33 -0
- package/dist/core/modules/file/file-metadata.helper.js.map +1 -0
- package/dist/core/modules/file/file-roles.helper.d.ts +4 -0
- package/dist/core/modules/file/file-roles.helper.js +53 -0
- package/dist/core/modules/file/file-roles.helper.js.map +1 -0
- package/dist/core/modules/file/file-storage.helper.d.ts +13 -0
- package/dist/core/modules/file/file-storage.helper.js +65 -0
- package/dist/core/modules/file/file-storage.helper.js.map +1 -0
- package/dist/core/modules/file/filesystem-file.helper.d.ts +22 -0
- package/dist/core/modules/file/filesystem-file.helper.js +77 -0
- package/dist/core/modules/file/filesystem-file.helper.js.map +1 -0
- package/dist/core/modules/file/interfaces/file-service-options.interface.d.ts +1 -0
- package/dist/core/modules/file/interfaces/file-upload.interface.d.ts +7 -1
- package/dist/core/modules/file/s3-file.helper.d.ts +37 -0
- package/dist/core/modules/file/s3-file.helper.js +81 -0
- package/dist/core/modules/file/s3-file.helper.js.map +1 -0
- package/dist/core/modules/hub/core-hub.controller.d.ts +8 -8
- package/dist/core/modules/hub/core-hub.controller.js +24 -22
- package/dist/core/modules/hub/core-hub.controller.js.map +1 -1
- package/dist/core/modules/hub/helpers/hub-mask.helper.js +2 -2
- package/dist/core/modules/hub/helpers/hub-mask.helper.js.map +1 -1
- package/dist/core/modules/hub/hub-buffer.d.ts +23 -0
- package/dist/core/modules/hub/hub-buffer.js +101 -0
- package/dist/core/modules/hub/hub-buffer.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-mailbox.service.d.ts +7 -5
- package/dist/core/modules/hub/services/core-hub-mailbox.service.js +14 -10
- package/dist/core/modules/hub/services/core-hub-mailbox.service.js.map +1 -1
- package/dist/core/modules/hub/services/hub-log-buffer.service.d.ts +5 -2
- package/dist/core/modules/hub/services/hub-log-buffer.service.js +17 -8
- package/dist/core/modules/hub/services/hub-log-buffer.service.js.map +1 -1
- package/dist/core/modules/hub/services/hub-query-profiler.service.d.ts +4 -2
- package/dist/core/modules/hub/services/hub-query-profiler.service.js +13 -7
- package/dist/core/modules/hub/services/hub-query-profiler.service.js.map +1 -1
- package/dist/core/modules/hub/services/hub-trace-buffer.service.d.ts +4 -2
- package/dist/core/modules/hub/services/hub-trace-buffer.service.js +13 -8
- package/dist/core/modules/hub/services/hub-trace-buffer.service.js.map +1 -1
- package/dist/core/modules/migrate/helpers/migration.helper.d.ts +2 -6
- package/dist/core/modules/migrate/helpers/migration.helper.js +4 -4
- package/dist/core/modules/migrate/helpers/migration.helper.js.map +1 -1
- package/dist/core/modules/migrate/migration-runner.d.ts +1 -0
- package/dist/core/modules/migrate/migration-runner.js +4 -0
- package/dist/core/modules/migrate/migration-runner.js.map +1 -1
- package/dist/core/modules/migrate/mongo-state-store.d.ts +1 -0
- package/dist/core/modules/migrate/mongo-state-store.js +98 -9
- package/dist/core/modules/migrate/mongo-state-store.js.map +1 -1
- package/dist/core/modules/system-setup/core-system-setup.service.d.ts +2 -0
- package/dist/core/modules/system-setup/core-system-setup.service.js +31 -1
- package/dist/core/modules/system-setup/core-system-setup.service.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.guard.d.ts +17 -3
- package/dist/core/modules/tenant/core-tenant.guard.js +92 -15
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.module.js +3 -4
- package/dist/core/modules/tenant/core-tenant.module.js.map +1 -1
- package/dist/core/modules/tus/core-tus.controller.d.ts +2 -0
- package/dist/core/modules/tus/core-tus.controller.js +29 -3
- package/dist/core/modules/tus/core-tus.controller.js.map +1 -1
- package/dist/core/modules/tus/core-tus.service.d.ts +24 -1
- package/dist/core/modules/tus/core-tus.service.js +182 -28
- package/dist/core/modules/tus/core-tus.service.js.map +1 -1
- package/dist/core/modules/tus/interfaces/tus-config.interface.js +3 -0
- package/dist/core/modules/tus/interfaces/tus-config.interface.js.map +1 -1
- package/dist/core/modules/tus/tus-redis-locker.d.ts +25 -0
- package/dist/core/modules/tus/tus-redis-locker.js +87 -0
- package/dist/core/modules/tus/tus-redis-locker.js.map +1 -0
- package/dist/core/modules/tus/tus.module.d.ts +1 -0
- package/dist/core/modules/tus/tus.module.js +30 -4
- package/dist/core/modules/tus/tus.module.js.map +1 -1
- package/dist/core.module.js +18 -1
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/main.js +2 -1
- package/dist/main.js.map +1 -1
- package/dist/server/modules/file/file.controller.js +2 -2
- package/dist/server/modules/file/file.controller.js.map +1 -1
- package/dist/server/modules/file/file.resolver.js +3 -14
- package/dist/server/modules/file/file.resolver.js.map +1 -1
- package/dist/server/modules/file/file.service.d.ts +10 -2
- package/dist/server/modules/file/file.service.js +23 -3
- package/dist/server/modules/file/file.service.js.map +1 -1
- package/dist/server/modules/user/avatar.controller.d.ts +5 -1
- package/dist/server/modules/user/avatar.controller.js +26 -10
- package/dist/server/modules/user/avatar.controller.js.map +1 -1
- package/dist/server/modules/user/user.model.js +3 -2
- package/dist/server/modules/user/user.model.js.map +1 -1
- package/dist/server/modules/user/user.module.js +6 -2
- package/dist/server/modules/user/user.module.js.map +1 -1
- package/dist/server/modules/user/user.service.d.ts +1 -1
- package/dist/server/modules/user/user.service.js +6 -13
- package/dist/server/modules/user/user.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +57 -8
- package/docs/security-overrides.md +21 -3
- package/migration-guides/11.32.3-to-11.32.4.md +15 -3
- package/migration-guides/11.32.x-to-11.33.x.md +1584 -0
- package/package.json +74 -42
- package/src/config.env.ts +45 -0
- package/src/core/common/helpers/content-disposition.helper.ts +90 -0
- package/src/core/common/helpers/file.helper.ts +53 -10
- package/src/core/common/helpers/graceful-shutdown.helper.ts +116 -0
- package/src/core/common/helpers/project-name.helper.ts +71 -0
- package/src/core/common/interfaces/cron-job-config.interface.ts +16 -0
- package/src/core/common/interfaces/server-options.interface.ts +439 -2
- package/src/core/common/services/core-cron-jobs.initializer.ts +39 -0
- package/src/core/common/services/core-cron-jobs.registry.ts +45 -0
- package/src/core/common/services/core-cron-jobs.service.ts +589 -47
- package/src/core/common/services/core-redis-pubsub.ts +183 -0
- package/src/core/common/services/core-redis.service.ts +249 -0
- package/src/core/common/services/core-s3.service.ts +465 -0
- package/src/core/common/services/core-trust-proxy.initializer.ts +112 -0
- package/src/core/common/services/rate-limit-store.ts +492 -0
- package/src/core/modules/ai/README.md +25 -0
- package/src/core/modules/ai/core-ai-mcp.controller.ts +253 -22
- package/src/core/modules/ai/services/core-ai.service.ts +33 -24
- package/src/core/modules/auth/core-auth.module.ts +7 -1
- package/src/core/modules/auth/guards/legacy-auth-rate-limit.guard.ts +12 -21
- package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +49 -93
- package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +202 -66
- package/src/core/modules/better-auth/core-better-auth-rate-limit.middleware.ts +11 -18
- package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +51 -132
- package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +7 -1
- package/src/core/modules/file/INTEGRATION-CHECKLIST.md +120 -0
- package/src/core/modules/file/README.md +308 -53
- package/src/core/modules/file/core-file.controller.ts +168 -12
- package/src/core/modules/file/core-file.resolver.ts +46 -12
- package/src/core/modules/file/core-file.service.ts +517 -17
- package/src/core/modules/file/file-metadata.helper.ts +142 -0
- package/src/core/modules/file/file-roles.helper.ts +110 -0
- package/src/core/modules/file/file-storage.helper.ts +163 -0
- package/src/core/modules/file/filesystem-file.helper.ts +184 -0
- package/src/core/modules/file/interfaces/file-service-options.interface.ts +14 -0
- package/src/core/modules/file/interfaces/file-upload.interface.ts +17 -1
- package/src/core/modules/file/s3-file.helper.ts +209 -0
- package/src/core/modules/hub/README.md +48 -0
- package/src/core/modules/hub/core-hub.controller.ts +18 -16
- package/src/core/modules/hub/helpers/hub-mask.helper.ts +21 -4
- package/src/core/modules/hub/hub-buffer.ts +198 -0
- package/src/core/modules/hub/services/core-hub-mailbox.service.ts +22 -12
- package/src/core/modules/hub/services/hub-log-buffer.service.ts +24 -8
- package/src/core/modules/hub/services/hub-query-profiler.service.ts +9 -6
- package/src/core/modules/hub/services/hub-trace-buffer.service.ts +14 -9
- package/src/core/modules/migrate/README.md +33 -6
- package/src/core/modules/migrate/helpers/migration.helper.ts +13 -6
- package/src/core/modules/migrate/migration-runner.ts +10 -1
- package/src/core/modules/migrate/mongo-state-store.ts +179 -11
- package/src/core/modules/system-setup/README.md +7 -2
- package/src/core/modules/system-setup/core-system-setup.service.ts +95 -1
- package/src/core/modules/tenant/README.md +21 -1
- package/src/core/modules/tenant/core-tenant.guard.ts +135 -14
- package/src/core/modules/tenant/core-tenant.module.ts +8 -4
- package/src/core/modules/tus/INTEGRATION-CHECKLIST.md +93 -21
- package/src/core/modules/tus/README.md +35 -2
- package/src/core/modules/tus/core-tus.controller.ts +64 -12
- package/src/core/modules/tus/core-tus.service.ts +358 -34
- package/src/core/modules/tus/interfaces/tus-config.interface.ts +9 -0
- package/src/core/modules/tus/tus-redis-locker.ts +139 -0
- package/src/core/modules/tus/tus.module.ts +78 -6
- package/src/core.module.ts +26 -1
- package/src/index.ts +10 -0
- package/src/main.ts +7 -1
- package/src/server/modules/file/file.controller.ts +14 -5
- package/src/server/modules/file/file.resolver.ts +11 -19
- package/src/server/modules/file/file.service.ts +89 -4
- package/src/server/modules/user/avatar.controller.ts +46 -9
- package/src/server/modules/user/user.model.ts +9 -3
- package/src/server/modules/user/user.module.ts +12 -3
- package/src/server/modules/user/user.service.ts +20 -17
- package/src/test/README.md +7 -2
- package/src/core/common/helpers/file.helper.spec.ts +0 -145
- package/src/core/common/helpers/logging.helper.spec.ts +0 -61
- package/src/core/common/helpers/process-diagnostics.helper.spec.ts +0 -310
- package/src/core/common/services/brevo.service.spec.ts +0 -266
- package/src/core/modules/file/core-file.controller.spec.ts +0 -164
- package/src/core/modules/hub/core-hub.module.spec.ts +0 -108
- package/src/core/modules/hub/helpers/hub-command-shape.helper.spec.ts +0 -48
- package/src/core/modules/hub/helpers/hub-mask.helper.spec.ts +0 -67
- package/src/core/modules/hub/helpers/hub-mermaid.helper.spec.ts +0 -61
- package/src/core/modules/hub/helpers/hub-shell.helper.spec.ts +0 -106
- package/src/core/modules/hub/hub-config.helper.spec.ts +0 -108
- package/src/core/modules/hub/hub-ring-buffer.spec.ts +0 -95
- package/src/core/modules/hub/services/core-hub-mailbox.service.spec.ts +0 -116
- package/src/core/modules/hub/services/hub-trace-buffer.service.spec.ts +0 -112
- package/src/core/modules/migrate/helpers/migration.helper.spec.ts +0 -85
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
|
1
|
+
import { Injectable, Logger, OnModuleInit, Optional } from '@nestjs/common';
|
|
2
2
|
|
|
3
3
|
import { IAuthRateLimit } from '../../../common/interfaces/server-options.interface';
|
|
4
4
|
import { ConfigService } from '../../../common/services/config.service';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
5
|
+
import { CoreRedisService } from '../../../common/services/core-redis.service';
|
|
6
|
+
import {
|
|
7
|
+
InMemoryRateLimitStore,
|
|
8
|
+
rateLimitKey,
|
|
9
|
+
rateLimitKeyPrefix,
|
|
10
|
+
RateLimitStore,
|
|
11
|
+
RedisRateLimitStore,
|
|
12
|
+
} from '../../../common/services/rate-limit-store';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Result of a rate limit check
|
|
@@ -41,10 +41,11 @@ const DEFAULT_CONFIG: Required<IAuthRateLimit> = {
|
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
44
|
+
* Rate limiter for Legacy Auth endpoints
|
|
45
45
|
*
|
|
46
46
|
* This service provides rate limiting to protect against brute-force attacks
|
|
47
|
-
* on authentication endpoints.
|
|
47
|
+
* on authentication endpoints. Counters live in a {@link RateLimitStore}: shared
|
|
48
|
+
* via Redis when `ServerOptions.redis` is configured, process-local otherwise.
|
|
48
49
|
*
|
|
49
50
|
* Features:
|
|
50
51
|
* - Configurable request limits and time windows
|
|
@@ -68,15 +69,11 @@ const DEFAULT_CONFIG: Required<IAuthRateLimit> = {
|
|
|
68
69
|
*/
|
|
69
70
|
@Injectable()
|
|
70
71
|
export class LegacyAuthRateLimiter implements OnModuleInit {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
private cleanupInterval: NodeJS.Timeout | null = null;
|
|
72
|
+
protected readonly logger = new Logger(LegacyAuthRateLimiter.name);
|
|
73
|
+
protected config: Required<IAuthRateLimit> = DEFAULT_CONFIG;
|
|
74
|
+
protected store?: RateLimitStore;
|
|
75
75
|
|
|
76
|
-
constructor() {
|
|
77
|
-
// Start cleanup interval (every 5 minutes)
|
|
78
|
-
this.startCleanup();
|
|
79
|
-
}
|
|
76
|
+
constructor(@Optional() protected readonly coreRedisService?: CoreRedisService) {}
|
|
80
77
|
|
|
81
78
|
/**
|
|
82
79
|
* Auto-configure from ConfigService on module initialization
|
|
@@ -127,7 +124,7 @@ export class LegacyAuthRateLimiter implements OnModuleInit {
|
|
|
127
124
|
* @param endpoint - Endpoint name (e.g., 'signIn', 'signUp')
|
|
128
125
|
* @returns Rate limit check result
|
|
129
126
|
*/
|
|
130
|
-
check(ip: string, endpoint: string): RateLimitResult {
|
|
127
|
+
async check(ip: string, endpoint: string): Promise<RateLimitResult> {
|
|
131
128
|
// If rate limiting is disabled, always allow
|
|
132
129
|
if (!this.config.enabled) {
|
|
133
130
|
return {
|
|
@@ -140,43 +137,21 @@ export class LegacyAuthRateLimiter implements OnModuleInit {
|
|
|
140
137
|
}
|
|
141
138
|
|
|
142
139
|
const limit = this.config.max;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
let entry = this.store.get(key);
|
|
148
|
-
|
|
149
|
-
if (!entry || now >= entry.resetTime) {
|
|
150
|
-
// Create new entry or reset expired one
|
|
151
|
-
entry = {
|
|
152
|
-
count: 1,
|
|
153
|
-
resetTime: now + this.config.windowSeconds * 1000,
|
|
154
|
-
};
|
|
155
|
-
this.store.set(key, entry);
|
|
156
|
-
|
|
157
|
-
return {
|
|
158
|
-
allowed: true,
|
|
159
|
-
current: 1,
|
|
160
|
-
limit,
|
|
161
|
-
remaining: limit - 1,
|
|
162
|
-
resetIn: this.config.windowSeconds,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Increment count
|
|
167
|
-
entry.count++;
|
|
140
|
+
// rateLimitKey(), not string concatenation: an IP that itself contains the separator (every
|
|
141
|
+
// IPv6 address does) would otherwise straddle the ip/endpoint boundary and share a counter
|
|
142
|
+
// with a different caller.
|
|
143
|
+
const { count, resetIn } = await this.getStore().hit(rateLimitKey(ip, endpoint), this.config.windowSeconds);
|
|
168
144
|
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
const remaining = Math.max(0, limit - entry.count);
|
|
145
|
+
const allowed = count <= limit;
|
|
146
|
+
const remaining = Math.max(0, limit - count);
|
|
172
147
|
|
|
173
148
|
if (!allowed) {
|
|
174
|
-
this.logger.warn(`Rate limit exceeded for IP ${this.maskIp(ip)} on ${endpoint}: ${
|
|
149
|
+
this.logger.warn(`Rate limit exceeded for IP ${this.maskIp(ip)} on ${endpoint}: ${count}/${limit}`);
|
|
175
150
|
}
|
|
176
151
|
|
|
177
152
|
return {
|
|
178
153
|
allowed,
|
|
179
|
-
current:
|
|
154
|
+
current: count,
|
|
180
155
|
limit,
|
|
181
156
|
remaining,
|
|
182
157
|
resetIn,
|
|
@@ -202,39 +177,50 @@ export class LegacyAuthRateLimiter implements OnModuleInit {
|
|
|
202
177
|
*
|
|
203
178
|
* @param ip - Client IP address
|
|
204
179
|
*/
|
|
205
|
-
reset(ip: string): void {
|
|
206
|
-
|
|
207
|
-
if (key.startsWith(`${ip}:`)) {
|
|
208
|
-
this.store.delete(key);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
180
|
+
async reset(ip: string): Promise<void> {
|
|
181
|
+
await this.getStore().resetByPrefix(rateLimitKeyPrefix(ip));
|
|
211
182
|
}
|
|
212
183
|
|
|
213
184
|
/**
|
|
214
185
|
* Clear all rate limit entries (useful for testing)
|
|
215
186
|
*/
|
|
216
|
-
clear(): void {
|
|
217
|
-
this.
|
|
187
|
+
async clear(): Promise<void> {
|
|
188
|
+
await this.getStore().clear();
|
|
218
189
|
}
|
|
219
190
|
|
|
220
191
|
/**
|
|
221
192
|
* Get statistics about the rate limiter
|
|
193
|
+
*
|
|
194
|
+
* `activeEntries` is `-1` when a Redis store is in use — the entry count is
|
|
195
|
+
* not cheaply known there.
|
|
222
196
|
*/
|
|
223
197
|
getStats(): { activeEntries: number; enabled: boolean } {
|
|
224
198
|
return {
|
|
225
|
-
activeEntries: this.store
|
|
199
|
+
activeEntries: this.store?.size() ?? 0,
|
|
226
200
|
enabled: this.config.enabled,
|
|
227
201
|
};
|
|
228
202
|
}
|
|
229
203
|
|
|
230
204
|
/**
|
|
231
|
-
* Stop the cleanup interval (for graceful shutdown)
|
|
205
|
+
* Stop the in-memory cleanup interval (for graceful shutdown)
|
|
232
206
|
*/
|
|
233
207
|
onModuleDestroy(): void {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
208
|
+
// Not `instanceof InMemoryRateLimitStore`: a RedisRateLimitStore owns an in-memory fallback
|
|
209
|
+
// whose cleanup interval would otherwise outlive the app.
|
|
210
|
+
this.store?.destroy?.();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Lazily select the counter store: Redis when configured and enabled,
|
|
215
|
+
* process-local otherwise.
|
|
216
|
+
*/
|
|
217
|
+
protected getStore(): RateLimitStore {
|
|
218
|
+
if (!this.store) {
|
|
219
|
+
this.store = this.coreRedisService?.enabled
|
|
220
|
+
? new RedisRateLimitStore(this.coreRedisService, 'legacy-auth')
|
|
221
|
+
: new InMemoryRateLimitStore();
|
|
237
222
|
}
|
|
223
|
+
return this.store;
|
|
238
224
|
}
|
|
239
225
|
|
|
240
226
|
/**
|
|
@@ -250,34 +236,4 @@ export class LegacyAuthRateLimiter implements OnModuleInit {
|
|
|
250
236
|
const parts = ip.split(':');
|
|
251
237
|
return `${parts[0]}:****`;
|
|
252
238
|
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Start periodic cleanup of expired entries
|
|
256
|
-
*/
|
|
257
|
-
private startCleanup(): void {
|
|
258
|
-
// Clean up every 5 minutes
|
|
259
|
-
this.cleanupInterval = setInterval(
|
|
260
|
-
() => {
|
|
261
|
-
const now = Date.now();
|
|
262
|
-
let cleaned = 0;
|
|
263
|
-
|
|
264
|
-
for (const [key, entry] of this.store.entries()) {
|
|
265
|
-
if (now >= entry.resetTime) {
|
|
266
|
-
this.store.delete(key);
|
|
267
|
-
cleaned++;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (cleaned > 0) {
|
|
272
|
-
this.logger.debug(`Cleaned up ${cleaned} expired rate limit entries`);
|
|
273
|
-
}
|
|
274
|
-
},
|
|
275
|
-
5 * 60 * 1000,
|
|
276
|
-
);
|
|
277
|
-
|
|
278
|
-
// Prevent the interval from keeping the process alive
|
|
279
|
-
if (this.cleanupInterval.unref) {
|
|
280
|
-
this.cleanupInterval.unref();
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
239
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Inject, Injectable, Logger, Optional } from '@nestjs/common';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import ejs = require('ejs');
|
|
3
4
|
import * as fs from 'fs';
|
|
4
5
|
import * as path from 'path';
|
|
@@ -7,6 +8,7 @@ import { maskEmail } from '../../common/helpers/logging.helper';
|
|
|
7
8
|
import { IBetterAuthEmailVerificationConfig } from '../../common/interfaces/server-options.interface';
|
|
8
9
|
import { BrevoService } from '../../common/services/brevo.service';
|
|
9
10
|
import { ConfigService } from '../../common/services/config.service';
|
|
11
|
+
import { CoreRedisService } from '../../common/services/core-redis.service';
|
|
10
12
|
import { EmailService } from '../../common/services/email.service';
|
|
11
13
|
import { TemplateService } from '../../common/services/template.service';
|
|
12
14
|
import { formatProjectName } from './better-auth.config';
|
|
@@ -75,7 +77,6 @@ export interface SendVerificationEmailOptions {
|
|
|
75
77
|
* @example
|
|
76
78
|
* ```typescript
|
|
77
79
|
* // Override to customize email sending
|
|
78
|
-
* @Injectable()
|
|
79
80
|
* export class MyEmailVerificationService extends CoreBetterAuthEmailVerificationService {
|
|
80
81
|
* override async sendVerificationEmail(options: SendVerificationEmailOptions): Promise<void> {
|
|
81
82
|
* // Custom logic before
|
|
@@ -87,6 +88,12 @@ export interface SendVerificationEmailOptions {
|
|
|
87
88
|
*
|
|
88
89
|
* @since 11.13.0
|
|
89
90
|
*/
|
|
91
|
+
/**
|
|
92
|
+
* Sentinel returned when the slot was taken on the process-local path, where there is no Redis key
|
|
93
|
+
* to compare against. Distinguishes "acquired locally" from "acquired in Redis with this token".
|
|
94
|
+
*/
|
|
95
|
+
const LOCAL_SLOT_TOKEN = 'local';
|
|
96
|
+
|
|
90
97
|
@Injectable()
|
|
91
98
|
export class CoreBetterAuthEmailVerificationService {
|
|
92
99
|
protected readonly logger = new Logger(CoreBetterAuthEmailVerificationService.name);
|
|
@@ -96,7 +103,12 @@ export class CoreBetterAuthEmailVerificationService {
|
|
|
96
103
|
* In-memory tracking of last send time per email address for cooldown enforcement.
|
|
97
104
|
* Key: email address (lowercase), Value: timestamp (ms) of last send
|
|
98
105
|
*/
|
|
99
|
-
|
|
106
|
+
protected readonly lastSendTimes = new Map<string, number>();
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Pending cleanup timer per email, so a released slot can never expire a LATER cooldown
|
|
110
|
+
*/
|
|
111
|
+
protected readonly sendTimers = new Map<string, NodeJS.Timeout>();
|
|
100
112
|
|
|
101
113
|
/**
|
|
102
114
|
* Token for optional BrevoService injection.
|
|
@@ -113,6 +125,7 @@ export class CoreBetterAuthEmailVerificationService {
|
|
|
113
125
|
@Optional()
|
|
114
126
|
@Inject(CoreBetterAuthEmailVerificationService.BREVO_SERVICE_TOKEN)
|
|
115
127
|
protected readonly brevoService?: BrevoService | null,
|
|
128
|
+
@Optional() protected readonly coreRedisService?: CoreRedisService,
|
|
116
129
|
) {
|
|
117
130
|
this.configure();
|
|
118
131
|
}
|
|
@@ -157,94 +170,209 @@ export class CoreBetterAuthEmailVerificationService {
|
|
|
157
170
|
const { token, user } = options;
|
|
158
171
|
let { url } = options;
|
|
159
172
|
|
|
160
|
-
//
|
|
161
|
-
|
|
173
|
+
// Reserve the cooldown slot per email address (atomic across replicas with Redis).
|
|
174
|
+
// `slotToken`, not `token` — `token` is already the verification token from the options.
|
|
175
|
+
const slotToken = await this.acquireSendSlot(user.email);
|
|
176
|
+
if (!slotToken) {
|
|
162
177
|
this.logger.debug(`Resend cooldown active for ${this.maskEmail(user.email)}, skipping email send`);
|
|
163
178
|
return;
|
|
164
179
|
}
|
|
165
180
|
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
url = this.buildFrontendVerificationUrl(token);
|
|
169
|
-
}
|
|
181
|
+
// Only a SUCCESSFUL send may burn the cooldown — the slot is released again below otherwise
|
|
182
|
+
let sent = false;
|
|
170
183
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
184
|
+
try {
|
|
185
|
+
// Override URL if callbackURL is configured (frontend-based verification)
|
|
186
|
+
if (this.config.callbackURL) {
|
|
187
|
+
url = this.buildFrontendVerificationUrl(token);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Log verification URL in non-production environments for debugging and test capture
|
|
191
|
+
// Uses console.log directly to ensure reliable capture in test environments (Vitest)
|
|
192
|
+
// NestJS Logger may buffer output which makes interception unreliable in tests
|
|
193
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
194
|
+
// oxlint-disable-next-line no-console
|
|
195
|
+
console.log(`[EMAIL VERIFICATION] User: ${user.email}, URL: ${url}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Brevo template path: send via Brevo transactional API if configured
|
|
199
|
+
if (this.config.brevoTemplateId && this.brevoService) {
|
|
200
|
+
try {
|
|
201
|
+
const appName = this.getAppName();
|
|
202
|
+
const result = await this.brevoService.sendMail(user.email, this.config.brevoTemplateId, {
|
|
203
|
+
appName,
|
|
204
|
+
expiresIn: this.formatExpiresIn(this.config.expiresIn),
|
|
205
|
+
link: url,
|
|
206
|
+
name: user.name || user.email.split('@')[0],
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// `sendMail()` swallows SDK errors and resolves to `null` (unless `brevo.throwOnError` is
|
|
210
|
+
// set), so "did not throw" is NOT "was delivered". Recording a send here on a null would
|
|
211
|
+
// mark the address as mailed, log success, and skip the SMTP fallback below — leaving the
|
|
212
|
+
// user with no verification email at all on a Brevo outage or a revoked key.
|
|
213
|
+
if (result === null) {
|
|
214
|
+
this.logger.error(
|
|
215
|
+
`Brevo verification send failed for ${this.maskEmail(user.email)} — falling back to SMTP`,
|
|
216
|
+
);
|
|
217
|
+
// Deliberately no `return`: fall through to the EmailService path.
|
|
218
|
+
} else {
|
|
219
|
+
sent = true;
|
|
220
|
+
this.logger.debug(`Verification email sent via Brevo to ${this.maskEmail(user.email)}`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
this.logger.error(
|
|
225
|
+
`Failed to send verification email via Brevo to ${this.maskEmail(user.email)}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
226
|
+
);
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!this.emailService) {
|
|
232
|
+
this.logger.warn('EmailService not available, cannot send verification email');
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
178
235
|
|
|
179
|
-
// Brevo template path: send via Brevo transactional API if configured
|
|
180
|
-
if (this.config.brevoTemplateId && this.brevoService) {
|
|
181
236
|
try {
|
|
237
|
+
const resolved = await this.resolveTemplatePath(this.config.template, this.config.locale);
|
|
182
238
|
const appName = this.getAppName();
|
|
183
|
-
|
|
239
|
+
|
|
240
|
+
const templateData = {
|
|
184
241
|
appName,
|
|
185
242
|
expiresIn: this.formatExpiresIn(this.config.expiresIn),
|
|
186
243
|
link: url,
|
|
187
244
|
name: user.name || user.email.split('@')[0],
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
this.
|
|
196
|
-
// Deliberately no `return`: fall through to the EmailService path.
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
if (resolved.isAbsolute) {
|
|
248
|
+
// Fallback template from nest-server: render directly via EJS
|
|
249
|
+
const templateContent = fs.readFileSync(`${resolved.path}.ejs`, 'utf-8');
|
|
250
|
+
const html = ejs.render(templateContent, templateData);
|
|
251
|
+
|
|
252
|
+
await this.emailService.sendMail(user.email, this.getEmailSubject(appName), { html });
|
|
197
253
|
} else {
|
|
198
|
-
|
|
199
|
-
this.
|
|
200
|
-
|
|
254
|
+
// Project template: use TemplateService (relative path)
|
|
255
|
+
await this.emailService.sendMail(user.email, this.getEmailSubject(appName), {
|
|
256
|
+
htmlTemplate: resolved.path,
|
|
257
|
+
templateData,
|
|
258
|
+
});
|
|
201
259
|
}
|
|
260
|
+
|
|
261
|
+
sent = true;
|
|
262
|
+
this.logger.debug(`Verification email sent to ${this.maskEmail(user.email)}`);
|
|
202
263
|
} catch (error) {
|
|
203
264
|
this.logger.error(
|
|
204
|
-
`Failed to send verification email
|
|
265
|
+
`Failed to send verification email to ${this.maskEmail(user.email)}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
205
266
|
);
|
|
206
267
|
throw error;
|
|
207
268
|
}
|
|
269
|
+
} finally {
|
|
270
|
+
if (!sent) {
|
|
271
|
+
await this.releaseSendSlot(user.email, slotToken);
|
|
272
|
+
}
|
|
208
273
|
}
|
|
274
|
+
}
|
|
209
275
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
276
|
+
/**
|
|
277
|
+
* Reserve the resend-cooldown slot for an email address.
|
|
278
|
+
*
|
|
279
|
+
* With Redis configured this is a single atomic `SET NX PX`, so two replicas
|
|
280
|
+
* handling concurrent resend requests cannot both pass the cooldown. Without
|
|
281
|
+
* Redis it falls back to the process-local {@link isInCooldown}/{@link trackSend}.
|
|
282
|
+
*
|
|
283
|
+
* @returns a truthy slot token when the caller may send, `null` while the cooldown is active.
|
|
284
|
+
* The token is what {@link releaseSendSlot} compares against before deleting the key, so a
|
|
285
|
+
* release can only ever clear the slot IT acquired. A subclass overriding this and returning a
|
|
286
|
+
* plain `true` still works — release then falls back to the previous unconditional delete.
|
|
287
|
+
*/
|
|
288
|
+
protected async acquireSendSlot(email: string): Promise<null | string> {
|
|
289
|
+
const cooldown = this.config.resendCooldownSeconds;
|
|
290
|
+
if (cooldown <= 0) {
|
|
291
|
+
return LOCAL_SLOT_TOKEN;
|
|
213
292
|
}
|
|
214
293
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
} else {
|
|
233
|
-
// Project template: use TemplateService (relative path)
|
|
234
|
-
await this.emailService.sendMail(user.email, this.getEmailSubject(appName), {
|
|
235
|
-
htmlTemplate: resolved.path,
|
|
236
|
-
templateData,
|
|
237
|
-
});
|
|
294
|
+
if (this.coreRedisService?.enabled) {
|
|
295
|
+
try {
|
|
296
|
+
const token = randomUUID();
|
|
297
|
+
const result = await this.coreRedisService
|
|
298
|
+
.getClient()
|
|
299
|
+
.set(this.cooldownKey(email), token, 'PX', cooldown * 1000, 'NX');
|
|
300
|
+
return result === 'OK' ? token : null;
|
|
301
|
+
} catch (error) {
|
|
302
|
+
// Degrade to the process-local cooldown rather than failing closed. An unhandled error
|
|
303
|
+
// here reads as "cooldown active" and would stop verification mail entirely for the
|
|
304
|
+
// duration of a Redis blip — locking new users out of their accounts. The local counter
|
|
305
|
+
// is the pre-Redis behavior: still a real anti-flood bound, just per replica.
|
|
306
|
+
this.logger.warn(
|
|
307
|
+
`Redis cooldown unavailable, falling back to the per-replica counter: ${
|
|
308
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
309
|
+
}`,
|
|
310
|
+
);
|
|
238
311
|
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (this.isInCooldown(email)) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
this.trackSend(email);
|
|
318
|
+
return LOCAL_SLOT_TOKEN;
|
|
319
|
+
}
|
|
239
320
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
321
|
+
/**
|
|
322
|
+
* Release a slot reserved by {@link acquireSendSlot} — used when no email was
|
|
323
|
+
* actually sent, so a failed send does not burn the cooldown.
|
|
324
|
+
*/
|
|
325
|
+
protected async releaseSendSlot(email: string, token?: null | string): Promise<void> {
|
|
326
|
+
if (this.config.resendCooldownSeconds <= 0) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (this.coreRedisService?.enabled) {
|
|
331
|
+
// Releasing runs in a `finally` after a FAILED send. Letting a Redis error escape here
|
|
332
|
+
// would replace the real send failure with a connection error in the caller's log — and
|
|
333
|
+
// the key expires on its own TTL anyway, so the worst case is one cooldown served out.
|
|
334
|
+
// Compare-and-delete, never a bare DEL. Releasing runs in a `finally` after a FAILED send,
|
|
335
|
+
// and a send can fail SLOWER than the cooldown (an SMTP socket timeout outlives a 60s
|
|
336
|
+
// window comfortably). By then our key has expired and a later request holds a new one — an
|
|
337
|
+
// unconditional delete would clear THAT request's cooldown, and repeating the trick floods
|
|
338
|
+
// a chosen address with valid verification links. Same pattern as the migration lock and
|
|
339
|
+
// the TUS locker.
|
|
340
|
+
if (typeof token === 'string' && token !== LOCAL_SLOT_TOKEN) {
|
|
341
|
+
const script = `if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end return 0`;
|
|
342
|
+
await this.coreRedisService
|
|
343
|
+
.getClient()
|
|
344
|
+
.eval(script, 1, this.cooldownKey(email), token)
|
|
345
|
+
.catch(() => undefined);
|
|
346
|
+
} else if (token === undefined) {
|
|
347
|
+
// No token supplied (a subclass acquired the slot the old way) — preserve the previous
|
|
348
|
+
// behavior rather than silently skipping the release.
|
|
349
|
+
await this.coreRedisService
|
|
350
|
+
.getClient()
|
|
351
|
+
.del(this.cooldownKey(email))
|
|
352
|
+
.catch(() => undefined);
|
|
353
|
+
}
|
|
354
|
+
// Deliberately NOT `return`: when acquire fell back to the local counter because Redis
|
|
355
|
+
// errored, the Redis key was never written and only the local entry exists. Branching on
|
|
356
|
+
// `enabled` alone would leave it in place, so the failed send would burn the cooldown —
|
|
357
|
+
// the exact semantics the acquire/release split exists to preserve. Clearing both is
|
|
358
|
+
// harmless: the local map is empty whenever Redis actually served the acquire.
|
|
247
359
|
}
|
|
360
|
+
|
|
361
|
+
// Clear the pending cleanup timer too. Left running, it would fire `cooldown` seconds
|
|
362
|
+
// after the FAILED send and delete whatever entry exists then — which is the entry of a
|
|
363
|
+
// later, successful send. The cooldown would end early and the anti-flood control it
|
|
364
|
+
// exists to be would quietly weaken.
|
|
365
|
+
const key = email.toLowerCase();
|
|
366
|
+
clearTimeout(this.sendTimers.get(key));
|
|
367
|
+
this.sendTimers.delete(key);
|
|
368
|
+
this.lastSendTimes.delete(key);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Redis key for the resend cooldown of an email address
|
|
373
|
+
*/
|
|
374
|
+
protected cooldownKey(email: string): string {
|
|
375
|
+
return this.coreRedisService!.key('email-verification-cooldown', email.toLowerCase());
|
|
248
376
|
}
|
|
249
377
|
|
|
250
378
|
/**
|
|
@@ -451,10 +579,18 @@ export class CoreBetterAuthEmailVerificationService {
|
|
|
451
579
|
|
|
452
580
|
this.lastSendTimes.set(key, Date.now());
|
|
453
581
|
|
|
454
|
-
// Schedule cleanup to prevent memory leak
|
|
582
|
+
// Schedule cleanup to prevent memory leak. Any timer left over from an earlier,
|
|
583
|
+
// RELEASED send is cancelled first — it would otherwise delete THIS entry when it
|
|
584
|
+
// fires and end the new cooldown early.
|
|
455
585
|
const cooldown = this.config.resendCooldownSeconds;
|
|
456
586
|
if (cooldown > 0) {
|
|
457
|
-
|
|
587
|
+
clearTimeout(this.sendTimers.get(key));
|
|
588
|
+
const timer = setTimeout(() => {
|
|
589
|
+
this.lastSendTimes.delete(key);
|
|
590
|
+
this.sendTimers.delete(key);
|
|
591
|
+
}, cooldown * 1000);
|
|
592
|
+
timer.unref?.();
|
|
593
|
+
this.sendTimers.set(key, timer);
|
|
458
594
|
}
|
|
459
595
|
}
|
|
460
596
|
|
|
@@ -36,7 +36,7 @@ export class CoreBetterAuthRateLimitMiddleware implements NestMiddleware {
|
|
|
36
36
|
private readonly betterAuthService: CoreBetterAuthService,
|
|
37
37
|
) {}
|
|
38
38
|
|
|
39
|
-
use(req: Request, res: Response, next: NextFunction) {
|
|
39
|
+
async use(req: Request, res: Response, next: NextFunction) {
|
|
40
40
|
// Skip if Better-Auth is not enabled
|
|
41
41
|
if (!this.betterAuthService.isEnabled()) {
|
|
42
42
|
return next();
|
|
@@ -55,7 +55,7 @@ export class CoreBetterAuthRateLimitMiddleware implements NestMiddleware {
|
|
|
55
55
|
const path = req.path.startsWith(basePath) ? req.path.substring(basePath.length) : req.path;
|
|
56
56
|
|
|
57
57
|
// Check rate limit
|
|
58
|
-
const result = this.rateLimiter.check(ip, path);
|
|
58
|
+
const result = await this.rateLimiter.check(ip, path);
|
|
59
59
|
|
|
60
60
|
// Add rate limit headers
|
|
61
61
|
this.addRateLimitHeaders(res, result);
|
|
@@ -77,24 +77,17 @@ export class CoreBetterAuthRateLimitMiddleware implements NestMiddleware {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
80
|
+
* Get the client IP that the rate limit counter is keyed on.
|
|
81
|
+
*
|
|
82
|
+
* `req.ip` — NOT `x-forwarded-for` directly. Express derives `req.ip` from the forwarded chain
|
|
83
|
+
* only as far as the app's `trust proxy` setting allows, and falls back to the socket address
|
|
84
|
+
* otherwise. Reading the header ourselves skips that check entirely, so any client simply picks
|
|
85
|
+
* its own bucket by sending a fresh value per request: the counter never reaches the limit, and
|
|
86
|
+
* the brute-force protection on the auth endpoints is off. A deployment behind a proxy must
|
|
87
|
+
* therefore configure `trust proxy` — that is the one place where "which hop do I believe"
|
|
88
|
+
* belongs.
|
|
82
89
|
*/
|
|
83
90
|
private getClientIp(req: Request): string {
|
|
84
|
-
// Check X-Forwarded-For header (for proxied requests)
|
|
85
|
-
const forwardedFor = req.headers['x-forwarded-for'];
|
|
86
|
-
if (forwardedFor) {
|
|
87
|
-
const ips = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor.split(',')[0];
|
|
88
|
-
return ips.trim();
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Check X-Real-IP header (nginx)
|
|
92
|
-
const realIp = req.headers['x-real-ip'];
|
|
93
|
-
if (realIp) {
|
|
94
|
-
return Array.isArray(realIp) ? realIp[0] : realIp;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Fall back to connection remote address
|
|
98
91
|
return req.ip || req.socket?.remoteAddress || 'unknown';
|
|
99
92
|
}
|
|
100
93
|
|