vite_deleteelectron
@@ -5,3 +5,4 @@
|
|||||||
/admin-server/node_modules
|
/admin-server/node_modules
|
||||||
/backend/logs
|
/backend/logs
|
||||||
/admin-server1/node_modules
|
/admin-server1/node_modules
|
||||||
|
/admin-frontend/node_modules
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# План оптимизации зависимостей
|
||||||
|
|
||||||
|
## Этап 1: Backend (наименее рискованный)
|
||||||
|
- [x] 1.1 Обновить Express до 5.1.0
|
||||||
|
- [x] 1.2 Заменить fs-extra на fs/promises в server.js, routes/employee.js, config/db.js
|
||||||
|
- [x] 1.3 Удалить winston-daily-rotate-file (не используется)
|
||||||
|
- [x] 1.4 Обновить dotenv до последней версии
|
||||||
|
|
||||||
|
## Этап 2: Чистка фронтенда от мусора
|
||||||
|
- [x] 2.1 Удалить встроенные модули Node.js: fs, http, https, os, path, stream, util, zlib
|
||||||
|
- [x] 2.2 Удалить mammoth и textract (не используются во frontend)
|
||||||
|
- [x] 2.3 Удалить полифиллы Node.js: buffer, crypto-browserify, https-browserify, os-browserify, path-browserify, process, querystring-es3, stream-browserify, stream-http, url
|
||||||
|
|
||||||
|
## Этап 3: Обновление React 18.3 → 19
|
||||||
|
- [x] 3.1 Обновить до React 18.3, исправить предупреждения
|
||||||
|
- [x] 3.2 Проверить совместимость зависимостей
|
||||||
|
- [x] 3.3 Обновить React до 19, react-dom до 19, @types/react и @types/react-dom до 19
|
||||||
|
- [x] 3.4 Обновить @testing-library/* до актуальных версий
|
||||||
|
|
||||||
|
## Этап 4: Обновление остальных пакетов
|
||||||
|
- [x] 4.1 Обновить react-pdf и pdfjs-dist до актуальных версий
|
||||||
|
- [x] 4.2 Обновить Electron ^21 → ^37
|
||||||
|
- [x] 4.3 Адаптировать PayslipViewer.tsx под новое API react-pdf
|
||||||
|
|
||||||
|
## Этап 5: Миграция на Vite (в последнюю очередь)
|
||||||
|
- [x] 5.1 Создать vite.config.ts
|
||||||
|
- [x] 5.2 Перенести index.html в корень, убрать CRA-теги
|
||||||
|
- [x] 5.3 Обновить tsconfig.json
|
||||||
|
- [x] 5.4 Обновить скрипты в package.json (vite, vitest)
|
||||||
|
- [x] 5.5 Удалить react-scripts
|
||||||
|
|
||||||
|
## Этап 6: Проверка
|
||||||
|
- [ ] 6.1 npm install для backend и frontend
|
||||||
|
- [ ] 6.2 npm run build для frontend
|
||||||
|
- [ ] 6.3 Запуск backend
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Admin Frontend</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -7,16 +7,15 @@
|
|||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"dompurify": "^3.4.13",
|
||||||
|
"quill": "^2.0.3",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8",
|
"react-dom": "^19.2.8"
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"web-vitals": "^2.1.4"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "vite",
|
||||||
"build": "react-scripts build",
|
"build": "vite build",
|
||||||
"test": "react-scripts test",
|
"preview": "vite preview"
|
||||||
"eject": "react-scripts eject"
|
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
@@ -35,5 +34,9 @@
|
|||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-react": "^6.0.5",
|
||||||
|
"vite": "^8.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
@@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import './index.css';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,17 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
base: './',
|
||||||
|
build: {
|
||||||
|
outDir: 'build',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3001,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.jsx', '.js', '.tsx', '.ts', '.json']
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
PORT=3005
|
|
||||||
DB_USER=prog
|
|
||||||
DB_PASSWORD=prog
|
|
||||||
DB_SERVER=192.168.31.34
|
|
||||||
DB_NAME=ASU
|
|
||||||
|
|
||||||
BACKEND_URL=http://localhost:3001
|
|
||||||
ADMIN_URL=http://localhost:3005
|
|
||||||
ADMIN_PASSWORD=2
|
|
||||||
SECRET_KEY=njdsn30423423fndjjfsn34u
|
|
||||||
LOGS_DIR=../backend/logs
|
|
||||||
CARD_LOG_PATH=../backend/logs/cards.log
|
|
||||||
ADMIN_LOG=./logs/admin.log
|
|
||||||
ADMIN_LOG_DIR=../logs
|
|
||||||
ALLOWED_ORIGINS=http://localhost:3005, http://localhost:3001
|
|
||||||
BACKEND_APP_LOG=../backend/logs/app.log
|
|
||||||
CARD_LOG=../backend/logs/cards.log
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
const allowedOrigins = process.env.ALLOWED_ORIGINS.split(',').map(origin => origin.trim());
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
origin: function(origin, callback) {
|
|
||||||
if (!origin) return callback(null, true);
|
|
||||||
if (allowedOrigins.includes(origin)) {
|
|
||||||
return callback(null, true);
|
|
||||||
}
|
|
||||||
return callback(new Error('CORS: Origin not allowed'), false);
|
|
||||||
},
|
|
||||||
credentials: true
|
|
||||||
};
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
const session = require('express-session');
|
|
||||||
const MySQLStore = require('express-mysql-session')(session);
|
|
||||||
|
|
||||||
module.exports = (session) => {
|
|
||||||
return {
|
|
||||||
secret: process.env.SECRET_KEY || 'mhmh4563463fds',
|
|
||||||
resave: false,
|
|
||||||
saveUninitialized: false,
|
|
||||||
store: new MySQLStore({
|
|
||||||
host: process.env.MYSQL_HOST,
|
|
||||||
port: process.env.MYSQL_PORT,
|
|
||||||
user: process.env.MYSQL_USER,
|
|
||||||
password: process.env.MYSQL_PASSWORD,
|
|
||||||
database: process.env.MYSQL_DATABASE
|
|
||||||
}),
|
|
||||||
cookie: {
|
|
||||||
secure: false,
|
|
||||||
httpOnly: true,
|
|
||||||
maxAge: 10 * 60 * 60 * 1000
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
const logger = require('../logger');
|
|
||||||
const bcrypt = require('bcrypt');
|
|
||||||
const moderDb = require('../moders/db');
|
|
||||||
require('dotenv').config({ path: __dirname + './config.env' });
|
|
||||||
exports.login = (req, res) => {
|
|
||||||
const { password } = req.body;
|
|
||||||
if (password === process.env.ADMIN_PASSWORD) {
|
|
||||||
req.session.isAuthenticated = true;
|
|
||||||
logger.info('Login successful');
|
|
||||||
res.status(200).send('Login successful');
|
|
||||||
} else {
|
|
||||||
res.status(401).send('Invalid password');
|
|
||||||
logger.warn('Неверный пароль');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.getCsrfToken = (req, res) => {
|
|
||||||
res.json({ csrfToken: req.csrfToken() });
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.moderLogin = async (req, res) => {
|
|
||||||
const { username, password } = req.body;
|
|
||||||
try {
|
|
||||||
const [users] = await moderDb.query('SELECT * FROM users WHERE username = ?', [username]);
|
|
||||||
if (users.length === 0) {
|
|
||||||
return res.status(401).json({ message: 'Неверные учетные данные' });
|
|
||||||
}
|
|
||||||
const user = users[0];
|
|
||||||
const match = await bcrypt.compare(password, user.password_hash);
|
|
||||||
if (!match) {
|
|
||||||
return res.status(401).json({ message: 'Неверные учетные данные' });
|
|
||||||
}
|
|
||||||
req.session.moderId = user.id;
|
|
||||||
req.session.moderName = user.username;
|
|
||||||
res.json({ message: 'Успешный вход', username: user.username });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Ошибка входа модератора:', error);
|
|
||||||
res.status(500).json({ message: 'Ошибка сервера' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
const fs = require('fs-extra');
|
|
||||||
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.writeFile(CARD_LOG_PATH, '');
|
|
||||||
logger.info(`Card log file created at: ${CARD_LOG_PATH}`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('Failed to initialize card logs:', err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
initCardLogs().catch(err => {
|
|
||||||
logger.error('Card logs initialization error:', err);
|
|
||||||
});
|
|
||||||
|
|
||||||
const cardEvent = (req, res) => {
|
|
||||||
logger.info('[CARD EVENT] Received:', req.body);
|
|
||||||
|
|
||||||
if (!req.body || !req.body.event) {
|
|
||||||
logger.error('Invalid request body:', req.body);
|
|
||||||
return res.status(400).json({ message: 'Missing required field: event' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { event, uid = 'N/A', timestamp = Date.now() } = req.body;
|
|
||||||
const logLine = `[${new Date(timestamp).toISOString()}] [${event}] UID: ${uid}\n`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!fs.existsSync(CARD_LOG_PATH)) {
|
|
||||||
initCardLogs();
|
|
||||||
}
|
|
||||||
fs.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 });
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('[CARD EVENT] Failed to write log:', err);
|
|
||||||
return res.status(500).json({
|
|
||||||
message: 'Failed to write log',
|
|
||||||
error: err.message,
|
|
||||||
path: CARD_LOG_PATH
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const getCardLogs = async (req, res) => {
|
|
||||||
try {
|
|
||||||
if (!(await fs.pathExists(CARD_LOG_PATH))) {
|
|
||||||
await initCardLogs();
|
|
||||||
return res.status(200).type('text/plain').send('Card logs initialized');
|
|
||||||
}
|
|
||||||
const logs = await fs.readFile(CARD_LOG_PATH, 'utf-8');
|
|
||||||
res.type('text/plain').send(logs);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('Error reading card logs:', err);
|
|
||||||
res.status(500).json({
|
|
||||||
error: err.message,
|
|
||||||
path: CARD_LOG_PATH
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
module.exports = {
|
|
||||||
cardEvent,
|
|
||||||
getCardLogs,
|
|
||||||
initCardLogs
|
|
||||||
};
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
const axios = require('axios');
|
|
||||||
const WebSocket = require('ws');
|
|
||||||
const iconv = require('iconv-lite');
|
|
||||||
const logger = require('../logger')
|
|
||||||
exports.checkDb = async (req, res) => {
|
|
||||||
let adminToDbStatus = { status: 'offline', message: 'Test not performed' };
|
|
||||||
try {
|
|
||||||
const dbConfig = {
|
|
||||||
user: process.env.DB_USER,
|
|
||||||
password: process.env.DB_PASSWORD,
|
|
||||||
server: process.env.DB_SERVER,
|
|
||||||
database: process.env.DB_NAME,
|
|
||||||
options: { encrypt: false, trustServerCertificate: true },
|
|
||||||
pool: { max: 1, min: 0, idleTimeoutMillis: 5000 },
|
|
||||||
requestTimeout: 5000
|
|
||||||
};
|
|
||||||
const pool = new sql.ConnectionPool(dbConfig);
|
|
||||||
await pool.connect();
|
|
||||||
await pool.query`SELECT 1`;
|
|
||||||
adminToDbStatus = { status: 'online', message: 'Прямое подключение успешно' };
|
|
||||||
await pool.close();
|
|
||||||
} catch (error) {
|
|
||||||
adminToDbStatus = { status: 'offline', message: `Ошибка прямого подключения: ${error.message}` };
|
|
||||||
}
|
|
||||||
|
|
||||||
let backendToDbStatus = { status: 'offline', message: 'Test not performed' };
|
|
||||||
try {
|
|
||||||
const response = await axios.get(`${process.env.BACKEND_URL}/api/db-status`, { timeout: 3000 });
|
|
||||||
if (response.data.status === 'ok') {
|
|
||||||
backendToDbStatus = { status: 'online', message: 'Бэкенд успешно подключен' };
|
|
||||||
} else {
|
|
||||||
backendToDbStatus = { status: 'offline', message: 'Бэкенд сообщил об ошибке' };
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
backendToDbStatus = { status: 'offline', message: 'Не удалось связаться с бэкендом' };
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
adminToDb: adminToDbStatus,
|
|
||||||
backendToDb: backendToDbStatus
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.checkStatus = async (req, res) => {
|
|
||||||
try {
|
|
||||||
await axios.get(`${process.env.BACKEND_URL}/api/test`, { timeout: 2000 });
|
|
||||||
res.json({ status: 'online' });
|
|
||||||
} catch (error) {
|
|
||||||
res.json({ status: 'offline' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
const fs = require('fs-extra');
|
|
||||||
const path = require('path');
|
|
||||||
const logger = require('../logger');
|
|
||||||
|
|
||||||
const ADMIN_LOG = path.resolve(__dirname, '..', process.env.ADMIN_LOG);
|
|
||||||
const BACKEND_APP_LOG = path.resolve(__dirname, '..', process.env.BACKEND_APP_LOG);
|
|
||||||
const CARD_LOG = path.resolve(__dirname, '..', process.env.CARD_LOG);
|
|
||||||
|
|
||||||
const getLogs = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const logPath = path.join(__dirname, '..', '../backend/logs/app.log');
|
|
||||||
const logs = await fs.readFile(logPath, 'utf-8');
|
|
||||||
res.type('text/plain');
|
|
||||||
res.send(logs);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error reading log file:', error);
|
|
||||||
res.status(500).send('Could not read log file.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearLogs = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const logPath = path.join(__dirname, '..', '../backend/logs/app.log');
|
|
||||||
await fs.truncate(logPath, 0);
|
|
||||||
res.status(200).json({ message: 'Logs cleared successfully' });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error clearing log file:', error);
|
|
||||||
res.status(500).send('Could not clear log file.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMonitoringLogs = async (req, res) => {
|
|
||||||
const logSources = [
|
|
||||||
{ name: 'Admin', path: ADMIN_LOG },
|
|
||||||
{ name: 'Backend', path: BACKEND_APP_LOG },
|
|
||||||
{ name: 'CardReader', path: CARD_LOG }
|
|
||||||
];
|
|
||||||
|
|
||||||
let allEvents = [];
|
|
||||||
|
|
||||||
for (const source of logSources) {
|
|
||||||
try {
|
|
||||||
if (!(await fs.pathExists(source.path))) {
|
|
||||||
logger.warn(`Log file not found: ${source.path}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = await fs.readFile(source.path, 'utf-8');
|
|
||||||
const lines = content.split(/\r?\n/);
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (!line) return;
|
|
||||||
try {
|
|
||||||
const jsonLog = JSON.parse(line);
|
|
||||||
if (['warn', 'error'].includes(jsonLog.level?.toLowerCase())) {
|
|
||||||
allEvents.push({
|
|
||||||
timestamp: jsonLog.timestamp || new Date().toISOString(),
|
|
||||||
level: jsonLog.level.toUpperCase(),
|
|
||||||
source: source.name,
|
|
||||||
message: jsonLog.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
const match = line.match(/\[([^\]]+)\] \[([^\]]+)\] (.*)/);
|
|
||||||
if (match) {
|
|
||||||
const level = match[2].toLowerCase();
|
|
||||||
if (['warn', 'error'].includes(level)) {
|
|
||||||
allEvents.push({
|
|
||||||
timestamp: new Date(match[1]).toISOString(),
|
|
||||||
level: level.toUpperCase(),
|
|
||||||
source: source.name,
|
|
||||||
message: match[3]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Error reading or parsing log file ${source.path}:`, err);
|
|
||||||
allEvents.push({
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
level: 'ERROR',
|
|
||||||
source: 'Monitoring',
|
|
||||||
message: `Could not read log file: ${source.name}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allEvents.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
||||||
|
|
||||||
res.json(allEvents);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getLogs,
|
|
||||||
clearLogs,
|
|
||||||
getMonitoringLogs
|
|
||||||
};
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
const moderDb = require('../moders/db');
|
|
||||||
const bcrypt = require('bcrypt');
|
|
||||||
const logger = require('../logger');
|
|
||||||
|
|
||||||
const getModers = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const [rows] = await moderDb.query('SELECT id, username FROM users ORDER BY id ASC');
|
|
||||||
res.json(rows);
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Ошибка получения списка модераторов:', e);
|
|
||||||
res.status(500).json({ message: 'Ошибка получения списка модераторов' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addModer = async (req, res) => {
|
|
||||||
const { username, password } = req.body;
|
|
||||||
if (!username || !password) {
|
|
||||||
return res.status(400).json({ message: 'username и password обязательны' });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const [exists] = await moderDb.query('SELECT id FROM users WHERE username = ?', [username]);
|
|
||||||
if (exists.length) {
|
|
||||||
return res.status(409).json({ message: 'Пользователь с таким username уже существует' });
|
|
||||||
}
|
|
||||||
const hash = await bcrypt.hash(password, 10);
|
|
||||||
const [result] = await moderDb.query('INSERT INTO users (username, password_hash) VALUES (?, ?)', [username, hash]);
|
|
||||||
res.json({ id: result.insertId, username });
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Ошибка добавления модератора:', e);
|
|
||||||
res.status(500).json({ message: 'Ошибка добавления модератора' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const changePassword = async (req, res) => {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { password } = req.body;
|
|
||||||
if (!password) {
|
|
||||||
return res.status(400).json({ message: 'password обязателен' });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const hash = await bcrypt.hash(password, 10);
|
|
||||||
const [result] = await moderDb.query('UPDATE users SET password_hash = ? WHERE id = ?', [hash, id]);
|
|
||||||
if (result.affectedRows === 0) {
|
|
||||||
return res.status(404).json({ message: 'Модератор не найден' });
|
|
||||||
}
|
|
||||||
res.json({ message: 'Пароль обновлён' });
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Ошибка смены пароля модератора:', e);
|
|
||||||
res.status(500).json({ message: 'Ошибка смены пароля модератора' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteModer = async (req, res) => {
|
|
||||||
const { id } = req.params;
|
|
||||||
try {
|
|
||||||
const [result] = await moderDb.query('DELETE FROM users WHERE id = ?', [id]);
|
|
||||||
if (result.affectedRows === 0) {
|
|
||||||
return res.status(404).json({ message: 'Модератор не найден' });
|
|
||||||
}
|
|
||||||
res.json({ message: 'Модератор удалён' });
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Ошибка удаления модератора:', e);
|
|
||||||
res.status(500).json({ message: 'Ошибка удаления модератора' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getModers,
|
|
||||||
addModer,
|
|
||||||
changePassword,
|
|
||||||
deleteModer
|
|
||||||
};
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
const fs = require('fs-extra');
|
|
||||||
const path = require('path');
|
|
||||||
const logger = require('../logger');
|
|
||||||
|
|
||||||
const configEnvPath = path.join(__dirname, '..', 'config.env');
|
|
||||||
const modersEnvPath = path.join(__dirname, '..', 'moders', 'config.env');
|
|
||||||
const backendConfigPath = path.join(__dirname, '..', '../backend/config.json');
|
|
||||||
|
|
||||||
const parseEnv = async (filePath) => {
|
|
||||||
const content = await fs.readFile(filePath, 'utf-8');
|
|
||||||
const lines = content.split(/\r?\n/);
|
|
||||||
const result = {};
|
|
||||||
for (const line of lines) {
|
|
||||||
if (!line.trim() || line.trim().startsWith('#')) continue;
|
|
||||||
const idx = line.indexOf('=');
|
|
||||||
if (idx === -1) continue;
|
|
||||||
const key = line.slice(0, idx).trim();
|
|
||||||
const value = line.slice(idx + 1).trim();
|
|
||||||
result[key] = value;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateEnv = async (filePath, updates) => {
|
|
||||||
const content = await fs.readFile(filePath, 'utf-8');
|
|
||||||
const lines = content.split(/\r?\n/);
|
|
||||||
const keys = Object.keys(updates);
|
|
||||||
const updated = new Set();
|
|
||||||
const newLines = lines.map(line => {
|
|
||||||
if (!line.trim() || line.trim().startsWith('#') || !line.includes('=')) return line;
|
|
||||||
const idx = line.indexOf('=');
|
|
||||||
const key = line.slice(0, idx).trim();
|
|
||||||
if (keys.includes(key)) {
|
|
||||||
updated.add(key);
|
|
||||||
return `${key}=${updates[key]}`;
|
|
||||||
}
|
|
||||||
return line;
|
|
||||||
});
|
|
||||||
for (const key of keys) {
|
|
||||||
if (!updated.has(key)) {
|
|
||||||
newLines.push(`${key}=${updates[key]}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await fs.writeFile(filePath, newLines.join('\n'), 'utf-8');
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMysqlSettings = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const env = await parseEnv(modersEnvPath);
|
|
||||||
res.json({
|
|
||||||
MYSQL_HOST: env.MYSQL_HOST || '',
|
|
||||||
MYSQL_PORT: env.MYSQL_PORT || '',
|
|
||||||
MYSQL_USER: env.MYSQL_USER || '',
|
|
||||||
MYSQL_PASSWORD: env.MYSQL_PASSWORD || '',
|
|
||||||
MYSQL_DATABASE: env.MYSQL_DATABASE || ''
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Ошибка чтения moders/config.env:', e);
|
|
||||||
res.status(500).json({ message: 'Ошибка чтения moders/config.env' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateMysqlSettings = async (req, res) => {
|
|
||||||
const updates = req.body;
|
|
||||||
if (updates.MYSQL_PASSWORD === '') {
|
|
||||||
delete updates.MYSQL_PASSWORD;
|
|
||||||
}
|
|
||||||
if (Object.keys(updates).length === 0) {
|
|
||||||
return res.status(400).json({ message: 'Нет данных для обновления' });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await updateEnv(modersEnvPath, updates);
|
|
||||||
res.json({ message: 'Параметры MySQL обновлены' });
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Ошибка записи moders/config.env:', e);
|
|
||||||
res.status(500).json({ message: 'Ошибка записи moders/config.env' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCorsSettings = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const env = await parseEnv(configEnvPath);
|
|
||||||
const origins = (env.ALLOWED_ORIGINS || '').split(',').map(o => o.trim()).filter(Boolean);
|
|
||||||
res.json({ ALLOWED_ORIGINS: origins });
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Ошибка чтения ALLOWED_ORIGINS:', e);
|
|
||||||
res.status(500).json({ message: 'Ошибка чтения ALLOWED_ORIGINS' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateCorsSettings = async (req, res) => {
|
|
||||||
const { ALLOWED_ORIGINS } = req.body;
|
|
||||||
if (!Array.isArray(ALLOWED_ORIGINS) || !ALLOWED_ORIGINS.length) {
|
|
||||||
return res.status(400).json({ message: 'Список ALLOWED_ORIGINS обязателен' });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await updateEnv(configEnvPath, { ALLOWED_ORIGINS: ALLOWED_ORIGINS.join(', ') });
|
|
||||||
res.json({ message: 'ALLOWED_ORIGINS обновлён' });
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Ошибка записи ALLOWED_ORIGINS:', e);
|
|
||||||
res.status(500).json({ message: 'Ошибка записи ALLOWED_ORIGINS' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAdminServerSettings = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const env = await parseEnv(configEnvPath);
|
|
||||||
res.json({
|
|
||||||
PORT: env.PORT || '',
|
|
||||||
ADMIN_URL: env.ADMIN_URL || ''
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Ошибка чтения config.env (admin-server):', e);
|
|
||||||
res.status(500).json({ message: 'Ошибка чтения config.env' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateAdminServerSettings = async (req, res) => {
|
|
||||||
const { PORT, ADMIN_URL } = req.body;
|
|
||||||
if (!PORT || !ADMIN_URL) {
|
|
||||||
return res.status(400).json({ message: 'PORT и ADMIN_URL обязательны' });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await updateEnv(configEnvPath, { PORT, ADMIN_URL });
|
|
||||||
res.json({ message: 'Параметры admin-server обновлены' });
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Ошибка записи config.env (admin-server):', e);
|
|
||||||
res.status(500).json({ message: 'Ошибка записи config.env' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getBackendServerSettings = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const config = await fs.readJson(backendConfigPath);
|
|
||||||
res.json({
|
|
||||||
host: config.server?.host || '',
|
|
||||||
port: config.server?.port || ''
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Ошибка чтения backend/config.json:', e);
|
|
||||||
res.status(500).json({ message: 'Ошибка чтения backend/config.json' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateBackendServerSettings = async (req, res) => {
|
|
||||||
const { host, port } = req.body;
|
|
||||||
if (!host || !port) {
|
|
||||||
return res.status(400).json({ message: 'host и port обязательны' });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const config = await fs.readJson(backendConfigPath);
|
|
||||||
config.server = config.server || {};
|
|
||||||
config.server.host = host;
|
|
||||||
config.server.port = port;
|
|
||||||
await fs.writeJson(backendConfigPath, config, { spaces: 4 });
|
|
||||||
res.json({ message: 'Параметры backend-server обновлены' });
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Ошибка записи backend/config.json:', e);
|
|
||||||
res.status(500).json({ message: 'Ошибка записи backend/config.json' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getMssqlSettings,
|
|
||||||
updateMssqlSettings,
|
|
||||||
getMysqlSettings,
|
|
||||||
updateMysqlSettings,
|
|
||||||
getCorsSettings,
|
|
||||||
updateCorsSettings,
|
|
||||||
getAdminServerSettings,
|
|
||||||
updateAdminServerSettings,
|
|
||||||
getBackendServerSettings,
|
|
||||||
updateBackendServerSettings
|
|
||||||
};
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const LOGS_DIR = path.join(__dirname, 'logs');
|
|
||||||
const LOG_FILE = path.join(LOGS_DIR, 'admin.log');
|
|
||||||
const MAX_SIZE = 20 * 1024 * 1024;
|
|
||||||
|
|
||||||
if (!fs.existsSync(LOGS_DIR)) {
|
|
||||||
fs.mkdirSync(LOGS_DIR, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
function rotateLogIfNeeded() {
|
|
||||||
try {
|
|
||||||
if (fs.existsSync(LOG_FILE)) {
|
|
||||||
const stats = fs.statSync(LOG_FILE);
|
|
||||||
if (stats.size >= MAX_SIZE) {
|
|
||||||
const date = new Date().toISOString().replace(/[:.]/g, '-');
|
|
||||||
const archiveName = `admin-${date}.log`;
|
|
||||||
const archivePath = path.join(LOGS_DIR, archiveName);
|
|
||||||
fs.renameSync(LOG_FILE, archivePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[LOGGER] Ошибка ротации лога:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function format(level, message) {
|
|
||||||
return `[${new Date().toISOString()}] [${level.toUpperCase()}] ${message}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function log(level, ...args) {
|
|
||||||
const msg = args.map(a => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ');
|
|
||||||
const line = format(level, msg);
|
|
||||||
rotateLogIfNeeded();
|
|
||||||
try {
|
|
||||||
fs.appendFileSync(LOG_FILE, line);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[LOGGER] Ошибка записи в лог:', err);
|
|
||||||
}
|
|
||||||
if (level === 'error') {
|
|
||||||
console.error(line.trim());
|
|
||||||
} else if (level === 'warn') {
|
|
||||||
console.warn(line.trim());
|
|
||||||
} else {
|
|
||||||
console.log(line.trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
info: (...args) => log('info', ...args),
|
|
||||||
warn: (...args) => log('warn', ...args),
|
|
||||||
error: (...args) => log('error', ...args),
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
module.exports = (req, res, next) => {
|
|
||||||
if (req.session && req.session.isAuthenticated) {
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
res.status(401).json({ message: 'Unauthorized' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
const logger = require('../logger')
|
|
||||||
module.exports = (err, req, res, next) => {
|
|
||||||
if (err.code === 'EBADCSRFTOKEN') {
|
|
||||||
return res.status(403).json({ message: 'Invalid CSRF token' });
|
|
||||||
}
|
|
||||||
logger.error(err.stack);
|
|
||||||
res.status(500).json({ message: 'Internal Server Error' });
|
|
||||||
};
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
MYSQL_HOST=localhost
|
|
||||||
MYSQL_PORT=3306
|
|
||||||
MYSQL_USER=root
|
|
||||||
MYSQL_PASSWORD=Vova-2003.
|
|
||||||
MYSQL_DATABASE=rasch_content
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
const mysql = require('mysql2/promise');
|
|
||||||
require('dotenv').config({ path: __dirname + './config.env' });
|
|
||||||
console.log({
|
|
||||||
host: process.env.MYSQL_HOST,
|
|
||||||
user: process.env.MYSQL_USER,
|
|
||||||
password: process.env.MYSQL_PASSWORD,
|
|
||||||
database: process.env.MYSQL_DATABASE
|
|
||||||
});
|
|
||||||
const pool = mysql.createPool({
|
|
||||||
host: process.env.MYSQL_HOST,
|
|
||||||
port: process.env.MYSQL_PORT || 3306,
|
|
||||||
user: process.env.MYSQL_USER,
|
|
||||||
password: process.env.MYSQL_PASSWORD,
|
|
||||||
database: process.env.MYSQL_DATABASE,
|
|
||||||
waitForConnections: true,
|
|
||||||
connectionLimit: 10,
|
|
||||||
queueLimit: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = pool;
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
function requireModerator(req, res, next) {
|
|
||||||
if (req.session && req.session.isModerator) return next();
|
|
||||||
res.status(401).send('Unauthorized');
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = requireModerator;
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const db = require('../db');
|
|
||||||
const requireModerator = require('../middleware/requireModerator');
|
|
||||||
const router = express.Router();
|
|
||||||
const multer = require('multer');
|
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
const uploadDir = path.join(__dirname, '../../uploads');
|
|
||||||
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
|
|
||||||
const storage = multer.diskStorage({
|
|
||||||
destination: (req, file, cb) => cb(null, uploadDir),
|
|
||||||
filename: (req, file, cb) => {
|
|
||||||
const ext = path.extname(file.originalname);
|
|
||||||
const name = Date.now() + '-' + Math.round(Math.random() * 1e6) + ext;
|
|
||||||
cb(null, name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const ALLOWED_MIME_TYPES = [
|
|
||||||
'image/jpeg', 'image/png',
|
|
||||||
'video/mp4', 'video/quicktime'
|
|
||||||
];
|
|
||||||
const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20 МБ
|
|
||||||
|
|
||||||
const upload = multer({
|
|
||||||
storage,
|
|
||||||
limits: { fileSize: MAX_FILE_SIZE },
|
|
||||||
fileFilter: (req, file, cb) => {
|
|
||||||
if (ALLOWED_MIME_TYPES.includes(file.mimetype)) {
|
|
||||||
cb(null, true);
|
|
||||||
} else {
|
|
||||||
cb(new Error('Недопустимый тип файла'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получить все блоки для вкладки
|
|
||||||
router.get('/:tab', async (req, res) => {
|
|
||||||
const { tab } = req.params;
|
|
||||||
if (!['info', 'union'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' });
|
|
||||||
try {
|
|
||||||
const [rows] = await db.query('SELECT * FROM content_blocks WHERE tab = ? ORDER BY created_at ASC', [tab]);
|
|
||||||
res.json(rows);
|
|
||||||
} catch (err) {
|
|
||||||
res.status(500).json({ message: 'Ошибка сервера' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Загрузка файла (image/video)
|
|
||||||
router.post('/:tab/upload', requireModerator, upload.single('file'), (req, res) => {
|
|
||||||
if (!req.file) return res.status(400).json({ message: 'Файл не загружен' });
|
|
||||||
const relPath = '/uploads/' + req.file.filename;
|
|
||||||
res.json({ path: relPath });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Добавить новый блок
|
|
||||||
router.post('/:tab', requireModerator, async (req, res) => {
|
|
||||||
const { tab } = req.params;
|
|
||||||
const { content } = req.body;
|
|
||||||
if (!['info', 'union'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' });
|
|
||||||
try {
|
|
||||||
const [result] = await db.query(
|
|
||||||
'INSERT INTO content_blocks (tab, type, content, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())',
|
|
||||||
[tab, 'text', content, req.session.userId]
|
|
||||||
);
|
|
||||||
res.json({ id: result.insertId });
|
|
||||||
} catch (err) {
|
|
||||||
res.status(500).json({ message: 'Ошибка сервера' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Обновить блок
|
|
||||||
router.put('/:tab/:id', requireModerator, async (req, res) => {
|
|
||||||
const { tab, id } = req.params;
|
|
||||||
const { content } = req.body;
|
|
||||||
if (!['info', 'union'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' });
|
|
||||||
try {
|
|
||||||
await db.query(
|
|
||||||
'UPDATE content_blocks SET content = ?, updated_at = NOW() WHERE id = ? AND tab = ?',
|
|
||||||
[content, id, tab]
|
|
||||||
);
|
|
||||||
res.json({ message: 'Обновлено' });
|
|
||||||
} catch (err) {
|
|
||||||
res.status(500).json({ message: 'Ошибка сервера' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Удалить блок
|
|
||||||
router.delete('/:tab/:id', requireModerator, async (req, res) => {
|
|
||||||
const { tab, id } = req.params;
|
|
||||||
if (!['info', 'union'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' });
|
|
||||||
try {
|
|
||||||
await db.query('DELETE FROM content_blocks WHERE id = ? AND tab = ?', [id, tab]);
|
|
||||||
res.json({ message: 'Удалено' });
|
|
||||||
} catch (err) {
|
|
||||||
res.status(500).json({ message: 'Ошибка сервера' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const bcrypt = require('bcrypt');
|
|
||||||
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 минут
|
|
||||||
max: 10,
|
|
||||||
message: { message: 'Слишком много попыток входа. Попробуйте позже.' },
|
|
||||||
standardHeaders: true,
|
|
||||||
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}`);
|
|
||||||
try {
|
|
||||||
logger.info('[LOGIN] Проверка подключения к базе...');
|
|
||||||
const [rows] = await db.query('SELECT * FROM users WHERE username = ?', [username]);
|
|
||||||
logger.info(`[LOGIN] Результат поиска пользователя: ${rows.length > 0 ? 'найден' : 'не найден'}`);
|
|
||||||
if (!rows.length) {
|
|
||||||
logger.warn(`[LOGIN] Пользователь не найден: ${username}`);
|
|
||||||
return res.status(401).json({ message: 'Неверный логин или пароль' });
|
|
||||||
}
|
|
||||||
const user = rows[0];
|
|
||||||
if (!user.password_hash) {
|
|
||||||
logger.error('[LOGIN] Отсутствует password_hash в БД');
|
|
||||||
return res.status(500).json({ message: 'Ошибка сервера: некорректные данные пользователя' });
|
|
||||||
}
|
|
||||||
logger.info('[LOGIN] Сравнение пароля через bcrypt...');
|
|
||||||
const match = await bcrypt.compare(password, user.password_hash);
|
|
||||||
logger.info(`[LOGIN] Результат сравнения пароля: ${match}`);
|
|
||||||
if (!match) {
|
|
||||||
logger.warn(`[LOGIN] Неверный пароль для пользователя: ${username}`);
|
|
||||||
return res.status(401).json({ message: 'Неверный логин или пароль' });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
req.session.isModerator = true;
|
|
||||||
req.session.userId = user.id;
|
|
||||||
req.session.username = user.username;
|
|
||||||
logger.info('[LOGIN] Сессия установлена:', {
|
|
||||||
isModerator: req.session.isModerator,
|
|
||||||
userId: req.session.userId,
|
|
||||||
username: req.session.username
|
|
||||||
});
|
|
||||||
res.json({ message: 'OK' });
|
|
||||||
logger.info('[LOGIN] Ответ отправлен');
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('[LOGIN] Ошибка при установке сессии или отправке ответа:', e);
|
|
||||||
res.status(500).json({ message: 'Ошибка сервера (сессия)' });
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('[LOGIN] Ошибка авторизации:', err);
|
|
||||||
res.status(500).json({ message: 'Ошибка сервера' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/moder-logout', (req, res) => {
|
|
||||||
req.session.destroy(() => {
|
|
||||||
res.json({ message: 'OK' });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/check-session', (req, res) => {
|
|
||||||
if (req.session && req.session.isModerator) {
|
|
||||||
res.json({ ok: true });
|
|
||||||
} else {
|
|
||||||
res.status(401).json({ ok: false });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
-- ============================================================
|
|
||||||
-- 📦 Схема базы данных модераторов: rasch_content
|
|
||||||
-- Таблицы: users (модераторы), content_blocks (контент), sessions
|
|
||||||
-- Совместимо с MySQL 8.0+ (CHECK-ограничения требуют 8.0.16+)
|
|
||||||
-- Для более старых версий закомментируйте строки с CHECK.
|
|
||||||
--
|
|
||||||
-- Запуск: mysql -u root -p < schema.sql
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
CREATE DATABASE IF NOT EXISTS rasch_content
|
|
||||||
CHARACTER SET utf8mb4
|
|
||||||
COLLATE utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
USE rasch_content;
|
|
||||||
|
|
||||||
-- ------------------------------------------------------------
|
|
||||||
-- Модераторы
|
|
||||||
-- ------------------------------------------------------------
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
||||||
username VARCHAR(100) NOT NULL,
|
|
||||||
password_hash VARCHAR(255) NOT NULL, -- bcrypt-хэш (60+ символов)
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
UNIQUE KEY uq_users_username (username)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- ------------------------------------------------------------
|
|
||||||
-- Модерский контент (вкладки "Информация" и "Профсоюз")
|
|
||||||
-- HTML-разметка из редактора Quill хранится в content
|
|
||||||
-- ------------------------------------------------------------
|
|
||||||
CREATE TABLE IF NOT EXISTS content_blocks (
|
|
||||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
||||||
tab VARCHAR(20) NOT NULL, -- 'info' | 'union'
|
|
||||||
type VARCHAR(20) NOT NULL DEFAULT 'text', -- пока только 'text'
|
|
||||||
content MEDIUMTEXT NOT NULL, -- HTML
|
|
||||||
created_by INT UNSIGNED DEFAULT NULL, -- FK -> users.id
|
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
KEY idx_content_blocks_tab (tab),
|
|
||||||
KEY idx_content_blocks_created_by (created_by),
|
|
||||||
CONSTRAINT fk_content_blocks_created_by
|
|
||||||
FOREIGN KEY (created_by) REFERENCES users (id)
|
|
||||||
ON DELETE SET NULL ON UPDATE CASCADE,
|
|
||||||
-- Ограничения значений (MySQL 8.0.16+)
|
|
||||||
CONSTRAINT chk_content_blocks_tab CHECK (tab IN ('info', 'union')),
|
|
||||||
CONSTRAINT chk_content_blocks_type CHECK (type IN ('text'))
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- ------------------------------------------------------------
|
|
||||||
-- Сессии (express-mysql-session)
|
|
||||||
-- Точная структура из node_modules/express-mysql-session/schema.sql
|
|
||||||
-- ------------------------------------------------------------
|
|
||||||
CREATE TABLE IF NOT EXISTS sessions (
|
|
||||||
session_id varchar(128) COLLATE utf8mb4_bin NOT NULL,
|
|
||||||
expires int(11) unsigned NOT NULL,
|
|
||||||
data mediumtext COLLATE utf8mb4_bin,
|
|
||||||
PRIMARY KEY (session_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- ------------------------------------------------------------
|
|
||||||
-- Демо-модератор
|
|
||||||
-- Пароль: "moderator123" (bcrypt, соль 10 раундов)
|
|
||||||
-- После создания измените пароль через админ-панель.
|
|
||||||
-- ------------------------------------------------------------
|
|
||||||
INSERT INTO users (username, password_hash) VALUES
|
|
||||||
('moderator', '$2b$10$7EqJtq98hPqEX7fNZaFWoOhi5SzuYrGTgEdf762vPz0J8wD0GmKMS');
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "admin-server",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"main": "server.js",
|
|
||||||
"scripts": {
|
|
||||||
"start": "node server.js",
|
|
||||||
"electron": "electron .",
|
|
||||||
"electron-dev": "set NODE_ENV=development&& electron .",
|
|
||||||
"postinstall": "electron-builder install-app-deps",
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"description": "",
|
|
||||||
"dependencies": {
|
|
||||||
"axios": "^1.10.0",
|
|
||||||
"bcrypt": "^6.0.0",
|
|
||||||
"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",
|
|
||||||
"morgan": "^1.10.0",
|
|
||||||
"mssql": "^11.0.1",
|
|
||||||
"multer": "^2.0.1",
|
|
||||||
"mysql2": "^3.14.2",
|
|
||||||
"quill": "^2.0.3",
|
|
||||||
"ws": "^8.18.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,868 +0,0 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const statusElement = document.getElementById('server-status');
|
|
||||||
const refreshBtn = document.getElementById('refresh-status-btn');
|
|
||||||
const logsElement = document.getElementById('logs-content');
|
|
||||||
const configForm = document.getElementById('config-form');
|
|
||||||
const payslipsPathInput = document.getElementById('payslipsPath');
|
|
||||||
const configStatus = document.getElementById('config-status');
|
|
||||||
const browseBtn = document.getElementById('browse-btn');
|
|
||||||
const dbStatusElement = document.getElementById('db-status');
|
|
||||||
const checkDbBtn = document.getElementById('check-db-btn');
|
|
||||||
const refreshLogsBtn = document.getElementById('refresh-logs-btn');
|
|
||||||
const clearLogsBtn = document.getElementById('clear-logs-btn');
|
|
||||||
const adminDbStatusEl = document.getElementById('admin-db-status');
|
|
||||||
const adminDbMessageEl = document.getElementById('admin-db-message');
|
|
||||||
const backendDbStatusEl = document.getElementById('backend-db-status');
|
|
||||||
const backendDbMessageEl = document.getElementById('backend-db-message');
|
|
||||||
const pingBtn = document.getElementById('ping-btn');
|
|
||||||
const pingStopBtn = document.getElementById('ping-stop-btn');
|
|
||||||
const pingClearBtn = document.getElementById('ping-clear-btn');
|
|
||||||
const pingIpInput = document.getElementById('ping-ip');
|
|
||||||
const pingOutputEl = document.getElementById('ping-output');
|
|
||||||
let ws;
|
|
||||||
|
|
||||||
const loginForm = document.getElementById('login-form');
|
|
||||||
if (loginForm) {
|
|
||||||
loginForm.addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const password = loginForm.elements['password'].value;
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/auth', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ password })
|
|
||||||
});
|
|
||||||
if (response.ok) {
|
|
||||||
window.location.href = '/';
|
|
||||||
} else {
|
|
||||||
alert('Неверный пароль');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
alert('Ошибка авторизации');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchStatus() {
|
|
||||||
fetch('/api/status')
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.status === 'online') {
|
|
||||||
statusElement.textContent = 'Онлайн';
|
|
||||||
statusElement.style.color = 'green';
|
|
||||||
} else {
|
|
||||||
statusElement.textContent = 'Оффлайн';
|
|
||||||
statusElement.style.color = 'red';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('Ошибка при получении статуса:', err);
|
|
||||||
statusElement.textContent = 'Ошибка';
|
|
||||||
statusElement.style.color = 'orange';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshBtn.addEventListener('click', fetchStatus);
|
|
||||||
function fetchLogs() {
|
|
||||||
logsElement.textContent = 'загрузка...';
|
|
||||||
fetch('/api/logs')
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) throw new Error('Не удалось загрузить логи');
|
|
||||||
return response.text();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
logsElement.textContent = data || 'Лог-файл пуст.';
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
logsElement.textContent = `Ошибка при загрузке логов: ${err.message}`;
|
|
||||||
logsElement.style.color = '#ff6b6b';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function fetchConfig() {
|
|
||||||
fetch('/api/config')
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(config => {
|
|
||||||
payslipsPathInput.value = config.payslipsPath;
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('Ошибка при получении конфигурации:', err);
|
|
||||||
configStatus.textContent = 'Не удалось загрузить конфигурацию.';
|
|
||||||
configStatus.style.color = 'red';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
configForm.addEventListener('submit', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const newConfig = {
|
|
||||||
payslipsPath: payslipsPathInput.value
|
|
||||||
};
|
|
||||||
|
|
||||||
apiFetch('/api/config', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(newConfig)
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
configStatus.textContent = 'Конфигурация успешно сохранена!';
|
|
||||||
configStatus.style.color = 'green';
|
|
||||||
setTimeout(() => { configStatus.textContent = ''; }, 3000);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('Ошибка при сохранении конфигурации:', err);
|
|
||||||
configStatus.textContent = 'Ошибка при сохранении.';
|
|
||||||
configStatus.style.color = 'red';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
browseBtn.addEventListener('click', async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/browse');
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
payslipsPathInput.value = data.path;
|
|
||||||
} else {
|
|
||||||
console.info('Folder selection was canceled.');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error opening folder dialog:', error);
|
|
||||||
alert('Не удалось открыть диалог выбора папки.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function renderStatus(element, messageEl, data) {
|
|
||||||
messageEl.textContent = data.message;
|
|
||||||
if (data.status === 'online') {
|
|
||||||
element.textContent = 'Онлайн';
|
|
||||||
element.style.color = 'green';
|
|
||||||
} else {
|
|
||||||
element.textContent = 'Оффлайн';
|
|
||||||
element.style.color = 'red';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function checkDbStatus() {
|
|
||||||
adminDbStatusEl.textContent = 'проверка...';
|
|
||||||
backendDbStatusEl.textContent = 'проверка...';
|
|
||||||
adminDbMessageEl.textContent = '';
|
|
||||||
backendDbMessageEl.textContent = '';
|
|
||||||
adminDbStatusEl.style.color = '#333';
|
|
||||||
backendDbStatusEl.style.color = '#333';
|
|
||||||
|
|
||||||
fetch('/api/health/check-db')
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
renderStatus(adminDbStatusEl, adminDbMessageEl, data.adminToDb);
|
|
||||||
renderStatus(backendDbStatusEl, backendDbMessageEl, data.backendToDb);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('Ошибка при проверке статуса БД:', err);
|
|
||||||
adminDbStatusEl.textContent = 'Ошибка';
|
|
||||||
adminDbStatusEl.style.color = 'orange';
|
|
||||||
backendDbStatusEl.textContent = 'Ошибка';
|
|
||||||
backendDbStatusEl.style.color = 'orange';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
checkDbBtn.addEventListener('click', checkDbStatus);
|
|
||||||
|
|
||||||
function clearLogs() {
|
|
||||||
if (!confirm('Это действие удалит лог-файл полностью и навсегда. Вы уверены?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fetch('/api/logs/clear', { method: 'POST' })
|
|
||||||
.then(response => {
|
|
||||||
if (response.ok) {
|
|
||||||
fetchLogs();
|
|
||||||
} else {
|
|
||||||
alert('Не удалось очистить логи.');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('Ошибка при очистке логов:', err);
|
|
||||||
alert('Произошла ошибка при очистке логов.');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshLogsBtn.addEventListener('click', fetchLogs);
|
|
||||||
clearLogsBtn.addEventListener('click', clearLogs);
|
|
||||||
|
|
||||||
function doPing() {
|
|
||||||
const ip = pingIpInput.value.trim();
|
|
||||||
if (!ip) {
|
|
||||||
pingOutputEl.textContent = 'Пожалуйста, введите IP-адрес.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (ws) {
|
|
||||||
ws.close();
|
|
||||||
}
|
|
||||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
|
||||||
ws = new WebSocket(`${wsProtocol}://${window.location.host}`);
|
|
||||||
|
|
||||||
ws.onopen = () => {
|
|
||||||
pingOutputEl.textContent = '';
|
|
||||||
pingBtn.disabled = true;
|
|
||||||
pingStopBtn.disabled = false;
|
|
||||||
ws.send(JSON.stringify({ type: 'ping', ip }));
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
|
||||||
const message = JSON.parse(event.data);
|
|
||||||
pingOutputEl.textContent += message.data;
|
|
||||||
pingOutputEl.scrollTop = pingOutputEl.scrollHeight;
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
|
||||||
console.error('WebSocket Error:', error);
|
|
||||||
pingOutputEl.textContent += '\nОшибка WebSocket соединения.';
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = () => {
|
|
||||||
console.info('WebSocket connection closed');
|
|
||||||
pingBtn.disabled = false;
|
|
||||||
pingStopBtn.disabled = true;
|
|
||||||
ws = null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopPing() {
|
|
||||||
if (ws) {
|
|
||||||
ws.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearPingOutput() {
|
|
||||||
pingOutputEl.textContent = '';
|
|
||||||
}
|
|
||||||
pingBtn.addEventListener('click', doPing);
|
|
||||||
pingStopBtn.addEventListener('click', stopPing);
|
|
||||||
pingClearBtn.addEventListener('click', clearPingOutput);
|
|
||||||
|
|
||||||
const cardLogsElement = document.getElementById('card-logs-content');
|
|
||||||
const refreshCardLogsBtn = document.getElementById('refresh-card-logs-btn');
|
|
||||||
|
|
||||||
function fetchCardLogs() {
|
|
||||||
cardLogsElement.textContent = 'загрузка...';
|
|
||||||
fetch('/api/card/card-logs')
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) throw new Error('Не удалось загрузить логи считывателя');
|
|
||||||
return response.text();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
cardLogsElement.textContent = data || 'Лог-файл пуст.';
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
cardLogsElement.textContent = `Ошибка при загрузке логов: ${err.message}`;
|
|
||||||
cardLogsElement.style.color = '#ff6b6b';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
refreshCardLogsBtn.addEventListener('click', fetchCardLogs);
|
|
||||||
|
|
||||||
// --- ТАБЫ ---
|
|
||||||
const tabMainBtn = document.getElementById('tab-main');
|
|
||||||
const tabSettingsBtn = document.getElementById('tab-settings');
|
|
||||||
const tabMonitoringBtn = document.getElementById('tab-monitoring');
|
|
||||||
const mainTab = document.getElementById('main-tab');
|
|
||||||
const settingsTab = document.getElementById('settings-tab');
|
|
||||||
const monitoringTab = document.getElementById('monitoring-tab');
|
|
||||||
|
|
||||||
function showTab(tab) {
|
|
||||||
mainTab.style.display = 'none';
|
|
||||||
settingsTab.style.display = 'none';
|
|
||||||
monitoringTab.style.display = 'none';
|
|
||||||
tabMainBtn.classList.remove('active');
|
|
||||||
tabSettingsBtn.classList.remove('active');
|
|
||||||
tabMonitoringBtn.classList.remove('active');
|
|
||||||
|
|
||||||
if (tab === 'main') {
|
|
||||||
mainTab.style.display = '';
|
|
||||||
tabMainBtn.classList.add('active');
|
|
||||||
} else if (tab === 'settings') {
|
|
||||||
settingsTab.style.display = '';
|
|
||||||
tabSettingsBtn.classList.add('active');
|
|
||||||
initSettingsTab();
|
|
||||||
} else if (tab === 'monitoring') {
|
|
||||||
monitoringTab.style.display = '';
|
|
||||||
tabMonitoringBtn.classList.add('active');
|
|
||||||
initMonitoringTab();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tabMainBtn.addEventListener('click', () => showTab('main'));
|
|
||||||
tabSettingsBtn.addEventListener('click', () => showTab('settings'));
|
|
||||||
tabMonitoringBtn.addEventListener('click', () => showTab('monitoring'));
|
|
||||||
|
|
||||||
// --- TOAST ---
|
|
||||||
function showToast(message, type = 'success') {
|
|
||||||
const container = document.getElementById('toast-container');
|
|
||||||
const toast = document.createElement('div');
|
|
||||||
toast.className = 'toast toast-' + type;
|
|
||||||
toast.textContent = message;
|
|
||||||
container.appendChild(toast);
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.style.opacity = '0';
|
|
||||||
setTimeout(() => container.removeChild(toast), 300);
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- CSRF TOKEN ---
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
// Обёртка для fetch с CSRF и credentials
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Модальное окно подтверждения ---
|
|
||||||
const confirmModal = document.getElementById('confirm-modal');
|
|
||||||
const confirmModalText = document.getElementById('confirm-modal-text');
|
|
||||||
const confirmModalYes = document.getElementById('confirm-modal-yes');
|
|
||||||
const confirmModalNo = document.getElementById('confirm-modal-no');
|
|
||||||
|
|
||||||
function showConfirm(text, onConfirm) {
|
|
||||||
confirmModalText.textContent = text;
|
|
||||||
confirmModal.style.display = 'flex';
|
|
||||||
|
|
||||||
const yesListener = () => {
|
|
||||||
onConfirm();
|
|
||||||
closeConfirm();
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeConfirm = () => {
|
|
||||||
confirmModal.style.display = 'none';
|
|
||||||
confirmModalYes.removeEventListener('click', yesListener);
|
|
||||||
confirmModalNo.removeEventListener('click', closeConfirm);
|
|
||||||
}
|
|
||||||
|
|
||||||
confirmModalYes.addEventListener('click', yesListener);
|
|
||||||
confirmModalNo.addEventListener('click', closeConfirm);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// --- ВКЛАДКА МОНИТОРИНГА ---
|
|
||||||
let monitoringInitialized = false;
|
|
||||||
let allMonitoringEvents = []; // Кэш для всех событий
|
|
||||||
|
|
||||||
async function initMonitoringTab() {
|
|
||||||
if (monitoringInitialized) return;
|
|
||||||
monitoringInitialized = true;
|
|
||||||
|
|
||||||
const tableBody = document.querySelector('#monitoring-log-table tbody');
|
|
||||||
const levelFilter = document.getElementById('log-level-filter');
|
|
||||||
const sourceFilter = document.getElementById('log-source-filter');
|
|
||||||
const refreshBtn = document.getElementById('refresh-monitoring-btn');
|
|
||||||
|
|
||||||
async function loadMonitoringLogs() {
|
|
||||||
tableBody.innerHTML = '<tr><td colspan="4">Загрузка...</td></tr>';
|
|
||||||
try {
|
|
||||||
const res = await apiFetch('/api/monitoring/logs');
|
|
||||||
if (!res.ok) throw new Error('Failed to fetch logs');
|
|
||||||
|
|
||||||
allMonitoringEvents = await res.json();
|
|
||||||
renderTable();
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
tableBody.innerHTML = '<tr><td colspan="4" style="color: red;">Ошибка загрузки логов.</td></tr>';
|
|
||||||
console.error('Error loading monitoring logs:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTable() {
|
|
||||||
const level = levelFilter.value;
|
|
||||||
const source = sourceFilter.value;
|
|
||||||
|
|
||||||
const filteredEvents = allMonitoringEvents.filter(event => {
|
|
||||||
const levelMatch = (level === 'all') || (event.level === level);
|
|
||||||
const sourceMatch = (source === 'all') || (event.source === source);
|
|
||||||
return levelMatch && sourceMatch;
|
|
||||||
});
|
|
||||||
|
|
||||||
tableBody.innerHTML = '';
|
|
||||||
if (filteredEvents.length === 0) {
|
|
||||||
tableBody.innerHTML = '<tr><td colspan="4">Нет событий для отображения.</td></tr>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
filteredEvents.forEach(event => {
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
tr.className = `log-level-${event.level.toLowerCase()}`;
|
|
||||||
tr.innerHTML = `
|
|
||||||
<td>${new Date(event.timestamp).toLocaleString()}</td>
|
|
||||||
<td><span class="log-level-label">${event.level}</span></td>
|
|
||||||
<td>${event.source}</td>
|
|
||||||
<td>${event.message}</td>
|
|
||||||
`;
|
|
||||||
tableBody.appendChild(tr);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
levelFilter.addEventListener('change', renderTable);
|
|
||||||
sourceFilter.addEventListener('change', renderTable);
|
|
||||||
refreshBtn.addEventListener('click', loadMonitoringLogs);
|
|
||||||
|
|
||||||
loadMonitoringLogs();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// --- НАСТРОЙКИ: инициализация вкладки ---
|
|
||||||
let settingsInitialized = false;
|
|
||||||
|
|
||||||
async function initSettingsTab() {
|
|
||||||
if(settingsInitialized) return;
|
|
||||||
settingsInitialized = true;
|
|
||||||
|
|
||||||
initServerParamsSection();
|
|
||||||
initDbSection();
|
|
||||||
initCorsSection();
|
|
||||||
initModersSection();
|
|
||||||
|
|
||||||
settingsTab.addEventListener('click', (e) => {
|
|
||||||
if (e.target.dataset.action === 'edit-pass') {
|
|
||||||
const targetId = e.target.dataset.target;
|
|
||||||
const input = document.getElementById(targetId);
|
|
||||||
input.readOnly = false;
|
|
||||||
input.value = '';
|
|
||||||
input.placeholder = 'Введите новый пароль...';
|
|
||||||
input.focus();
|
|
||||||
e.target.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initServerParamsSection() {
|
|
||||||
// Загрузка текущих значений
|
|
||||||
try {
|
|
||||||
const results = await Promise.allSettled([
|
|
||||||
apiFetch('/api/settings/admin-server'),
|
|
||||||
apiFetch('/api/settings/backend-server')
|
|
||||||
]);
|
|
||||||
|
|
||||||
const adminResult = results[0];
|
|
||||||
if (adminResult.status === 'fulfilled') {
|
|
||||||
const adminData = await adminResult.value.json();
|
|
||||||
document.getElementById('admin-port').value = adminData.PORT || '';
|
|
||||||
document.getElementById('admin-url').value = adminData.ADMIN_URL || '';
|
|
||||||
} else {
|
|
||||||
console.error('Failed to load admin-server settings:', adminResult.reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
const backendResult = results[1];
|
|
||||||
if (backendResult.status === 'fulfilled') {
|
|
||||||
const backendData = await backendResult.value.json();
|
|
||||||
document.getElementById('backend-host').value = backendData.host || '';
|
|
||||||
document.getElementById('backend-port').value = backendData.port || '';
|
|
||||||
} else {
|
|
||||||
console.error('Failed to load backend-server settings:', backendResult.reason);
|
|
||||||
document.getElementById('server-params-status').textContent += ' Ошибка загрузки параметров Backend.';
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
document.getElementById('server-params-status').textContent = 'Критическая ошибка загрузки параметров';
|
|
||||||
document.getElementById('server-params-status').style.color = 'red';
|
|
||||||
}
|
|
||||||
// Обработка сохранения
|
|
||||||
document.getElementById('server-params-form').addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const PORT = document.getElementById('admin-port').value;
|
|
||||||
const ADMIN_URL = document.getElementById('admin-url').value;
|
|
||||||
const host = document.getElementById('backend-host').value;
|
|
||||||
const port = document.getElementById('backend-port').value;
|
|
||||||
const statusEl = document.getElementById('server-params-status');
|
|
||||||
statusEl.textContent = '';
|
|
||||||
try {
|
|
||||||
const [adminRes, backendRes] = await Promise.all([
|
|
||||||
apiFetch('/api/settings/admin-server', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ PORT, ADMIN_URL })
|
|
||||||
}),
|
|
||||||
apiFetch('/api/settings/backend-server', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ host, port })
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
if (adminRes.ok && backendRes.ok) {
|
|
||||||
statusEl.innerHTML = 'Параметры успешно сохранены.<br><strong class="warning-text">Требуется перезапуск обоих серверов для применения!</strong>';
|
|
||||||
statusEl.style.color = 'green';
|
|
||||||
showToast('Параметры успешно сохранены', 'success');
|
|
||||||
} else {
|
|
||||||
statusEl.textContent = 'Ошибка сохранения';
|
|
||||||
statusEl.style.color = 'red';
|
|
||||||
showToast('Ошибка сохранения', 'error');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
statusEl.textContent = 'Ошибка сохранения';
|
|
||||||
statusEl.style.color = 'red';
|
|
||||||
showToast('Ошибка сохранения', 'error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function initDbSection() {
|
|
||||||
// --- БД ---
|
|
||||||
// MS SQL
|
|
||||||
async function loadMssql() {
|
|
||||||
try {
|
|
||||||
const res = await apiFetch('/api/settings/mssql');
|
|
||||||
const data = await res.json();
|
|
||||||
document.getElementById('mssql-user').value = data.DB_USER || '';
|
|
||||||
|
|
||||||
const passWrapper = document.getElementById('mssql-pass').closest('.password-wrapper');
|
|
||||||
passWrapper.innerHTML = `
|
|
||||||
<input type="password" id="mssql-pass" readonly>
|
|
||||||
<button type="button" class="btn-small" data-action="edit-pass" data-target="mssql-pass">Изменить</button>
|
|
||||||
`;
|
|
||||||
if (data.DB_PASSWORD) {
|
|
||||||
passWrapper.querySelector('input').placeholder = '********';
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('mssql-server').value = data.DB_SERVER || '';
|
|
||||||
document.getElementById('mssql-db').value = data.DB_NAME || '';
|
|
||||||
} catch (e) {
|
|
||||||
document.getElementById('mssql-status').textContent = 'Ошибка загрузки';
|
|
||||||
document.getElementById('mssql-status').style.color = 'red';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loadMssql();
|
|
||||||
document.getElementById('mssql-form').addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
DB_USER: document.getElementById('mssql-user').value,
|
|
||||||
DB_SERVER: document.getElementById('mssql-server').value,
|
|
||||||
DB_NAME: document.getElementById('mssql-db').value
|
|
||||||
};
|
|
||||||
const passInput = document.getElementById('mssql-pass');
|
|
||||||
if (!passInput.readOnly) {
|
|
||||||
payload.DB_PASSWORD = passInput.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusEl = document.getElementById('mssql-status');
|
|
||||||
statusEl.textContent = '';
|
|
||||||
try {
|
|
||||||
const res = await apiFetch('/api/settings/mssql', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
});
|
|
||||||
if (res.ok) {
|
|
||||||
statusEl.innerHTML = 'Сохранено.<br><strong class="warning-text">Требуется перезапуск Admin-server для применения!</strong>';
|
|
||||||
statusEl.style.color = 'green';
|
|
||||||
showToast('Сохранено', 'success');
|
|
||||||
loadMssql(); // Перезагружаем секцию, чтобы сбросить поле пароля
|
|
||||||
} else {
|
|
||||||
statusEl.textContent = 'Ошибка сохранения';
|
|
||||||
statusEl.style.color = 'red';
|
|
||||||
showToast('Ошибка сохранения', 'error');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
statusEl.textContent = 'Ошибка сохранения';
|
|
||||||
statusEl.style.color = 'red';
|
|
||||||
showToast('Ошибка сохранения', 'error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// MySQL
|
|
||||||
async function loadMysql() {
|
|
||||||
try {
|
|
||||||
const res = await apiFetch('/api/settings/mysql');
|
|
||||||
const data = await res.json();
|
|
||||||
document.getElementById('mysql-host').value = data.MYSQL_HOST || '';
|
|
||||||
document.getElementById('mysql-port').value = data.MYSQL_PORT || '';
|
|
||||||
document.getElementById('mysql-user').value = data.MYSQL_USER || '';
|
|
||||||
|
|
||||||
const passWrapper = document.getElementById('mysql-pass').closest('.password-wrapper');
|
|
||||||
passWrapper.innerHTML = `
|
|
||||||
<input type="password" id="mysql-pass" readonly>
|
|
||||||
<button type="button" class="btn-small" data-action="edit-pass" data-target="mysql-pass">Изменить</button>
|
|
||||||
`;
|
|
||||||
if (data.MYSQL_PASSWORD) {
|
|
||||||
passWrapper.querySelector('input').placeholder = '********';
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('mysql-db').value = data.MYSQL_DATABASE || '';
|
|
||||||
} catch (e) {
|
|
||||||
document.getElementById('mysql-status').textContent = 'Ошибка загрузки';
|
|
||||||
document.getElementById('mysql-status').style.color = 'red';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loadMysql();
|
|
||||||
document.getElementById('mysql-form').addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
MYSQL_HOST: document.getElementById('mysql-host').value,
|
|
||||||
MYSQL_PORT: document.getElementById('mysql-port').value,
|
|
||||||
MYSQL_USER: document.getElementById('mysql-user').value,
|
|
||||||
MYSQL_DATABASE: document.getElementById('mysql-db').value
|
|
||||||
};
|
|
||||||
const passInput = document.getElementById('mysql-pass');
|
|
||||||
if (!passInput.readOnly) {
|
|
||||||
payload.MYSQL_PASSWORD = passInput.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusEl = document.getElementById('mysql-status');
|
|
||||||
statusEl.textContent = '';
|
|
||||||
try {
|
|
||||||
const res = await apiFetch('/api/settings/mysql', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
});
|
|
||||||
if (res.ok) {
|
|
||||||
statusEl.innerHTML = 'Сохранено.<br><strong class="warning-text">Требуется перезапуск Admin-server для применения!</strong>';
|
|
||||||
statusEl.style.color = 'green';
|
|
||||||
showToast('Сохранено', 'success');
|
|
||||||
loadMysql(); // Перезагружаем секцию, чтобы сбросить поле пароля
|
|
||||||
} else {
|
|
||||||
statusEl.textContent = 'Ошибка сохранения';
|
|
||||||
statusEl.style.color = 'red';
|
|
||||||
showToast('Ошибка сохранения', 'error');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
statusEl.textContent = 'Ошибка сохранения';
|
|
||||||
statusEl.style.color = 'red';
|
|
||||||
showToast('Ошибка сохранения', 'error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function initCorsSection() {
|
|
||||||
// --- CORS ---
|
|
||||||
let allowedOrigins = [];
|
|
||||||
async function loadCors() {
|
|
||||||
const list = document.getElementById('cors-list');
|
|
||||||
list.innerHTML = '<li>Загрузка...</li>';
|
|
||||||
try {
|
|
||||||
const res = await apiFetch('/api/settings/cors');
|
|
||||||
const data = await res.json();
|
|
||||||
allowedOrigins = Array.isArray(data.ALLOWED_ORIGINS) ? data.ALLOWED_ORIGINS : [];
|
|
||||||
renderCorsList();
|
|
||||||
} catch (e) {
|
|
||||||
list.innerHTML = '<li>Ошибка загрузки</li>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const corsInput = document.getElementById('cors-new-origin');
|
|
||||||
const corsAddBtn = document.getElementById('cors-add-btn');
|
|
||||||
corsInput.addEventListener('input', () => {
|
|
||||||
const val = corsInput.value.trim();
|
|
||||||
corsAddBtn.disabled = !val || allowedOrigins.includes(val);
|
|
||||||
});
|
|
||||||
function renderCorsList() {
|
|
||||||
const list = document.getElementById('cors-list');
|
|
||||||
list.innerHTML = '';
|
|
||||||
if (!allowedOrigins.length) {
|
|
||||||
list.innerHTML = '<li>Нет origin</li>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
allowedOrigins.forEach((origin, idx) => {
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.textContent = origin + ' ';
|
|
||||||
const delBtn = document.createElement('button');
|
|
||||||
delBtn.textContent = 'Удалить';
|
|
||||||
delBtn.className = 'btn-small';
|
|
||||||
delBtn.addEventListener('click', () => {
|
|
||||||
allowedOrigins.splice(idx, 1);
|
|
||||||
renderCorsList();
|
|
||||||
corsAddBtn.disabled = !corsInput.value.trim() || allowedOrigins.includes(corsInput.value.trim());
|
|
||||||
});
|
|
||||||
li.appendChild(delBtn);
|
|
||||||
list.appendChild(li);
|
|
||||||
});
|
|
||||||
corsAddBtn.disabled = !corsInput.value.trim() || allowedOrigins.includes(corsInput.value.trim());
|
|
||||||
}
|
|
||||||
loadCors();
|
|
||||||
document.getElementById('cors-add-btn').addEventListener('click', () => {
|
|
||||||
const input = document.getElementById('cors-new-origin');
|
|
||||||
const val = input.value.trim();
|
|
||||||
if (val && !allowedOrigins.includes(val)) {
|
|
||||||
allowedOrigins.push(val);
|
|
||||||
input.value = '';
|
|
||||||
renderCorsList();
|
|
||||||
corsAddBtn.disabled = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.getElementById('cors-form').addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const statusEl = document.getElementById('cors-status');
|
|
||||||
statusEl.textContent = '';
|
|
||||||
try {
|
|
||||||
const res = await apiFetch('/api/settings/cors', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ ALLOWED_ORIGINS: allowedOrigins })
|
|
||||||
});
|
|
||||||
if (res.ok) {
|
|
||||||
statusEl.innerHTML = 'Сохранено.<br><strong class="warning-text">Требуется перезапуск Admin-server для применения!</strong>';
|
|
||||||
statusEl.style.color = 'green';
|
|
||||||
showToast('Сохранено', 'success');
|
|
||||||
} else {
|
|
||||||
statusEl.textContent = 'Ошибка сохранения';
|
|
||||||
statusEl.style.color = 'red';
|
|
||||||
showToast('Ошибка сохранения', 'error');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
statusEl.textContent = 'Ошибка сохранения';
|
|
||||||
statusEl.style.color = 'red';
|
|
||||||
showToast('Ошибка сохранения', 'error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function initModersSection() {
|
|
||||||
// --- МОДЕРАТОРЫ ---
|
|
||||||
async function loadModers() {
|
|
||||||
const tableBody = document.querySelector('#moders-table tbody');
|
|
||||||
tableBody.innerHTML = '<tr><td colspan="3">Загрузка...</td></tr>';
|
|
||||||
try {
|
|
||||||
const res = await apiFetch('/api/moders');
|
|
||||||
const moders = await res.json();
|
|
||||||
if (!Array.isArray(moders) || !moders.length) {
|
|
||||||
tableBody.innerHTML = '<tr><td colspan="3">Нет модераторов</td></tr>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
tableBody.innerHTML = '';
|
|
||||||
for (const moder of moders) {
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
tr.dataset.id = moder.id;
|
|
||||||
tr.innerHTML = `<td>${moder.id}</td><td>${moder.username}</td>` +
|
|
||||||
`<td>` +
|
|
||||||
`<button class="btn-small" data-action="reset" title="Сменить пароль">🔑</button> ` +
|
|
||||||
`<button class="btn-small" data-action="delete" title="Удалить">🗑️</button>` +
|
|
||||||
`</td>`;
|
|
||||||
tableBody.appendChild(tr);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
tableBody.innerHTML = '<tr><td colspan="3">Ошибка загрузки</td></tr>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loadModers();
|
|
||||||
// Добавление модератора
|
|
||||||
document.getElementById('add-moder-form').addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const username = document.getElementById('add-moder-username').value.trim();
|
|
||||||
const password = document.getElementById('add-moder-password').value;
|
|
||||||
const statusEl = document.getElementById('add-moder-status');
|
|
||||||
statusEl.textContent = '';
|
|
||||||
if (!username || !password) {
|
|
||||||
statusEl.textContent = 'Заполните все поля';
|
|
||||||
statusEl.style.color = 'red';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await apiFetch('/api/moders', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ username, password })
|
|
||||||
});
|
|
||||||
if (res.ok) {
|
|
||||||
statusEl.textContent = 'Модератор добавлен';
|
|
||||||
statusEl.style.color = 'green';
|
|
||||||
document.getElementById('add-moder-form').reset();
|
|
||||||
loadModers();
|
|
||||||
showToast('Модератор добавлен', 'success');
|
|
||||||
} else {
|
|
||||||
const data = await res.json();
|
|
||||||
statusEl.textContent = data.message || 'Ошибка';
|
|
||||||
statusEl.style.color = 'red';
|
|
||||||
showToast(data.message || 'Ошибка', 'error');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
statusEl.textContent = 'Ошибка';
|
|
||||||
statusEl.style.color = 'red';
|
|
||||||
showToast('Ошибка', 'error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Действия: удалить/сменить пароль
|
|
||||||
document.getElementById('moders-table').addEventListener('click', async (e) => {
|
|
||||||
const btn = e.target.closest('button');
|
|
||||||
if (!btn) return;
|
|
||||||
const id = btn.closest('tr').dataset.id;
|
|
||||||
const action = btn.getAttribute('data-action');
|
|
||||||
if (action === 'delete') {
|
|
||||||
showConfirm('Удалить модератора?', async () => {
|
|
||||||
try {
|
|
||||||
const res = await apiFetch(`/api/moders/${id}`, { method: 'DELETE' });
|
|
||||||
if (res.ok) {
|
|
||||||
loadModers();
|
|
||||||
showToast('Модератор удален', 'success');
|
|
||||||
} else {
|
|
||||||
showToast('Ошибка удаления', 'error');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
showToast('Ошибка удаления', 'error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (action === 'reset') {
|
|
||||||
const row = btn.closest('tr');
|
|
||||||
const actionsCell = row.cells[2];
|
|
||||||
|
|
||||||
// Если мы уже в режиме редактирования, ничего не делаем
|
|
||||||
if (actionsCell.querySelector('input')) return;
|
|
||||||
|
|
||||||
// Сохраняем оригинальные кнопки
|
|
||||||
const originalButtons = actionsCell.innerHTML;
|
|
||||||
|
|
||||||
// Создаем поле ввода и кнопки
|
|
||||||
actionsCell.innerHTML = `
|
|
||||||
<input type="password" placeholder="Новый пароль" class="moder-pass-input">
|
|
||||||
<button class="btn-small" data-action="save-pass" title="Сохранить">✅</button>
|
|
||||||
<button class="btn-small" data-action="cancel-pass" title="Отмена">❌</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const input = actionsCell.querySelector('input');
|
|
||||||
input.focus();
|
|
||||||
|
|
||||||
const saveBtn = actionsCell.querySelector('[data-action="save-pass"]');
|
|
||||||
const cancelBtn = actionsCell.querySelector('[data-action="cancel-pass"]');
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
const newPass = input.value;
|
|
||||||
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');
|
|
||||||
actionsCell.innerHTML = originalButtons; // Возвращаем кнопки
|
|
||||||
} else {
|
|
||||||
showToast('Ошибка смены пароля', 'error');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
showToast('Ошибка смены пароля', 'error');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
actionsCell.innerHTML = originalButtons;
|
|
||||||
};
|
|
||||||
|
|
||||||
saveBtn.addEventListener('click', handleSave);
|
|
||||||
cancelBtn.addEventListener('click', handleCancel);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchStatus();
|
|
||||||
fetchConfig();
|
|
||||||
});
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ru">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Модератор: наполнение вкладок</title>
|
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
<link href="/libs/quill/quill.snow.css" rel="stylesheet">
|
|
||||||
<script src="/libs/quill/quill.js"></script>
|
|
||||||
<script src="/libs/dompurify/purify.min.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="notify"></div>
|
|
||||||
<div class="container">
|
|
||||||
<h1>Панель модератора</h1>
|
|
||||||
<div id="login-section">
|
|
||||||
<h2>Вход для модератора</h2>
|
|
||||||
<form id="moder-login-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="username">Логин:</label>
|
|
||||||
<input type="text" id="username" name="username" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">Пароль:</label>
|
|
||||||
<input type="password" id="password" name="password" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit">Войти</button>
|
|
||||||
<p id="login-error" style="color: red;"></p>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div id="editor-section" style="display:none;">
|
|
||||||
<button id="logout-btn" style="float:right;">Выйти из системы</button>
|
|
||||||
<h2>Редактирование вкладок</h2>
|
|
||||||
<div class="tab-controls">
|
|
||||||
<button class="tab-btn active" data-tab="info">Общая информация</button>
|
|
||||||
<button class="tab-btn" data-tab="union">Профсоюз</button>
|
|
||||||
</div>
|
|
||||||
<div style="margin: 18px 0;">
|
|
||||||
<button id="add-block-btn">Добавить блок</button>
|
|
||||||
</div>
|
|
||||||
<div id="blocks-list"></div>
|
|
||||||
</div>
|
|
||||||
<div id="edit-modal" class="modal" style="display:none;">
|
|
||||||
<div class="modal-content">
|
|
||||||
<span class="close" id="edit-modal-close">×</span>
|
|
||||||
<h3 id="edit-modal-title">Блок</h3>
|
|
||||||
<form id="edit-block-form">
|
|
||||||
<input type="hidden" id="edit-block-id">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="edit-quill-editor">Холст:</label>
|
|
||||||
<div id="edit-quill-editor" style="height: 250px;"></div>
|
|
||||||
</div>
|
|
||||||
<div id="edit-preview-label" style="margin-top:10px;font-weight:bold;">Предпросмотр:</div>
|
|
||||||
<div id="edit-block-preview" class="block-text" style="min-height:40px;margin-bottom:10px;"></div>
|
|
||||||
<button type="submit">Сохранить</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="confirm-modal" class="modal" style="display:none; z-index:2000;">
|
|
||||||
<div class="modal-content" style="max-width:400px; text-align:center;">
|
|
||||||
<p style="margin-bottom:20px; ">Вы уверены, что хотите закрыть окно?<br>Несохранённые изменения будут потеряны!</p>
|
|
||||||
<button id="confirm-close-yes" style="background:#e74c3c; margin-right:12px;">Да, закрыть</button>
|
|
||||||
<button id="confirm-close-no">Отмена</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script src="editor.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const loginSection = document.getElementById('login-section');
|
|
||||||
const editorSection = document.getElementById('editor-section');
|
|
||||||
const loginForm = document.getElementById('moder-login-form');
|
|
||||||
const loginError = document.getElementById('login-error');
|
|
||||||
const tabBtns = document.querySelectorAll('.tab-btn');
|
|
||||||
const addBlockBtn = document.getElementById('add-block-btn');
|
|
||||||
const blocksList = document.getElementById('blocks-list');
|
|
||||||
const logoutBtn = document.getElementById('logout-btn');
|
|
||||||
loginForm.addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
loginError.textContent = '';
|
|
||||||
const username = loginForm.username.value.trim();
|
|
||||||
const password = loginForm.password.value;
|
|
||||||
try {
|
|
||||||
const res = await fetch('/moders/moder-login', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ username, password })
|
|
||||||
});
|
|
||||||
if (res.ok) {
|
|
||||||
loginSection.style.display = 'none';
|
|
||||||
editorSection.style.display = '';
|
|
||||||
loadBlocks(currentTab);
|
|
||||||
} else {
|
|
||||||
const data = await res.json();
|
|
||||||
loginError.textContent = data.message || 'Ошибка входа';
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
loginError.textContent = 'Ошибка сервера';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (logoutBtn) {
|
|
||||||
logoutBtn.onclick = async () => {
|
|
||||||
await fetch('/moders/moder-logout');
|
|
||||||
editorSection.style.display = 'none';
|
|
||||||
loginSection.style.display = '';
|
|
||||||
showNotify('Вы вышли из системы', 'success');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
tabBtns.forEach(btn => {
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
tabBtns.forEach(b => b.classList.remove('active'));
|
|
||||||
btn.classList.add('active');
|
|
||||||
currentTab = btn.dataset.tab;
|
|
||||||
loadBlocks(currentTab);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
function loadBlocks(tab) {
|
|
||||||
fetch(`/moders/content/${tab}`)
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
blocksList.innerHTML = '';
|
|
||||||
if (!Array.isArray(data) || !data.length) {
|
|
||||||
blocksList.innerHTML = '<p>Нет блоков</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
data.forEach(block => {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.className = 'content-block';
|
|
||||||
div.innerHTML = `<div class="block-preview" style="margin-bottom:10px;">${DOMPurify.sanitize(block.content)}</div>`;
|
|
||||||
const editBtn = document.createElement('button');
|
|
||||||
editBtn.textContent = 'Редактировать';
|
|
||||||
editBtn.onclick = () => openEditModal(block);
|
|
||||||
const delBtn = document.createElement('button');
|
|
||||||
delBtn.textContent = 'Удалить';
|
|
||||||
delBtn.onclick = async () => {
|
|
||||||
if (confirm('Удалить блок?')) {
|
|
||||||
await fetch(`/moders/content/${tab}/${block.id}`, { method: 'DELETE' });
|
|
||||||
showNotify('Блок удалён', 'success');
|
|
||||||
loadBlocks(tab);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
div.appendChild(editBtn);
|
|
||||||
div.appendChild(delBtn);
|
|
||||||
blocksList.appendChild(div);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const editModal = document.getElementById('edit-modal');
|
|
||||||
const editModalClose = document.getElementById('edit-modal-close');
|
|
||||||
const editBlockForm = document.getElementById('edit-block-form');
|
|
||||||
const editBlockId = document.getElementById('edit-block-id');
|
|
||||||
const editModalTitle = document.getElementById('edit-modal-title');
|
|
||||||
let editQuill;
|
|
||||||
let currentTab = 'info';
|
|
||||||
let editingBlockId = null;
|
|
||||||
let originalContent = '';
|
|
||||||
function showNotify(msg, type = 'success') {
|
|
||||||
const notify = document.getElementById('notify');
|
|
||||||
notify.textContent = msg;
|
|
||||||
notify.className = 'show ' + type;
|
|
||||||
setTimeout(() => { notify.className = ''; }, 2200);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof Quill === 'undefined' || typeof DOMPurify === 'undefined') {
|
|
||||||
console.error('Required libraries not loaded');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Инициализируйте Quill
|
|
||||||
if (!initQuill()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function initQuill() {
|
|
||||||
if (typeof Quill === 'undefined') {
|
|
||||||
console.error('Quill not loaded');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
editQuill = new Quill('#edit-quill-editor', {
|
|
||||||
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']
|
|
||||||
]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
function openEditModal(block) {
|
|
||||||
editingBlockId = block ? block.id : null;
|
|
||||||
editBlockId.value = block ? block.id : '';
|
|
||||||
editModalTitle.textContent = block ? 'Редактировать блок' : 'Добавить блок';
|
|
||||||
editQuill.root.innerHTML = block ? block.content : '';
|
|
||||||
document.getElementById('edit-block-preview').innerHTML = block ? DOMPurify.sanitize(block.content) : '';
|
|
||||||
originalContent = block ? block.content : '';
|
|
||||||
editModal.style.display = 'block';
|
|
||||||
}
|
|
||||||
editQuill.on('text-change', () => {
|
|
||||||
document.getElementById('edit-block-preview').innerHTML = DOMPurify.sanitize(editQuill.root.innerHTML);
|
|
||||||
});
|
|
||||||
function tryCloseEditModal() {
|
|
||||||
const currentContent = editQuill.root.innerHTML;
|
|
||||||
if (currentContent !== originalContent) {
|
|
||||||
document.getElementById('confirm-modal').style.display = 'flex';
|
|
||||||
const yesBtn = document.getElementById('confirm-close-yes');
|
|
||||||
const noBtn = document.getElementById('confirm-close-no');
|
|
||||||
yesBtn.onclick = function() {
|
|
||||||
document.getElementById('confirm-modal').style.display = 'none';
|
|
||||||
editModal.style.display = 'none';
|
|
||||||
};
|
|
||||||
noBtn.onclick = function() {
|
|
||||||
document.getElementById('confirm-modal').style.display = 'none';
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
editModal.style.display = 'none';
|
|
||||||
}
|
|
||||||
editModalClose.onclick = tryCloseEditModal;
|
|
||||||
window.onclick = (event) => {
|
|
||||||
if (event.target === editModal) tryCloseEditModal();
|
|
||||||
};
|
|
||||||
addBlockBtn.onclick = () => openEditModal(null);
|
|
||||||
editBlockForm.addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const content = DOMPurify.sanitize(editQuill.root.innerHTML.trim());
|
|
||||||
if (!content || content === '<p><br></p>') return;
|
|
||||||
if (editingBlockId) {
|
|
||||||
await fetch(`/moders/content/${currentTab}/${editingBlockId}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ type: 'text', content })
|
|
||||||
});
|
|
||||||
showNotify('Блок обновлён', 'success');
|
|
||||||
} else {
|
|
||||||
await fetch(`/moders/content/${currentTab}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ type: 'text', content })
|
|
||||||
});
|
|
||||||
showNotify('Блок добавлен', 'success');
|
|
||||||
}
|
|
||||||
editModal.style.display = 'none';
|
|
||||||
loadBlocks(currentTab);
|
|
||||||
});
|
|
||||||
fetch('/moders/check-session', { credentials: 'include' }).then(res => {
|
|
||||||
if (res.ok) {
|
|
||||||
loginSection.style.display = 'none';
|
|
||||||
editorSection.style.display = '';
|
|
||||||
loadBlocks(currentTab);
|
|
||||||
} else {
|
|
||||||
loginSection.style.display = '';
|
|
||||||
editorSection.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let csrfToken = '';
|
|
||||||
fetch('/moders/csrf-token').then(res => res.json()).then(data => {
|
|
||||||
csrfToken = data.csrfToken;
|
|
||||||
});
|
|
||||||
|
|
||||||
const originalFetch = window.fetch;
|
|
||||||
window.fetch = function(url, options = {}) {
|
|
||||||
if (options && options.method && ['POST','PUT','DELETE'].includes(options.method.toUpperCase())) {
|
|
||||||
options.headers = options.headers || {};
|
|
||||||
options.headers['x-csrf-token'] = csrfToken;
|
|
||||||
}
|
|
||||||
return originalFetch(url, options);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ru">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Панель администратора</title>
|
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="toast-container"></div>
|
|
||||||
<div class="container">
|
|
||||||
<h1>Панель администратора</h1>
|
|
||||||
<div class="tabs">
|
|
||||||
<button id="tab-main" class="tab-btn active">Общее</button>
|
|
||||||
<button id="tab-settings" class="tab-btn">Настройки</button>
|
|
||||||
<button id="tab-monitoring" class="tab-btn">Мониторинг</button>
|
|
||||||
</div>
|
|
||||||
<div id="main-tab">
|
|
||||||
<div id="status">
|
|
||||||
<p>Статус сервера: <span id="server-status">загрузка...</span></p>
|
|
||||||
<button id="refresh-status-btn" class="btn-small">Обновить</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="db-status-container">
|
|
||||||
<div>
|
|
||||||
<p>Админ -> БД: <span id="admin-db-status">неизвестно</span></p>
|
|
||||||
<small id="admin-db-message"></small>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p>Программа -> БД: <span id="backend-db-status">неизвестно</span></p>
|
|
||||||
<small id="backend-db-message"></small>
|
|
||||||
</div>
|
|
||||||
<button id="check-db-btn" class="btn-small">Проверить</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="network-diag-container">
|
|
||||||
<h2>Сетевая диагностика (Ping)</h2>
|
|
||||||
<div class="ping-controls">
|
|
||||||
<input type="text" id="ping-ip" placeholder="Введите IP-адрес или хост">
|
|
||||||
<button id="ping-btn" class="btn-small">Пинг</button>
|
|
||||||
<button id="ping-stop-btn" class="btn-small" disabled>Стоп</button>
|
|
||||||
<button id="ping-clear-btn" class="btn-small">Очистить</button>
|
|
||||||
</div>
|
|
||||||
<pre id="ping-output"></pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="logs-container">
|
|
||||||
<h2>Логи приложения</h2>
|
|
||||||
<div class="log-controls">
|
|
||||||
<button id="refresh-logs-btn" class="btn-small">Обновить логи</button>
|
|
||||||
<button id="clear-logs-btn" class="btn-small">Очистить</button>
|
|
||||||
</div>
|
|
||||||
<pre id="logs-content">Нажмите "Обновить логи" для просмотра</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="logs-container">
|
|
||||||
<h2>Логи считывателя</h2>
|
|
||||||
<div class="log-controls">
|
|
||||||
<button id="refresh-card-logs-btn" class="btn-small">Обновить логи считывателя</button>
|
|
||||||
</div>
|
|
||||||
<pre id="ping-output">
|
|
||||||
<pre id="card-logs-content">Нажмите "Обновить логи считывателя" для просмотра</pre>
|
|
||||||
</pre></pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="config-container">
|
|
||||||
<h2>Конфигурация</h2>
|
|
||||||
<form id="config-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="payslipsPath">Путь к расчетным листкам:</label>
|
|
||||||
<div class="input-with-button">
|
|
||||||
<input type="text" id="payslipsPath" name="payslipsPath" required>
|
|
||||||
<button type="button" id="browse-btn">Обзор...</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="submit">Сохранить</button>
|
|
||||||
</form>
|
|
||||||
<p id="config-status"></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="settings-tab" style="display:none">
|
|
||||||
<h2>Настройки</h2>
|
|
||||||
<div id="server-params-section"><h3>Параметры серверов</h3>
|
|
||||||
<form id="server-params-form" novalidate>
|
|
||||||
<fieldset><legend>Admin-server</legend>
|
|
||||||
<label>PORT: <input type="number" id="admin-port" min="1" max="65535" required></label><br>
|
|
||||||
<label>ADMIN_URL: <input type="text" id="admin-url" pattern="https?://.+" required></label>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset><legend>Backend</legend>
|
|
||||||
<label>host: <input type="text" id="backend-host" required></label><br>
|
|
||||||
<label>port: <input type="number" id="backend-port" min="1" max="65535" required></label>
|
|
||||||
</fieldset>
|
|
||||||
<button type="submit">Сохранить</button>
|
|
||||||
<span id="server-params-status" class="form-error"></span>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div id="db-section"><h3>Подключения к БД</h3>
|
|
||||||
<form id="mssql-form" novalidate><fieldset><legend>MS SQL</legend>
|
|
||||||
<label>DB_USER: <input type="text" id="mssql-user" required></label><br>
|
|
||||||
<label>DB_PASSWORD:
|
|
||||||
<div class="password-wrapper">
|
|
||||||
<input type="password" id="mssql-pass" readonly>
|
|
||||||
<button type="button" class="btn-small" data-action="edit-pass" data-target="mssql-pass">Изменить</button>
|
|
||||||
</div>
|
|
||||||
</label><br>
|
|
||||||
<label>DB_SERVER: <input type="text" id="mssql-server" required></label><br>
|
|
||||||
<label>DB_NAME: <input type="text" id="mssql-db" required></label><br>
|
|
||||||
<button type="submit">Сохранить</button>
|
|
||||||
<span id="mssql-status" class="form-error"></span>
|
|
||||||
</fieldset></form>
|
|
||||||
<form id="mysql-form" novalidate><fieldset><legend>MySQL (модераторы)</legend>
|
|
||||||
<label>MYSQL_HOST: <input type="text" id="mysql-host" required></label><br>
|
|
||||||
<label>MYSQL_PORT: <input type="number" id="mysql-port" min="1" max="65535" required></label><br>
|
|
||||||
<label>MYSQL_USER: <input type="text" id="mysql-user" required></label><br>
|
|
||||||
<label>MYSQL_PASSWORD:
|
|
||||||
<div class="password-wrapper">
|
|
||||||
<input type="password" id="mysql-pass" readonly>
|
|
||||||
<button type="button" class="btn-small" data-action="edit-pass" data-target="mysql-pass">Изменить</button>
|
|
||||||
</div>
|
|
||||||
</label><br>
|
|
||||||
<label>MYSQL_DATABASE: <input type="text" id="mysql-db" required></label><br>
|
|
||||||
<button type="submit">Сохранить</button>
|
|
||||||
<span id="mysql-status" class="form-error"></span>
|
|
||||||
</fieldset></form>
|
|
||||||
</div>
|
|
||||||
<div id="cors-section"><h3>CORS (ALLOWED_ORIGINS)</h3>
|
|
||||||
<form id="cors-form">
|
|
||||||
<ul id="cors-list"></ul>
|
|
||||||
<input type="text" id="cors-new-origin" placeholder="Добавить origin...">
|
|
||||||
<button type="button" id="cors-add-btn" disabled>Добавить</button>
|
|
||||||
<button type="submit">Сохранить</button>
|
|
||||||
<span id="cors-status" class="form-error"></span>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div id="moders-section"><h3>Модераторы</h3>
|
|
||||||
<form id="add-moder-form" novalidate>
|
|
||||||
<input type="text" id="add-moder-username" placeholder="Логин" required>
|
|
||||||
<input type="password" id="add-moder-password" placeholder="Пароль (мин. 4 символа)" minlength="4" required>
|
|
||||||
<button type="submit">Добавить</button>
|
|
||||||
<span id="add-moder-status" class="form-error"></span>
|
|
||||||
</form>
|
|
||||||
<table id="moders-table" class="moders-table"><thead><tr><th>ID</th><th>Логин</th><th>Действия</th></tr></thead><tbody></tbody></table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="monitoring-tab" style="display:none;">
|
|
||||||
<h2>Мониторинг системы</h2>
|
|
||||||
<div id="monitoring-status-container">
|
|
||||||
<!-- Статусы будут здесь -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3>Проблемные события (Warn/Error)</h3>
|
|
||||||
<div class="monitoring-filters">
|
|
||||||
<label>Уровень:
|
|
||||||
<select id="log-level-filter">
|
|
||||||
<option value="all">Все</option>
|
|
||||||
<option value="ERROR">Error</option>
|
|
||||||
<option value="WARN">Warn</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>Источник:
|
|
||||||
<select id="log-source-filter">
|
|
||||||
<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" class="btn-small">Обновить</button>
|
|
||||||
</div>
|
|
||||||
<table id="monitoring-log-table" class="moders-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style="width: 20%;">Время</th>
|
|
||||||
<th style="width: 10%;">Уровень</th>
|
|
||||||
<th style="width: 15%;">Источник</th>
|
|
||||||
<th>Сообщение</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<!-- Данные будут здесь -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Модальное окно для подтверждений -->
|
|
||||||
<div id="confirm-modal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<p id="confirm-modal-text">Вы уверены?</p>
|
|
||||||
<div class="modal-buttons">
|
|
||||||
<button id="confirm-modal-yes" class="btn-danger">Да</button>
|
|
||||||
<button id="confirm-modal-no" class="btn-secondary">Отмена</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="app.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ru">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Вход в панель администратора</title>
|
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>Вход</h1>
|
|
||||||
<form id="login-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">Пароль:</label>
|
|
||||||
<input type="password" id="password" name="password" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit">Войти</button>
|
|
||||||
</form>
|
|
||||||
<p id="error-message" style="color: red;"></p>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
document.getElementById('login-form').addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const password = document.getElementById('password').value;
|
|
||||||
const response = await fetch('/api/auth', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ password })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
window.location.href = '/';
|
|
||||||
} else {
|
|
||||||
document.getElementById('error-message').textContent = 'Неверный пароль';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
const cardController = require('../controllers/cardController');
|
|
||||||
|
|
||||||
router.post('/card-event', cardController.cardEvent);
|
|
||||||
router.get('/card-logs', cardController.getCardLogs);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
const healthController = require('../controllers/healthController');
|
|
||||||
|
|
||||||
router.get('/check-db', healthController.checkDb);
|
|
||||||
router.get('/status', healthController.checkStatus);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router(); // Сначала инициализируем router
|
|
||||||
const logsController = require('../controllers/logsController');
|
|
||||||
const checkAuth = require('../middleware/checkAuth');
|
|
||||||
console.log('Imported router:', router.toString());
|
|
||||||
|
|
||||||
router.get('/', checkAuth, logsController.getLogs);
|
|
||||||
router.post('/clear', checkAuth, logsController.clearLogs);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
const modersController = require('../controllers/modersController');
|
|
||||||
const checkAuth = require('../middleware/checkAuth');
|
|
||||||
|
|
||||||
router.get('/', checkAuth, modersController.getModers);
|
|
||||||
router.post('/', checkAuth, modersController.addModer);
|
|
||||||
router.put('/:id', checkAuth, modersController.changePassword);
|
|
||||||
router.delete('/:id', checkAuth, modersController.deleteModer);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
const logsController = require('../controllers/logsController');
|
|
||||||
const checkAuth = require('../middleware/checkAuth');
|
|
||||||
|
|
||||||
router.get('/logs', checkAuth, logsController.getMonitoringLogs);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
const settingsController = require('../controllers/settingsController');
|
|
||||||
const checkAuth = require('../middleware/checkAuth');
|
|
||||||
|
|
||||||
router.get('/mysql', checkAuth, settingsController.getMysqlSettings);
|
|
||||||
router.post('/mysql', checkAuth, settingsController.updateMysqlSettings);
|
|
||||||
|
|
||||||
router.get('/cors', checkAuth, settingsController.getCorsSettings);
|
|
||||||
router.post('/cors', checkAuth, settingsController.updateCorsSettings);
|
|
||||||
|
|
||||||
router.get('/admin-server', checkAuth, settingsController.getAdminServerSettings);
|
|
||||||
router.post('/admin-server', checkAuth, settingsController.updateAdminServerSettings);
|
|
||||||
|
|
||||||
router.get('/backend-server', checkAuth, settingsController.getBackendServerSettings);
|
|
||||||
router.post('/backend-server', checkAuth, settingsController.updateBackendServerSettings);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
const fs = require('fs-extra');
|
|
||||||
const cors = require('cors');
|
|
||||||
const express = require('express');
|
|
||||||
const morgan = require('morgan');
|
|
||||||
const path = require('path');
|
|
||||||
const session = require('express-session');
|
|
||||||
require('dotenv').config({ path: path.join(__dirname, './config.env') });
|
|
||||||
require('dotenv').config({ path: __dirname + '/moders/config.env' });
|
|
||||||
const logger = require('./logger');
|
|
||||||
const moderAuthRouter = require('./moders/routes/moderAuth');
|
|
||||||
const moderContentRouter = require('./moders/routes/content');
|
|
||||||
const authRouter = require('./routes/auth');
|
|
||||||
const cardRouter = require('./routes/card');
|
|
||||||
const logsRouter = require('./routes/logs');
|
|
||||||
const settingsRouter = require('./routes/settings');
|
|
||||||
const modersRouter = require('./routes/moders');
|
|
||||||
const healthRouter = require('./routes/health');
|
|
||||||
const monitoringRouter = require('./routes/monitoring');
|
|
||||||
const setupWebSocket = require('./services/websocket');
|
|
||||||
const cardController = require('./controllers/cardController');
|
|
||||||
const app = express();
|
|
||||||
const server = require('http').createServer(app);
|
|
||||||
|
|
||||||
// Конфигурации
|
|
||||||
const corsConfig = require('./config/cors');
|
|
||||||
const sessionConfig = require('./config/session');
|
|
||||||
app.use('/libs/quill', express.static(path.join(__dirname, 'node_modules/quill/dist')));
|
|
||||||
app.use('/libs/dompurify', express.static(path.join(__dirname, 'node_modules/dompurify/dist')));
|
|
||||||
// Middleware
|
|
||||||
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('/login.html', express.static(path.join(__dirname, 'public', 'login.html')));
|
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
|
||||||
app.use('/uploads', express.static(__dirname + '/uploads'));
|
|
||||||
// Роуты
|
|
||||||
app.use('/api/card', cardRouter);
|
|
||||||
app.use('/api/logs', logsRouter);
|
|
||||||
app.use('/api/settings', settingsRouter);
|
|
||||||
app.use('/api/moders', modersRouter);
|
|
||||||
app.use('/api/health', healthRouter);
|
|
||||||
app.use('/api/monitoring', monitoringRouter);
|
|
||||||
app.use('/moders', moderAuthRouter);
|
|
||||||
app.use('/moders/content', moderContentRouter);
|
|
||||||
// Initialize card logs
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
await cardController.initCardLogs();
|
|
||||||
logger.info('Card logs initialized successfully');
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('Failed to initialize card logs:', err);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
// WebSocket
|
|
||||||
setupWebSocket(server);
|
|
||||||
|
|
||||||
// Обработчик ошибок (должен быть последним!)
|
|
||||||
app.use(require('./middleware/errorHandler'));
|
|
||||||
app.get('/api/status', async (req, res) => {
|
|
||||||
try {
|
|
||||||
res.json({ status: 'online' }); // или другая логика проверки
|
|
||||||
} catch (err) {
|
|
||||||
res.status(500).json({ status: 'error', message: err.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/config', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const backendConfigPath = path.join(__dirname, '../backend/config.json');
|
|
||||||
const backendConfig = await fs.readJson(backendConfigPath);
|
|
||||||
const config = {
|
|
||||||
payslipsPath: backendConfig.payslipsPath
|
|
||||||
};
|
|
||||||
res.json(config);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('Ошибка чтения backend/config.json:', err);
|
|
||||||
res.status(500).json({ error: 'Ошибка загрузки конфигурации' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Добавьте рядом с существующим GET обработчиком
|
|
||||||
app.post('/api/config', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const backendConfigPath = path.join(__dirname, '../backend/config.json');
|
|
||||||
const currentConfig = await fs.readJson(backendConfigPath);
|
|
||||||
|
|
||||||
// Обновляем только разрешенные поля
|
|
||||||
if (req.body.payslipsPath) {
|
|
||||||
currentConfig.payslipsPath = req.body.payslipsPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сохраняем обновленный конфиг
|
|
||||||
await fs.writeJson(backendConfigPath, currentConfig, { spaces: 4 });
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: 'Конфигурация успешно обновлена'
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('Ошибка сохранения конфигурации:', err);
|
|
||||||
res.status(500).json({
|
|
||||||
error: 'Ошибка сохранения конфигурации',
|
|
||||||
details: err.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
server.listen(process.env.PORT || 3005, () => {
|
|
||||||
logger.info(`Server running on ${process.env.ADMIN_URL}`);
|
|
||||||
});
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
const WebSocket = require('ws');
|
|
||||||
const iconv = require('iconv-lite');
|
|
||||||
const { spawn } = require('child_process');
|
|
||||||
const logger = require('../logger');
|
|
||||||
|
|
||||||
function setupWebSocket(server) {
|
|
||||||
const wss = new WebSocket.Server({ server });
|
|
||||||
|
|
||||||
wss.on('connection', (ws) => {
|
|
||||||
logger.info('New WebSocket connection');
|
|
||||||
|
|
||||||
let pingProcess = null;
|
|
||||||
|
|
||||||
ws.on('message', (message) => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(message);
|
|
||||||
|
|
||||||
if (data.type === 'ping') {
|
|
||||||
const ip = data.ip;
|
|
||||||
|
|
||||||
// Валидация IP
|
|
||||||
if (!ip || !/^[a-zA-Z0-9\.\-_:]+$/.test(ip)) {
|
|
||||||
ws.send(JSON.stringify({
|
|
||||||
type: 'error',
|
|
||||||
message: 'Invalid IP address format'
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Завершаем предыдущий процесс, если есть
|
|
||||||
if (pingProcess) {
|
|
||||||
pingProcess.kill();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Запускаем ping (для Windows)
|
|
||||||
pingProcess = spawn('ping', ['-t', ip]);
|
|
||||||
|
|
||||||
// Обработка вывода
|
|
||||||
pingProcess.stdout.on('data', (data) => {
|
|
||||||
const decodedData = iconv.decode(data, 'cp866');
|
|
||||||
ws.send(JSON.stringify({
|
|
||||||
type: 'data',
|
|
||||||
data: decodedData
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
pingProcess.stderr.on('data', (data) => {
|
|
||||||
const decodedData = iconv.decode(data, 'cp866');
|
|
||||||
ws.send(JSON.stringify({
|
|
||||||
type: 'error',
|
|
||||||
data: decodedData
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
pingProcess.on('close', (code) => {
|
|
||||||
ws.send(JSON.stringify({
|
|
||||||
type: 'close',
|
|
||||||
data: `Process exited with code ${code}`
|
|
||||||
}));
|
|
||||||
pingProcess = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('WebSocket message error:', e);
|
|
||||||
ws.send(JSON.stringify({
|
|
||||||
type: 'error',
|
|
||||||
message: 'Invalid message format',
|
|
||||||
details: e.message
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.on('close', () => {
|
|
||||||
logger.info('WebSocket connection closed');
|
|
||||||
if (pingProcess) {
|
|
||||||
pingProcess.kill();
|
|
||||||
pingProcess = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.on('error', (error) => {
|
|
||||||
logger.error('WebSocket error:', error);
|
|
||||||
if (pingProcess) {
|
|
||||||
pingProcess.kill();
|
|
||||||
pingProcess = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return wss;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = setupWebSocket;
|
|
||||||
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 8.6 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>
|
|
||||||
@@ -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,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 +1,5 @@
|
|||||||
const logger = require('../logger');
|
const logger = require('../logger');
|
||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcryptjs');
|
||||||
const moderDb = require('../moders/db');
|
const moderDb = require('../moders/db');
|
||||||
require('dotenv').config({ path: __dirname + './config.env' });
|
require('dotenv').config({ path: __dirname + './config.env' });
|
||||||
exports.login = (req, res) => {
|
exports.login = (req, res) => {
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs').promises;
|
||||||
|
const { existsSync, appendFileSync } = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const logger = require('../logger');
|
const logger = require('../logger');
|
||||||
|
|
||||||
const CARD_LOG_PATH = path.resolve(__dirname, '..', process.env.CARD_LOG_PATH);
|
const CARD_LOG_PATH = path.resolve(__dirname, '..', process.env.CARD_LOG_PATH);
|
||||||
const initCardLogs = async () => {
|
const initCardLogs = async () => {
|
||||||
try {
|
try {
|
||||||
await fs.ensureDir(path.dirname(CARD_LOG_PATH));
|
await fs.mkdir(path.dirname(CARD_LOG_PATH), { recursive: true });
|
||||||
if (!(await fs.pathExists(CARD_LOG_PATH))) {
|
try {
|
||||||
|
await fs.access(CARD_LOG_PATH);
|
||||||
|
} catch (e) {
|
||||||
await fs.writeFile(CARD_LOG_PATH, '');
|
await fs.writeFile(CARD_LOG_PATH, '');
|
||||||
logger.info(`Card log file created at: ${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`;
|
const logLine = `[${new Date(timestamp).toISOString()}] [${event}] UID: ${uid}\n`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(CARD_LOG_PATH)) {
|
if (!existsSync(CARD_LOG_PATH)) {
|
||||||
initCardLogs();
|
initCardLogs();
|
||||||
}
|
}
|
||||||
fs.appendFileSync(CARD_LOG_PATH, logLine);
|
appendFileSync(CARD_LOG_PATH, logLine);
|
||||||
logger.info('[CARD EVENT] Log written to:', CARD_LOG_PATH);
|
logger.info('[CARD EVENT] Log written to:', CARD_LOG_PATH);
|
||||||
return res.status(200).json({ message: 'OK', path: 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) => {
|
const getCardLogs = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
if (!(await fs.pathExists(CARD_LOG_PATH))) {
|
try {
|
||||||
|
await fs.access(CARD_LOG_PATH);
|
||||||
|
} catch (e) {
|
||||||
await initCardLogs();
|
await initCardLogs();
|
||||||
return res.status(200).type('text/plain').send('Card logs initialized');
|
return res.status(200).type('text/plain').send('Card logs initialized');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs').promises;
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const logger = require('../logger');
|
const logger = require('../logger');
|
||||||
|
|
||||||
@@ -40,7 +40,9 @@ const getMonitoringLogs = async (req, res) => {
|
|||||||
|
|
||||||
for (const source of logSources) {
|
for (const source of logSources) {
|
||||||
try {
|
try {
|
||||||
if (!(await fs.pathExists(source.path))) {
|
try {
|
||||||
|
await fs.access(source.path);
|
||||||
|
} catch (e) {
|
||||||
logger.warn(`Log file not found: ${source.path}`);
|
logger.warn(`Log file not found: ${source.path}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const moderDb = require('../moders/db');
|
const moderDb = require('../moders/db');
|
||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcryptjs');
|
||||||
const logger = require('../logger');
|
const logger = require('../logger');
|
||||||
|
|
||||||
const getModers = async (req, res) => {
|
const getModers = async (req, res) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs').promises;
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const logger = require('../logger');
|
const logger = require('../logger');
|
||||||
|
|
||||||
@@ -131,7 +131,8 @@ const updateAdminServerSettings = async (req, res) => {
|
|||||||
|
|
||||||
const getBackendServerSettings = async (req, res) => {
|
const getBackendServerSettings = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const config = await fs.readJson(backendConfigPath);
|
const content = await fs.readFile(backendConfigPath, 'utf8');
|
||||||
|
const config = JSON.parse(content);
|
||||||
res.json({
|
res.json({
|
||||||
host: config.server?.host || '',
|
host: config.server?.host || '',
|
||||||
port: config.server?.port || ''
|
port: config.server?.port || ''
|
||||||
@@ -148,11 +149,12 @@ const updateBackendServerSettings = async (req, res) => {
|
|||||||
return res.status(400).json({ message: 'host и port обязательны' });
|
return res.status(400).json({ message: 'host и port обязательны' });
|
||||||
}
|
}
|
||||||
try {
|
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 = config.server || {};
|
||||||
config.server.host = host;
|
|
||||||
config.server.port = port;
|
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 обновлены' });
|
res.json({ message: 'Параметры backend-server обновлены' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Ошибка записи backend/config.json:', e);
|
logger.error('Ошибка записи backend/config.json:', e);
|
||||||
|
|||||||
@@ -1516,3 +1516,75 @@
|
|||||||
[2026-08-08T13:59:07.971Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content
|
[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: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-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] Ответ отправлен
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcryptjs');
|
||||||
const db = require('../db');
|
const db = require('../db');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const logger = require('../../logger');
|
const logger = require('../../logger');
|
||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
const csurf = require('csurf');
|
|
||||||
|
|
||||||
const csrfProtection = csurf({ cookie: false });
|
|
||||||
|
|
||||||
const loginLimiter = rateLimit({
|
const loginLimiter = rateLimit({
|
||||||
windowMs: 30 * 60 * 1000, // 30 минут
|
windowMs: 30 * 60 * 1000, // 30 минут
|
||||||
@@ -16,10 +13,6 @@ const loginLimiter = rateLimit({
|
|||||||
legacyHeaders: false
|
legacyHeaders: false
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/csrf-token', csrfProtection, (req, res) => {
|
|
||||||
res.json({ csrfToken: req.csrfToken() });
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/moder-login', loginLimiter, async (req, res) => {
|
router.post('/moder-login', loginLimiter, async (req, res) => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
logger.info(`[LOGIN] Попытка входа: ${username}`);
|
logger.info(`[LOGIN] Попытка входа: ${username}`);
|
||||||
|
|||||||
@@ -15,24 +15,19 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcryptjs": "^3.0.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"csurf": "^1.11.0",
|
|
||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
"electron": "^37.0.0",
|
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-mysql-session": "^3.0.3",
|
"express-mysql-session": "^3.0.3",
|
||||||
"express-rate-limit": "^8.0.1",
|
"express-rate-limit": "^8.0.1",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"fs-extra": "^11.3.0",
|
"helmet": "^8.3.0",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^2.0.1",
|
"multer": "^2.0.1",
|
||||||
"mysql2": "^3.14.2",
|
"mysql2": "^3.14.2",
|
||||||
"quill": "^2.0.3",
|
"quill": "^2.0.3",
|
||||||
"ws": "^8.18.3"
|
"ws": "^8.18.3"
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"electron-builder": "^26.15.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const authController = require('../controllers/authController');
|
const authController = require('../controllers/authController');
|
||||||
const csurf = require('csurf');
|
|
||||||
|
|
||||||
const csrfProtection = csurf({ cookie: false });
|
|
||||||
|
|
||||||
router.post('/', authController.login);
|
router.post('/', authController.login);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs').promises;
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
|
const helmet = require('helmet');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const morgan = require('morgan');
|
const morgan = require('morgan');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
@@ -26,15 +27,15 @@ const corsConfig = require('./config/cors');
|
|||||||
const sessionConfig = require('./config/session');
|
const sessionConfig = require('./config/session');
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
|
app.use(helmet()); // Добавление helmet для защиты
|
||||||
app.use(cors(corsConfig));
|
app.use(cors(corsConfig));
|
||||||
app.use(morgan('dev'));
|
app.use(morgan('dev'));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(session(sessionConfig(session)));
|
app.use(session(sessionConfig(session)));
|
||||||
// Роуты, которые не требуют CSRF
|
// Роуты
|
||||||
app.use('/api/auth', authRouter);
|
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'));
|
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')) {
|
if (req.path.startsWith('/api/') || req.path.startsWith('/moders')) {
|
||||||
return next();
|
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) => {
|
app.get('/api/config', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const backendConfigPath = path.join(__dirname, '../backend/config.json');
|
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 = {
|
const config = {
|
||||||
payslipsPath: backendConfig.payslipsPath
|
payslipsPath: backendConfig.payslipsPath
|
||||||
};
|
};
|
||||||
@@ -94,7 +96,8 @@ app.get('/api/config', async (req, res) => {
|
|||||||
app.post('/api/config', async (req, res) => {
|
app.post('/api/config', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const backendConfigPath = path.join(__dirname, '../backend/config.json');
|
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) {
|
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({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -13,14 +13,12 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^17.2.1",
|
||||||
"express": "^4.18.2",
|
"express": "^5.1.0",
|
||||||
"fs-extra": "^11.3.0",
|
|
||||||
"mammoth": "^1.9.1",
|
"mammoth": "^1.9.1",
|
||||||
"mysql2": "^3.14.2",
|
"mysql2": "^3.14.2",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0"
|
||||||
"winston-daily-rotate-file": "^5.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^2.0.22"
|
"nodemon": "^2.0.22"
|
||||||
|
|||||||
@@ -4,9 +4,14 @@
|
|||||||
|
|
||||||
const mysql = require('mysql2/promise'); // ✅ Используем promise-версию
|
const mysql = require('mysql2/promise'); // ✅ Используем promise-версию
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs').promises;
|
||||||
const { logger } = require('./logger');
|
const { logger } = require('./logger');
|
||||||
|
|
||||||
|
async function readJson(filePath) {
|
||||||
|
const data = await fs.readFile(filePath, 'utf8');
|
||||||
|
return JSON.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
// Загружаем переменные окружения
|
// Загружаем переменные окружения
|
||||||
require('dotenv').config({ path: path.join(__dirname, '../../config.env') });
|
require('dotenv').config({ path: path.join(__dirname, '../../config.env') });
|
||||||
|
|
||||||
@@ -42,7 +47,7 @@ let isReconnecting = false;
|
|||||||
async function checkTestMode() {
|
async function checkTestMode() {
|
||||||
try {
|
try {
|
||||||
const configPath = path.join(__dirname, '../../config.json');
|
const configPath = path.join(__dirname, '../../config.json');
|
||||||
const config = await fs.readJson(configPath);
|
const config = await readJson(configPath);
|
||||||
return config.testMode === true;
|
return config.testMode === true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn('⚠️ Не удалось прочитать config.json для проверки testMode', { error: err.message });
|
logger.warn('⚠️ Не удалось прочитать config.json для проверки testMode', { error: err.message });
|
||||||
|
|||||||
@@ -5,10 +5,15 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs').promises;
|
||||||
const { query, queryOne, isDBConnected, getDBStatus } = require('../config/db');
|
const { query, queryOne, isDBConnected, getDBStatus } = require('../config/db');
|
||||||
const { logger } = require('../config/logger_cards');
|
const { logger } = require('../config/logger_cards');
|
||||||
|
|
||||||
|
async function readJson(filePath) {
|
||||||
|
const data = await fs.readFile(filePath, 'utf8');
|
||||||
|
return JSON.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 📊 Вспомогательные функции
|
// 📊 Вспомогательные функции
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -19,7 +24,7 @@ const { logger } = require('../config/logger_cards');
|
|||||||
async function getMockEmployee(searchParam, type = 'card') {
|
async function getMockEmployee(searchParam, type = 'card') {
|
||||||
try {
|
try {
|
||||||
const configPath = path.join(__dirname, '../../config.json');
|
const configPath = path.join(__dirname, '../../config.json');
|
||||||
const config = await fs.readJson(configPath);
|
const config = await readJson(configPath);
|
||||||
|
|
||||||
if (!config.testMode || !config.mockData) {
|
if (!config.testMode || !config.mockData) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,13 +1,27 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs').promises;
|
||||||
const { connectDB } = require('./config/db');
|
const { connectDB } = require('./config/db');
|
||||||
const { logger } = require('./config/logger');
|
const { logger } = require('./config/logger');
|
||||||
const handbookRoutes = require('./routes/handbook');
|
const handbookRoutes = require('./routes/handbook');
|
||||||
const employeeRoutes = require('./routes/employee');
|
const employeeRoutes = require('./routes/employee');
|
||||||
require('dotenv').config({ path: path.join(__dirname, '../config.env') });
|
require('dotenv').config({ path: path.join(__dirname, '../config.env') });
|
||||||
|
|
||||||
|
async function readJson(filePath) {
|
||||||
|
const data = await fs.readFile(filePath, 'utf8');
|
||||||
|
return JSON.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pathExists(filePath) {
|
||||||
|
try {
|
||||||
|
await fs.access(filePath);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function connectToNetworkShare(config) {
|
async function connectToNetworkShare(config) {
|
||||||
if (config.networkCredentials && config.networkCredentials.username) {
|
if (config.networkCredentials && config.networkCredentials.username) {
|
||||||
try {
|
try {
|
||||||
@@ -67,7 +81,7 @@ app.get('/api/test', (req, res) => {
|
|||||||
app.get('/api/system-status', async (req, res) => {
|
app.get('/api/system-status', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const configPath = path.join(__dirname, '../config.json');
|
const configPath = path.join(__dirname, '../config.json');
|
||||||
const config = await fs.readJson(configPath);
|
const config = await readJson(configPath);
|
||||||
const { getDBStatus } = require('./config/db');
|
const { getDBStatus } = require('./config/db');
|
||||||
const dbStatus = await getDBStatus();
|
const dbStatus = await getDBStatus();
|
||||||
|
|
||||||
@@ -76,7 +90,7 @@ app.get('/api/system-status', async (req, res) => {
|
|||||||
let networkContents = [];
|
let networkContents = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
networkAccess = await fs.pathExists(config.payslipsPath);
|
networkAccess = await pathExists(config.payslipsPath);
|
||||||
if (networkAccess) {
|
if (networkAccess) {
|
||||||
const stats = await fs.stat(config.payslipsPath);
|
const stats = await fs.stat(config.payslipsPath);
|
||||||
networkAccess = stats.isDirectory();
|
networkAccess = stats.isDirectory();
|
||||||
@@ -138,7 +152,7 @@ app.get('/api/system-status', async (req, res) => {
|
|||||||
app.get('/api/test-network', async (req, res) => {
|
app.get('/api/test-network', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const configPath = path.join(__dirname, '../config.json');
|
const configPath = path.join(__dirname, '../config.json');
|
||||||
const config = await fs.readJson(configPath);
|
const config = await readJson(configPath);
|
||||||
const payslipBasePath = config.payslipsPath;
|
const payslipBasePath = config.payslipsPath;
|
||||||
|
|
||||||
logger.info(`Testing network connection to: ${payslipBasePath}`);
|
logger.info(`Testing network connection to: ${payslipBasePath}`);
|
||||||
@@ -156,7 +170,7 @@ app.get('/api/test-network', async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
testResults.basePathExists = await fs.pathExists(payslipBasePath);
|
testResults.basePathExists = await pathExists(payslipBasePath);
|
||||||
logger.info(`Base path exists: ${testResults.basePathExists}`);
|
logger.info(`Base path exists: ${testResults.basePathExists}`);
|
||||||
|
|
||||||
if (testResults.basePathExists) {
|
if (testResults.basePathExists) {
|
||||||
@@ -189,7 +203,7 @@ app.get('/api/payslip/:period/:fileName', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { period, fileName } = req.params;
|
const { period, fileName } = req.params;
|
||||||
const configPath = path.join(__dirname, '../config.json');
|
const configPath = path.join(__dirname, '../config.json');
|
||||||
const config = await fs.readJson(configPath);
|
const config = await readJson(configPath);
|
||||||
const payslipBasePath = config.payslipsPath;
|
const payslipBasePath = config.payslipsPath;
|
||||||
|
|
||||||
logger.info(`Payslip request - Period: ${period}, File: ${fileName}`);
|
logger.info(`Payslip request - Period: ${period}, File: ${fileName}`);
|
||||||
@@ -209,7 +223,7 @@ app.get('/api/payslip/:period/:fileName', async (req, res) => {
|
|||||||
logger.info(`Final payslip path: ${finalPayslipPath}`);
|
logger.info(`Final payslip path: ${finalPayslipPath}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const basePathExists = await fs.pathExists(payslipBasePath);
|
const basePathExists = await pathExists(payslipBasePath);
|
||||||
logger.info(`Base path exists: ${basePathExists}`);
|
logger.info(`Base path exists: ${basePathExists}`);
|
||||||
|
|
||||||
if (basePathExists) {
|
if (basePathExists) {
|
||||||
@@ -224,7 +238,7 @@ app.get('/api/payslip/:period/:fileName', async (req, res) => {
|
|||||||
logger.info(`Base path contents: ${JSON.stringify(basePathContents)}`);
|
logger.info(`Base path contents: ${JSON.stringify(basePathContents)}`);
|
||||||
|
|
||||||
const periodPath = path.join(payslipBasePath, period);
|
const periodPath = path.join(payslipBasePath, period);
|
||||||
const periodPathExists = await fs.pathExists(periodPath);
|
const periodPathExists = await pathExists(periodPath);
|
||||||
logger.info(`Period path exists: ${periodPathExists} (${periodPath})`);
|
logger.info(`Period path exists: ${periodPathExists} (${periodPath})`);
|
||||||
|
|
||||||
if (periodPathExists) {
|
if (periodPathExists) {
|
||||||
@@ -239,7 +253,7 @@ app.get('/api/payslip/:period/:fileName', async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await fs.pathExists(finalPayslipPath)) {
|
if (await pathExists(finalPayslipPath)) {
|
||||||
logger.info(`Payslip found, sending file: ${finalPayslipPath}`);
|
logger.info(`Payslip found, sending file: ${finalPayslipPath}`);
|
||||||
res.sendFile(finalPayslipPath);
|
res.sendFile(finalPayslipPath);
|
||||||
} else {
|
} else {
|
||||||
@@ -267,7 +281,7 @@ app.get('/api/payslip/:fileName', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { fileName } = req.params;
|
const { fileName } = req.params;
|
||||||
const configPath = path.join(__dirname, '../config.json');
|
const configPath = path.join(__dirname, '../config.json');
|
||||||
const config = await fs.readJson(configPath);
|
const config = await readJson(configPath);
|
||||||
const payslipBasePath = config.payslipsPath;
|
const payslipBasePath = config.payslipsPath;
|
||||||
|
|
||||||
let finalPayslipPath;
|
let finalPayslipPath;
|
||||||
@@ -280,7 +294,7 @@ app.get('/api/payslip/:fileName', async (req, res) => {
|
|||||||
|
|
||||||
logger.info(`Attempting to find payslip at resolved path: ${finalPayslipPath}`);
|
logger.info(`Attempting to find payslip at resolved path: ${finalPayslipPath}`);
|
||||||
|
|
||||||
if (await fs.pathExists(finalPayslipPath)) {
|
if (await pathExists(finalPayslipPath)) {
|
||||||
res.sendFile(finalPayslipPath);
|
res.sendFile(finalPayslipPath);
|
||||||
} else {
|
} else {
|
||||||
logger.warn(`Payslip not found: ${finalPayslipPath}`);
|
logger.warn(`Payslip not found: ${finalPayslipPath}`);
|
||||||
@@ -310,7 +324,7 @@ app.get('/api/db-status', async (req, res) => {
|
|||||||
async function startServer() {
|
async function startServer() {
|
||||||
try {
|
try {
|
||||||
const configPath = path.join(__dirname, '../config.json');
|
const configPath = path.join(__dirname, '../config.json');
|
||||||
const config = await fs.readJson(configPath);
|
const config = await readJson(configPath);
|
||||||
const { host, port } = config.server;
|
const { host, port } = config.server;
|
||||||
|
|
||||||
await connectDB();
|
await connectDB();
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Инфокиоск"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<title>Инфокиоск</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
<style>
|
||||||
|
.ql-align-center { text-align: center; }
|
||||||
|
.ql-align-right { text-align: right; }
|
||||||
|
.ql-align-justify { text-align: justify; }
|
||||||
|
</style>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -4,38 +4,26 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"main": "public/electron.js",
|
"main": "public/electron.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@types/jest": "^27.5.2",
|
"@types/react": "^19.2.0",
|
||||||
"@types/node": "^16.18.0",
|
"@types/react-dom": "^19.2.0",
|
||||||
"@types/react": "^18.0.0",
|
"dotenv": "^17.4.2",
|
||||||
"@types/react-dom": "^18.0.0",
|
|
||||||
"electron-is-dev": "^2.0.0",
|
"electron-is-dev": "^2.0.0",
|
||||||
"fs": "^0.0.1-security",
|
"pdfjs-dist": "^4.10.38",
|
||||||
"http": "^0.0.1-security",
|
"react": "^19.2.0",
|
||||||
"https": "^1.0.0",
|
"react-dom": "^19.2.0",
|
||||||
"mammoth": "^1.9.1",
|
"react-pdf": "^10.0.0",
|
||||||
"os": "^0.1.2",
|
|
||||||
"path": "^0.12.7",
|
|
||||||
"pdfjs-dist": "2.16.105",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"react-pdf": "6.2.2",
|
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"serialport": "^13.0.0",
|
"serialport": "^13.0.0",
|
||||||
"stream": "^0.0.3",
|
"typescript": "^5.8.0",
|
||||||
"textract": "^2.5.0",
|
"web-vitals": "^2.1.4"
|
||||||
"typescript": "^4.8.4",
|
|
||||||
"util": "^0.12.5",
|
|
||||||
"web-vitals": "^2.1.4",
|
|
||||||
"zlib": "^1.0.5"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "vite",
|
||||||
"build": "react-scripts build",
|
"build": "tsc -b && vite build",
|
||||||
"test": "react-scripts test",
|
"preview": "vite preview",
|
||||||
"eject": "react-scripts eject",
|
"test": "vitest",
|
||||||
"electron-dev": "concurrently \"npm start\" \"wait-on http://localhost:3000 && electron .\"",
|
"electron-dev": "concurrently \"npm start\" \"wait-on http://localhost:3000 && electron .\"",
|
||||||
"electron-pack": "electron-builder -c.extraMetadata.main=build/electron.js",
|
"electron-pack": "electron-builder -c.extraMetadata.main=build/electron.js",
|
||||||
"preelectron-pack": "npm run build",
|
"preelectron-pack": "npm run build",
|
||||||
@@ -79,21 +67,15 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react-pdf": "^6.2.0",
|
"@types/jest": "^29.5.14",
|
||||||
"buffer": "^6.0.3",
|
"@types/node": "^26.2.0",
|
||||||
|
"@vitejs/plugin-react": "^4.7.0",
|
||||||
"concurrently": "^7.5.0",
|
"concurrently": "^7.5.0",
|
||||||
"crypto-browserify": "^3.12.1",
|
"electron": "^37.0.0",
|
||||||
"electron": "^21.2.0",
|
"electron-builder": "^26.15.3",
|
||||||
"electron-builder": "^23.6.0",
|
|
||||||
"electron-rebuild": "^3.2.9",
|
"electron-rebuild": "^3.2.9",
|
||||||
"https-browserify": "^1.0.0",
|
"vite": "^6.0.0",
|
||||||
"os-browserify": "^0.3.0",
|
"vitest": "^2.1.8",
|
||||||
"path-browserify": "^1.0.1",
|
|
||||||
"process": "^0.11.10",
|
|
||||||
"querystring-es3": "^0.2.1",
|
|
||||||
"stream-browserify": "^3.0.0",
|
|
||||||
"stream-http": "^3.2.0",
|
|
||||||
"url": "^0.11.4",
|
|
||||||
"wait-on": "^6.0.1"
|
"wait-on": "^6.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
||||||
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
|
|
||||||
<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>Инфокиоск</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
<style>
|
|
||||||
.ql-align-center { text-align: center; }
|
|
||||||
.ql-align-right { text-align: right; }
|
|
||||||
.ql-align-justify { text-align: justify; }
|
|
||||||
</style>
|
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Document, Page, pdfjs } from 'react-pdf';
|
import { Document, Page, pdfjs } from 'react-pdf';
|
||||||
|
|
||||||
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
|
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs';
|
||||||
|
|
||||||
interface PayslipViewerProps {
|
interface PayslipViewerProps {
|
||||||
fileUrl: string;
|
fileUrl: string;
|
||||||
|
|||||||
@@ -6,10 +6,6 @@ declare module '*.gif';
|
|||||||
declare module '*.bmp';
|
declare module '*.bmp';
|
||||||
declare module '*.tiff';
|
declare module '*.tiff';
|
||||||
|
|
||||||
declare module 'fs';
|
|
||||||
declare module 'path';
|
|
||||||
declare module 'os';
|
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
backendUrl: string;
|
backendUrl: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
/// <reference types="react-scripts" />
|
/// <reference types="vite/client" />
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
declare module 'textract' {
|
|
||||||
interface TextractOptions {
|
|
||||||
preserveLineBreaks?: boolean;
|
|
||||||
preserveOnlyMultipleLineBreaks?: boolean;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fromBufferWithMime(
|
|
||||||
mimeType: string,
|
|
||||||
buffer: Buffer,
|
|
||||||
options: TextractOptions,
|
|
||||||
callback: (error: Error | null, text: string) => void
|
|
||||||
): void;
|
|
||||||
|
|
||||||
export = {
|
|
||||||
fromBufferWithMime
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "ES2020",
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
"es6"
|
"ES2020"
|
||||||
],
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
@@ -13,18 +13,20 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"module": "esnext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
"types": ["vite/client", "node"],
|
||||||
"typeRoots": [
|
"typeRoots": [
|
||||||
"./node_modules/@types",
|
"./node_modules/@types",
|
||||||
"./src/types"
|
"./src/types"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"src",
|
||||||
|
"vite.config.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { fileURLToPath, URL } from 'node:url';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
base: './',
|
||||||
|
build: {
|
||||||
|
outDir: 'build',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
});
|
||||||