improve security: back port, base port, login guard, scripts
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user