diff --git a/backend/package-lock.json b/backend/package-lock.json index 71eb8fd..0ac44d3 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,6 +13,7 @@ "@nestjs/config": "^4.0.3", "@nestjs/core": "^11.0.1", "@nestjs/platform-express": "^11.0.1", + "@nestjs/throttler": "^6.7.1", "@nestjs/typeorm": "^12.0.2", "altcha-lib": "^2.6.0", "class-transformer": "^0.5.1", @@ -1995,6 +1996,20 @@ "tslib": "^2.1.0" } }, + "node_modules/@nestjs/throttler": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.7.1.tgz", + "integrity": "sha512-gAyY1AkU+cSK5DCjFOB43xax9oakameH4xZa9kAgqBqSBQZgmv3DZylLV8KyZNzOGhceL3kDVtby+CF8VAdmEA==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0" + } + }, "node_modules/@nestjs/typeorm": { "version": "12.0.2", "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-12.0.2.tgz", diff --git a/backend/package.json b/backend/package.json index e856fd1..de9e26b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -24,6 +24,7 @@ "@nestjs/config": "^4.0.3", "@nestjs/core": "^11.0.1", "@nestjs/platform-express": "^11.0.1", + "@nestjs/throttler": "^6.7.1", "@nestjs/typeorm": "^12.0.2", "altcha-lib": "^2.6.0", "class-transformer": "^0.5.1", diff --git a/backend/src/AppModule.ts b/backend/src/AppModule.ts index 19734d5..762e603 100644 --- a/backend/src/AppModule.ts +++ b/backend/src/AppModule.ts @@ -1,5 +1,7 @@ import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService, registerAs } from '@nestjs/config'; +import { APP_GUARD } from '@nestjs/core'; +import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; import { TypeOrmModule } from '@nestjs/typeorm'; import { getAltchaConfig, getAppConfig, getPostgresConfig } from './config'; import { validate } from './config/validate'; @@ -20,6 +22,22 @@ import { Config } from './types/Config'; registerAs('altcha', getAltchaConfig) ] }), + ThrottlerModule.forRootAsync({ + inject: [ConfigService], + useFactory: (configService: ConfigService) => { + const { throttle } = configService.get('app') as Config['app']; + + return { + throttlers: [ + { + name: 'default', + ttl: throttle.ttlMs, + limit: throttle.limit + } + ] + }; + } + }), TypeOrmModule.forRootAsync({ useFactory: (configService: ConfigService) => configService.get('postgres') as Config['postgres'], inject: [ConfigService] @@ -28,6 +46,12 @@ import { Config } from './types/Config'; AltchaModule, StorageModule, UploadModule + ], + providers: [ + { + provide: APP_GUARD, + useClass: ThrottlerGuard + } ] }) export class AppModule {} diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 0e185b3..e544bbd 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -41,6 +41,14 @@ export const getAppConfig = (): AppConfig => { corsOrigins: rawOrigins .split(',') .map(o => o.trim()) - .filter(Boolean) + .filter(Boolean), + throttle: { + ttlMs: envInt('THROTTLE_TTL_MS'), + limit: envInt('THROTTLE_LIMIT') + }, + uploadThrottle: { + ttlMs: envInt('THROTTLE_UPLOAD_TTL_MS'), + limit: envInt('THROTTLE_UPLOAD_LIMIT') + } }; }; diff --git a/backend/src/config/throttleProfiles.ts b/backend/src/config/throttleProfiles.ts new file mode 100644 index 0000000..cc44df5 --- /dev/null +++ b/backend/src/config/throttleProfiles.ts @@ -0,0 +1,9 @@ +import { getAppConfig } from './index'; + +const { + uploadThrottle: { limit, ttlMs } +} = getAppConfig(); + +export const throttleProfiles = { + upload: { default: { limit, ttl: ttlMs } } +} as const; diff --git a/backend/src/config/validate.ts b/backend/src/config/validate.ts index a593f8f..f95945d 100644 --- a/backend/src/config/validate.ts +++ b/backend/src/config/validate.ts @@ -75,6 +75,18 @@ class EnvironmentVariables { @Max(3650) NEVER_MODE_INACTIVITY_DAYS: number; + @IsNotEmpty() + @IsNumber() + @Min(1000) + @Max(3_600_000) + THROTTLE_TTL_MS: number; + + @IsNotEmpty() + @IsNumber() + @Min(1) + @Max(1_000_000) + THROTTLE_LIMIT: number; + @IsNotEmpty() @IsNumber() @Min(1000) @@ -86,30 +98,6 @@ class EnvironmentVariables { @Min(1) @Max(10_000) THROTTLE_UPLOAD_LIMIT: number; - - @IsNotEmpty() - @IsNumber() - @Min(1000) - @Max(3_600_000) - THROTTLE_HOTLINK_PER_IP_TTL_MS: number; - - @IsNotEmpty() - @IsNumber() - @Min(1) - @Max(1_000_000) - THROTTLE_HOTLINK_PER_IP_LIMIT: number; - - @IsNotEmpty() - @IsNumber() - @Min(1000) - @Max(3_600_000) - THROTTLE_HOTLINK_PER_ID_TTL_MS: number; - - @IsNotEmpty() - @IsNumber() - @Min(1) - @Max(1_000_000) - THROTTLE_HOTLINK_PER_ID_LIMIT: number; } export const validate = (config: Record) => { diff --git a/backend/src/types/AppConfig.ts b/backend/src/types/AppConfig.ts index 1b39094..808eb2f 100644 --- a/backend/src/types/AppConfig.ts +++ b/backend/src/types/AppConfig.ts @@ -1,7 +1,10 @@ import { NodeEnv } from './NodeEnv'; +import { ThrottleConfig } from './ThrottleConfig'; export type AppConfig = { port: number; nodeEnv: NodeEnv; corsOrigins: string[]; + throttle: ThrottleConfig; + uploadThrottle: ThrottleConfig; }; diff --git a/backend/src/types/ThrottleConfig.ts b/backend/src/types/ThrottleConfig.ts new file mode 100644 index 0000000..18d2845 --- /dev/null +++ b/backend/src/types/ThrottleConfig.ts @@ -0,0 +1,4 @@ +export type ThrottleConfig = { + ttlMs: number; + limit: number; +};