Compare commits

...

3 Commits

Author SHA1 Message Date
DmitriyA dabdda4afe Merge branch 'redisign' of http://git.enode/deployer3000/trust-module-frontend into redisign
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
2025-07-16 09:59:52 -04:00
DmitriyA 6a73bd8104 Merge branch 'rc' of http://git.enode/deployer3000/trust-module-frontend into redisign 2025-07-16 09:59:33 -04:00
DmitriyA 87a79f98d7 adding roles 2025-07-16 09:50:19 -04:00
8 changed files with 237 additions and 77 deletions

View File

@ -22,23 +22,75 @@ function App() {
useEffect(() => { useEffect(() => {
const verifyAuth = async () => { const verifyAuth = async () => {
try { try {
const savedToken = localStorage.getItem('access_token');
// Если есть токен, но нет пользователя - делаем запрос к серверу
if (savedToken && !localStorage.getItem('user')) {
const authStatus = await checkAuth();
handleAuthResponse(authStatus);
return;
}
// Если есть сохраненный пользователь
const savedUser = JSON.parse(localStorage.getItem('user'));
if (savedUser && savedToken) {
// Если у сохраненного пользователя нет роли - запрашиваем свежие данные
if (!savedUser.role) {
const authStatus = await checkAuth();
handleAuthResponse(authStatus);
} else {
setAuthState({
isAuthenticated: true,
isLoading: false,
user: savedUser
});
setShowLoginModal(false);
}
return;
}
// Стандартная проверка авторизации
const authStatus = await checkAuth(); const authStatus = await checkAuth();
setAuthState({ handleAuthResponse(authStatus);
isAuthenticated: authStatus.isAuthenticated,
isLoading: false,
user: authStatus.user || null
});
setShowLoginModal(!authStatus.isAuthenticated);
} catch (error) { } catch (error) {
console.error('Auth verification error:', error); console.error('Auth verification error:', error);
setAuthState({ handleAuthFailure();
isAuthenticated: false,
isLoading: false,
user: null
});
setShowLoginModal(true);
} }
}; };
const handleAuthResponse = (authStatus) => {
if (authStatus.isAuthenticated && authStatus.user?.role) {
const userToSave = {
id: authStatus.user.id,
login: authStatus.user.login,
role: authStatus.user.role
};
console.log('Saving user:', userToSave);
localStorage.setItem('user', JSON.stringify(userToSave));
setAuthState({
isAuthenticated: true,
isLoading: false,
user: userToSave
});
setShowLoginModal(false);
} else {
handleAuthFailure();
}
};
const handleAuthFailure = () => {
localStorage.removeItem('user');
localStorage.removeItem('access_token');
setAuthState({
isAuthenticated: false,
isLoading: false,
user: null
});
setShowLoginModal(true);
};
verifyAuth(); verifyAuth();
}, []); }, []);
@ -46,29 +98,33 @@ function App() {
setAuthState({ setAuthState({
isAuthenticated: true, isAuthenticated: true,
isLoading: false, isLoading: false,
user: userData user: {
id: userData.id,
login: userData.login,
role: userData.role
}
}); });
setShowLoginModal(false); setShowLoginModal(false);
}; };
const handleLogout = async () => { const handleLogout = async () => {
try { try {
await axios.post(`${import.meta.env.VITE_BACK_URL}/api/auth/logout`, null, { await axios.post(`${import.meta.env.VITE_BACK_URL}/api/auth/logout`, null, {
withCredentials: true, // чтобы отправлялись куки withCredentials: true,
}); });
localStorage.removeItem('access_token');
setAuthState({
isAuthenticated: false,
isLoading: false,
user: null,
});
setShowLoginModal(true);
} catch (error) {
console.error('Logout failed:', error);
}
};
localStorage.removeItem('access_token');
localStorage.removeItem('user');
setAuthState({
isAuthenticated: false,
isLoading: false,
user: null,
});
setShowLoginModal(true);
} catch (error) {
console.error('Logout failed:', error);
}
};
// Полноэкранный лоадер во время проверки авторизации // Полноэкранный лоадер во время проверки авторизации
if (authState.isLoading) { if (authState.isLoading) {
return ( return (

View File

@ -37,7 +37,7 @@ const Content = styled(Box)(({ theme }) => ({
color: theme.palette.custom.modalText, color: theme.palette.custom.modalText,
})); }));
const Dashboard = ({ isDarkMode, setIsDarkMode }) => { const Dashboard = ({ isDarkMode, setIsDarkMode, user }) => {
const { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab } = useTabs("Главная"); const { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab } = useTabs("Главная");
const [tabContent, setTabContent] = useState({}); const [tabContent, setTabContent] = useState({});
const [treeData1, setTreeData1] = useState(menuData); const [treeData1, setTreeData1] = useState(menuData);
@ -91,11 +91,11 @@ const Dashboard = ({ isDarkMode, setIsDarkMode }) => {
filters: item.filters, filters: item.filters,
title: item.title, title: item.title,
description: item.description, description: item.description,
ranges: item.ranges, ranges: item.ranges,
context: { context: {
device: item.filters?.device, device: item.filters?.device,
source_id: item.filters?.source_id, source_id: item.filters?.source_id,
parent: item parent: item
} }
}} }}
/> />
@ -111,13 +111,13 @@ const Dashboard = ({ isDarkMode, setIsDarkMode }) => {
type: item.metric ? 'metric' : 'menuItem', type: item.metric ? 'metric' : 'menuItem',
metric: item.metric, metric: item.metric,
filters: item.filters, filters: item.filters,
ranges: item.ranges ranges: item.ranges
}; };
handleOpenTab(newTab); handleOpenTab(newTab);
} else { } else {
setActiveTab(tabId); setActiveTab(tabId);
} }
}; };
// Вспомогательная функция для получения всех дочерних элементов // Вспомогательная функция для получения всех дочерних элементов
const getAllChildren = (node) => { const getAllChildren = (node) => {
@ -138,6 +138,7 @@ const Dashboard = ({ isDarkMode, setIsDarkMode }) => {
isDarkMode={isDarkMode} isDarkMode={isDarkMode}
setIsDarkMode={setIsDarkMode} setIsDarkMode={setIsDarkMode}
onMenuSelect={handleMenuSelect} onMenuSelect={handleMenuSelect}
user={user}
/> />
{/* Основной контент */} {/* Основной контент */}

View File

@ -21,7 +21,8 @@ const SidebarMenu = ({
isDarkMode, isDarkMode,
setIsDarkMode, setIsDarkMode,
onSelectItem, onSelectItem,
forceRefreshMenu forceRefreshMenu,
user
}) => { }) => {
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const { sidebarWidth, startResizing } = useSidebarResize(290); const { sidebarWidth, startResizing } = useSidebarResize(290);
@ -148,6 +149,7 @@ const SidebarMenu = ({
isDarkMode={isDarkMode} isDarkMode={isDarkMode}
setIsDarkMode={setIsDarkMode} setIsDarkMode={setIsDarkMode}
forceRefreshMenu={forceRefreshMenu} forceRefreshMenu={forceRefreshMenu}
user={user}
/> />
</Box> </Box>
{!collapsed && ( {!collapsed && (

View File

@ -1,4 +1,3 @@
// components/SidebarMenuComponents/SidebarFooter.jsx
import React, { useState } from "react"; import React, { useState } from "react";
import { Brightness4, Brightness7 } from "@mui/icons-material"; import { Brightness4, Brightness7 } from "@mui/icons-material";
import { IconButton, Tooltip } from "@mui/material"; import { IconButton, Tooltip } from "@mui/material";
@ -12,6 +11,7 @@ import {
Button Button
} from "@mui/material"; } from "@mui/material";
import SettingsModal from "../SettingsModal"; import SettingsModal from "../SettingsModal";
import { RoleBasedRender } from "../../UI/RoleBasedRender";
const FooterList = styled(List)(({ theme }) => ({ const FooterList = styled(List)(({ theme }) => ({
backgroundColor: theme.palette.custom.sidebar, backgroundColor: theme.palette.custom.sidebar,
@ -30,7 +30,13 @@ const FooterListItem = styled(ListItem)(({ theme }) => ({
alignItems: 'center' alignItems: 'center'
})); }));
const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode, forceRefreshMenu }) => { const SidebarFooter = ({
collapsed,
isDarkMode,
setIsDarkMode,
forceRefreshMenu,
user
}) => {
const [settingsOpen, setSettingsOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false);
const handleSettingsOpen = () => { const handleSettingsOpen = () => {
@ -40,7 +46,11 @@ const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode, forceRefreshMenu
const handleSettingsClose = () => { const handleSettingsClose = () => {
setSettingsOpen(false); setSettingsOpen(false);
}; };
console.log('SidebarFooter user with role:', {
...user,
hasRole: 'role' in user,
roleValue: user?.role
});
return ( return (
<> <>
<FooterList> <FooterList>
@ -56,26 +66,29 @@ const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode, forceRefreshMenu
</FooterListItem> </FooterListItem>
)} )}
<FooterListItem> <FooterListItem>
{!collapsed && ( {/* кнопка настроек */}
<Button <RoleBasedRender user={user} allowedRoles={['admin']}>
onClick={handleSettingsOpen} {!collapsed && (
sx={{ <Button
color: 'custom.sidebarText', onClick={handleSettingsOpen}
textTransform: 'none', sx={{
minWidth: 0,
padding: 0,
marginRight: 'auto'
}}
>
<ListItemText
primary="Настройки"
primaryTypographyProps={{
color: 'custom.sidebarText', color: 'custom.sidebarText',
variant: 'body2' textTransform: 'none',
minWidth: 0,
padding: 0,
marginRight: 'auto'
}} }}
/> >
</Button> <ListItemText
)} primary="Настройки"
primaryTypographyProps={{
color: 'custom.sidebarText',
variant: 'body2'
}}
/>
</Button>
)}
</RoleBasedRender>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Tooltip title="Переключить тему"> <Tooltip title="Переключить тему">
@ -98,9 +111,14 @@ const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode, forceRefreshMenu
</FooterListItem> </FooterListItem>
</FooterList> </FooterList>
<SettingsModal open={settingsOpen} {/* Используем RoleBasedRender для модального окна */}
onClose={handleSettingsClose} <RoleBasedRender user={user} allowedRoles={['admin']}>
onMenuUpdate={forceRefreshMenu} /> <SettingsModal
open={settingsOpen}
onClose={handleSettingsClose}
onMenuUpdate={forceRefreshMenu}
/>
</RoleBasedRender>
</> </>
); );
}; };

View File

@ -3,7 +3,7 @@ import SidebarMenu from './SidebarMenu';
import { Box, CircularProgress, Typography } from '@mui/material'; import { Box, CircularProgress, Typography } from '@mui/material';
import axios from 'axios'; import axios from 'axios';
const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect }) => { const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect, user }) => {
const [menuData, setMenuData] = useState(null); const [menuData, setMenuData] = useState(null);
const [lastModified, setLastModified] = useState(null); const [lastModified, setLastModified] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -177,6 +177,7 @@ const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect }) => {
onCloseEditModal={() => setEditModalOpen(false)} onCloseEditModal={() => setEditModalOpen(false)}
onSaveChanges={handleSaveChanges} onSaveChanges={handleSaveChanges}
forceRefreshMenu={forceRefreshMenu} forceRefreshMenu={forceRefreshMenu}
user={user}
/> />
); );
}; };

View File

@ -1,21 +1,27 @@
import React, { useState } from "react"; import React, { useState } from "react";
import Modal from "./Modal"; import Modal from "./Modal";
import "../../Style/LoginModal.css"; import "../../Style/LoginModal.css";
import Logo from '../../assets/images/logo.svg?react'; import {
import TextField from '@mui/material/TextField'; TextField,
IconButton,
Button,
Typography,
InputAdornment
} from "@mui/material";
import {
Visibility,
VisibilityOff
} from "@mui/icons-material";
import axios from 'axios'; import axios from 'axios';
const LoginModal = ({ onLogin, onClose }) => { const LoginModal = ({ onLogin, onClose }) => {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
const [showPassword, setShowPassword] = React.useState(false); const [showPassword, setShowPassword] = useState(false);
const handleClickShowPassword = () => setShowPassword((show) => !show);
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
try { try {
const { data } = await axios.post( const { data } = await axios.post(
`${import.meta.env.VITE_BACK_URL}/api/auth/login`, `${import.meta.env.VITE_BACK_URL}/api/auth/login`,
@ -28,16 +34,33 @@ const LoginModal = ({ onLogin, onClose }) => {
} }
); );
console.log('Login response:', data);
if (data.success) { if (data.success) {
if (!data.user?.role) {
console.error('Role missing in response:', data);
throw new Error('Роль пользователя не получена');
}
const userData = {
id: data.user.id,
login: data.user.login,
role: data.user.role
};
localStorage.setItem('access_token', data.access_token); localStorage.setItem('access_token', data.access_token);
onLogin(data.user); localStorage.setItem('user', JSON.stringify(userData));
console.log('User data saved:', userData);
onLogin(userData);
onClose(); onClose();
} else { } else {
setError(data.message || "Неверный логин или пароль"); setError(data.message || 'Ошибка авторизации');
} }
} catch (err) { } catch (err) {
console.error('Ошибка при отправке запроса:', err); console.error('Login error:', err);
setError(err.response?.data?.message || "Ошибка при подключении к серверу"); setError(err.response?.data?.message || err.message || 'Ошибка при входе');
} }
}; };
@ -52,8 +75,8 @@ const LoginModal = ({ onLogin, onClose }) => {
variant="filled" variant="filled"
margin="normal" margin="normal"
required required
value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
size="normal"
/> />
<TextField <TextField
@ -64,12 +87,45 @@ const LoginModal = ({ onLogin, onClose }) => {
margin="normal" margin="normal"
required required
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
size="normal" InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => setShowPassword(!showPassword)}
edge="end"
sx={{
marginRight: '-12px',
alignSelf: 'flex-end'
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
)
}}
/> />
{error && <p className="error">{error}</p>} {error && (
<button type="submit">Войти</button> <Typography color="error" sx={{ mt: 1 }}>
{error}
</Typography>
)}
<Button
type="submit"
variant="contained"
fullWidth
sx={{
mt: 2,
py: 1.5,
fontSize: '1rem'
}}
>
Войти
</Button>
</form> </form>
</Modal> </Modal>
); );

View File

@ -0,0 +1,15 @@
import React from 'react';
export const RoleBasedRender = ({ user, allowedRoles, children }) => {
console.log('RoleBasedRender check:', {
user,
hasRole: user?.role,
allowedRoles,
hasAccess: user && allowedRoles.includes(user.role)
});
if (!user || !allowedRoles.includes(user.role)) {
return null;
}
return children;
};

View File

@ -1,5 +1,3 @@
import axios from 'axios';
export const checkAuth = async () => { export const checkAuth = async () => {
try { try {
const { data } = await axios.get( const { data } = await axios.get(
@ -12,7 +10,20 @@ export const checkAuth = async () => {
} }
); );
return data; console.log('Auth check response:', data);
if (!data.user) {
return { isAuthenticated: false };
}
return {
isAuthenticated: data.isAuthenticated,
user: {
id: data.user.id,
login: data.user.login,
role: data.user.role
}
};
} catch (err) { } catch (err) {
console.error('Auth check failed:', err); console.error('Auth check failed:', err);
return { isAuthenticated: false }; return { isAuthenticated: false };