Compare commits

..

4 Commits

Author SHA1 Message Date
Vladislav Drozdov b1a760336d Merge pull request 'redisign' (#56) from redisign into rc
test-org/trust-module-frontend/pipeline/pr-main There was a failure building this commit Details
Reviewed-on: http://git.enode/deployer3000/trust-module-frontend/pulls/56
Reviewed-by: Vladislav Drozdov <ya2@ya.ru>
2025-08-08 16:07:09 +03:00
SovietSpiderCat 00866d9d57 added UserManagement
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
2025-08-08 15:54:58 +03:00
SovietSpiderCat f55fb2df56 Merge branch 'redisign' of http://git.enode/deployer3000/trust-module-frontend into redisign 2025-08-07 04:45:21 +03:00
SovietSpiderCat be3bf3b21e fixed a bug with logout 2025-08-07 04:44:54 +03:00
9 changed files with 321 additions and 54 deletions

View File

@ -8,4 +8,8 @@ RUN npm install --verbose
COPY vite.config.js eslint.config.js ./
COPY . .
ENV HOST=0.0.0.0
EXPOSE 5173
ENTRYPOINT ["npm", "run", "dev"]

46
package-lock.json generated
View File

@ -18,6 +18,7 @@
"chartjs-adapter-date-fns": "^3.0.0",
"chartjs-chart-box-and-violin-plot": "^4.0.0",
"d3": "^7.9.0",
"esbuild": "^0.25.8",
"react": "^18.3.1",
"react-chartjs-2": "^5.0.0",
"react-datepicker": "^8.1.0",
@ -40,7 +41,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"vite": "^6.0.5"
"vite": "^7.1.0"
}
},
"node_modules/@alloc/quick-lru": {
@ -8880,9 +8881,10 @@
},
"node_modules/esbuild": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.8.tgz",
"integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
@ -18642,16 +18644,17 @@
}
},
"node_modules/typescript": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"version": "4.9.5",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
"node": ">=4.2.0"
}
},
"node_modules/unbox-primitive": {
@ -18903,22 +18906,23 @@
}
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"version": "7.1.0",
"resolved": "https://registry.npmmirror.com/vite/-/vite-7.1.0.tgz",
"integrity": "sha512-3jdAy3NhBJYsa/lCFcnRfbK4kNkO/bhijFCnv5ByUQk/eekYagoV2yQSISUrhpV+5JiY5hmwOh7jNnQ68dFMuQ==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
"picomatch": "^4.0.2",
"postcss": "^8.5.3",
"rollup": "^4.34.9",
"tinyglobby": "^0.2.13"
"fdir": "^6.4.6",
"picomatch": "^4.0.3",
"postcss": "^8.5.6",
"rollup": "^4.43.0",
"tinyglobby": "^0.2.14"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
"node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
@ -18927,14 +18931,14 @@
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"@types/node": "^20.19.0 || >=22.12.0",
"jiti": ">=1.21.0",
"less": "*",
"less": "^4.0.0",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"sass": "^1.70.0",
"sass-embedded": "^1.70.0",
"stylus": ">=0.54.8",
"sugarss": "^5.0.0",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"

View File

@ -10,27 +10,28 @@
"preview": "vite preview"
},
"dependencies": {
"chartjs-adapter-date-fns": "^3.0.0",
"recharts": "^2.15.1",
"d3": "^7.9.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"chart.js": "^4.0.0",
"chartjs-chart-box-and-violin-plot": "^4.0.0",
"react-chartjs-2": "^5.0.0",
"axios": "^1.7.9",
"react-datepicker": "^8.1.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/material": "^6.4.7",
"@mui/icons-material": "^6.4.8",
"reactflow": "^11.11.4",
"vite-plugin-svgr": "^4.3.0",
"react-scripts": "^5.0.1",
"socket.io-client": "^4.8.1",
"@mui/material": "^6.4.7",
"antd": "^5.24.7",
"axios": "^1.7.9",
"chart.js": "^4.0.0",
"chartjs-adapter-date-fns": "^3.0.0",
"chartjs-chart-box-and-violin-plot": "^4.0.0",
"d3": "^7.9.0",
"esbuild": "^0.25.8",
"react": "^18.3.1",
"react-chartjs-2": "^5.0.0",
"react-datepicker": "^8.1.0",
"react-dom": "^18.3.1",
"react-scripts": "^5.0.1",
"react-virtualized-auto-sizer": "1.0.26",
"react-window": "1.8.11",
"react-virtualized-auto-sizer": "1.0.26"
"reactflow": "^11.11.4",
"recharts": "^2.15.1",
"socket.io-client": "^4.8.1",
"vite-plugin-svgr": "^4.3.0"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
@ -42,6 +43,6 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"vite": "^6.0.5"
"vite": "^7.1.0"
}
}

View File

@ -110,10 +110,32 @@ function App() {
const handleLogout = async () => {
try {
await axios.post(`/api/auth/logout`, null, {
withCredentials: true,
});
const token = localStorage.getItem('access_token');
if (!token) {
// Если нет токена - просто очищаем данные
cleanup();
return;
}
try {
await axios.post('/api/auth/logout', {}, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
} finally {
cleanup();
}
} catch (error) {
console.error('Logout error:', error);
cleanup();
}
};
function cleanup() {
localStorage.removeItem('access_token');
localStorage.removeItem('user');
setAuthState({
@ -122,10 +144,7 @@ function App() {
user: null,
});
setShowLoginModal(true);
} catch (error) {
console.error('Logout failed:', error);
}
};
// Полноэкранный лоадер во время проверки авторизации
if (authState.isLoading) {
return (

View File

@ -21,7 +21,7 @@ class MetricsService {
}
console.log('Connecting WebSocket...');
this.socket = io(`${this.baseUrl.replace('http', 'ws')}/api/metrics-ws`, {
this.socket = io(`${this.baseUrl.replace('http', 'ws')}/ws`, {
transports: ['websocket'],
withCredentials: true,
});
@ -152,6 +152,6 @@ class MetricsService {
}
// Создаем экземпляр сервиса
const metricsService = new MetricsService(import.meta.env.VITE_BACK_URL);
const metricsService = new MetricsService();
export default metricsService;

View File

@ -0,0 +1,227 @@
import React, { useState, useEffect } from 'react';
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Button,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
Select,
MenuItem,
FormControl,
InputLabel,
IconButton,
Typography,
Box,
CircularProgress,
Alert,
Snackbar,
Divider,
Tooltip
} from '@mui/material';
import { Add, Delete } from '@mui/icons-material';
import axios from 'axios';
const UserManagement = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [openDialog, setOpenDialog] = useState(false);
const [newUser, setNewUser] = useState({
login: '',
password: '',
role: 'user'
});
useEffect(() => {
fetchUsers();
}, []);
const fetchUsers = async () => {
setLoading(true);
try {
const response = await axios.get('/api/auth/users', {
withCredentials: true
});
setUsers(response.data);
setError('');
} catch (err) {
setError('Не удалось загрузить пользователей');
console.error(err);
} finally {
setLoading(false);
}
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setNewUser(prev => ({ ...prev, [name]: value }));
};
const handleRoleChange = (e) => {
setNewUser(prev => ({ ...prev, role: e.target.value }));
};
const handleSubmit = async () => {
try {
await axios.post('/api/auth/users', newUser, {
withCredentials: true
});
setOpenDialog(false);
setNewUser({
login: '',
password: '',
role: 'user'
});
setSuccess('Пользователь успешно создан');
fetchUsers();
} catch (err) {
setError(err.response?.data?.message || 'Не удалось создать пользователя');
console.error(err);
}
};
const handleDelete = async (id) => {
try {
await axios.delete(`/api/auth/users/${id}`, {
withCredentials: true
});
setSuccess('Пользователь успешно удален');
fetchUsers();
} catch (err) {
setError(err.response?.data?.message || 'Не удалось удалить пользователя');
console.error(err);
}
};
return (
<Box sx={{ position: 'relative' }}>
{loading && (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
)}
{error && (
<Alert severity="error" sx={{ mb: 2 }} onClose={() => setError('')}>
{error}
</Alert>
)}
{success && (
<Alert severity="success" sx={{ mb: 2 }} onClose={() => setSuccess('')}>
{success}
</Alert>
)}
<Typography variant="h6" gutterBottom>
Управление пользователями
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<Button
variant="contained"
startIcon={<Add />}
onClick={() => setOpenDialog(true)}
>
Добавить пользователя
</Button>
</Box>
<Divider sx={{ mb: 2 }} />
{!loading && (
<TableContainer component={Paper} sx={{ maxHeight: '60vh', overflow: 'auto' }}>
<Table stickyHeader>
<TableHead>
<TableRow>
<TableCell sx={{ fontWeight: 'bold' }}>ID</TableCell>
<TableCell sx={{ fontWeight: 'bold' }}>Логин</TableCell>
<TableCell sx={{ fontWeight: 'bold' }}>Роль</TableCell>
<TableCell sx={{ fontWeight: 'bold' }}>Действия</TableCell>
</TableRow>
</TableHead>
<TableBody>
{users.map((user) => (
<TableRow key={user.id} hover>
<TableCell>{user.id}</TableCell>
<TableCell>{user.login}</TableCell>
<TableCell>{user.role === 'admin' ? 'Администратор' : 'Пользователь'}</TableCell>
<TableCell>
<Tooltip title={user.role === 'admin' ? 'Нельзя удалить администратора' : 'Удалить пользователя'}>
<IconButton
onClick={() => handleDelete(user.id)}
color="error"
disabled={user.role === 'admin'}
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
<Dialog open={openDialog} onClose={() => setOpenDialog(false)}>
<DialogTitle>Добавить нового пользователя</DialogTitle>
<DialogContent>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: 2, minWidth: 400 }}>
<TextField
label="Логин"
name="login"
value={newUser.login}
onChange={handleInputChange}
fullWidth
variant="standard"
required
/>
<TextField
label="Пароль"
name="password"
type="password"
value={newUser.password}
onChange={handleInputChange}
fullWidth
variant="standard"
required
/>
<FormControl fullWidth variant="standard">
<InputLabel>Роль</InputLabel>
<Select
value={newUser.role}
onChange={handleRoleChange}
label="Роль"
>
<MenuItem value="user">Пользователь</MenuItem>
<MenuItem value="admin">Администратор</MenuItem>
</Select>
</FormControl>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenDialog(false)}>Отмена</Button>
<Button
onClick={handleSubmit}
variant="contained"
disabled={!newUser.login || !newUser.password}
>
Создать
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default UserManagement;

View File

@ -20,6 +20,7 @@ import {
import CloseIcon from '@mui/icons-material/Close';
import SaveIcon from '@mui/icons-material/Save';
import MetricRangeEditor from './SettingsComponents/MetricRangeEditor';
import UserManagement from './SettingsComponents/UserManagement';
const Transition = React.forwardRef(function Transition(props, ref) {
return <Slide direction="up" ref={ref} {...props} />;
@ -147,6 +148,7 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => {
<Tabs value={tabValue} onChange={handleTabChange} aria-label="settings tabs">
<Tab label="Меню" id="settings-tab-0" aria-controls="settings-tabpanel-0" />
<Tab label="Границы метрик" id="settings-tab-1" aria-controls="settings-tabpanel-1" />
<Tab label="Управление пользователями" id="settings-tab-2" aria-controls="settings-tabpanel-2" />
{/* Добавляйте новые вкладки здесь */}
</Tabs>
</Box>
@ -161,9 +163,14 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => {
<MetricRangeEditor onSave={handleMetricEditorChange} />
</TabPanel>
<TabPanel value={tabValue} index={2}>
<UserManagement />
</TabPanel>
{/* Добавляйте новые TabPanel для новых вкладок */}
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Закрыть</Button>
<Button

View File

@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />

View File

@ -14,8 +14,12 @@ export default defineConfig({
rewrite: (path) => path.replace(/^\/ai-api/, ''),
},
'/api': {
target: 'http://192.168.2.39:3000',
target: 'http://localhost:3000',
changeOrigin: true,
bypass(req, res, options) {
console.log('Proxying request:', req.url);
}
}
}
}