improve security: back port, base port, login guard, scripts

This commit is contained in:
iqubik
2026-04-03 02:23:05 +03:00
parent 41265ccf37
commit e23de664ac
26 changed files with 1172 additions and 221 deletions
+11
View File
@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@@ -27,6 +28,12 @@ import { SessionModule } from './session/session.module';
ConfigModule.forRoot({
isGlobal: true,
}),
ThrottlerModule.forRoot([
{
ttl: 60000,
limit: 5,
},
]),
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DB_HOST,
@@ -51,6 +58,10 @@ import { SessionModule } from './session/session.module';
controllers: [AppController],
providers: [
AppService,
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
+63 -3
View File
@@ -4,9 +4,15 @@ import {
Body,
HttpException,
HttpStatus,
Res,
Logger,
Req,
} from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
import { AuthService } from './auth.service';
import { Public } from './public.decorator';
import type { Request, Response } from 'express';
import { ConfigService } from '@nestjs/config';
interface LoginDto {
login: string;
@@ -15,19 +21,73 @@ interface LoginDto {
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
private readonly logger = new Logger(AuthController.name);
constructor(
private authService: AuthService,
private configService: ConfigService,
) {}
@Public()
@Throttle({
default: { limit: 5, ttl: 60000 },
})
@Post('login')
async login(@Body() req: LoginDto) {
async login(
@Body() req: LoginDto,
@Res({ passthrough: true }) res: Response,
) {
this.logger.debug(`Login request received for user: ${req.login}`);
const user = await this.authService.validateUser(req.login, req.password);
if (!user) {
this.logger.warn(`Login failed for user: ${req.login}`);
throw new HttpException(
'Неверный логин или пароль',
HttpStatus.UNAUTHORIZED,
);
}
return this.authService.login(user as { login: string });
const { access_token } = this.authService.login(user as { login: string });
// Устанавливаем httpOnly cookie
const isProduction =
this.configService.get<string>('NODE_ENV') === 'production';
this.logger.debug(
`Login succeeded for user: ${req.login}. Setting auth cookie (secure=${isProduction}, sameSite=lax, maxAgeMs=86400000)`,
);
res.cookie('access_token', access_token, {
httpOnly: true,
secure: isProduction, // HTTPS только в production
sameSite: 'lax',
maxAge: 24 * 60 * 60 * 1000, // 24 часа
path: '/',
});
this.logger.debug(`Login response prepared for user: ${req.login}`);
return { access_token };
}
@Public()
@Post('logout')
logout(@Req() req: Request, @Res({ passthrough: true }) res: Response) {
const hadCookie = Boolean(
(req.cookies as Record<string, unknown> | undefined)?.access_token,
);
this.logger.debug(`Logout request received. Cookie present: ${hadCookie}`);
// Очищаем httpOnly cookie
const isProduction =
this.configService.get<string>('NODE_ENV') === 'production';
res.clearCookie('access_token', {
httpOnly: true,
secure: isProduction,
sameSite: 'lax',
path: '/',
});
this.logger.debug(
`Auth cookie cleared (secure=${isProduction}, sameSite=lax, path=/)`,
);
return { success: true };
}
@Post('change-password')
+4 -7
View File
@@ -30,13 +30,9 @@ export class AuthService {
where: { key: 'admin_password' },
});
if (!dbLogin) {
this.logger.error('Пользователь admin_login не найден в базе данных!');
return null;
}
if (!dbPass) {
this.logger.error('Пароль admin_password не найден в базе данных!');
// Проверяем наличие учётных данных (без деталей для безопасности)
if (!dbLogin || !dbPass) {
this.logger.error('Учётные данные не найдены в базе данных');
return null;
}
@@ -55,6 +51,7 @@ export class AuthService {
login(user: { login: string }) {
const payload = { username: user.login };
this.logger.debug(`Генерация access token для пользователя: ${user.login}`);
return {
access_token: this.jwtService.sign(payload),
};
+34 -1
View File
@@ -8,6 +8,10 @@ import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
type RequestWithCookies = Request & {
cookies?: unknown;
};
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
private readonly logger = new Logger(JwtAuthGuard.name);
@@ -17,7 +21,11 @@ export class JwtAuthGuard extends AuthGuard('jwt') {
}
canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest<Request>();
const request = context.switchToHttp().getRequest<RequestWithCookies>();
const cookies: Record<string, unknown> =
request.cookies && typeof request.cookies === 'object'
? (request.cookies as Record<string, unknown>)
: {};
this.logger.debug(
`canActivate called for: ${request.url} ${request.method}`,
);
@@ -33,6 +41,23 @@ export class JwtAuthGuard extends AuthGuard('jwt') {
return true;
}
// Поддержка токена из cookie (httpOnly)
const tokenFromCookieValue = cookies.access_token;
const tokenFromCookie =
typeof tokenFromCookieValue === 'string'
? tokenFromCookieValue
: undefined;
if (tokenFromCookie && !request.headers.authorization) {
this.logger.debug(
`Token found in cookie, adding to Authorization header`,
);
request.headers.authorization = `Bearer ${tokenFromCookie}`;
} else if (tokenFromCookie) {
this.logger.debug(
`Token found in cookie, but Authorization header already exists`,
);
}
// Support token from query parameter (for SSE connections)
const tokenFromQuery = request.query.token as string | undefined;
if (tokenFromQuery && !request.headers.authorization) {
@@ -40,6 +65,14 @@ export class JwtAuthGuard extends AuthGuard('jwt') {
`Token found in query parameter, adding to Authorization header`,
);
request.headers.authorization = `Bearer ${tokenFromQuery}`;
} else if (tokenFromQuery) {
this.logger.debug(
`Token found in query parameter, but Authorization header already exists`,
);
} else if (!request.headers.authorization) {
this.logger.debug(
`No token found in Authorization header, cookie, or query`,
);
}
this.logger.debug(`Calling super.canActivate()`);
+46 -4
View File
@@ -3,7 +3,8 @@ import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
import { AuthService } from './auth/auth.service';
import { RequestMethod, Logger, LogLevel } from '@nestjs/common';
import { Request, Response } from 'express';
import { Request, Response, NextFunction, RequestHandler } from 'express';
import cookieParser from 'cookie-parser';
import { HttpExceptionFilter } from './client/client.exception-filter';
import { ConfigService } from '@nestjs/config';
@@ -23,12 +24,53 @@ async function bootstrap() {
app.useLogger(logLevels);
app.use((req: Request, res: Response, next: NextFunction) => {
const startedAt = Date.now();
res.on('finish', () => {
logger.debug(
`${req.method} ${req.originalUrl} -> ${res.statusCode} (${Date.now() - startedAt}ms)`,
);
});
next();
});
app.set('trust proxy', 1);
// Cookie parser для работы с httpOnly cookies
const cookieParserFactory = cookieParser as unknown as () => RequestHandler;
app.use(cookieParserFactory());
const authService = app.get(AuthService);
await authService.seedAdmin();
app.enableCors();
const allowedOrigins = (
configService.get<string>('ALLOWED_ORIGINS', '') || ''
)
.split(',')
.map((origin) => origin.trim())
.filter(Boolean);
logger.log(
`CORS origins: ${
allowedOrigins.length > 0
? allowedOrigins.join(', ')
: 'all origins allowed'
}`,
);
app.enableCors({
origin: (origin, callback) => {
// Разрешаем запросы без origin (например, из мобильных приложений или curl)
if (!origin) return callback(null, true);
if (allowedOrigins.length === 0 || allowedOrigins.includes(origin)) {
return callback(null, true);
}
logger.warn(`CORS blocked origin: ${origin}`);
return callback(new Error('Not allowed by CORS'), false);
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
});
app.useGlobalFilters(new HttpExceptionFilter());
app.setGlobalPrefix('api', {
exclude: [
@@ -37,8 +79,8 @@ async function bootstrap() {
],
});
const port = configService.get<number>('PORT', 3000);
await app.listen(port);
const port = configService.get<number>('PORT', 3100);
await app.listen(port, '0.0.0.0');
logger.log(`Application started on port ${port}`);
}
void bootstrap();