first commit

This commit is contained in:
2026-08-08 00:15:48 +03:00
parent ac2bc13da0
commit d98b4c1343
146 changed files with 122827 additions and 0 deletions
View File
+17
View File
@@ -0,0 +1,17 @@
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:3000, http://localhost:3005, http://localhost:3001, http://127.0.0.1:3000
BACKEND_APP_LOG=../backend/logs/app.log
CARD_LOG=../backend/logs/cards.log
+12
View File
@@ -0,0 +1,12 @@
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
};
+22
View File
@@ -0,0 +1,22 @@
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
}
};
};
@@ -0,0 +1,40 @@
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: 'Ошибка сервера' });
}
};
@@ -0,0 +1,70 @@
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
};
@@ -0,0 +1,165 @@
const axios = require('axios');
const mysql = require('mysql2/promise');
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 {
// ✅ Конфигурация для MySQL (правильный синтаксис)
const dbConfig = {
host: process.env.MYSQL_HOST || 'localhost',
user: process.env.MYSQL_USER || 'root',
password: process.env.MYSQL_PASSWORD || '',
database: process.env.MYSQL_DATABASE || 'test',
port: process.env.MYSQL_PORT || 3306,
connectionLimit: 1,
connectTimeout: 15000,
waitForConnections: true,
queueLimit: 0
};
logger.info(`[DB CHECK] Проверка подключения к MySQL: ${dbConfig.host}:${dbConfig.port}/${dbConfig.database}`);
// 🔌 Создаем пул соединений (правильный синтаксис для mysql2)
const pool = mysql.createPool(dbConfig);
// 🔍 Получаем соединение из пула
const connection = await pool.getConnection();
// ✅ Выполняем тестовый запрос (правильный синтаксис)
const [rows] = await connection.query('SELECT 1 as result');
// ✅ Успешно!
adminToDbStatus = {
status: 'online',
message: '✅ Прямое подключение к MySQL успешно',
dbType: 'MySQL',
host: dbConfig.host,
database: dbConfig.database
};
logger.info(`[DB CHECK] ✅ Успешное подключение к MySQL`);
// Освобождаем соединение
connection.release();
// Закрываем пул
await pool.end();
} catch (error) {
logger.error(`[DB CHECK] ❌ Ошибка подключения к MySQL:`, error.message);
adminToDbStatus = {
status: 'offline',
message: `❌ Ошибка подключения: ${error.message}`,
dbType: 'MySQL',
errorCode: error.code || 'UNKNOWN'
};
if (error.code === 'ECONNREFUSED') {
adminToDbStatus.diagnostic = 'Сервер MySQL не запущен или недоступен. Проверьте, запущен ли MySQL.';
} else if (error.code === 'ER_ACCESS_DENIED_ERROR') {
adminToDbStatus.diagnostic = 'Неверный логин или пароль. Проверьте MYSQL_USER и MYSQL_PASSWORD в .env';
} else if (error.code === 'ER_BAD_DB_ERROR') {
adminToDbStatus.diagnostic = 'База данных не существует. Проверьте MYSQL_DATABASE в .env';
} else if (error.code === 'ENOTFOUND') {
adminToDbStatus.diagnostic = 'Хост не найден. Проверьте MYSQL_HOST в .env';
}
}
let backendToDbStatus = {
status: 'offline',
message: 'Test not performed'
};
try {
const backendUrl = process.env.BACKEND_URL || 'http://localhost:3001';
const response = await axios.get(`${backendUrl}/api/db-status`, {
timeout: 3000,
validateStatus: false // Не выбрасывать ошибку при статусе != 200
});
if (response.status === 200 && response.data && response.data.status === 'ok') {
backendToDbStatus = {
status: 'online',
message: '✅ Бэкенд успешно подключен к БД',
details: response.data
};
} else {
backendToDbStatus = {
status: 'offline',
message: '⚠️ Бэкенд сообщил об ошибке',
details: response.data || { status: 'unknown' }
};
}
} catch (error) {
logger.error(`[DB CHECK] ❌ Ошибка связи с бэкендом:`, error.message);
backendToDbStatus = {
status: 'offline',
message: `❌ Не удалось связаться с бэкендом: ${error.message}`
};
}
// 📤 Отправляем результат
res.json({
timestamp: new Date().toISOString(),
adminToDb: adminToDbStatus,
backendToDb: backendToDbStatus,
environment: {
nodeVersion: process.version,
platform: process.platform,
envFile: process.env.NODE_ENV || 'development'
}
});
};
exports.checkStatus = async (req, res) => {
try {
const backendUrl = process.env.BACKEND_URL || 'http://localhost:3001';
await axios.get(`${backendUrl}/api/test`, {
timeout: 2000
});
res.json({
status: 'online',
message: 'Сервер работает',
timestamp: new Date().toISOString()
});
} catch (error) {
logger.error(`[STATUS] ❌ Ошибка проверки статуса:`, error.message);
res.json({
status: 'offline',
message: 'Сервер недоступен',
error: error.message,
timestamp: new Date().toISOString()
});
}
};
exports.testDbConnection = async () => {
try {
const dbConfig = {
host: process.env.MYSQL_HOST || 'localhost',
user: process.env.MYSQL_USER || 'root',
password: process.env.MYSQL_PASSWORD || '',
database: process.env.MYSQL_DATABASE || 'test',
port: process.env.MYSQL_PORT || 3306,
connectionLimit: 1,
connectTimeout: 5000
};
const pool = mysql.createPool(dbConfig);
const connection = await pool.getConnection();
await connection.query('SELECT 1');
connection.release();
await pool.end();
return { success: true, message: 'Подключение успешно' };
} catch (error) {
return { success: false, message: error.message };
}
};
@@ -0,0 +1,99 @@
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
};
@@ -0,0 +1,72 @@
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
};
@@ -0,0 +1,172 @@
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 = {
getMysqlSettings,
updateMysqlSettings,
getCorsSettings,
updateCorsSettings,
getAdminServerSettings,
updateAdminServerSettings,
getBackendServerSettings,
updateBackendServerSettings
};
+53
View File
@@ -0,0 +1,53 @@
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),
};
File diff suppressed because it is too large Load Diff
View File
+41
View File
@@ -0,0 +1,41 @@
const { app, BrowserWindow } = require('electron');
const path = require('path');
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
// Загружаем ваш сервер
mainWindow.loadURL('http://localhost:3005');
// Открываем DevTools
mainWindow.webContents.openDevTools();
mainWindow.on('closed', () => {
mainWindow = null;
});
}
app.whenReady().then(() => {
createWindow();
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (mainWindow === null) {
createWindow();
}
});
+7
View File
@@ -0,0 +1,7 @@
module.exports = (req, res, next) => {
if (req.session && req.session.isAuthenticated) {
next();
} else {
res.status(401).json({ message: 'Unauthorized' });
}
};
+17
View File
@@ -0,0 +1,17 @@
// 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;
+8
View File
@@ -0,0 +1,8 @@
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' });
};
+5
View File
@@ -0,0 +1,5 @@
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=Vova-2003.
MYSQL_DATABASE=rasch_content
+20
View File
@@ -0,0 +1,20 @@
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;
@@ -0,0 +1,6 @@
function requireModerator(req, res, next) {
if (req.session && req.session.isModerator) return next();
res.status(401).send('Unauthorized');
}
module.exports = requireModerator;
+100
View File
@@ -0,0 +1,100 @@
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;
+81
View File
@@ -0,0 +1,81 @@
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;
+3423
View File
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
{
"name": "admin-server",
"version": "1.0.0",
"main": "main.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"
}
}
+805
View File
@@ -0,0 +1,805 @@
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() {
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();
});
+69
View File
@@ -0,0 +1,69 @@
<!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="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.8/dist/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">&times;</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="https://cdn.quilljs.com/1.3.6/quill.js"></script>
<script src="editor.js"></script>
</body>
</html>
+190
View File
@@ -0,0 +1,190 @@
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);
}
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']
]
}
});
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);
};
});
+188
View File
@@ -0,0 +1,188 @@
<!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="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>
+39
View File
@@ -0,0 +1,39 @@
<!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>
+557
View File
@@ -0,0 +1,557 @@
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;
}
+10
View File
@@ -0,0 +1,10 @@
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;
+8
View File
@@ -0,0 +1,8 @@
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;
+8
View File
@@ -0,0 +1,8 @@
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;
+10
View File
@@ -0,0 +1,10 @@
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;
+11
View File
@@ -0,0 +1,11 @@
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;
+8
View File
@@ -0,0 +1,8 @@
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;
+18
View File
@@ -0,0 +1,18 @@
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;
+114
View File
@@ -0,0 +1,114 @@
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');
// 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}`);
});
+93
View File
@@ -0,0 +1,93 @@
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;
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB