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} - +