Merge pull request 'authorization-token' (#38) from authorization-token into rc

Reviewed-on: http://git.enode/deployer3000/trust-module-frontend/pulls/38
Reviewed-by: Vladislav Drozdov <ya2@ya.ru>
Reviewed-by: YurijO <ya@ya.ru>
pull/41/head
YurijO 2025-04-21 16:10:30 +03:00
commit fd53b187d5
13 changed files with 413 additions and 158 deletions

View File

@ -27,7 +27,8 @@
"reactflow": "^11.11.4",
"vite-plugin-svgr": "^4.3.0",
"react-scripts": "^5.0.1",
"socket.io-client": "^4.8.1"
"socket.io-client": "^4.8.1",
"antd": "^5.24.7"
},
"devDependencies": {
"@eslint/js": "^9.17.0",

View File

@ -1,37 +1,109 @@
import React, { useState, useMemo } from "react";
import { ThemeProvider, CssBaseline, Switch, Box } from "@mui/material";
import React, { useState, useMemo, useEffect } from "react";
import { ThemeProvider, CssBaseline, Switch, Box, CircularProgress } from "@mui/material";
import Dashboard from "./Components/Layout/Dashboard";
import LoginModal from "./Components/UI/LoginModal";
import { lightTheme, darkTheme } from "./Style/theme";
import Logo from './assets/images/logo.svg?react'; // Импорт как компонента
import Logo from './assets/images/logo.svg?react';
import { checkAuth } from "./Components/UI/auth";
import axios from 'axios';
function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [showLoginModal, setShowLoginModal] = useState(true);
const [authState, setAuthState] = useState({
isAuthenticated: false,
isLoading: true,
user: null
});
const [showLoginModal, setShowLoginModal] = useState(false);
const [isDarkMode, setIsDarkMode] = useState(
window.matchMedia("(prefers-color-scheme: dark)").matches
);
const theme = useMemo(() => (isDarkMode ? darkTheme : lightTheme), [isDarkMode]);
const handleLogin = () => {
setIsAuthenticated(true);
useEffect(() => {
const verifyAuth = async () => {
try {
const authStatus = await checkAuth();
setAuthState({
isAuthenticated: authStatus.isAuthenticated,
isLoading: false,
user: authStatus.user || null
});
setShowLoginModal(!authStatus.isAuthenticated);
} catch (error) {
console.error('Auth verification error:', error);
setAuthState({
isAuthenticated: false,
isLoading: false,
user: null
});
setShowLoginModal(true);
}
};
verifyAuth();
}, []);
const handleLogin = (userData) => {
setAuthState({
isAuthenticated: true,
isLoading: false,
user: userData
});
setShowLoginModal(false);
};
const handleLogout = async () => {
try {
await axios.get(`${import.meta.env.VITE_BACK_URL}/api/metrics`, {
method: 'POST',
credentials: 'include'
});
localStorage.removeItem('access_token');
setAuthState({
isAuthenticated: false,
isLoading: false,
user: null
});
setShowLoginModal(true);
} catch (error) {
console.error('Logout failed:', error);
}
};
if (authState.isLoading) {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Box sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
flexDirection: 'column',
gap: 2
}}>
<CircularProgress />
<p>Проверка авторизации...</p>
</Box>
</ThemeProvider>
);
}
return (
<ThemeProvider theme={theme}>
<CssBaseline />
{!isAuthenticated && showLoginModal ? (
{!authState.isAuthenticated && showLoginModal ? (
<>
{/* Логотип */}
<Box
component="div"
sx={{
position: "fixed",
top: 24,
left: "50%", // Сдвигаем начало логотипа в центр
transform: "translateX(-50%)", // Смещаем назад на половину ширины логотипа
left: "50%",
transform: "translateX(-50%)",
zIndex: 1200,
'& svg': {
width: 400,
@ -55,9 +127,15 @@ function App() {
bgcolor: "background.default",
color: "text.primary"
}}>
<Dashboard />
<Dashboard
user={authState.user}
onLogout={handleLogout}
/>
<Box sx={{ position: "absolute", top: 10, right: 10 }}>
<Switch checked={isDarkMode} onChange={() => setIsDarkMode((prev) => !prev)} />
<Switch
checked={isDarkMode}
onChange={() => setIsDarkMode((prev) => !prev)}
/>
</Box>
</Box>
)}

View File

@ -203,7 +203,7 @@ const LineChartComponent = ({
{instanceKeys.map((instance, index) => (
<Line
key={instance}
type="monotone"
type=""
dataKey={instance}
name={instance}
stroke={colors[index % colors.length]}

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState, useRef, useCallback } from 'react';
import { io } from 'socket.io-client';
import { webSocketManager } from './WebSocketManager';
import LineChartComponent from './Components/LineChartComponent';
import { TimeRangeSelector } from './Components/TimeRangeSelector';
import { ConnectionStatusIndicator } from './Components/ConnectionStatusIndicator';
@ -19,7 +19,6 @@ const PrometheusChart = ({ metricName }) => {
const [isSelectingRange, setIsSelectingRange] = useState(false);
const [lastCustomRange, setLastCustomRange] = useState(null);
const intervalRef = useRef(null);
const socketRef = useRef(null);
const debounceRef = useRef(null);
const formatTime = useCallback((timestamp, rangeSeconds) => {
@ -70,25 +69,6 @@ const PrometheusChart = ({ metricName }) => {
return 1800; // > 24 часов
}, []);
const fetchData = useCallback(() => {
if (isSelectingRange) return;
const now = Math.floor(Date.now() / 1000);
const start = now - selectedRange.value;
const end = now;
const step = calculateStep(start, end);
if (socketRef.current?.connected) {
socketRef.current.emit('get-metrics', {
metric: metricName,
start,
end,
step,
_t: Date.now()
});
}
}, [metricName, selectedRange.value, isSelectingRange]);
const processMetricsData = useCallback((response) => {
console.log('Processing metrics data:', response);
@ -137,58 +117,32 @@ const PrometheusChart = ({ metricName }) => {
});
}, [metricName, selectedRange.value, formatTime, useCustomRange, startDate, endDate]);
const setupWebSocket = useCallback(() => {
if (socketRef.current) {
// Если соединение уже существует, возвращаем его
if (socketRef.current.connected) return socketRef.current;
// Если соединение в процессе переподключения, тоже возвращаем
if (socketRef.current.reconnecting) return socketRef.current;
}
//VITE_BACK_WS_URL
const socket = io(`${import.meta.env.VITE_BACK_WS_URL}/api/metrics-ws`, {
transports: ['websocket'],
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
const fetchData = useCallback(() => {
if (isSelectingRange) return;
const now = Math.floor(Date.now() / 1000);
const start = now - selectedRange.value;
const end = now;
const step = calculateStep(start, end);
webSocketManager.getMetricsRange(metricName, start, end, step)
.then(data => {
processMetricsData({ metric: metricName, data });
})
.catch(error => {
console.error('Error fetching metrics:', error);
});
socketRef.current = socket;
socket.on('connect', () => {
console.log('WebSocket connected');
setConnectionStatus('connected');
fetchData();
});
socket.on('disconnect', (reason) => {
console.log('WebSocket disconnected:', reason);
setConnectionStatus('disconnected');
if (reason === 'io server disconnect') socket.connect();
});
socket.on('connect_error', (error) => {
console.error('WebSocket connection error:', error);
setConnectionStatus('error');
setTimeout(() => socket.connect(), 1000);
});
socket.on('metrics-data', (response) => {
console.log('Received raw metrics data:', response);
processMetricsData(response);
});
socket.on('metrics-error', (error) => {
console.error('Metrics error:', error);
setConnectionStatus('error');
});
return socket;
}, []);
}, [metricName, selectedRange.value, isSelectingRange, calculateStep, processMetricsData]);
const fetchCustomRangeData = useCallback(async () => {
// Добавляем проверку на валидность дат
if (!startDate || !endDate || startDate >= endDate) {
console.error('Invalid date range');
return;
}
const start = Math.floor(startDate.getTime() / 1000);
const end = Math.floor(endDate.getTime() / 1000);
const end = Math.ceil(endDate.getTime() / 1000); // Используем Math.ceil для конечной даты
const rangeSeconds = end - start;
try {
@ -202,11 +156,11 @@ const PrometheusChart = ({ metricName }) => {
});
if (response.data?.length) {
// Преобразуем данные перед передачей в processMetricsData
// Добавляем нормализацию timestamp
const processedData = response.data.map(item => ({
...item,
timestamp: item.timestamp, // оставляем в секундах - processMetricsData конвертирует
value: item.value.toString()
timestamp: item.timestamp > 1e12 ? item.timestamp : item.timestamp * 1000,
value: parseFloat(item.value)
}));
processMetricsData({
@ -220,8 +174,7 @@ const PrometheusChart = ({ metricName }) => {
}, [metricName, startDate, endDate, calculateStep, processMetricsData]);
const handleRangeChange = useCallback((event) => {
// Очищаем текущий интервал
const handleRangeChange = useCallback(async (event) => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
@ -230,9 +183,10 @@ const PrometheusChart = ({ metricName }) => {
const selectedValue = event.target.value;
const range = TIME_RANGES.find(r => r.value === parseInt(selectedValue, 10));
// Полный сброс состояния перед загрузкой новых данных
setChartData(null);
setSelectedRange(range);
setUseCustomRange(false);
setChartData(null);
setSelectedGraphRange(null);
setFilteredData(null);
@ -240,20 +194,12 @@ const PrometheusChart = ({ metricName }) => {
setEndDate(now);
setStartDate(new Date(now.getTime() - range.value * 1000));
// Переподключение сокета
if (!socketRef.current?.connected) {
socketRef.current?.connect();
}
}, []);
// Ждем завершения обновления состояния перед загрузкой
await new Promise(resolve => setTimeout(resolve, 0));
fetchData();
}, [fetchData]);
const handleCustomRangeChange = useCallback(() => {
// Отключаем WebSocket соединение
if (socketRef.current?.connected) {
socketRef.current.disconnect();
setConnectionStatus('disconnected');
}
// Очищаем интервал обновления
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
@ -266,25 +212,7 @@ const PrometheusChart = ({ metricName }) => {
fetchCustomRangeData();
}, [fetchCustomRangeData]);
const handleResetZoom = useCallback(() => {
setSelectedGraphRange(null);
setFilteredData(null);
setIsSelectingRange(false);
if (useCustomRange) {
fetchCustomRangeData();
} else {
if (!socketRef.current?.connected) {
socketRef.current?.connect();
}
fetchData();
}
if (lastCustomRange) {
handleRangeSelect(lastCustomRange);
return;
}
}, [fetchData, fetchCustomRangeData, useCustomRange]);
const interpolateData = useCallback((data, targetPointCount) => {
if (!data || data.length < 2) return data;
@ -337,10 +265,6 @@ const PrometheusChart = ({ metricName }) => {
setIsSelectingRange(true);
setSelectedGraphRange(range);
// Отключаем автоматические обновления
if (socketRef.current?.connected) {
socketRef.current.disconnect();
}
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
@ -366,23 +290,52 @@ const PrometheusChart = ({ metricName }) => {
setIsSelectingRange(false);
}, [chartData, interpolateData, formatTime]);
useEffect(() => {
const socket = setupWebSocket();
return () => {
clearInterval(intervalRef.current);
socket.disconnect();
};
}, [setupWebSocket]);
const handleResetZoom = useCallback(() => {
setSelectedGraphRange(null);
setFilteredData(null);
setIsSelectingRange(false);
if (useCustomRange) {
fetchCustomRangeData();
} else {
fetchData();
}
if (lastCustomRange) {
handleRangeSelect(lastCustomRange);
}
}, [fetchData, fetchCustomRangeData, useCustomRange, lastCustomRange, handleRangeSelect]);
useEffect(() => {
// Обработчик данных с сервера
const handleMetricsData = (data) => {
processMetricsData({ metric: metricName, data });
};
// Подписываемся на обновления метрики
const unsubscribe = webSocketManager.subscribe(metricName, handleMetricsData);
// Подписываемся на изменения статуса соединения
const unsubscribeStatus = webSocketManager.onConnectionStatusChange(setConnectionStatus);
return () => {
// Отписываемся при размонтировании компонента
unsubscribe();
unsubscribeStatus();
// Очищаем интервал обновления
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [metricName, processMetricsData]);
// Обновим useEffect для кастомного диапазона
useEffect(() => {
if (useCustomRange && !isSelectingRange) {
// Очищаем предыдущий таймер
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
// Устанавливаем новый таймер с задержкой 500 мс
debounceRef.current = setTimeout(() => {
fetchCustomRangeData();
}, 500);
@ -406,12 +359,11 @@ const PrometheusChart = ({ metricName }) => {
}
};
// Очищаем предыдущий интервал
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
// Запускаем сразу и затем по интервалу
fetchDataWrapper();
intervalRef.current = setInterval(fetchDataWrapper, selectedRange.interval);
@ -491,4 +443,3 @@ const PrometheusChart = ({ metricName }) => {
};
export default React.memo(PrometheusChart);

View File

@ -0,0 +1,115 @@
// src/services/WebSocketManager.js
import { io } from 'socket.io-client';
class WebSocketManager {
constructor() {
this.socket = null;
this.subscribers = new Map();
this.connectionStatus = 'disconnected';
this.connectionCallbacks = new Set();
}
connect() {
if (this.socket && (this.socket.connected || this.socket.reconnecting)) {
return this.socket;
}
this.socket = io(`${import.meta.env.VITE_BACK_WS_URL}/api/metrics-ws`, {
transports: ['websocket'],
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
});
this.socket.on('connect', () => {
this.connectionStatus = 'connected';
this.notifyConnectionStatus();
});
this.socket.on('disconnect', (reason) => {
this.connectionStatus = 'disconnected';
this.notifyConnectionStatus();
if (reason === 'io server disconnect') this.socket.connect();
});
this.socket.on('connect_error', (error) => {
this.connectionStatus = 'error';
this.notifyConnectionStatus();
setTimeout(() => this.socket.connect(), 1000);
});
this.socket.on('metrics-data', (response) => {
const callbacks = this.subscribers.get(response.metric);
if (callbacks) {
callbacks.forEach(callback => callback(response.data));
}
});
return this.socket;
}
subscribe(metricName, callback) {
this.connect();
if (!this.subscribers.has(metricName)) {
this.subscribers.set(metricName, new Set());
this.socket.emit('subscribe-metric', {
metric: metricName,
isSubscription: true // Флаг для подписки
});
}
this.subscribers.get(metricName).add(callback);
return () => this.unsubscribe(metricName, callback);
}
unsubscribe(metricName, callback) {
const callbacks = this.subscribers.get(metricName);
if (callbacks) {
callbacks.delete(callback);
if (callbacks.size === 0) {
this.subscribers.delete(metricName);
this.socket.emit('unsubscribe-metric', { metric: metricName });
}
}
}
getMetricsRange(metricName, start, end, step) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error('Timeout while waiting for metrics data'));
}, 10000);
// Временный обработчик для разового запроса
const tempHandler = (data) => {
clearTimeout(timer);
this.socket.off(`metrics-range-${metricName}`, tempHandler);
resolve(data);
};
this.socket.on(`metrics-range-${metricName}`, tempHandler);
this.socket.emit('get-metrics', {
metric: metricName,
start,
end,
step,
isRangeQuery: true // Флаг для разового запроса
});
});
}
onConnectionStatusChange(callback) {
this.connectionCallbacks.add(callback);
callback(this.connectionStatus);
return () => this.connectionCallbacks.delete(callback);
}
notifyConnectionStatus() {
this.connectionCallbacks.forEach(callback => callback(this.connectionStatus));
}
}
export const webSocketManager = new WebSocketManager();

View File

@ -48,9 +48,9 @@ const MainContent = styled(Box)(({ theme }) => ({
const Content = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.custom.modalBackground,
padding: theme.spacing(2.5),
//padding: theme.spacing(2.5),
borderRadius: '10px',
boxShadow: theme.shadows[2],
//boxShadow: theme.shadows[2],
maxWidth: '100%',
overflow: 'auto',
color: theme.palette.custom.modalText,

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import {
Drawer,
List,
@ -15,6 +15,7 @@ import {
} from "@mui/icons-material";
import MenuItem from "./SidebarMenuComponents/MenuItem";
import SidebarFooter from "./SidebarMenuComponents/SidebarFooter";
import { statusManager1 } from "../TreeChart/dataUtils";
const SidebarResizer = styled('div')(({ theme }) => ({
width: "5px",
@ -34,6 +35,17 @@ const SidebarResizer = styled('div')(({ theme }) => ({
const SidebarMenu = ({ data, onOpenTab, sidebarWidth, startResizing }) => {
const [collapsed, setCollapsed] = useState(false);
const [hovered, setHovered] = useState(false);
const [menuData, setMenuData] = useState(data);
// Обновляем статусы при изменении данных
useEffect(() => {
if (data) {
// Создаем глубокую копию данных, чтобы не мутировать исходные
const dataCopy = JSON.parse(JSON.stringify(data));
statusManager1.updateStatuses(dataCopy);
setMenuData(dataCopy);
}
}, [data]);
const handleToggleCollapse = () => {
setCollapsed(!collapsed);
@ -126,11 +138,13 @@ const SidebarMenu = ({ data, onOpenTab, sidebarWidth, startResizing }) => {
Меню
</Typography>
)}
{menuData && (
<MenuItem
item={data}
item={menuData}
onSelectItem={handleSelectItem}
collapsed={collapsed}
/>
)}
</List>
{/* Футер */}

View File

@ -8,10 +8,14 @@ import {
styled
} from "@mui/material";
import { ExpandLess, ExpandMore, Folder, FolderOpen } from "@mui/icons-material";
import { getStatusColor } from "../../TreeChart/dataUtils";
const StyledListItem = styled(ListItem)(({ theme, level }) => ({
cursor: "pointer",
paddingLeft: theme.spacing(2 + level * 2),
position: 'relative', // Добавляем для позиционирования индикатора
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
@ -20,6 +24,17 @@ const StyledListItem = styled(ListItem)(({ theme, level }) => ({
},
}));
const StatusIndicator = styled('div')(({ theme, status }) => ({
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
width: '4px',
backgroundColor: status ? getStatusColor(status) : 'transparent',
borderTopRightRadius: '4px',
borderBottomRightRadius: '4px',
}));
const IconWrapper = styled('div')(({ theme }) => ({
cursor: "pointer",
borderRadius: theme.shape.borderRadius,
@ -33,7 +48,6 @@ const MenuItem = ({ item, onSelectItem, level = 0, collapsed }) => {
const [isOpen, setIsOpen] = React.useState(false);
const hasChildren = Array.isArray(item.items) && item.items.length > 0;
const handleToggle = (e) => {
e.stopPropagation();
setIsOpen(!isOpen);
@ -52,17 +66,20 @@ const MenuItem = ({ item, onSelectItem, level = 0, collapsed }) => {
onClick={hasChildren ? handleToggle : handleOpenTab}
level={level}
sx={{
pl: collapsed ? 2 : 2 + level * 2, // Адаптируем отступы
pl: collapsed ? 2 : 2 + level * 2,
justifyContent: collapsed ? 'center' : 'flex-start',
}}
>
{/* Индикатор статуса */}
{!collapsed && <StatusIndicator status={item.status} />}
<ListItemIcon sx={{ minWidth: collapsed ? 'auto' : 56 }}>
<IconWrapper onClick={handleOpenTab}>
{hasChildren ? (isOpen ? <FolderOpen /> : <Folder />) : <Folder />}
</IconWrapper>
</ListItemIcon>
{!collapsed && ( // Показываем текст только в развернутом состоянии
{!collapsed && (
<>
<ListItemText
primary={item.title}
@ -75,7 +92,7 @@ const MenuItem = ({ item, onSelectItem, level = 0, collapsed }) => {
)}
</StyledListItem>
{hasChildren && !collapsed && ( // Показываем детей только в развернутом состоянии
{hasChildren && !collapsed && (
<Collapse in={isOpen} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{item.items.map((child, index) => (
@ -94,7 +111,6 @@ const MenuItem = ({ item, onSelectItem, level = 0, collapsed }) => {
);
};
// Вспомогательная функция (остается без изменений)
const getAllChildren = (node) => {
let children = [];
if (node.items && node.items.length > 0) {

View File

@ -0,0 +1,25 @@
import React from "react";
import { styled } from "@mui/material";
const StatusIndicator = styled('div')(({ theme, status }) => ({
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
width: '4px',
backgroundColor: getStatusColor(status),
borderRadius: '0 2px 2px 0',
transition: 'background-color 0.3s ease'
}));
const getStatusColor = (status) => {
switch (status) {
case 'red': return '#F44336';
case 'orange': return '#FF9800';
case 'yellow': return '#cebd21';
case 'green': return '#4CAF50';
default: return 'transparent';
}
};
export default StatusIndicator;

View File

@ -1,4 +1,6 @@
import React, { lazy, Suspense } from "react";
import Skeleton from '@mui/material/Skeleton';
import Box from '@mui/material/Box';
const PrometheusChart = lazy(() => import('../../Charts/PrometheusChart'));
import LazyChartBatchRenderer from "../hooks/LazyChartBatchRender";
@ -22,9 +24,32 @@ const getAllChildIds = (node) => {
return ids;
};
// Компонент Skeleton для графика
const ChartSkeleton = () => (
<Box sx={{ width: '100%' }}>
<Skeleton variant="text" width="60%" height={30} /> {/* Заголовок */}
<Skeleton variant="rectangular" width="100%" height={300} sx={{ mt: 2 }} /> {/* График */}
</Box>
);
// Компонент Skeleton для родительского контейнера
const ContainerSkeleton = () => (
<Box sx={{ width: '100%' }}>
<Skeleton variant="text" width="40%" height={40} /> {/* Заголовок */}
<Skeleton variant="text" width="80%" height={20} sx={{ mt: 1 }} /> {/* Описание */}
{/* Место для дочерних элементов */}
<Box sx={{ mt: 2 }}>
{[...Array(3)].map((_, i) => (
<ChartSkeleton key={i} />
))}
</Box>
</Box>
);
const tabContent = (data) => {
const tabContent = {};
// Функция для рекурсивного обхода и сбора данных
// Функция для рекурсивного обхода и сбора данных
const generateContent = (nodes) => {
nodes.forEach((node) => {
@ -36,9 +61,11 @@ const tabContent = (data) => {
const content = (
<div>
<h2>{node.title}</h2>
<Suspense fallback={<ContainerSkeleton />}>
<LazyChartBatchRenderer charts={node.items.map((child) => tabContent[child.id].content)} />
</Suspense>
<p>Контент для {node.title}.</p>
{childrenContent}
{/*childrenContent*/}
</div>
);
@ -53,7 +80,7 @@ const tabContent = (data) => {
const content = (
<div key={node.id}>
<h3>{node.title}</h3> {/* Используем title узла */}
<Suspense fallback={<div>Загрузка графика...</div>}>
<Suspense fallback={<ChartSkeleton />}>
<PrometheusChart metricName={metricName} />
</Suspense>
</div>

View File

@ -3,6 +3,7 @@ import Modal from "./Modal";
import "../../Style/LoginModal.css";
import Logo from '../../assets/images/logo.svg?react';
import TextField from '@mui/material/TextField';
import axios from 'axios';
const LoginModal = ({ onLogin, onClose }) => {
const [username, setUsername] = useState("");
@ -16,9 +17,10 @@ const LoginModal = ({ onLogin, onClose }) => {
e.preventDefault();
try {
// Отправляем данные на бэкенд
const response = await fetch(`${import.meta.env.VITE_BACK_URL}/api/auth/login`, {
const response = await axios.post(
`${import.meta.env.VITE_BACK_URL}/api/auth/login`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
@ -28,8 +30,9 @@ const LoginModal = ({ onLogin, onClose }) => {
const data = await response.json();
if (data.success) {
onLogin(); // Успешная авторизация
onClose(); // Закрыть модальное окно
localStorage.setItem('access_token', data.access_token);
onLogin(data.user); // Передаем данные пользователя
onClose();
} else {
setError(data.message || "Неверный логин или пароль");
}

View File

@ -0,0 +1,25 @@
import axios from 'axios';
export const checkAuth = async () => {
try {
const response = await axios.get(
`${import.meta.env.VITE_BACK_URL}/api/auth/check`,
{
withCredentials: true, // аналог `credentials: 'include'` в fetch
headers: {
Authorization: `Bearer ${localStorage.getItem('access_token') || ''}`,
},
}
);
// У axios нет свойства .ok, проверяем статус 200-299
if (response.status >= 200 && response.status < 300) {
return response.data; // Данные уже в JSON, не нужно .json()
} else {
throw new Error('Not authenticated');
}
} catch (err) {
console.error('Auth check failed:', err);
return { isAuthenticated: false };
}
};

View File

@ -76,7 +76,7 @@ export const lightTheme = createTheme({
// Фоновые цвета
background: {
default: "#6CACE4", // Основной фон приложения
default: "#FFFFFF", // Основной фон приложения
paper: "#FFFFFF", // Фон "бумажных" поверхностей (карточек, панелей)
},