From f399ec4a0461804954e72c3b5c6c5d2d19e00e5e Mon Sep 17 00:00:00 2001 From: radmehr Date: Sat, 11 Jul 2026 17:34:18 +0330 Subject: [PATCH] feat: add some security --- http-client.env.json | 2 +- src/config/app.config.ts | 7 +++ src/config/environment.validation.ts | 3 ++ src/main.ts | 52 ++++++++++++++++--- .../admin/auth/admin-auth.controller.ts | 2 + .../admin/choices/admin-choices.controller.ts | 2 + .../questions/admin-questions.controller.ts | 2 + .../admin/scoring/admin-scoring.controller.ts | 2 + .../admin/tests/admin-tests.controller.ts | 2 + src/modules/auth/auth.controller.ts | 4 ++ 10 files changed, 69 insertions(+), 9 deletions(-) diff --git a/http-client.env.json b/http-client.env.json index 1a444be..8523d1e 100644 --- a/http-client.env.json +++ b/http-client.env.json @@ -1,6 +1,6 @@ { "local": { - "baseUrl": "http://localhost:3000" + "baseUrl": "http://localhost:3000/v1" }, "dev": { "baseUrl": "http://localhost:3000" diff --git a/src/config/app.config.ts b/src/config/app.config.ts index 68e88b5..3e0d79a 100644 --- a/src/config/app.config.ts +++ b/src/config/app.config.ts @@ -2,4 +2,11 @@ import { registerAs } from '@nestjs/config'; export default registerAs('app', () => ({ environment: process.env.NODE_ENV || 'production', + corsOrigins: process.env.CORS_ORIGINS?.split(',').map((origin) => + origin.trim(), + ), + corsSubdomain: process.env.CORS_SUBDOMAIN, + swaggerServers: process.env.SWAGGER_SERVERS?.split(',').map((origin) => + origin.trim(), + ), })); diff --git a/src/config/environment.validation.ts b/src/config/environment.validation.ts index 9806bb9..b041b4e 100644 --- a/src/config/environment.validation.ts +++ b/src/config/environment.validation.ts @@ -18,4 +18,7 @@ export default Joi.object({ JWT_AUDIENCE: Joi.string().required(), REDIS_HOST: Joi.string().required(), REDIS_PORT: Joi.number().port().default(6379), + CORS_ORIGINS: Joi.string().required(), + CORS_SUBDOMAIN: Joi.string().required(), + SWAGGER_SERVERS: Joi.string().required(), }); diff --git a/src/main.ts b/src/main.ts index 1f18497..72a04ff 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,10 +4,13 @@ import { I18nValidationExceptionFilter, I18nValidationPipe } from 'nestjs-i18n'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { ResponseInterceptor } from './common/interceptors/response.interceptor'; import helmet from 'helmet'; +import { ConfigService } from '@nestjs/config'; async function bootstrap() { const app = await NestFactory.create(AppModule); + app.setGlobalPrefix('v1'); + app.useGlobalPipes( new I18nValidationPipe({ whitelist: true, @@ -32,24 +35,57 @@ async function bootstrap() { app.useGlobalInterceptors(new ResponseInterceptor()); + const configService = app.get(ConfigService); + + const origins = configService.get('app.corsOrigins'); + const subdomain = configService.get('app.corsSubdomain'); + + console.log(subdomain, origins); + app.enableCors({ - origin: ['http://localhost:5173'], + origin: (origin: string, callback: any) => { + if (!origin) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call + return callback(null, true); + } + + if (origins?.includes(origin)) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call + return callback(null, true); + } + + try { + const hostname = new URL(origin).hostname; + + if (hostname === subdomain || hostname.endsWith(`.${subdomain}`)) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call + return callback(null, true); + } + } catch { + /* empty */ + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + callback(new Error('Not allowed by CORS')); + }, credentials: true, }); app.use(helmet()); const config = new DocumentBuilder() - .setTitle('Borna API') - .setDescription('Borna API Documentation') + .setTitle('Heala API') + .setDescription('Heala API Documentation') .setVersion('1.0') - .setLicense('MIT', 'https://opensource.org/license/mit') - .addServer('http://localhost:3000') - .build(); + .setLicense('MIT', 'https://opensource.org/license/mit'); - const document = SwaggerModule.createDocument(app, config); + const swaggerServers = configService.get('app.swaggerServers'); - SwaggerModule.setup('api/v1', app, document); + swaggerServers?.forEach((server) => config.addServer(server)); + + const document = SwaggerModule.createDocument(app, config.build()); + + SwaggerModule.setup('v1/docs', app, document); await app.listen(process.env.PORT ?? 3000); } diff --git a/src/modules/admin/auth/admin-auth.controller.ts b/src/modules/admin/auth/admin-auth.controller.ts index b2ec5aa..870db40 100644 --- a/src/modules/admin/auth/admin-auth.controller.ts +++ b/src/modules/admin/auth/admin-auth.controller.ts @@ -2,8 +2,10 @@ import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; import { AdminAuthService } from './providers/admin-auth.service'; import { AdminLoginDTO } from './dtos/admin-login.dto'; import { Public } from '@/modules/auth/decorators/public.decorator'; +import { Throttle } from '@nestjs/throttler'; @Controller('admin/auth') +@Throttle({ login: {} }) export class AdminAuthController { constructor( /** diff --git a/src/modules/admin/choices/admin-choices.controller.ts b/src/modules/admin/choices/admin-choices.controller.ts index 9d8e15c..498497f 100644 --- a/src/modules/admin/choices/admin-choices.controller.ts +++ b/src/modules/admin/choices/admin-choices.controller.ts @@ -17,9 +17,11 @@ import { Post, Query, } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; @Controller('admin/choices') @Roles(Role.SUPERUSER, Role.ADMIN) +@Throttle({ admin: {} }) export class AdminChoicesController { constructor( /** diff --git a/src/modules/admin/questions/admin-questions.controller.ts b/src/modules/admin/questions/admin-questions.controller.ts index 2d55d5c..2ec39c9 100644 --- a/src/modules/admin/questions/admin-questions.controller.ts +++ b/src/modules/admin/questions/admin-questions.controller.ts @@ -21,9 +21,11 @@ import { Post, Query, } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; @Controller('admin/questions') @Roles(Role.SUPERUSER, Role.ADMIN) +@Throttle({ admin: {} }) export class AdminQuestionsController { constructor( /** diff --git a/src/modules/admin/scoring/admin-scoring.controller.ts b/src/modules/admin/scoring/admin-scoring.controller.ts index c176cf2..1b7922d 100644 --- a/src/modules/admin/scoring/admin-scoring.controller.ts +++ b/src/modules/admin/scoring/admin-scoring.controller.ts @@ -15,9 +15,11 @@ import { Post, Query, } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; @Controller('admin/scoring') @Roles(Role.SUPERUSER, Role.ADMIN) +@Throttle({ admin: {} }) export class AdminScoringController { constructor( /** diff --git a/src/modules/admin/tests/admin-tests.controller.ts b/src/modules/admin/tests/admin-tests.controller.ts index a8958d3..8f0e001 100644 --- a/src/modules/admin/tests/admin-tests.controller.ts +++ b/src/modules/admin/tests/admin-tests.controller.ts @@ -17,9 +17,11 @@ import { Post, Query, } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; @Controller('admin/tests') @Roles(Role.SUPERUSER, Role.ADMIN) +@Throttle({ admin: {} }) export class AdminTestsController { constructor( /** diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 2f4b52d..c33d611 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -4,6 +4,7 @@ import { AuthService } from './providers/auth.service'; import { VerifyOtpDTO } from './dtos/verify-otp.dto'; import { Public } from './decorators/public.decorator'; import { RefreshTokenDTO } from './dtos/refresh-token.dto'; +import { Throttle } from '@nestjs/throttler'; @Controller('auth') export class AuthController { @@ -17,6 +18,7 @@ export class AuthController { @Public() @Post('login') @HttpCode(HttpStatus.OK) + @Throttle({ login: {} }) public async logIn(@Body() loginDto: LoginDTO) { return await this.authService.logIn(loginDto); } @@ -24,6 +26,7 @@ export class AuthController { @Public() @Post('verify-otp') @HttpCode(HttpStatus.OK) + @Throttle({ otp: {} }) public async verifyOTP(@Body() verifyOtpDto: VerifyOtpDTO) { return await this.authService.verifyOTP(verifyOtpDto); } @@ -31,6 +34,7 @@ export class AuthController { @Public() @Post('refresh') @HttpCode(HttpStatus.OK) + @Throttle({ login: {} }) public async refreshToken(@Body() refreshTokenDto: RefreshTokenDTO) { return await this.authService.refreshToken(refreshTokenDto); }