From 2c06038b0e78d3fb42375fda00cad1230e0ecf21 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Mon, 14 Apr 2025 04:32:15 -0400 Subject: [PATCH] set up an authorization session using tokens and cookies --- .env | 19 +++++++- package.json | 9 +++- src/auth/auth.controller.ts | 88 ++++++++++++++++++++++++++++++++----- src/auth/auth.guard.ts | 12 +++++ src/auth/auth.module.ts | 23 ++++++++-- src/auth/auth.service.ts | 19 +++++--- src/auth/jwt-auth.guard.ts | 28 ++++++++++++ src/auth/jwt.strategy.ts | 28 ++++++++++++ src/main.ts | 13 +++--- src/metrics.gateway.ts | 12 ++--- 10 files changed, 217 insertions(+), 34 deletions(-) create mode 100644 src/auth/auth.guard.ts create mode 100644 src/auth/jwt-auth.guard.ts create mode 100644 src/auth/jwt.strategy.ts diff --git a/.env b/.env index 4d3d1de..0322715 100644 --- a/.env +++ b/.env @@ -1,9 +1,26 @@ #Прометеус #PROMETHEUS_API=http://192.168.2.34:9090/api/v1 +#FRONTEND_URL=192.168.2.39:5173 + #Постгресс #DB_HOST=192.168.2.37 #DB_PORT=5432 #DB_USER=trust #DB_PASSWORD=kaiqolzp2a4aH -#DB_NAME=trust-db \ No newline at end of file +#DB_NAME=trust-db + + +#JWT +#JWT_SECRET=x7F!2p9L#q1$z0*8R5vYgMnBk +#JWT_SECRET=x7Fcdp9L#q1$z0*8R5vYgMnBk + +#COOKIE +# Для production +#COOKIE_SECURE=true +#COOKIE_SAME_SITE=strict + +# Для development +# COOKIE_SECURE=false +# COOKIE_SAME_SITE=lax + diff --git a/package.json b/package.json index 6469933..388adcb 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,14 @@ "@types/bcrypt": "^5.0.2", "socket.io": "^4.8.1", "@nestjs/websockets": "11.0.12", - "@nestjs/platform-socket.io": "11.0.12" + "@nestjs/platform-socket.io": "11.0.12", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "cookie-parser": "^1.4.7", + "@types/passport-jwt": "^4.0.1", + "@types/cookie-parser": "^1.4.8", + "@nestjs/jwt": "^11.0.0", + "@nestjs/passport": "^11.0.5" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index bd328e3..da29a51 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -1,16 +1,82 @@ -import { Controller, Post, Body, UnauthorizedException } from '@nestjs/common'; +import { Controller, Post, Get, Body, Res, Req, UnauthorizedException, UseGuards } from '@nestjs/common'; import { AuthService } from './auth.service'; +import { Response, Request } from 'express'; +import { JwtAuthGuard } from './jwt-auth.guard'; +import { Logger } from '@nestjs/common/services'; @Controller('auth') export class AuthController { - constructor(private authService: AuthService) { } + private readonly logger = new Logger(AuthController.name); - @Post('login') - async login(@Body() body: { login: string; password: string }) { - const user = await this.authService.validateUser(body.login, body.password); - if (!user) { - throw new UnauthorizedException('Неверный логин или пароль'); - } - return { success: true, user }; - } -} + constructor(private authService: AuthService) { } + + @Get('check') + @UseGuards(JwtAuthGuard) + async checkAuth(@Req() req: Request) { + this.logger.debug(`Check auth request. Cookies: ${JSON.stringify(req.cookies)}`); + this.logger.debug(`Check auth request. Headers: ${JSON.stringify(req.headers)}`); + + if (!req.user) { + this.logger.warn('Unauthorized access attempt'); + throw new UnauthorizedException('Пользователь не аутентифицирован'); + } + + // Явно указываем тип для req.user + const user = req.user as { userId: number; username: string; login?: string }; + const userWithoutPassword = { ...user }; + + this.logger.log(`User authenticated: ${user.username}`); + return { + isAuthenticated: true, + user: userWithoutPassword + }; + } + + @Post('login') + async login( + @Body() body: { login: string; password: string }, + @Res({ passthrough: true }) res: Response, + @Req() req: Request + ) { + this.logger.debug(`Login attempt for user: ${body.login}`); + this.logger.debug(`Request cookies: ${JSON.stringify(req.cookies)}`); + this.logger.debug(`Request headers: ${JSON.stringify(req.headers)}`); + + const user = await this.authService.validateUser(body.login, body.password); + if (!user) { + this.logger.warn(`Failed login attempt for user: ${body.login}`); + throw new UnauthorizedException('Неверный логин или пароль'); + } + + const { access_token } = await this.authService.login(user); + + res.cookie('access_token', access_token, { + httpOnly: true, + secure: process.env.COOKIE_SECURE === 'true', + sameSite: (process.env.COOKIE_SAME_SITE as 'strict' | 'lax' | 'none') || 'strict', + maxAge: 3600000, + path: '/', + }); + + this.logger.log(`User ${body.login} successfully logged in`); + this.logger.debug(`Set cookie: access_token=${access_token.substring(0, 10)}...`); + + return { + success: true, + user: { + id: user.id, + login: user.login + }, + access_token + }; + } + + @Post('logout') + @UseGuards(JwtAuthGuard) + async logout(@Res({ passthrough: true }) res: Response, @Req() req: Request) { + const user = req.user as { userId: number; username: string }; + this.logger.log(`User ${user.username} logging out`); + res.clearCookie('access_token'); + return { success: true }; + } +} \ No newline at end of file diff --git a/src/auth/auth.guard.ts b/src/auth/auth.guard.ts new file mode 100644 index 0000000..bb06b11 --- /dev/null +++ b/src/auth/auth.guard.ts @@ -0,0 +1,12 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Observable } from 'rxjs'; + +@Injectable() +export class AuthGuard implements CanActivate { + canActivate( + context: ExecutionContext, + ): boolean | Promise | Observable { + const request = context.switchToHttp().getRequest(); + return !!request.user; // Проверка что пользователь аутентифицирован + } +} \ No newline at end of file diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 8753795..14ae022 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -1,12 +1,27 @@ -import { Module } from '@nestjs/common'; +import { Module, MiddlewareConsumer } from '@nestjs/common'; // Добавлен импорт MiddlewareConsumer +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AuthService } from './auth.service'; import { AuthController } from './auth.controller'; import { User } from './user.entity'; +import { JwtStrategy } from './jwt.strategy'; +import * as cookieParser from 'cookie-parser'; @Module({ - imports: [TypeOrmModule.forFeature([User])], + imports: [ + TypeOrmModule.forFeature([User]), + PassportModule, + JwtModule.register({ + secret: process.env.JWT_SECRET || 'your-secret-key', + signOptions: { expiresIn: '1h' }, + }), + ], controllers: [AuthController], - providers: [AuthService], + providers: [AuthService, JwtStrategy], }) -export class AuthModule { } +export class AuthModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(cookieParser()).forRoutes('*'); + } +} \ No newline at end of file diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 206b5f9..2773efb 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from './user.entity'; @@ -8,20 +9,26 @@ export class AuthService { constructor( @InjectRepository(User) private usersRepository: Repository, + private jwtService: JwtService, ) { } async validateUser(login: string, password: string): Promise { - - // Ищем пользователя по login const user = await this.usersRepository.findOne({ where: { login } }); - // Проверяем, что нашли пользователя и пароль совпадает if (user && user.password === password) { - const { password, ...result } = user; return result; } - return null; } -} + + async login(user: any) { + const payload = { + username: user.login, // Используем username в payload + sub: user.id + }; + return { + access_token: this.jwtService.sign(payload), + }; + } +} \ No newline at end of file diff --git a/src/auth/jwt-auth.guard.ts b/src/auth/jwt-auth.guard.ts new file mode 100644 index 0000000..79f0b0a --- /dev/null +++ b/src/auth/jwt-auth.guard.ts @@ -0,0 +1,28 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { JwtService } from '@nestjs/jwt'; + +@Injectable() +export class JwtAuthGuard implements CanActivate { + constructor(private jwtService: JwtService) {} + + canActivate(context: ExecutionContext): boolean | Promise | Observable { + const request = context.switchToHttp().getRequest(); + try { + const token = this.extractToken(request); + if (!token) return false; + + const payload = this.jwtService.verify(token); + request.user = payload; + return true; + } catch (e) { + return false; + } + } + + private extractToken(request): string | null { + return request.cookies?.access_token || + request.headers.authorization?.split(' ')[1] || + null; + } +} \ No newline at end of file diff --git a/src/auth/jwt.strategy.ts b/src/auth/jwt.strategy.ts new file mode 100644 index 0000000..6cdc40a --- /dev/null +++ b/src/auth/jwt.strategy.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { Request } from 'express'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor() { + super({ + jwtFromRequest: ExtractJwt.fromExtractors([ + (request: Request) => { + return request?.cookies?.access_token || + request?.headers?.authorization?.split(' ')[1]; + }, + ]), + ignoreExpiration: false, + secretOrKey: process.env.JWT_SECRET || 'your-secret-key', + }); + } + + async validate(payload: any) { + return { + userId: payload.sub, + username: payload.username, + login: payload.username // Добавляем для совместимости + }; + } +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 85934af..7cc2c6e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,20 +1,23 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; - async function bootstrap() { const app = await NestFactory.create(AppModule); // Установка глобального префикса для всех маршрутов app.setGlobalPrefix('api'); - //настройка CORS - + // Настройка CORS app.enableCors({ - origin: '*', + origin: [process.env.FRONTEND_URL, "http://dev.msf.enode"], //|| 'http://192.168.2.39:5173', // Точный URL фронтенда + credentials: true, methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS', - allowedHeaders: 'Content-Type, Authorization', + allowedHeaders: 'Content-Type, Authorization, X-Requested-With', + exposedHeaders: 'Authorization', + preflightContinue: false, + optionsSuccessStatus: 204 }); + await app.listen(process.env.PORT ?? 3000); } bootstrap(); \ No newline at end of file diff --git a/src/metrics.gateway.ts b/src/metrics.gateway.ts index 1d1f669..2b68304 100644 --- a/src/metrics.gateway.ts +++ b/src/metrics.gateway.ts @@ -4,14 +4,14 @@ import { PrometheusService } from './prometheus.service'; import { Logger } from '@nestjs/common'; @WebSocketGateway({ - /* - cors: { - origin: '*', // В production укажите конкретные домены - methods: ['GET', 'POST'], - credentials: true - }, */ + cors: { + origin: process.env.FRONTEND_URL || 'http://192.168.2.39:5173', // Тот же origin что и в основном CORS + methods: ['GET', 'POST'], + credentials: true + }, namespace: '/api/metrics-ws' }) + export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; private readonly logger = new Logger(MetricsGateway.name);