admin_to_react

This commit is contained in:
2026-08-09 14:13:06 +03:00
commit 5d2fc5773c
163 changed files with 140326 additions and 0 deletions
+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;
+69
View File
@@ -0,0 +1,69 @@
-- ============================================================
-- 📦 Схема базы данных модераторов: 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');