set up an authorization session using tokens and cookies

pull/16/head
DmitriyA 2025-04-14 04:32:15 -04:00
parent 7d8a207728
commit 2c06038b0e
10 changed files with 217 additions and 34 deletions

19
.env
View File

@ -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
#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

View File

@ -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",

View File

@ -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 };
}
}

12
src/auth/auth.guard.ts Normal file
View File

@ -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<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return !!request.user; // Проверка что пользователь аутентифицирован
}
}

View File

@ -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('*');
}
}

View File

@ -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<User>,
private jwtService: JwtService,
) { }
async validateUser(login: string, password: string): Promise<any> {
// Ищем пользователя по 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),
};
}
}

View File

@ -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<boolean> | Observable<boolean> {
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;
}
}

28
src/auth/jwt.strategy.ts Normal file
View File

@ -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 // Добавляем для совместимости
};
}
}

View File

@ -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();

View File

@ -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);