From e23de664acb340bffd29eee8b0024473999eebc6 Mon Sep 17 00:00:00 2001 From: iqubik Date: Fri, 3 Apr 2026 02:23:05 +0300 Subject: [PATCH] improve security: back port, base port, login guard, scripts --- client/nginx.conf | 25 ++- client/src/api.ts | 1 + client/src/auth/AuthContext.tsx | 44 +++- client/src/auth/AxiosInterceptor.tsx | 50 ++++- client/src/components/Header.tsx | 28 ++- client/src/pages/LoginPage.tsx | 29 ++- client/test/auth/AuthContext.spec.tsx | 115 ++++++---- client/test/setup.ts | 7 + docker-compose.yml | 1 + install.sh | 55 ++++- server/Dockerfile | 4 +- server/package-lock.json | 32 +++ server/package.json | 5 + server/src/app.module.ts | 11 + server/src/auth/auth.controller.ts | 66 +++++- server/src/auth/auth.service.ts | 11 +- server/src/auth/jwt-auth.guard.ts | 35 ++- server/src/main.ts | 50 ++++- server/test/app/app.config.spec.ts | 115 ++++++++++ server/test/auth/auth.controller.spec.ts | 160 +++++++++++++- server/test/auth/jwt-auth.guard.spec.ts | 200 ++++++++++-------- server/test/jest.setup.ts | 12 ++ server/test/main.spec.ts | 181 ++++++++++++++++ .../test/settings/settings.controller.spec.ts | 8 +- server/test/xui/xui.service.spec.ts | 8 +- update.sh | 140 ++++++++++-- 26 files changed, 1172 insertions(+), 221 deletions(-) create mode 100644 server/test/app/app.config.spec.ts create mode 100644 server/test/jest.setup.ts create mode 100644 server/test/main.spec.ts diff --git a/client/nginx.conf b/client/nginx.conf index e65f6a3..33fc50b 100644 --- a/client/nginx.conf +++ b/client/nginx.conf @@ -13,8 +13,22 @@ server { } location /api/ { - proxy_pass http://server:3000; - + proxy_pass http://backend:3100; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $http_host; + proxy_set_header Authorization $http_authorization; + proxy_cache_bypass $http_upgrade; + proxy_connect_timeout 10s; + proxy_send_timeout 650s; + proxy_read_timeout 650s; + } + + location /bus/ { + proxy_pass http://backend:3100; + proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; @@ -25,11 +39,12 @@ server { proxy_read_timeout 650s; } } + server { - listen 3000; + listen 3100; server_name localhost; location / { - proxy_pass http://backend:3000/; - proxy_set_header Host \$http_host; + proxy_pass http://backend:3100/; + proxy_set_header Host $http_host; } } diff --git a/client/src/api.ts b/client/src/api.ts index 68cf3fb..046455a 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -3,6 +3,7 @@ import { Logger } from './utils/logger'; const api = axios.create({ baseURL: '/api', + withCredentials: true, // Отправлять cookies }); // Interceptor для добавления токена к каждому запросу diff --git a/client/src/auth/AuthContext.tsx b/client/src/auth/AuthContext.tsx index 4a2e927..35ca23a 100644 --- a/client/src/auth/AuthContext.tsx +++ b/client/src/auth/AuthContext.tsx @@ -1,11 +1,13 @@ /* eslint-disable react-refresh/only-export-components */ import React, { createContext, useContext, useState } from 'react'; +import api from '../api'; +import { Logger } from '../utils/logger'; interface AuthContextType { token: string | null; isAuthenticated: boolean; login: (token: string) => void; - logout: () => void; + logout: () => Promise; } const AuthContext = createContext(null); @@ -20,17 +22,53 @@ export const useAuth = () => { export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [token, setToken] = useState(() => { - return localStorage.getItem('token'); + const initialToken = localStorage.getItem('token'); + Logger.debug('AuthProvider initialized', 'AuthContext', { + hasToken: Boolean(initialToken), + }); + return initialToken; }); const login = (newToken: string) => { + Logger.debug('login() called', 'AuthContext', { + tokenLength: newToken.length, + }); + // Сохраняем токен в localStorage для обратной совместимости + // Основной токен теперь в httpOnly cookie localStorage.setItem('token', newToken); setToken(newToken); + Logger.debug('Token persisted to localStorage and auth state updated', 'AuthContext'); }; - const logout = () => { + const logout = async () => { + Logger.debug('logout() called', 'AuthContext'); + try { + // Вызываем backend для очистки httpOnly cookie + await api.post('/auth/logout'); + Logger.debug('Backend logout request succeeded', 'AuthContext'); + } catch (error) { + const status = + typeof error === 'object' && + error !== null && + 'response' in error && + typeof (error as { response?: unknown }).response === 'object' && + (error as { response?: unknown }).response !== null + ? ((error as { response?: { status?: number } }).response?.status ?? null) + : null; + Logger.warn( + 'Backend logout request failed, continuing local cleanup', + 'AuthContext', + { status }, + ); + } + localStorage.removeItem('token'); setToken(null); + Logger.debug('Local auth state cleared', 'AuthContext'); + + // Редирект на страницу входа + Logger.debug('Redirecting to /login after logout', 'AuthContext'); + window.location.href = '/login'; }; return ( diff --git a/client/src/auth/AxiosInterceptor.tsx b/client/src/auth/AxiosInterceptor.tsx index 2f2830c..c90fc1b 100644 --- a/client/src/auth/AxiosInterceptor.tsx +++ b/client/src/auth/AxiosInterceptor.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import api from '../api'; import { useAuth } from './AuthContext'; @@ -8,19 +8,50 @@ export function AxiosInterceptor() { const { logout } = useAuth(); const navigate = useNavigate(); const location = useLocation(); + const logoutRef = useRef(logout); + const navigateRef = useRef(navigate); + const pathnameRef = useRef(location.pathname); useEffect(() => { + logoutRef.current = logout; + }, [logout]); + + useEffect(() => { + navigateRef.current = navigate; + }, [navigate]); + + useEffect(() => { + pathnameRef.current = location.pathname; + }, [location.pathname]); + + useEffect(() => { + Logger.debug('Registering axios response interceptor', 'AxiosInterceptor'); const interceptor = api.interceptors.response.use( (response) => response, - (error) => { + async (error) => { if (error.response && error.response.status === 401) { Logger.warn('401 Unauthorized detected → logging out and redirecting to /login', 'AxiosInterceptor'); // Не делаем logout если уже на странице логина - if (location.pathname !== '/login') { - Logger.debug('Calling logout()', 'AxiosInterceptor'); - logout(); - Logger.debug('Navigating to /login...', 'AxiosInterceptor'); - navigate('/login'); + if (pathnameRef.current !== '/login') { + try { + Logger.debug('Calling logout()', 'AxiosInterceptor'); + await logoutRef.current(); + Logger.debug('Navigating to /login...', 'AxiosInterceptor'); + navigateRef.current('/login'); + } catch (logoutError) { + Logger.error( + 'logout() failed inside interceptor', + 'AxiosInterceptor', + { + message: + logoutError instanceof Error + ? logoutError.message + : 'unknown error', + }, + ); + } + } else { + Logger.debug('Already on /login, skipping auto-logout flow', 'AxiosInterceptor'); } } return Promise.reject(error); @@ -28,9 +59,10 @@ export function AxiosInterceptor() { ); return () => { + Logger.debug('Ejecting axios response interceptor', 'AxiosInterceptor'); api.interceptors.response.eject(interceptor); }; - }, [logout, navigate, location.pathname]); + }, []); return null; -} \ No newline at end of file +} diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx index d581070..1fe2d85 100644 --- a/client/src/components/Header.tsx +++ b/client/src/components/Header.tsx @@ -12,6 +12,7 @@ import { useThemeContext } from '../ThemeContext'; import { useAuth } from '../auth/AuthContext'; import { Menu as MenuIcon } from '@mui/icons-material'; import { APP_VERSION } from '../utils/version'; +import { Logger } from '../utils/logger'; interface HeaderProps { onMenuClick?: () => void; @@ -27,11 +28,14 @@ export default function Header({ onMenuClick, isMobile }: HeaderProps) { const [confirmDialog, setConfirmDialog] = useState({ open: false, title: '', onConfirm: () => {} }); const handleLogout = () => { + Logger.debug('Opening logout confirmation dialog', 'Header'); setConfirmDialog({ open: true, title: 'Вы действительно хотите выйти?', - onConfirm: () => { - logout(); + onConfirm: async () => { + Logger.debug('Logout confirmed by user', 'Header'); + await logout(); + Logger.debug('logout() resolved in Header', 'Header'); navigate('/login'); } }); @@ -154,12 +158,26 @@ export default function Header({ onMenuClick, isMobile }: HeaderProps) { {confirmDialog.title} - +