vite_deleteelectron

This commit is contained in:
2026-08-09 21:02:10 +03:00
parent 5d2fc5773c
commit 4ba9dd8012
99 changed files with 7080 additions and 48649 deletions
-23
View File
@@ -1,23 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
-70
View File
@@ -1,70 +0,0 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
The page will reload when you make changes.\
You may also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
File diff suppressed because it is too large Load Diff
-39
View File
@@ -1,39 +0,0 @@
{
"name": "admin-frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^13.5.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

@@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

@@ -1,25 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
-557
View File
@@ -1,557 +0,0 @@
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: #f4f7f9;
color: #333;
margin: 0;
padding: 20px;
}
.container {
max-width: 80%;
margin: 0 auto;
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
h1 {
color: #0056b3;
}
#status {
display: flex;
align-items: center;
gap: 15px;
}
#refresh-status-btn {
padding: 5px 10px;
font-size: 14px;
}
#server-status {
font-weight: bold;
}
.logs-container {
margin-top: 20px;
}
.log-controls {
margin-bottom: 10px;
display: flex;
gap: 10px;
}
.ql-align-center { text-align: center; }
.ql-align-right { text-align: right; }
.ql-align-justify { text-align: justify; }
#logs-content {
background-color: #2d2d2d;
color: #f1f1f1;
padding: 15px;
border-radius: 5px;
white-space: pre-wrap; /* Перенос строк */
word-wrap: break-word; /* Перенос длинных строк */
max-height: 400px;
overflow-y: auto;
}
.config-container {
margin-top: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
}
.form-group input {
width: 100%;
padding: 8px;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 4px;
}
.input-with-button {
display: flex;
gap: 5px;
}
.input-with-button input {
flex-grow: 1; /* Поле ввода займет все доступное место */
}
button {
background-color: #0056b3;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #004494;
}
#db-status-container {
display: flex;
align-items: flex-start; /* Выравнивание по верху */
gap: 20px;
margin-top: 10px;
padding: 10px;
border: 1px solid #eee;
border-radius: 5px;
}
#db-status-container p {
margin: 0 0 5px 0;
}
#db-status-container small {
color: #666;
font-style: italic;
}
#refresh-status-btn, .btn-small {
padding: 5px 10px;
font-size: 14px;
}
.network-diag-container {
margin-top: 20px;
}
.ping-controls {
display: inline-flex;
gap: 10px;
margin-bottom: 10px;
}
.ping-controls input {
flex-grow: 1;
}
#ping-output {
background-color: #2d2d2d;
color: #f1f1f1;
padding: 15px;
border-radius: 5px;
white-space: pre-wrap;
word-wrap: break-word;
min-height: 50px;
}
/* --- Карточки блоков --- */
.content-block {
background: #f9f9fb;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
padding: 18px 18px 12px 18px;
margin-bottom: 18px;
position: relative;
transition: box-shadow 0.2s;
}
.content-block:hover {
box-shadow: 0 4px 16px rgba(0,0,0,0.13);
}
.block-text, .block-image, .block-video {
margin-bottom: 10px;
}
.block-image img, .block-video video {
max-width: 100%;
border-radius: 6px;
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
}
/* --- Кнопки --- */
button, .btn-small, .tab-btn {
background: #0056b3;
color: #fff;
border: none;
border-radius: 5px;
padding: 8px 16px;
margin-right: 8px;
font-size: 15px;
cursor: pointer;
transition: background 0.15s, box-shadow 0.15s;
box-shadow: 0 1px 2px rgba(0,0,0,0.04);
}
button:hover, .btn-small:hover, .tab-btn:hover {
background: #003e80;
}
button:active, .btn-small:active, .tab-btn:active {
background: #002b5c;
}
.tab-btn.active {
background: #0074e4;
font-weight: bold;
}
/* --- Модальное окно --- */
.modal {
display: none; /* Скрыто по умолчанию */
position: fixed; /* Остается на месте при скролле */
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto; /* Включает скролл, если нужно */
background-color: rgba(0,0,0,0.5); /* Полупрозрачный фон */
justify-content: center;
align-items: center;
}
.modal-content {
background-color: #fefefe;
margin: auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
border-radius: 8px;
}
.monitoring-filters {
margin-bottom: 15px;
}
.monitoring-filters label {
margin-right: 20px;
}
.log-level-label {
padding: 2px 6px;
border-radius: 4px;
color: white;
font-weight: bold;
font-size: 0.9em;
}
tr.log-level-error {
background-color: #ffdddd;
}
tr.log-level-error .log-level-label {
background-color: #d63031;
}
tr.log-level-warn {
background-color: #fff3cd;
}
tr.log-level-warn .log-level-label {
background-color: #fdcb6e;
}
.modal-buttons {
margin-top: 20px;
}
.modal-buttons button {
margin: 0 10px;
padding: 10px 20px;
}
.close {
position: absolute;
right: 18px;
top: 12px;
font-size: 2rem;
color: #888;
cursor: pointer;
font-weight: bold;
transition: color 0.15s;
}
.close:hover { color: #0056b3; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideDown { from { transform: translateY(-30px); opacity: 0; } to { transform: none; opacity: 1; } }
/* --- Уведомления --- */
#notify {
position: fixed;
top: 24px;
right: 24px;
z-index: 2000;
min-width: 220px;
background: #fff;
color: #222;
border-radius: 7px;
box-shadow: 0 2px 12px rgba(0,0,0,0.13);
padding: 16px 22px;
font-size: 16px;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
#notify.show {
opacity: 1;
pointer-events: auto;
}
#notify.error {
background: #ffeaea;
color: #b30000;
border: 1px solid #ffb3b3;
}
#notify.success {
background: #eaffea;
color: #1a7f1a;
border: 1px solid #b3ffb3;
}
/* --- Формы --- */
input[type="text"], input[type="password"], textarea, select {
border: 1.5px solid #b3c6e0;
border-radius: 5px;
padding: 8px 10px;
font-size: 15px;
margin-bottom: 8px;
width: 100%;
box-sizing: border-box;
transition: border 0.15s;
}
input[type="text"]:focus, input[type="password"]:focus, textarea:focus, select:focus {
border: 1.5px solid #0056b3;
outline: none;
background: #f4f8ff;
}
/* --- Logout --- */
#logout-btn {
background: #e74c3c;
float: right;
margin-top: 2px;
margin-bottom: 10px;
font-size: 15px;
padding: 7px 16px;
}
#logout-btn:hover { background: #c0392b; }
/* Кнопка "Добавить блок" */
#add-block-btn {
background: #0074e4;
color: #fff;
border: none;
border-radius: 5px;
padding: 8px 18px;
font-size: 16px;
margin-bottom: 10px;
cursor: pointer;
transition: background 0.15s;
}
#add-block-btn:hover { background: #005bb5; }
/* Кнопка "Редактировать" */
.content-block button:nth-of-type(1) {
background: #ff9800;
color: #fff;
border: none;
border-radius: 5px;
padding: 6px 14px;
margin-right: 8px;
font-size: 15px;
cursor: pointer;
transition: background 0.15s;
}
.content-block button:nth-of-type(1):hover { background: #e65100; }
/* Кнопка "Удалить" */
.content-block button:nth-of-type(2) {
background: #e74c3c;
color: #fff;
border: none;
border-radius: 5px;
padding: 6px 14px;
font-size: 15px;
cursor: pointer;
transition: background 0.15s;
}
.content-block button:nth-of-type(2):hover { background: #b71c1c; }
/* Кнопка "Сохранить" в модалке */
#edit-block-form button[type="submit"] {
background: #43a047;
color: #fff;
border: none;
border-radius: 5px;
padding: 8px 18px;
font-size: 16px;
margin-top: 10px;
cursor: pointer;
transition: background 0.15s;
}
#edit-block-form button[type="submit"]:hover { background: #2e7031; }
/* --- Адаптивность --- */
@media (max-width: 600px) {
.container { padding: 8px; }
.modal-content { min-width: 90vw; padding: 18px 8px; }
#notify { right: 8px; top: 8px; min-width: 120px; font-size: 14px; }
}
#edit-block-preview {
max-width: 100%;
max-height: 45vh; /* Ограничение по высоте предпросмотра */
overflow-y: auto;
overflow-x: hidden;
word-break: break-word;
}
#confirm-modal .modal-content {
max-width: 400px;
padding: 10px 28px 24px 28px;
text-align: center;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.22);
}
#confirm-modal h3 {
font-size: 1.15rem;
color: #222;
margin-bottom: 18px;
}
#confirm-close-yes {
background: #e74c3c;
color: #fff;
border: none;
border-radius: 5px;
padding: 8px 18px;
font-size: 16px;
margin-right: 12px;
cursor: pointer;
transition: background 0.15s;
}
#confirm-close-yes:hover { background: #b71c1c; }
#confirm-close-no {
background: #f4f7f9;
color: #222;
border: 1.5px solid #b3c6e0;
border-radius: 5px;
padding: 8px 18px;
font-size: 16px;
cursor: pointer;
transition: background 0.15s;
}
#confirm-close-no:hover { background: #e0e7ef; }
#toast-container {
position: fixed;
top: 30px;
right: 30px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 10px;
}
.toast {
visibility: hidden;
min-width: 250px;
max-width: 350px;
background: #333;
color: #fff;
padding: 14px 22px;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
font-size: 16px;
opacity: 0.97;
animation: fadein 0.3s, fadeout 0.3s 2.7s;
transition: opacity 0.3s;
}
.toast-success { background: #2ecc40; color: #fff; }
.toast-error { background: #ff4136; color: #fff; }
@keyframes fadein { from { opacity: 0; transform: translateY(-20px);} to { opacity: 0.97; transform: translateY(0);} }
@keyframes fadeout { from { opacity: 0.97; } to { opacity: 0; } }
/* --- Настройки: секции и формы --- */
#settings-tab > div {
margin-bottom: 32px;
padding: 18px 20px 16px 20px;
background: #f8fafc;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
border: 1px solid #e3e8ee;
}
#settings-tab h3 { margin-top: 0; }
fieldset {
border: 1px solid #bfc9d8;
border-radius: 6px;
margin-bottom: 12px;
padding: 12px 16px 10px 16px;
background: #fff;
}
legend { color: #0056b3; font-weight: 500; }
input[type="text"], input[type="password"], input[type="number"] {
padding: 7px 10px;
border: 1px solid #bfc9d8;
border-radius: 4px;
margin: 4px 0 10px 0;
width: 220px;
font-size: 15px;
background: #fafdff;
transition: border 0.2s;
}
input:focus { border: 1.5px solid #0056b3; outline: none; background: #fff; }
input:invalid { border: 1.5px solid #ff4136; background: #fff6f6; }
.form-error { color: #ff4136; font-size: 14px; margin: 2px 0 8px 0; }
.warning-text {
color: #ff8c00; /* DarkOrange */
}
.btn-small {
background: #e3e8ee;
color: #0056b3;
border: none;
border-radius: 4px;
padding: 5px 12px;
font-size: 14px;
margin-left: 2px;
cursor: pointer;
transition: background 0.2s, color 0.2s;
}
.btn-small:hover { background: #0056b3; color: #fff; }
button[type="submit"] {
background: #2ecc40;
color: #fff;
border: none;
border-radius: 4px;
padding: 8px 18px;
font-size: 15px;
margin-top: 8px;
cursor: pointer;
transition: background 0.2s;
}
button[type="submit"]:hover { background: #27ae38; }
#cors-list li { margin-bottom: 6px; }
.moders-table {
width: 100%;
border-collapse: collapse;
margin-top: 12px;
background: #fff;
}
.moders-table th, .moders-table td {
border: 1px solid #e3e8ee;
padding: 7px 10px;
text-align: left;
}
.moders-table th {
background: #f4f7fa;
color: #0056b3;
font-weight: 500;
}
.moders-table tr:nth-child(even) { background: #fafdff; }
.moders-table tr:hover { background: #eaf3ff; }
.moder-pass-input {
width: 100px;
margin-right: 5px;
}
.password-wrapper {
display: inline-flex;
align-items: center;
}
-41
View File
@@ -1,41 +0,0 @@
import React, { useState, useEffect } from 'react';
import './App.css';
import Login from './components/Login.jsx';
import Index from './components/Index.jsx';
import Editor from './components/Editor.jsx';
function App() {
const [currentPage, setCurrentPage] = useState('loading');
useEffect(() => {
const path = window.location.pathname;
if (path === '/login' || path === '/login.html') {
setCurrentPage('login');
} else if (path === '/editor' || path === '/editor.html' || path === '/moders') {
setCurrentPage('editor');
} else {
setCurrentPage('index');
}
}, []);
const renderPage = () => {
switch (currentPage) {
case 'login':
return <Login />;
case 'editor':
return <Editor />;
case 'index':
return <Index />;
default:
return <div>Загрузка...</div>;
}
};
return (
<div className="App">
{renderPage()}
</div>
);
}
export default App;
@@ -1,8 +0,0 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
@@ -1,307 +0,0 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import '../App.css';
import DOMPurify from 'dompurify';
function Editor() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [loginError, setLoginError] = useState('');
const [activeTab, setActiveTab] = useState('info');
const [blocks, setBlocks] = useState([]);
const [showEditModal, setShowEditModal] = useState(false);
const [editingBlockId, setEditingBlockId] = useState(null);
const [editContent, setEditContent] = useState('');
const [editPreviewContent, setEditPreviewContent] = useState('');
const [showConfirmModal, setShowConfirmModal] = useState(false);
const [csrfToken, setCsrfToken] = useState('');
const [notify, setNotify] = useState({ message: '', type: '', show: false });
const editorRef = useRef(null);
const quillRef = useRef(null);
const originalContentRef = useRef('');
// Notify helper
const showNotify = useCallback((message, type = 'success') => {
setNotify({ message, type, show: true });
setTimeout(() => {
setNotify({ message: '', type: '', show: false });
}, 2200);
}, []);
// Fetch CSRF token
useEffect(() => {
fetch('/moders/csrf-token')
.then(res => res.json())
.then(data => {
setCsrfToken(data.csrfToken);
})
.catch(() => {});
}, []);
// Check session on mount
useEffect(() => {
fetch('/moders/check-session', { credentials: 'include' })
.then(res => {
if (res.ok) {
setIsLoggedIn(true);
loadBlocks(activeTab);
} else {
setIsLoggedIn(false);
}
})
.catch(() => setIsLoggedIn(false));
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const authenticatedFetch = useCallback((url, options = {}) => {
if (!options.headers) options.headers = {};
if (options.method && ['POST', 'PUT', 'DELETE'].includes(options.method.toUpperCase())) {
options.headers['x-csrf-token'] = csrfToken;
}
return fetch(url, options);
}, [csrfToken]);
const loadBlocks = useCallback(async (tab) => {
try {
const res = await fetch(`/moders/content/${tab}`);
const data = await res.json();
setBlocks(Array.isArray(data) ? data : []);
} catch (e) {
setBlocks([]);
}
}, []);
const handleLogin = async (e) => {
e.preventDefault();
setLoginError('');
try {
const res = await fetch('/moders/moder-login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
if (res.ok) {
setIsLoggedIn(true);
setLoginError('');
loadBlocks(activeTab);
} else {
const data = await res.json();
setLoginError(data.message || 'Ошибка входа');
}
} catch (err) {
setLoginError('Ошибка сервера');
}
};
const handleLogout = async () => {
try {
await authenticatedFetch('/moders/moder-logout');
} catch (e) {
// ignore
}
setIsLoggedIn(false);
setUsername('');
setPassword('');
showNotify('Вы вышли из системы', 'success');
};
const handleTabChange = (tab) => {
setActiveTab(tab);
if (isLoggedIn) {
loadBlocks(tab);
}
};
const handleAddBlock = () => {
setEditingBlockId(null);
setEditContent('<p>Новый блок</p>');
setEditPreviewContent(DOMPurify.sanitize('<p>Новый блок</p>'));
originalContentRef.current = '<p>Новый блок</p>';
setShowEditModal(true);
};
const handleEditBlock = (block) => {
setEditingBlockId(block.id);
setEditContent(block.content);
setEditPreviewContent(DOMPurify.sanitize(block.content));
originalContentRef.current = block.content;
setShowEditModal(true);
};
const handleDeleteBlock = async (blockId) => {
if (!window.confirm('Удалить блок?')) return;
try {
await authenticatedFetch(`/moders/content/${activeTab}/${blockId}`, { method: 'DELETE' });
showNotify('Блок удалён', 'success');
loadBlocks(activeTab);
} catch (e) {
showNotify('Ошибка удаления', 'error');
}
};
const handleSaveBlock = async (e) => {
e.preventDefault();
const content = DOMPurify.sanitize(editContent.trim());
if (!content || content === '<p><br></p>') return;
try {
if (editingBlockId) {
await authenticatedFetch(`/moders/content/${activeTab}/${editingBlockId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'text', content })
});
showNotify('Блок обновлён', 'success');
} else {
await authenticatedFetch(`/moders/content/${activeTab}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'text', content })
});
showNotify('Блок добавлен', 'success');
}
setShowEditModal(false);
loadBlocks(activeTab);
} catch (e) {
showNotify('Ошибка сохранения', 'error');
}
};
const handleTryCloseEditModal = () => {
if (editContent !== originalContentRef.current) {
setShowConfirmModal(true);
} else {
setShowEditModal(false);
}
};
const handleConfirmClose = () => {
setShowConfirmModal(false);
setShowEditModal(false);
};
const handleCancelClose = () => {
setShowConfirmModal(false);
};
// Initialize Quill when modal opens
useEffect(() => {
if (showEditModal && editorRef.current && !quillRef.current) {
// Dynamically import Quill
import('quill').then((QuillModule) => {
const Quill = QuillModule.default || QuillModule;
if (editorRef.current) {
quillRef.current = new Quill(editorRef.current, {
theme: 'snow',
modules: {
toolbar: [
[{ header: [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ 'align': [] }],
[{ 'color': [] }, { 'background': [] }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['blockquote', 'code-block'],
['link', 'image', 'video'],
['clean']
]
}
});
quillRef.current.clipboard.dangerouslyPasteHTML(editContent);
quillRef.current.on('text-change', () => {
const html = quillRef.current.root.innerHTML;
setEditContent(html);
setEditPreviewContent(DOMPurify.sanitize(html));
});
}
});
}
// Cleanup Quill when modal closes
if (!showEditModal && quillRef.current) {
quillRef.current = null;
}
}, [showEditModal, editContent]);
return (
<>
<div id="notify" className={notify.show ? `show ${notify.type}` : ''}>{notify.message}</div>
<div className="container">
<h1>Панель модератора</h1>
{!isLoggedIn ? (
<div id="login-section">
<h2>Вход для модератора</h2>
<form id="moder-login-form" onSubmit={handleLogin}>
<div className="form-group">
<label htmlFor="username">Логин:</label>
<input type="text" id="username" name="username" required value={username} onChange={(e) => setUsername(e.target.value)} />
</div>
<div className="form-group">
<label htmlFor="password">Пароль:</label>
<input type="password" id="password" name="password" required value={password} onChange={(e) => setPassword(e.target.value)} />
</div>
<button type="submit">Войти</button>
{loginError && <p id="login-error" style={{ color: 'red' }}>{loginError}</p>}
</form>
</div>
) : (
<div id="editor-section">
<button id="logout-btn" style={{ float: 'right' }} onClick={handleLogout}>Выйти из системы</button>
<h2>Редактирование вкладок</h2>
<div className="tab-controls">
<button className={`tab-btn ${activeTab === 'info' ? 'active' : ''}`} onClick={() => handleTabChange('info')} data-tab="info">Общая информация</button>
<button className={`tab-btn ${activeTab === 'union' ? 'active' : ''}`} onClick={() => handleTabChange('union')} data-tab="union">Профсоюз</button>
</div>
<div style={{ margin: '18px 0' }}>
<button id="add-block-btn" onClick={handleAddBlock}>Добавить блок</button>
</div>
<div id="blocks-list">
{blocks.length === 0 && <p>Нет блоков</p>}
{blocks.map(block => (
<div key={block.id} className="content-block">
<div className="block-preview" style={{ marginBottom: '10px' }} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(block.content) }} />
<button onClick={() => handleEditBlock(block)}>Редактировать</button>
<button onClick={() => handleDeleteBlock(block.id)}>Удалить</button>
</div>
))}
</div>
</div>
)}
{showEditModal && (
<div id="edit-modal" className="modal" style={{ display: 'block' }} onClick={(e) => {
if (e.target === e.currentTarget) handleTryCloseEditModal();
}}>
<div className="modal-content">
<span className="close" onClick={handleTryCloseEditModal}>&times;</span>
<h3 id="edit-modal-title">{editingBlockId ? 'Редактировать блок' : 'Добавить блок'}</h3>
<form id="edit-block-form" onSubmit={handleSaveBlock}>
<input type="hidden" id="edit-block-id" value={editingBlockId || ''} />
<div className="form-group">
<label htmlFor="edit-quill-editor">Холст:</label>
<div id="edit-quill-editor" ref={editorRef} style={{ height: '250px' }}></div>
</div>
<div id="edit-preview-label" style={{ marginTop: '10px', fontWeight: 'bold' }}>Предпросмотр:</div>
<div id="edit-block-preview" className="block-text" style={{ minHeight: '40px', marginBottom: '10px' }} dangerouslySetInnerHTML={{ __html: editPreviewContent }}></div>
<button type="submit">Сохранить</button>
</form>
</div>
</div>
)}
{showConfirmModal && (
<div id="confirm-modal" className="modal" style={{ display: 'block', zIndex: 2000 }}>
<div className="modal-content" style={{ maxWidth: '400px', textAlign: 'center' }}>
<p style={{ marginBottom: '20px' }}>Вы уверены, что хотите закрыть окно?<br />Несохранённые изменения будут потеряны!</p>
<button id="confirm-close-yes" style={{ background: '#e74c3c', marginRight: '12px' }} onClick={handleConfirmClose}>Да, закрыть</button>
<button id="confirm-close-no" onClick={handleCancelClose}>Отмена</button>
</div>
</div>
)}
</div>
</>
);
}
export default Editor;
@@ -1,789 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react';
import '../App.css';
// CSRF token management
let csrfToken = null;
async function fetchCsrfToken() {
try {
const res = await fetch('/moders/csrf-token', { credentials: 'include' });
if (res.ok) {
const data = await res.json();
csrfToken = data.csrfToken;
}
} catch (e) { csrfToken = null; }
}
async function apiFetch(url, options = {}) {
if (!options.headers) options.headers = {};
if (['POST', 'PUT', 'DELETE'].includes((options.method || 'GET').toUpperCase())) {
if (!csrfToken) await fetchCsrfToken();
options.headers['X-CSRF-Token'] = csrfToken;
}
options.credentials = 'include';
return fetch(url, options);
}
function Index() {
const [activeTab, setActiveTab] = useState('main');
const [serverStatus, setServerStatus] = useState('загрузка...');
const [serverStatusColor, setServerStatusColor] = useState('#333');
const [adminDbStatus, setAdminDbStatus] = useState('неизвестно');
const [adminDbMessage, setAdminDbMessage] = useState('');
const [adminDbColor, setAdminDbColor] = useState('#333');
const [backendDbStatus, setBackendDbStatus] = useState('неизвестно');
const [backendDbMessage, setBackendDbMessage] = useState('');
const [backendDbColor, setBackendDbColor] = useState('#333');
const [pingIp, setPingIp] = useState('');
const [pingOutput, setPingOutput] = useState('');
const [isPinging, setIsPinging] = useState(false);
const [appLogs, setAppLogs] = useState('Нажмите "Обновить логи" для просмотра');
const [cardLogs, setCardLogs] = useState('Нажмите "Обновить логи считывателя" для просмотра');
const [payslipsPath, setPayslipsPath] = useState('');
const [configStatus, setConfigStatus] = useState('');
const [configStatusColor, setConfigStatusColor] = useState('green');
// Server Params
const [adminPort, setAdminPort] = useState('');
const [adminUrl, setAdminUrl] = useState('');
const [backendHost, setBackendHost] = useState('');
const [backendPort, setBackendPort] = useState('');
const [serverParamsStatus, setServerParamsStatus] = useState('');
// MySQL Params
const [mysqlHost, setMysqlHost] = useState('');
const [mysqlPort, setMysqlPort] = useState('');
const [mysqlUser, setMysqlUser] = useState('');
const [mysqlPass, setMysqlPass] = useState('');
const [mysqlPassReadOnly, setMysqlPassReadOnly] = useState(true);
const [mysqlDb, setMysqlDb] = useState('');
const [mysqlStatus, setMysqlStatus] = useState('');
// CORS
const [corsOrigins, setCorsOrigins] = useState([]);
const [newCorsOrigin, setNewCorsOrigin] = useState('');
const [corsStatus, setCorsStatus] = useState('');
// Moders
const [moderUsername, setModerUsername] = useState('');
const [moderPassword, setModerPassword] = useState('');
const [addModerStatus, setAddModerStatus] = useState('');
const [moders, setModers] = useState([]);
// Monitoring
const [logLevelFilter, setLogLevelFilter] = useState('all');
const [logSourceFilter, setLogSourceFilter] = useState('all');
const [monitoringLogs, setMonitoringLogs] = useState([]);
const [monitoringInitialized, setMonitoringInitialized] = useState(false);
const [settingsInitialized, setSettingsInitialized] = useState(false);
// Toast
const [toasts, setToasts] = useState([]);
const showToast = useCallback((message, type = 'success') => {
const id = Date.now();
setToasts(prev => [...prev, { id, message, type }]);
setTimeout(() => {
setToasts(prev => prev.filter(t => t.id !== id));
}, 3000);
}, []);
const fetchServerStatus = useCallback(async () => {
try {
const response = await fetch('/api/status');
const data = await response.json();
if (data.status === 'online') {
setServerStatus('Онлайн');
setServerStatusColor('green');
} else {
setServerStatus('Оффлайн');
setServerStatusColor('red');
}
} catch (err) {
setServerStatus('Ошибка');
setServerStatusColor('orange');
}
}, []);
const fetchDbStatus = useCallback(async () => {
setAdminDbStatus('проверка...');
setBackendDbStatus('проверка...');
setAdminDbMessage('');
setBackendDbMessage('');
setAdminDbColor('#333');
setBackendDbColor('#333');
try {
const response = await fetch('/api/health/check-db');
const data = await response.json();
const renderStatus = (status, message) => {
return {
text: status === 'online' ? 'Онлайн' : 'Оффлайн',
color: status === 'online' ? 'green' : 'red',
message: message || ''
};
};
const admin = renderStatus(data.adminToDb.status, data.adminToDb.message);
setAdminDbStatus(admin.text);
setAdminDbColor(admin.color);
setAdminDbMessage(admin.message);
const backend = renderStatus(data.backendToDb.status, data.backendToDb.message);
setBackendDbStatus(backend.text);
setBackendDbColor(backend.color);
setBackendDbMessage(backend.message);
} catch (err) {
setAdminDbStatus('Ошибка');
setAdminDbColor('orange');
setBackendDbStatus('Ошибка');
setBackendDbColor('orange');
}
}, []);
const fetchConfig = useCallback(async () => {
try {
const response = await fetch('/api/config');
const config = await response.json();
setPayslipsPath(config.payslipsPath || '');
} catch (err) {
setConfigStatus('Не удалось загрузить конфигурацию.');
setConfigStatusColor('red');
}
}, []);
const fetchLogs = useCallback(async () => {
setAppLogs('загрузка...');
try {
const response = await fetch('/api/logs');
if (!response.ok) throw new Error('Не удалось загрузить логи');
const data = await response.text();
setAppLogs(data || 'Лог-файл пуст.');
} catch (err) {
setAppLogs(`Ошибка при загрузке логов: ${err.message}`);
}
}, []);
const fetchCardLogs = useCallback(async () => {
setCardLogs('загрузка...');
try {
const response = await fetch('/api/card/card-logs');
if (!response.ok) throw new Error('Не удалось загрузить логи считывателя');
const data = await response.text();
setCardLogs(data || 'Лог-файл пуст.');
} catch (err) {
setCardLogs(`Ошибка при загрузке логов: ${err.message}`);
}
}, []);
const clearLogs = useCallback(async () => {
if (!window.confirm('Это действие удалит лог-файл полностью и навсегда. Вы уверены?')) return;
try {
const response = await fetch('/api/logs/clear', { method: 'POST' });
if (response.ok) {
fetchLogs();
} else {
alert('Не удалось очистить логи.');
}
} catch (err) {
alert('Произошла ошибка при очистке логов.');
}
}, [fetchLogs]);
const handlePing = useCallback(() => {
const ip = pingIp.trim();
if (!ip) {
setPingOutput('Пожалуйста, введите IP-адрес.');
return;
}
setIsPinging(true);
setPingOutput('');
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
const ws = new WebSocket(`${wsProtocol}://${window.location.host}`);
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'ping', ip }));
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
setPingOutput(prev => prev + message.data);
};
ws.onerror = () => {
setPingOutput(prev => prev + '\nОшибка WebSocket соединения.');
};
ws.onclose = () => {
setIsPinging(false);
};
}, [pingIp]);
const handleBrowse = useCallback(async () => {
try {
const response = await fetch('/api/browse');
if (response.ok) {
const data = await response.json();
setPayslipsPath(data.path);
}
} catch (error) {
alert('Не удалось открыть диалог выбора папки.');
}
}, []);
const handleConfigSubmit = useCallback(async (e) => {
e.preventDefault();
try {
const response = await apiFetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ payslipsPath })
});
const data = await response.json();
setConfigStatus('Конфигурация успешно сохранена!');
setConfigStatusColor('green');
showToast('Конфигурация сохранена', 'success');
setTimeout(() => setConfigStatus(''), 3000);
} catch (err) {
setConfigStatus('Ошибка при сохранении.');
setConfigStatusColor('red');
showToast('Ошибка сохранения', 'error');
}
}, [payslipsPath, showToast]);
// Settings tab initialization
const initSettingsTab = useCallback(async () => {
if (settingsInitialized) return;
setSettingsInitialized(true);
// Load server params
try {
const results = await Promise.allSettled([
apiFetch('/api/settings/admin-server'),
apiFetch('/api/settings/backend-server')
]);
if (results[0].status === 'fulfilled') {
const adminData = await results[0].value.json();
setAdminPort(adminData.PORT || '');
setAdminUrl(adminData.ADMIN_URL || '');
}
if (results[1].status === 'fulfilled') {
const backendData = await results[1].value.json();
setBackendHost(backendData.host || '');
setBackendPort(backendData.port || '');
}
} catch (e) {
setServerParamsStatus('Критическая ошибка загрузки параметров');
}
// Load MySQL params
try {
const res = await apiFetch('/api/settings/mysql');
const data = await res.json();
setMysqlHost(data.MYSQL_HOST || '');
setMysqlPort(data.MYSQL_PORT || '');
setMysqlUser(data.MYSQL_USER || '');
setMysqlPass(data.MYSQL_PASSWORD ? '********' : '');
setMysqlPassReadOnly(true);
setMysqlDb(data.MYSQL_DATABASE || '');
} catch (e) {
setMysqlStatus('Ошибка загрузки');
}
// Load CORS
try {
const res = await apiFetch('/api/settings/cors');
const data = await res.json();
setCorsOrigins(Array.isArray(data.ALLOWED_ORIGINS) ? data.ALLOWED_ORIGINS : []);
} catch (e) {
// ignore
}
// Load Moders
try {
const res = await apiFetch('/api/moders');
const modersData = await res.json();
setModers(Array.isArray(modersData) ? modersData : []);
} catch (e) {
// ignore
}
}, [settingsInitialized]);
// Monitoring tab initialization
const initMonitoringTab = useCallback(async () => {
if (monitoringInitialized) return;
setMonitoringInitialized(true);
fetchMonitoringLogs();
}, [monitoringInitialized]);
const fetchMonitoringLogs = useCallback(async () => {
try {
const res = await apiFetch('/api/monitoring/logs');
if (!res.ok) throw new Error('Failed to fetch logs');
const data = await res.json();
setMonitoringLogs(data);
} catch (e) {
console.error('Error loading monitoring logs:', e);
}
}, []);
useEffect(() => {
fetchServerStatus();
fetchDbStatus();
fetchConfig();
}, [fetchServerStatus, fetchDbStatus, fetchConfig]);
useEffect(() => {
if (activeTab === 'settings') {
initSettingsTab();
} else if (activeTab === 'monitoring') {
initMonitoringTab();
}
}, [activeTab, initSettingsTab, initMonitoringTab]);
const handleServerParamsSubmit = useCallback(async (e) => {
e.preventDefault();
setServerParamsStatus('');
try {
const [adminRes, backendRes] = await Promise.all([
apiFetch('/api/settings/admin-server', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ PORT: adminPort, ADMIN_URL: adminUrl })
}),
apiFetch('/api/settings/backend-server', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ host: backendHost, port: backendPort })
})
]);
if (adminRes.ok && backendRes.ok) {
setServerParamsStatus('Параметры успешно сохранены. Требуется перезапуск обоих серверов для применения!');
showToast('Параметры успешно сохранены', 'success');
} else {
setServerParamsStatus('Ошибка сохранения');
showToast('Ошибка сохранения', 'error');
}
} catch (e) {
setServerParamsStatus('Ошибка сохранения');
showToast('Ошибка сохранения', 'error');
}
}, [adminPort, adminUrl, backendHost, backendPort, showToast]);
const handleMysqlSubmit = useCallback(async (e) => {
e.preventDefault();
const payload = {
MYSQL_HOST: mysqlHost,
MYSQL_PORT: mysqlPort,
MYSQL_USER: mysqlUser,
MYSQL_DATABASE: mysqlDb
};
if (!mysqlPassReadOnly) {
payload.MYSQL_PASSWORD = mysqlPass;
}
setMysqlStatus('');
try {
const res = await apiFetch('/api/settings/mysql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (res.ok) {
setMysqlStatus('Сохранено. Требуется перезапуск Admin-server для применения!');
showToast('Сохранено', 'success');
// Reload MySQL params
const reloadRes = await apiFetch('/api/settings/mysql');
const data = await reloadRes.json();
setMysqlHost(data.MYSQL_HOST || '');
setMysqlPort(data.MYSQL_PORT || '');
setMysqlUser(data.MYSQL_USER || '');
setMysqlPass(data.MYSQL_PASSWORD ? '********' : '');
setMysqlPassReadOnly(true);
setMysqlDb(data.MYSQL_DATABASE || '');
} else {
setMysqlStatus('Ошибка сохранения');
showToast('Ошибка сохранения', 'error');
}
} catch (e) {
setMysqlStatus('Ошибка сохранения');
showToast('Ошибка сохранения', 'error');
}
}, [mysqlHost, mysqlPort, mysqlUser, mysqlPass, mysqlPassReadOnly, mysqlDb, showToast]);
const handleCorsAdd = useCallback(() => {
if (newCorsOrigin && !corsOrigins.includes(newCorsOrigin)) {
setCorsOrigins([...corsOrigins, newCorsOrigin]);
setNewCorsOrigin('');
}
}, [newCorsOrigin, corsOrigins]);
const handleCorsRemove = useCallback((originToRemove) => {
setCorsOrigins(corsOrigins.filter(origin => origin !== originToRemove));
}, []);
const handleCorsSubmit = useCallback(async (e) => {
e.preventDefault();
setCorsStatus('');
try {
const res = await apiFetch('/api/settings/cors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ALLOWED_ORIGINS: corsOrigins })
});
if (res.ok) {
setCorsStatus('Сохранено. Требуется перезапуск Admin-server для применения!');
showToast('Сохранено', 'success');
} else {
setCorsStatus('Ошибка сохранения');
showToast('Ошибка сохранения', 'error');
}
} catch (e) {
setCorsStatus('Ошибка сохранения');
showToast('Ошибка сохранения', 'error');
}
}, [corsOrigins, showToast]);
const handleAddModer = useCallback(async (e) => {
e.preventDefault();
setAddModerStatus('');
if (!moderUsername || !moderPassword) {
setAddModerStatus('Заполните все поля');
return;
}
try {
const res = await apiFetch('/api/moders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: moderUsername, password: moderPassword })
});
if (res.ok) {
setAddModerStatus('Модератор добавлен');
setModerUsername('');
setModerPassword('');
showToast('Модератор добавлен', 'success');
// Reload moders
const modersRes = await apiFetch('/api/moders');
const modersData = await modersRes.json();
setModers(Array.isArray(modersData) ? modersData : []);
} else {
const data = await res.json();
setAddModerStatus(data.message || 'Ошибка');
showToast(data.message || 'Ошибка', 'error');
}
} catch (e) {
setAddModerStatus('Ошибка');
showToast('Ошибка', 'error');
}
}, [moderUsername, moderPassword, showToast]);
const handleDeleteModer = useCallback(async (id) => {
try {
const res = await apiFetch(`/api/moders/${id}`, { method: 'DELETE' });
if (res.ok) {
setModers(moders.filter(moder => moder.id !== id));
showToast('Модератор удален', 'success');
} else {
showToast('Ошибка удаления', 'error');
}
} catch (e) {
showToast('Ошибка удаления', 'error');
}
}, [moders, showToast]);
const handleResetModerPass = useCallback(async (id, newPass) => {
if (!newPass || newPass.length < 4) {
showToast('Пароль должен быть не менее 4 символов', 'error');
return;
}
try {
const res = await apiFetch(`/api/moders/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: newPass })
});
if (res.ok) {
showToast('Пароль обновлён', 'success');
return true;
} else {
showToast('Ошибка смены пароля', 'error');
return false;
}
} catch (e) {
showToast('Ошибка смены пароля', 'error');
return false;
}
}, [showToast]);
const getLogLevelClass = (level) => {
if (level === 'ERROR') return 'log-level-error';
if (level === 'WARN') return 'log-level-warn';
return '';
};
const filteredMonitoringLogs = monitoringLogs
.filter(log => (logLevelFilter === 'all' || log.level === logLevelFilter))
.filter(log => (logSourceFilter === 'all' || log.source === logSourceFilter));
return (
<div className="container">
{/* Toast container */}
<div id="toast-container">
{toasts.map(toast => (
<div key={toast.id} className={`toast toast-${toast.type}`}>
{toast.message}
</div>
))}
</div>
<h1>Панель администратора</h1>
<div className="tabs">
<button id="tab-main" className={`tab-btn ${activeTab === 'main' ? 'active' : ''}`} onClick={() => setActiveTab('main')}>Общее</button>
<button id="tab-settings" className={`tab-btn ${activeTab === 'settings' ? 'active' : ''}`} onClick={() => setActiveTab('settings')}>Настройки</button>
<button id="tab-monitoring" className={`tab-btn ${activeTab === 'monitoring' ? 'active' : ''}`} onClick={() => setActiveTab('monitoring')}>Мониторинг</button>
</div>
{activeTab === 'main' && (
<div id="main-tab">
<div id="status">
<p>Статус сервера: <span id="server-status" style={{ color: serverStatusColor }}>{serverStatus}</span></p>
<button id="refresh-status-btn" className="btn-small" onClick={fetchServerStatus}>Обновить</button>
</div>
<div id="db-status-container">
<div>
<p>Админ БД: <span id="admin-db-status" style={{ color: adminDbColor }}>{adminDbStatus}</span></p>
<small id="admin-db-message">{adminDbMessage}</small>
</div>
<div>
<p>Программа БД: <span id="backend-db-status" style={{ color: backendDbColor }}>{backendDbStatus}</span></p>
<small id="backend-db-message">{backendDbMessage}</small>
</div>
<button id="check-db-btn" className="btn-small" onClick={fetchDbStatus}>Проверить</button>
</div>
<div className="network-diag-container">
<h2>Сетевая диагностика (Ping)</h2>
<div className="ping-controls">
<input type="text" id="ping-ip" placeholder="Введите IP-адрес или хост" value={pingIp} onChange={(e) => setPingIp(e.target.value)} />
<button id="ping-btn" className="btn-small" onClick={handlePing} disabled={isPinging}>Пинг</button>
<button id="ping-clear-btn" className="btn-small" onClick={() => setPingOutput('')}>Очистить</button>
</div>
<pre id="ping-output">{pingOutput}</pre>
</div>
<div className="logs-container">
<h2>Логи приложения</h2>
<div className="log-controls">
<button id="refresh-logs-btn" className="btn-small" onClick={fetchLogs}>Обновить логи</button>
<button id="clear-logs-btn" className="btn-small" onClick={clearLogs}>Очистить</button>
</div>
<pre id="logs-content">{appLogs}</pre>
</div>
<div className="logs-container">
<h2>Логи считывателя</h2>
<div className="log-controls">
<button id="refresh-card-logs-btn" className="btn-small" onClick={fetchCardLogs}>Обновить логи считывателя</button>
</div>
<pre id="card-logs-content">{cardLogs}</pre>
</div>
<div className="config-container">
<h2>Конфигурация</h2>
<form id="config-form" onSubmit={handleConfigSubmit}>
<div className="form-group">
<label htmlFor="payslipsPath">Путь к расчетным листкам:</label>
<div className="input-with-button">
<input type="text" id="payslipsPath" name="payslipsPath" required value={payslipsPath} onChange={(e) => setPayslipsPath(e.target.value)} />
<button type="button" id="browse-btn" onClick={handleBrowse}>Обзор...</button>
</div>
</div>
<button type="submit">Сохранить</button>
</form>
<p id="config-status" style={{ color: configStatusColor }}>{configStatus}</p>
</div>
</div>
)}
{activeTab === 'settings' && (
<div id="settings-tab">
<div id="server-params-section">
<h3>Параметры серверов</h3>
<form id="server-params-form" onSubmit={handleServerParamsSubmit} noValidate>
<fieldset>
<legend>Admin-server</legend>
<label>PORT: <input type="number" id="admin-port" min="1" max="65535" required value={adminPort} onChange={(e) => setAdminPort(e.target.value)} /></label><br />
<label>ADMIN_URL: <input type="text" id="admin-url" pattern="https?://.+" required value={adminUrl} onChange={(e) => setAdminUrl(e.target.value)} /></label>
</fieldset>
<fieldset>
<legend>Backend</legend>
<label>host: <input type="text" id="backend-host" required value={backendHost} onChange={(e) => setBackendHost(e.target.value)} /></label><br />
<label>port: <input type="number" id="backend-port" min="1" max="65535" required value={backendPort} onChange={(e) => setBackendPort(e.target.value)} /></label>
</fieldset>
<button type="submit">Сохранить</button>
<span id="server-params-status" className="form-error">{serverParamsStatus}</span>
</form>
</div>
<div id="db-section">
<h3>Подключения к БД</h3>
<form id="mysql-form" onSubmit={handleMysqlSubmit} noValidate>
<fieldset>
<legend>MySQL (модераторы)</legend>
<label>MYSQL_HOST: <input type="text" id="mysql-host" required value={mysqlHost} onChange={(e) => setMysqlHost(e.target.value)} /></label><br />
<label>MYSQL_PORT: <input type="number" id="mysql-port" min="1" max="65535" required value={mysqlPort} onChange={(e) => setMysqlPort(e.target.value)} /></label><br />
<label>MYSQL_USER: <input type="text" id="mysql-user" required value={mysqlUser} onChange={(e) => setMysqlUser(e.target.value)} /></label><br />
<label>MYSQL_PASSWORD:
<div className="password-wrapper">
<input type="password" id="mysql-pass" readOnly={mysqlPassReadOnly} value={mysqlPassReadOnly ? '********' : mysqlPass} onChange={(e) => setMysqlPass(e.target.value)} />
{mysqlPassReadOnly && (
<button type="button" className="btn-small" onClick={() => { setMysqlPassReadOnly(false); setMysqlPass(''); }}>Изменить</button>
)}
</div>
</label><br />
<label>MYSQL_DATABASE: <input type="text" id="mysql-db" required value={mysqlDb} onChange={(e) => setMysqlDb(e.target.value)} /></label><br />
<button type="submit">Сохранить</button>
<span id="mysql-status" className="form-error">{mysqlStatus}</span>
</fieldset>
</form>
</div>
<div id="cors-section">
<h3>CORS (ALLOWED_ORIGINS)</h3>
<form id="cors-form" onSubmit={handleCorsSubmit}>
<ul id="cors-list">
{corsOrigins.length === 0 && <li>Нет origin</li>}
{corsOrigins.map((origin, index) => (
<li key={index}>
{origin}{' '}
<button type="button" className="btn-small" onClick={() => handleCorsRemove(origin)}>Удалить</button>
</li>
))}
</ul>
<input type="text" id="cors-new-origin" placeholder="Добавить origin..." value={newCorsOrigin} onChange={(e) => setNewCorsOrigin(e.target.value)} />
<button type="button" id="cors-add-btn" onClick={handleCorsAdd} disabled={!newCorsOrigin || corsOrigins.includes(newCorsOrigin)}>Добавить</button>
<button type="submit">Сохранить</button>
<span id="cors-status" className="form-error">{corsStatus}</span>
</form>
</div>
<div id="moders-section">
<h3>Модераторы</h3>
<form id="add-moder-form" onSubmit={handleAddModer} noValidate>
<input type="text" id="add-moder-username" placeholder="Логин" required value={moderUsername} onChange={(e) => setModerUsername(e.target.value)} />
<input type="password" id="add-moder-password" placeholder="Пароль (мин. 4 символа)" minLength="4" required value={moderPassword} onChange={(e) => setModerPassword(e.target.value)} />
<button type="submit">Добавить</button>
<span id="add-moder-status" className="form-error">{addModerStatus}</span>
</form>
<table id="moders-table" className="moders-table">
<thead>
<tr><th>ID</th><th>Логин</th><th>Действия</th></tr>
</thead>
<tbody>
{moders.length === 0 && (
<tr><td colSpan="3">Нет модераторов</td></tr>
)}
{moders.map(moder => (
<ModerRow
key={moder.id}
moder={moder}
onDelete={handleDeleteModer}
onResetPass={handleResetModerPass}
/>
))}
</tbody>
</table>
</div>
</div>
)}
{activeTab === 'monitoring' && (
<div id="monitoring-tab">
<h2>Мониторинг системы</h2>
<div id="monitoring-status-container">
{/* Статусы будут здесь */}
</div>
<h3>Проблемные события (Warn/Error)</h3>
<div className="monitoring-filters">
<label>Уровень:
<select id="log-level-filter" value={logLevelFilter} onChange={(e) => setLogLevelFilter(e.target.value)}>
<option value="all">Все</option>
<option value="ERROR">Error</option>
<option value="WARN">Warn</option>
</select>
</label>
<label>Источник:
<select id="log-source-filter" value={logSourceFilter} onChange={(e) => setLogSourceFilter(e.target.value)}>
<option value="all">Все</option>
<option value="Admin">Admin</option>
<option value="Backend">Backend</option>
<option value="CardReader">CardReader</option>
</select>
</label>
<button id="refresh-monitoring-btn" className="btn-small" onClick={fetchMonitoringLogs}>Обновить</button>
</div>
<table id="monitoring-log-table" className="moders-table">
<thead>
<tr>
<th style={{ width: '20%' }}>Время</th>
<th style={{ width: '10%' }}>Уровень</th>
<th style={{ width: '15%' }}>Источник</th>
<th>Сообщение</th>
</tr>
</thead>
<tbody>
{filteredMonitoringLogs.length === 0 && (
<tr><td colSpan="4">Нет событий для отображения.</td></tr>
)}
{filteredMonitoringLogs.map((log, index) => (
<tr key={index} className={getLogLevelClass(log.level)}>
<td>{new Date(log.timestamp).toLocaleString()}</td>
<td><span className="log-level-label">{log.level}</span></td>
<td>{log.source}</td>
<td>{log.message}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}
// Moderator row component with inline password reset
function ModerRow({ moder, onDelete, onResetPass }) {
const [isResetting, setIsResetting] = useState(false);
const [newPass, setNewPass] = useState('');
const handleSavePass = async () => {
const success = await onResetPass(moder.id, newPass);
if (success) {
setIsResetting(false);
setNewPass('');
}
};
if (isResetting) {
return (
<tr>
<td>{moder.id}</td>
<td>{moder.username}</td>
<td>
<input type="password" placeholder="Новый пароль" className="moder-pass-input" value={newPass} onChange={(e) => setNewPass(e.target.value)} />
<button className="btn-small" onClick={handleSavePass}>✅</button>
<button className="btn-small" onClick={() => { setIsResetting(false); setNewPass(''); }}>❌</button>
</td>
</tr>
);
}
return (
<tr>
<td>{moder.id}</td>
<td>{moder.username}</td>
<td>
<button className="btn-small" onClick={() => setIsResetting(true)} title="Сменить пароль">🔑</button>{' '}
<button className="btn-small" onClick={() => {
if (window.confirm('Удалить модератора?')) onDelete(moder.id);
}} title="Удалить">🗑️</button>
</td>
</tr>
);
}
export default Index;
@@ -1,46 +0,0 @@
import React, { useState } from 'react';
import '../App.css'; // Assuming you move the CSS here
function Login() {
const [password, setPassword] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
const response = await fetch('/api/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password })
});
if (response.ok) {
window.location.href = '/';
} else {
setErrorMessage('Неверный пароль');
}
};
return (
<div className="container">
<h1>Вход</h1>
<form id="login-form" onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="password">Пароль:</label>
<input
type="password"
id="password"
name="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<button type="submit">Войти</button>
</form>
{errorMessage && <p id="error-message" style={{ color: 'red' }}>{errorMessage}</p>}
</div>
);
}
export default Login;
@@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
-17
View File
@@ -1,17 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

@@ -1,13 +0,0 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
@@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
+1 -1
View File
@@ -1,5 +1,5 @@
const logger = require('../logger');
const bcrypt = require('bcrypt');
const bcrypt = require('bcryptjs');
const moderDb = require('../moders/db');
require('dotenv').config({ path: __dirname + './config.env' });
exports.login = (req, res) => {
+11 -6
View File
@@ -1,12 +1,15 @@
const fs = require('fs-extra');
const fs = require('fs').promises;
const { existsSync, appendFileSync } = require('fs');
const path = require('path');
const logger = require('../logger');
const CARD_LOG_PATH = path.resolve(__dirname, '..', process.env.CARD_LOG_PATH);
const initCardLogs = async () => {
try {
await fs.ensureDir(path.dirname(CARD_LOG_PATH));
if (!(await fs.pathExists(CARD_LOG_PATH))) {
await fs.mkdir(path.dirname(CARD_LOG_PATH), { recursive: true });
try {
await fs.access(CARD_LOG_PATH);
} catch (e) {
await fs.writeFile(CARD_LOG_PATH, '');
logger.info(`Card log file created at: ${CARD_LOG_PATH}`);
}
@@ -31,10 +34,10 @@ const cardEvent = (req, res) => {
const logLine = `[${new Date(timestamp).toISOString()}] [${event}] UID: ${uid}\n`;
try {
if (!fs.existsSync(CARD_LOG_PATH)) {
if (!existsSync(CARD_LOG_PATH)) {
initCardLogs();
}
fs.appendFileSync(CARD_LOG_PATH, logLine);
appendFileSync(CARD_LOG_PATH, logLine);
logger.info('[CARD EVENT] Log written to:', CARD_LOG_PATH);
return res.status(200).json({ message: 'OK', path: CARD_LOG_PATH });
@@ -49,7 +52,9 @@ const cardEvent = (req, res) => {
};
const getCardLogs = async (req, res) => {
try {
if (!(await fs.pathExists(CARD_LOG_PATH))) {
try {
await fs.access(CARD_LOG_PATH);
} catch (e) {
await initCardLogs();
return res.status(200).type('text/plain').send('Card logs initialized');
}
+4 -2
View File
@@ -1,4 +1,4 @@
const fs = require('fs-extra');
const fs = require('fs').promises;
const path = require('path');
const logger = require('../logger');
@@ -40,7 +40,9 @@ const getMonitoringLogs = async (req, res) => {
for (const source of logSources) {
try {
if (!(await fs.pathExists(source.path))) {
try {
await fs.access(source.path);
} catch (e) {
logger.warn(`Log file not found: ${source.path}`);
continue;
}
@@ -1,5 +1,5 @@
const moderDb = require('../moders/db');
const bcrypt = require('bcrypt');
const bcrypt = require('bcryptjs');
const logger = require('../logger');
const getModers = async (req, res) => {
@@ -1,4 +1,4 @@
const fs = require('fs-extra');
const fs = require('fs').promises;
const path = require('path');
const logger = require('../logger');
@@ -131,7 +131,8 @@ const updateAdminServerSettings = async (req, res) => {
const getBackendServerSettings = async (req, res) => {
try {
const config = await fs.readJson(backendConfigPath);
const content = await fs.readFile(backendConfigPath, 'utf8');
const config = JSON.parse(content);
res.json({
host: config.server?.host || '',
port: config.server?.port || ''
@@ -148,11 +149,12 @@ const updateBackendServerSettings = async (req, res) => {
return res.status(400).json({ message: 'host и port обязательны' });
}
try {
const config = await fs.readJson(backendConfigPath);
const content = await fs.readFile(backendConfigPath, 'utf8');
const config = JSON.parse(content);
config.server = config.server || {};
config.server.host = host;
config.server.port = port;
await fs.writeJson(backendConfigPath, config, { spaces: 4 });
await fs.writeFile(backendConfigPath, JSON.stringify(config, null, 4));
res.json({ message: 'Параметры backend-server обновлены' });
} catch (e) {
logger.error('Ошибка записи backend/config.json:', e);
+72
View File
@@ -1516,3 +1516,75 @@
[2026-08-08T13:59:07.971Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content
[2026-08-08T13:59:07.985Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL
[2026-08-08T13:59:08.030Z] [ERROR] [DB CHECK] ❌ Ошибка связи с бэкендом: connect ECONNREFUSED 127.0.0.1:3001; connect ECONNREFUSED ::1:3001
[2026-08-09T13:26:44.709Z] [INFO] Server running on http://localhost:3005
[2026-08-09T13:26:44.713Z] [INFO] Card logs initialized successfully
[2026-08-09T13:27:10.816Z] [INFO] Login successful
[2026-08-09T13:27:11.076Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content
[2026-08-09T13:27:11.087Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL
[2026-08-09T17:21:30.353Z] [INFO] Server running on http://localhost:3005
[2026-08-09T17:21:30.356Z] [INFO] Card logs initialized successfully
[2026-08-09T17:21:33.826Z] [ERROR] Error: ENOENT: no such file or directory, stat 'D:\rasch\admin-server1\admin-frontend\build\index.html'
[2026-08-09T17:21:34.012Z] [ERROR] Error: ENOENT: no such file or directory, stat 'D:\rasch\admin-server1\admin-frontend\build\index.html'
[2026-08-09T17:23:37.715Z] [INFO] Server running on http://localhost:3005
[2026-08-09T17:23:37.718Z] [INFO] Card logs initialized successfully
[2026-08-09T17:23:40.570Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content
[2026-08-09T17:23:40.622Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL
[2026-08-09T17:23:40.637Z] [ERROR] [DB CHECK] ❌ Ошибка связи с бэкендом: connect ECONNREFUSED 127.0.0.1:3001; connect ECONNREFUSED ::1:3001
[2026-08-09T17:24:23.770Z] [INFO] Server running on http://localhost:3005
[2026-08-09T17:24:23.773Z] [INFO] Card logs initialized successfully
[2026-08-09T17:24:32.276Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content
[2026-08-09T17:24:32.363Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL
[2026-08-09T17:24:32.430Z] [ERROR] [DB CHECK] ❌ Ошибка связи с бэкендом: connect ECONNREFUSED 127.0.0.1:3001; connect ECONNREFUSED ::1:3001
[2026-08-09T17:24:41.643Z] [INFO] Login successful
[2026-08-09T17:24:41.947Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content
[2026-08-09T17:24:41.958Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL
[2026-08-09T17:24:41.963Z] [ERROR] [DB CHECK] ❌ Ошибка связи с бэкендом: connect ECONNREFUSED 127.0.0.1:3001; connect ECONNREFUSED ::1:3001
[2026-08-09T17:25:39.071Z] [INFO] [LOGIN] Попытка входа: artur
[2026-08-09T17:25:39.071Z] [INFO] [LOGIN] Проверка подключения к базе...
[2026-08-09T17:25:39.073Z] [INFO] [LOGIN] Результат поиска пользователя: найден
[2026-08-09T17:25:39.074Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
[2026-08-09T17:25:39.148Z] [INFO] [LOGIN] Результат сравнения пароля: true
[2026-08-09T17:25:39.149Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":2,"username":"artur"}
[2026-08-09T17:25:39.150Z] [INFO] [LOGIN] Ответ отправлен
[2026-08-09T17:28:17.937Z] [INFO] Server running on http://localhost:3005
[2026-08-09T17:28:17.941Z] [INFO] Card logs initialized successfully
[2026-08-09T17:30:11.485Z] [INFO] Server running on http://localhost:3005
[2026-08-09T17:30:11.489Z] [INFO] Card logs initialized successfully
[2026-08-09T17:30:35.385Z] [INFO] [LOGIN] Попытка входа: artur
[2026-08-09T17:30:35.386Z] [INFO] [LOGIN] Проверка подключения к базе...
[2026-08-09T17:30:35.402Z] [INFO] [LOGIN] Результат поиска пользователя: найден
[2026-08-09T17:30:35.403Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
[2026-08-09T17:30:35.488Z] [INFO] [LOGIN] Результат сравнения пароля: true
[2026-08-09T17:30:35.489Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":2,"username":"artur"}
[2026-08-09T17:30:35.491Z] [INFO] [LOGIN] Ответ отправлен
[2026-08-09T17:40:26.650Z] [INFO] Server running on http://localhost:3005
[2026-08-09T17:40:26.653Z] [INFO] Card logs initialized successfully
[2026-08-09T17:40:41.435Z] [INFO] [LOGIN] Попытка входа: vova
[2026-08-09T17:40:41.436Z] [INFO] [LOGIN] Проверка подключения к базе...
[2026-08-09T17:40:41.440Z] [INFO] [LOGIN] Результат поиска пользователя: найден
[2026-08-09T17:40:41.441Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
[2026-08-09T17:40:41.531Z] [INFO] [LOGIN] Результат сравнения пароля: true
[2026-08-09T17:40:41.532Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
[2026-08-09T17:40:41.533Z] [INFO] [LOGIN] Ответ отправлен
[2026-08-09T17:40:50.893Z] [INFO] [LOGIN] Попытка входа: aa
[2026-08-09T17:40:50.894Z] [INFO] [LOGIN] Проверка подключения к базе...
[2026-08-09T17:40:50.896Z] [INFO] [LOGIN] Результат поиска пользователя: не найден
[2026-08-09T17:40:50.896Z] [WARN] [LOGIN] Пользователь не найден: aa
[2026-08-09T17:52:33.265Z] [INFO] Server running on http://localhost:3005
[2026-08-09T17:52:33.269Z] [INFO] Card logs initialized successfully
[2026-08-09T17:52:43.287Z] [INFO] [LOGIN] Попытка входа: vova
[2026-08-09T17:52:43.288Z] [INFO] [LOGIN] Проверка подключения к базе...
[2026-08-09T17:52:43.307Z] [INFO] [LOGIN] Результат поиска пользователя: найден
[2026-08-09T17:52:43.308Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
[2026-08-09T17:52:43.403Z] [INFO] [LOGIN] Результат сравнения пароля: true
[2026-08-09T17:52:43.404Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
[2026-08-09T17:52:43.406Z] [INFO] [LOGIN] Ответ отправлен
[2026-08-09T17:56:53.147Z] [INFO] Server running on http://localhost:3005
[2026-08-09T17:56:53.150Z] [INFO] Card logs initialized successfully
[2026-08-09T17:57:08.357Z] [INFO] [LOGIN] Попытка входа: vova
[2026-08-09T17:57:08.358Z] [INFO] [LOGIN] Проверка подключения к базе...
[2026-08-09T17:57:08.361Z] [INFO] [LOGIN] Результат поиска пользователя: найден
[2026-08-09T17:57:08.361Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
[2026-08-09T17:57:08.445Z] [INFO] [LOGIN] Результат сравнения пароля: true
[2026-08-09T17:57:08.446Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
[2026-08-09T17:57:08.447Z] [INFO] [LOGIN] Ответ отправлен
-17
View File
@@ -1,17 +0,0 @@
// middleware/csrf.js
const csurf = require('csurf');
const csrfProtection = csurf({ cookie: false });
const csrfMiddleware = (req, res, next) => {
if (
req.path === '/login' ||
req.path === '/moders/moder-login' ||
req.path === '/moders/csrf-token'
) {
return next();
}
return csrfProtection(req, res, next);
};
module.exports = csrfMiddleware;
+2 -9
View File
@@ -1,12 +1,9 @@
const express = require('express');
const bcrypt = require('bcrypt');
const bcrypt = require('bcryptjs');
const db = require('../db');
const router = express.Router();
const logger = require('../../logger');
const rateLimit = require('express-rate-limit');
const csurf = require('csurf');
const csrfProtection = csurf({ cookie: false });
const loginLimiter = rateLimit({
windowMs: 30 * 60 * 1000, // 30 минут
@@ -16,10 +13,6 @@ const loginLimiter = rateLimit({
legacyHeaders: false
});
router.get('/csrf-token', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
router.post('/moder-login', loginLimiter, async (req, res) => {
const { username, password } = req.body;
logger.info(`[LOGIN] Попытка входа: ${username}`);
@@ -78,4 +71,4 @@ router.get('/check-session', (req, res) => {
}
});
module.exports = router;
module.exports = router;
+48 -3685
View File
File diff suppressed because it is too large Load Diff
+2 -7
View File
@@ -15,24 +15,19 @@
"description": "",
"dependencies": {
"axios": "^1.10.0",
"bcrypt": "^6.0.0",
"bcryptjs": "^3.0.3",
"cors": "^2.8.5",
"csurf": "^1.11.0",
"dompurify": "^3.2.6",
"dotenv": "^17.2.1",
"electron": "^37.0.0",
"express": "^5.1.0",
"express-mysql-session": "^3.0.3",
"express-rate-limit": "^8.0.1",
"express-session": "^1.18.1",
"fs-extra": "^11.3.0",
"helmet": "^8.3.0",
"morgan": "^1.10.0",
"multer": "^2.0.1",
"mysql2": "^3.14.2",
"quill": "^2.0.3",
"ws": "^8.18.3"
},
"devDependencies": {
"electron-builder": "^26.15.3"
}
}
-3
View File
@@ -1,9 +1,6 @@
const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
const csurf = require('csurf');
const csrfProtection = csurf({ cookie: false });
router.post('/', authController.login);
+11 -8
View File
@@ -1,5 +1,6 @@
const fs = require('fs-extra');
const fs = require('fs').promises;
const cors = require('cors');
const helmet = require('helmet');
const express = require('express');
const morgan = require('morgan');
const path = require('path');
@@ -26,15 +27,15 @@ const corsConfig = require('./config/cors');
const sessionConfig = require('./config/session');
// Middleware
app.use(helmet()); // Добавление helmet для защиты
app.use(cors(corsConfig));
app.use(morgan('dev'));
app.use(express.json());
app.use(session(sessionConfig(session)));
// Роуты, которые не требуют CSRF
// Роуты
app.use('/api/auth', authRouter);
app.use(require('./middleware/csrf'));
// Статические файлы
app.use(express.static(path.join(__dirname, 'admin-frontend', 'build')));
app.use(express.static(path.join(__dirname, '..','admin-frontend', 'build')));
app.use('/uploads', express.static(__dirname + '/uploads'));
// Роуты
@@ -64,7 +65,7 @@ app.get(/.*/, (req, res, next) => {
if (req.path.startsWith('/api/') || req.path.startsWith('/moders')) {
return next();
}
res.sendFile(path.join(__dirname, 'admin-frontend', 'build', 'index.html'));
res.sendFile(path.join(__dirname, '..','admin-frontend', 'build', 'index.html'));
});
// Обработчик ошибок (должен быть последним!)
@@ -80,7 +81,8 @@ app.get('/api/status', async (req, res) => {
app.get('/api/config', async (req, res) => {
try {
const backendConfigPath = path.join(__dirname, '../backend/config.json');
const backendConfig = await fs.readJson(backendConfigPath);
const content = await fs.readFile(backendConfigPath, 'utf8');
const backendConfig = JSON.parse(content);
const config = {
payslipsPath: backendConfig.payslipsPath
};
@@ -94,7 +96,8 @@ app.get('/api/config', async (req, res) => {
app.post('/api/config', async (req, res) => {
try {
const backendConfigPath = path.join(__dirname, '../backend/config.json');
const currentConfig = await fs.readJson(backendConfigPath);
const content = await fs.readFile(backendConfigPath, 'utf8');
const currentConfig = JSON.parse(content);
// Обновляем только разрешенные поля
if (req.body.payslipsPath) {
@@ -102,7 +105,7 @@ app.post('/api/config', async (req, res) => {
}
// Сохраняем обновленный конфиг
await fs.writeJson(backendConfigPath, currentConfig, { spaces: 4 });
await fs.writeFile(backendConfigPath, JSON.stringify(currentConfig, null, 4));
res.json({
success: true,