feat: add some security

This commit is contained in:
2026-07-11 17:34:18 +03:30
parent f80cf95f65
commit f399ec4a04
10 changed files with 69 additions and 9 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"local": { "local": {
"baseUrl": "http://localhost:3000" "baseUrl": "http://localhost:3000/v1"
}, },
"dev": { "dev": {
"baseUrl": "http://localhost:3000" "baseUrl": "http://localhost:3000"
+7
View File
@@ -2,4 +2,11 @@ import { registerAs } from '@nestjs/config';
export default registerAs('app', () => ({ export default registerAs('app', () => ({
environment: process.env.NODE_ENV || 'production', 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(),
),
})); }));
+3
View File
@@ -18,4 +18,7 @@ export default Joi.object({
JWT_AUDIENCE: Joi.string().required(), JWT_AUDIENCE: Joi.string().required(),
REDIS_HOST: Joi.string().required(), REDIS_HOST: Joi.string().required(),
REDIS_PORT: Joi.number().port().default(6379), REDIS_PORT: Joi.number().port().default(6379),
CORS_ORIGINS: Joi.string().required(),
CORS_SUBDOMAIN: Joi.string().required(),
SWAGGER_SERVERS: Joi.string().required(),
}); });
+44 -8
View File
@@ -4,10 +4,13 @@ import { I18nValidationExceptionFilter, I18nValidationPipe } from 'nestjs-i18n';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ResponseInterceptor } from './common/interceptors/response.interceptor'; import { ResponseInterceptor } from './common/interceptors/response.interceptor';
import helmet from 'helmet'; import helmet from 'helmet';
import { ConfigService } from '@nestjs/config';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('v1');
app.useGlobalPipes( app.useGlobalPipes(
new I18nValidationPipe({ new I18nValidationPipe({
whitelist: true, whitelist: true,
@@ -32,24 +35,57 @@ async function bootstrap() {
app.useGlobalInterceptors(new ResponseInterceptor()); app.useGlobalInterceptors(new ResponseInterceptor());
const configService = app.get(ConfigService);
const origins = configService.get<string[]>('app.corsOrigins');
const subdomain = configService.get<string>('app.corsSubdomain');
console.log(subdomain, origins);
app.enableCors({ 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, credentials: true,
}); });
app.use(helmet()); app.use(helmet());
const config = new DocumentBuilder() const config = new DocumentBuilder()
.setTitle('Borna API') .setTitle('Heala API')
.setDescription('Borna API Documentation') .setDescription('Heala API Documentation')
.setVersion('1.0') .setVersion('1.0')
.setLicense('MIT', 'https://opensource.org/license/mit') .setLicense('MIT', 'https://opensource.org/license/mit');
.addServer('http://localhost:3000')
.build();
const document = SwaggerModule.createDocument(app, config); const swaggerServers = configService.get<string[]>('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); await app.listen(process.env.PORT ?? 3000);
} }
@@ -2,8 +2,10 @@ import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { AdminAuthService } from './providers/admin-auth.service'; import { AdminAuthService } from './providers/admin-auth.service';
import { AdminLoginDTO } from './dtos/admin-login.dto'; import { AdminLoginDTO } from './dtos/admin-login.dto';
import { Public } from '@/modules/auth/decorators/public.decorator'; import { Public } from '@/modules/auth/decorators/public.decorator';
import { Throttle } from '@nestjs/throttler';
@Controller('admin/auth') @Controller('admin/auth')
@Throttle({ login: {} })
export class AdminAuthController { export class AdminAuthController {
constructor( constructor(
/** /**
@@ -17,9 +17,11 @@ import {
Post, Post,
Query, Query,
} from '@nestjs/common'; } from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
@Controller('admin/choices') @Controller('admin/choices')
@Roles(Role.SUPERUSER, Role.ADMIN) @Roles(Role.SUPERUSER, Role.ADMIN)
@Throttle({ admin: {} })
export class AdminChoicesController { export class AdminChoicesController {
constructor( constructor(
/** /**
@@ -21,9 +21,11 @@ import {
Post, Post,
Query, Query,
} from '@nestjs/common'; } from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
@Controller('admin/questions') @Controller('admin/questions')
@Roles(Role.SUPERUSER, Role.ADMIN) @Roles(Role.SUPERUSER, Role.ADMIN)
@Throttle({ admin: {} })
export class AdminQuestionsController { export class AdminQuestionsController {
constructor( constructor(
/** /**
@@ -15,9 +15,11 @@ import {
Post, Post,
Query, Query,
} from '@nestjs/common'; } from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
@Controller('admin/scoring') @Controller('admin/scoring')
@Roles(Role.SUPERUSER, Role.ADMIN) @Roles(Role.SUPERUSER, Role.ADMIN)
@Throttle({ admin: {} })
export class AdminScoringController { export class AdminScoringController {
constructor( constructor(
/** /**
@@ -17,9 +17,11 @@ import {
Post, Post,
Query, Query,
} from '@nestjs/common'; } from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
@Controller('admin/tests') @Controller('admin/tests')
@Roles(Role.SUPERUSER, Role.ADMIN) @Roles(Role.SUPERUSER, Role.ADMIN)
@Throttle({ admin: {} })
export class AdminTestsController { export class AdminTestsController {
constructor( constructor(
/** /**
+4
View File
@@ -4,6 +4,7 @@ import { AuthService } from './providers/auth.service';
import { VerifyOtpDTO } from './dtos/verify-otp.dto'; import { VerifyOtpDTO } from './dtos/verify-otp.dto';
import { Public } from './decorators/public.decorator'; import { Public } from './decorators/public.decorator';
import { RefreshTokenDTO } from './dtos/refresh-token.dto'; import { RefreshTokenDTO } from './dtos/refresh-token.dto';
import { Throttle } from '@nestjs/throttler';
@Controller('auth') @Controller('auth')
export class AuthController { export class AuthController {
@@ -17,6 +18,7 @@ export class AuthController {
@Public() @Public()
@Post('login') @Post('login')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Throttle({ login: {} })
public async logIn(@Body() loginDto: LoginDTO) { public async logIn(@Body() loginDto: LoginDTO) {
return await this.authService.logIn(loginDto); return await this.authService.logIn(loginDto);
} }
@@ -24,6 +26,7 @@ export class AuthController {
@Public() @Public()
@Post('verify-otp') @Post('verify-otp')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Throttle({ otp: {} })
public async verifyOTP(@Body() verifyOtpDto: VerifyOtpDTO) { public async verifyOTP(@Body() verifyOtpDto: VerifyOtpDTO) {
return await this.authService.verifyOTP(verifyOtpDto); return await this.authService.verifyOTP(verifyOtpDto);
} }
@@ -31,6 +34,7 @@ export class AuthController {
@Public() @Public()
@Post('refresh') @Post('refresh')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Throttle({ login: {} })
public async refreshToken(@Body() refreshTokenDto: RefreshTokenDTO) { public async refreshToken(@Body() refreshTokenDto: RefreshTokenDTO) {
return await this.authService.refreshToken(refreshTokenDto); return await this.authService.refreshToken(refreshTokenDto);
} }