set up an authorization session using tokens and cookies
parent
7d8a207728
commit
2c06038b0e
17
.env
17
.env
|
|
@ -1,9 +1,26 @@
|
||||||
#Прометеус
|
#Прометеус
|
||||||
#PROMETHEUS_API=http://192.168.2.34:9090/api/v1
|
#PROMETHEUS_API=http://192.168.2.34:9090/api/v1
|
||||||
|
|
||||||
|
#FRONTEND_URL=192.168.2.39:5173
|
||||||
|
|
||||||
#Постгресс
|
#Постгресс
|
||||||
#DB_HOST=192.168.2.37
|
#DB_HOST=192.168.2.37
|
||||||
#DB_PORT=5432
|
#DB_PORT=5432
|
||||||
#DB_USER=trust
|
#DB_USER=trust
|
||||||
#DB_PASSWORD=kaiqolzp2a4aH
|
#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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,14 @@
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"@nestjs/websockets": "11.0.12",
|
"@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": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
|
|
||||||
|
|
@ -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 { AuthService } from './auth.service';
|
||||||
|
import { Response, Request } from 'express';
|
||||||
|
import { JwtAuthGuard } from './jwt-auth.guard';
|
||||||
|
import { Logger } from '@nestjs/common/services';
|
||||||
|
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
|
private readonly logger = new Logger(AuthController.name);
|
||||||
|
|
||||||
constructor(private authService: AuthService) { }
|
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')
|
@Post('login')
|
||||||
async login(@Body() body: { login: string; password: string }) {
|
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);
|
const user = await this.authService.validateUser(body.login, body.password);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
this.logger.warn(`Failed login attempt for user: ${body.login}`);
|
||||||
throw new UnauthorizedException('Неверный логин или пароль');
|
throw new UnauthorizedException('Неверный логин или пароль');
|
||||||
}
|
}
|
||||||
return { success: true, user };
|
|
||||||
|
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 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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; // Проверка что пользователь аутентифицирован
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { AuthController } from './auth.controller';
|
import { AuthController } from './auth.controller';
|
||||||
import { User } from './user.entity';
|
import { User } from './user.entity';
|
||||||
|
import { JwtStrategy } from './jwt.strategy';
|
||||||
|
import * as cookieParser from 'cookie-parser';
|
||||||
|
|
||||||
@Module({
|
@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],
|
controllers: [AuthController],
|
||||||
providers: [AuthService],
|
providers: [AuthService, JwtStrategy],
|
||||||
})
|
})
|
||||||
export class AuthModule { }
|
export class AuthModule {
|
||||||
|
configure(consumer: MiddlewareConsumer) {
|
||||||
|
consumer.apply(cookieParser()).forRoutes('*');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { User } from './user.entity';
|
import { User } from './user.entity';
|
||||||
|
|
@ -8,20 +9,26 @@ export class AuthService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(User)
|
@InjectRepository(User)
|
||||||
private usersRepository: Repository<User>,
|
private usersRepository: Repository<User>,
|
||||||
|
private jwtService: JwtService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
async validateUser(login: string, password: string): Promise<any> {
|
async validateUser(login: string, password: string): Promise<any> {
|
||||||
|
|
||||||
// Ищем пользователя по login
|
|
||||||
const user = await this.usersRepository.findOne({ where: { login } });
|
const user = await this.usersRepository.findOne({ where: { login } });
|
||||||
|
|
||||||
// Проверяем, что нашли пользователя и пароль совпадает
|
|
||||||
if (user && user.password === password) {
|
if (user && user.password === password) {
|
||||||
|
|
||||||
const { password, ...result } = user;
|
const { password, ...result } = user;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async login(user: any) {
|
||||||
|
const payload = {
|
||||||
|
username: user.login, // Используем username в payload
|
||||||
|
sub: user.id
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
access_token: this.jwtService.sign(payload),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 // Добавляем для совместимости
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/main.ts
13
src/main.ts
|
|
@ -1,20 +1,23 @@
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
// Установка глобального префикса для всех маршрутов
|
// Установка глобального префикса для всех маршрутов
|
||||||
app.setGlobalPrefix('api');
|
app.setGlobalPrefix('api');
|
||||||
|
|
||||||
//настройка CORS
|
// Настройка CORS
|
||||||
|
|
||||||
app.enableCors({
|
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',
|
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);
|
await app.listen(process.env.PORT ?? 3000);
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|
@ -4,14 +4,14 @@ import { PrometheusService } from './prometheus.service';
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
|
|
||||||
@WebSocketGateway({
|
@WebSocketGateway({
|
||||||
/*
|
|
||||||
cors: {
|
cors: {
|
||||||
origin: '*', // В production укажите конкретные домены
|
origin: process.env.FRONTEND_URL || 'http://192.168.2.39:5173', // Тот же origin что и в основном CORS
|
||||||
methods: ['GET', 'POST'],
|
methods: ['GET', 'POST'],
|
||||||
credentials: true
|
credentials: true
|
||||||
}, */
|
},
|
||||||
namespace: '/api/metrics-ws'
|
namespace: '/api/metrics-ws'
|
||||||
})
|
})
|
||||||
|
|
||||||
export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
|
export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
|
||||||
@WebSocketServer() server: Server;
|
@WebSocketServer() server: Server;
|
||||||
private readonly logger = new Logger(MetricsGateway.name);
|
private readonly logger = new Logger(MetricsGateway.name);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue