diff --git a/backend/.env.example b/backend/.env.example index 9e03ac1..42558ff 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -17,6 +17,16 @@ USE_REDIS_CACHE=true PORT=3001 APP_NAME=STATION BACKEND +# Rate Limiting (TTL in milliseconds) +THROTTLE_TTL=60000 +THROTTLE_LIMIT=100 +AUTH_LOGIN_THROTTLE_TTL=60000 +AUTH_LOGIN_THROTTLE_LIMIT=10 +AUTH_REGISTER_THROTTLE_TTL=60000 +AUTH_REGISTER_THROTTLE_LIMIT=5 +AUTH_FORGOT_THROTTLE_TTL=60000 +AUTH_FORGOT_THROTTLE_LIMIT=5 + # UEX Sync Configuration UEX_SYNC_ENABLED=true UEX_CATEGORIES_SYNC_ENABLED=true diff --git a/backend/package.json b/backend/package.json index 76649e9..b06a1d9 100644 --- a/backend/package.json +++ b/backend/package.json @@ -42,6 +42,7 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/schedule": "^6.0.1", "@nestjs/swagger": "^7.4.2", + "@nestjs/throttler": "^6.5.0", "@nestjs/typeorm": "^10.0.2", "@types/bcrypt": "^6.0.0", "axios": "^1.13.2", diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 80e507e..3ae1f6f 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,8 +1,10 @@ import { Module, DynamicModule } from '@nestjs/common'; +import { APP_GUARD } from '@nestjs/core'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { CacheModule } from '@nestjs/cache-manager'; import { ScheduleModule } from '@nestjs/schedule'; +import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; import { redisStore } from 'cache-manager-redis-yet'; import { UsersModule } from './modules/users/users.module'; import { AuthModule } from './modules/auth/auth.module'; @@ -36,6 +38,17 @@ if (!isTest) { ConfigModule.forRoot({ isGlobal: true, }), + ThrottlerModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => [ + { + name: 'default', + ttl: configService.get('THROTTLE_TTL', 60000), + limit: configService.get('THROTTLE_LIMIT', 100), + }, + ], + }), ...conditionalImports, CacheModule.registerAsync({ isGlobal: true, @@ -118,6 +131,12 @@ if (!isTest) { OrgInventoryModule, ], controllers: [AppController], - providers: [AppService], + providers: [ + AppService, + { + provide: APP_GUARD, + useClass: ThrottlerGuard, + }, + ], }) export class AppModule {} diff --git a/backend/src/modules/auth/auth.controller.ts b/backend/src/modules/auth/auth.controller.ts index 080e06a..7eded24 100644 --- a/backend/src/modules/auth/auth.controller.ts +++ b/backend/src/modules/auth/auth.controller.ts @@ -6,6 +6,7 @@ import { ApiBody, ApiBearerAuth, } from '@nestjs/swagger'; +import { Throttle } from '@nestjs/throttler'; import { AuthService } from './auth.service'; import { LocalAuthGuard } from './local-auth.guard'; import { JwtAuthGuard } from './jwt-auth.guard'; @@ -35,6 +36,12 @@ export class AuthController { }) @ApiResponse({ status: 200, description: 'Successfully logged in' }) @ApiResponse({ status: 401, description: 'Invalid credentials' }) + @Throttle({ + default: { + ttl: parseInt(process.env['AUTH_LOGIN_THROTTLE_TTL'] ?? '60000'), + limit: parseInt(process.env['AUTH_LOGIN_THROTTLE_LIMIT'] ?? '10'), + }, + }) @UseGuards(LocalAuthGuard) @Post('login') async login(@Request() req: ExpressRequest) { @@ -44,7 +51,12 @@ export class AuthController { @ApiOperation({ summary: 'Register new user' }) @ApiResponse({ status: 201, description: 'User successfully registered' }) @ApiResponse({ status: 400, description: 'Invalid input data' }) - // Registration Route: No guards required + @Throttle({ + default: { + ttl: parseInt(process.env['AUTH_REGISTER_THROTTLE_TTL'] ?? '60000'), + limit: parseInt(process.env['AUTH_REGISTER_THROTTLE_LIMIT'] ?? '5'), + }, + }) @Post('register') async register(@Body() userDto: UserDto) { return this.authService.register(userDto); @@ -80,6 +92,12 @@ export class AuthController { description: 'If an account with that email exists, a password reset link has been sent', }) + @Throttle({ + default: { + ttl: parseInt(process.env['AUTH_FORGOT_THROTTLE_TTL'] ?? '60000'), + limit: parseInt(process.env['AUTH_FORGOT_THROTTLE_LIMIT'] ?? '5'), + }, + }) @Post('forgot-password') async forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto) { return this.authService.requestPasswordReset(forgotPasswordDto.email); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6c590f..018d436 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: '@nestjs/swagger': specifier: ^7.4.2 version: 7.4.2(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) + '@nestjs/throttler': + specifier: ^6.5.0 + version: 6.5.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(reflect-metadata@0.2.2) '@nestjs/typeorm': specifier: ^10.0.2 version: 10.0.2(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(babel-plugin-macros@3.1.0)(pg@8.16.3)(redis@5.9.0)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3))) @@ -1067,6 +1070,13 @@ packages: '@nestjs/platform-express': optional: true + '@nestjs/throttler@6.5.0': + resolution: {integrity: sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 + '@nestjs/typeorm@10.0.2': resolution: {integrity: sha512-H738bJyydK4SQkRCTeh1aFBxoO1E9xdL/HaLGThwrqN95os5mEyAtK7BLADOS+vldP4jDZ2VQPLj4epWwRqCeQ==} peerDependencies: @@ -2609,11 +2619,12 @@ packages: glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@13.24.0: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} @@ -5446,6 +5457,12 @@ snapshots: optionalDependencies: '@nestjs/platform-express': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) + '@nestjs/throttler@6.5.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + '@nestjs/typeorm@10.0.2(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(babel-plugin-macros@3.1.0)(pg@8.16.3)(redis@5.9.0)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)))': dependencies: '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)