diff --git a/package-lock.json b/package-lock.json index bfd6179..a3d3864 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,11 +17,13 @@ "@nestjs/platform-express": "^11.0.1", "@nestjs/schedule": "^6.1.1", "@nestjs/swagger": "^11.2.6", + "@nestjs/throttler": "^6.5.0", "@nestjs/typeorm": "^11.0.0", "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.3", "dotenv": "^17.4.2", + "helmet": "^8.2.0", "ioredis": "^5.10.1", "joi": "^18.0.2", "nestjs-i18n": "^10.6.0", @@ -2538,6 +2540,17 @@ } } }, + "node_modules/@nestjs/throttler": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.5.0.tgz", + "integrity": "sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==", + "license": "MIT", + "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" + } + }, "node_modules/@nestjs/typeorm": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.0.tgz", @@ -6091,6 +6104,18 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.2.0.tgz", + "integrity": "sha512-DRgTIUgnWcJ62KyarxxziuqYxKGnR6Rgg19BlbucN/dpmJbl1XOit6qvoOX0ZT+HhWe5OUVhU/a1zpGyc1xA0Q==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/EvanHahn" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", diff --git a/package.json b/package.json index 24cd593..3160d07 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,13 @@ "@nestjs/platform-express": "^11.0.1", "@nestjs/schedule": "^6.1.1", "@nestjs/swagger": "^11.2.6", + "@nestjs/throttler": "^6.5.0", "@nestjs/typeorm": "^11.0.0", "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.3", "dotenv": "^17.4.2", + "helmet": "^8.2.0", "ioredis": "^5.10.1", "joi": "^18.0.2", "nestjs-i18n": "^10.6.0", diff --git a/src/app.module.ts b/src/app.module.ts index 9c60e00..b1b5433 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -13,7 +13,7 @@ import { RedisModule } from './common/modules/redis/redis.module'; import path from 'path'; import jwtConfig from './config/jwt.config'; import redisConfig from './config/redis.config'; -import { APP_FILTER } from '@nestjs/core'; +import { APP_FILTER, APP_GUARD } from '@nestjs/core'; import { GlobalExceptionFilter } from './common/filters/global-exception.filter'; import { ScheduleModule } from '@nestjs/schedule'; import { TestsModule } from './modules/tests/tests.module'; @@ -21,6 +21,8 @@ import { QuestionsModule } from './modules/questions/questions.module'; import { SubmissionsModule } from './modules/submissions/submissions.module'; import { ScoringModule } from './modules/scoring/scoring.module'; import { AdminModule } from './modules/admin/admin.module'; +import { ThrottlerModule } from '@nestjs/throttler'; +import { AppThrottlerGuard } from './common/guards/app-throttler.guard'; const ENV = process.env.NODE_ENV; @@ -68,6 +70,14 @@ const ENV = process.env.NODE_ENV; // Schedule ScheduleModule.forRoot(), + // Throttler + ThrottlerModule.forRoot([ + { name: 'default', limit: 100, ttl: 60_000 }, + { name: 'login', limit: 5, ttl: 60_000 }, + { name: 'otp', limit: 3, ttl: 300_000 }, + { name: 'admin', limit: 30, ttl: 60_000 }, + ]), + /** * Local Modules */ @@ -87,6 +97,10 @@ const ENV = process.env.NODE_ENV; provide: APP_FILTER, useClass: GlobalExceptionFilter, }, + { + provide: APP_GUARD, + useClass: AppThrottlerGuard, + }, ], }) export class AppModule {} diff --git a/src/common/guards/app-throttler.guard.ts b/src/common/guards/app-throttler.guard.ts new file mode 100644 index 0000000..caf9a58 --- /dev/null +++ b/src/common/guards/app-throttler.guard.ts @@ -0,0 +1,13 @@ +import { HttpStatus } from '@nestjs/common'; +import { ThrottlerGuard } from '@nestjs/throttler'; +import { AppException } from '../exceptions/app.exception'; + +export class AppThrottlerGuard extends ThrottlerGuard { + protected throwThrottlingException(): Promise { + throw new AppException( + 'common.errors.tooManyRequests', + HttpStatus.TOO_MANY_REQUESTS, + { minutes: 1 }, + ); + } +} diff --git a/src/main.ts b/src/main.ts index b87fa41..1f18497 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import { AppModule } from './app.module'; import { I18nValidationExceptionFilter, I18nValidationPipe } from 'nestjs-i18n'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { ResponseInterceptor } from './common/interceptors/response.interceptor'; +import helmet from 'helmet'; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -36,6 +37,8 @@ async function bootstrap() { credentials: true, }); + app.use(helmet()); + const config = new DocumentBuilder() .setTitle('Borna API') .setDescription('Borna API Documentation')