/** * src/routes/entries.js — основные маршруты работы с белым списком IP. * * Маршруты: * GET / — главная страница: список адресов (admin видит все компании) * POST /add — добавление новой записи * POST /edit/:id — редактирование существующей записи * POST /delete/:id — удаление записи * * Все маршруты требуют авторизации (auth.middleware уже применён в server.js выше). * POST-маршруты дополнительно проверяются: * - mutationLimiter: не более 30 запросов/мин с одного IP * - doubleCsrfProtection: проверка CSRF-токена из скрытого поля формы * * Admin vs user логика: * Admin (isAdmin === true, clientId === ADMIN_CLIENT_ID): * - На главной видит все компании, может переключаться через ?company= * - В POST-запросах принимает company_id из тела формы * Обычный пользователь: * - Видит только свою компанию (из JWT-токена) * - company_id из тела формы игнорируется (нельзя переключиться) * * @param {{ q, doubleCsrfProtection, mutationLimiter, generateCsrfToken, backUrl, requireAdmin }} deps * @returns {import('express').Router} */ 'use strict'; const { Router } = require('express'); function createRouter({ q, doubleCsrfProtection, mutationLimiter, generateCsrfToken, backUrl }) { const router = Router(); // ── GET / ───────────────────────────────────────────────────────────────────── // Главная страница: список активных CIDR для компании. // Admin: видит все компании + переключатель ?company=<числовой PK>. // User: только своя компания. router.get('/', async (req, res) => { const { clientId, companyName, isAdmin } = req.user; // Генерируем CSRF-токен здесь, один раз для всей страницы. // Он попадёт в три формы: /add, /edit (modal), /delete (каждая строка). const csrfToken = generateCsrfToken(req, res); try { if (isAdmin) { // ── Ветка admin ─────────────────────────────────────────────────────── // Загружаем все компании (с active_count через LEFT JOIN) const companies = await q.getAllCompanies(); // ?company=X — числовой PK выбранной компании. // Если параметра нет — берём первую компанию из списка. const selectedId = req.query.company ? parseInt(req.query.company, 10) : (companies[0] ? companies[0].id : null); // Ищем объект компании по id. Если id не найден — берём первую. const selectedCompany = companies.find(c => c.id === selectedId) || companies[0] || null; const entries = selectedCompany ? await q.listEntries(selectedCompany.id) : []; const limit = selectedCompany ? await q.getLimit(selectedCompany) : 0; return res.render('index', { entries, limit, used: entries.length, user: req.user, isAdmin: true, companies, // для dropdown-переключателя selectedCompany, // текущая выбранная компания error: req.query.error || null, message: req.query.message || null, wasNormalized: req.query.wasNormalized === '1', csrfToken, }); } // ── Ветка обычного пользователя ──────────────────────────────────────── // getOrCreateCompany: создаёт запись в companies если её ещё нет. // clientId и companyName берутся из JWT (нельзя подменить через форму). const company = await q.getOrCreateCompany(clientId, companyName); const limit = await q.getLimit(company); const entries = await q.listEntries(company.id); res.render('index', { entries, limit, used: entries.length, user: req.user, isAdmin: false, companies: null, // пользователь не видит список компаний selectedCompany: company, // его единственная компания error: req.query.error || null, message: req.query.message || null, wasNormalized: req.query.wasNormalized === '1', csrfToken, }); } catch (e) { console.error('GET / error:', e); // При ошибке рендерим страницу с сообщением, не падаем с 500 res.render('index', { entries: [], limit: 15, used: 0, user: req.user, isAdmin, companies: null, selectedCompany: null, error: 'Ошибка загрузки данных: ' + e.message, message: null, wasNormalized: false, csrfToken, }); } }); // ── POST /add ───────────────────────────────────────────────────────────────── // Добавление новой записи в белый список. // createEntry: валидирует CIDR, проверяет лимит, пишет в audit_log (транзакция). router.post('/add', mutationLimiter, doubleCsrfProtection, async (req, res) => { const { value, comment } = req.body; const { clientId, companyName, email, isAdmin } = req.user; let company; try { // Определяем компанию: admin — из скрытого поля company_id, // обычный пользователь — только своя (из токена, нельзя подменить). if (isAdmin && req.body.company_id) { const id = parseInt(req.body.company_id, 10); if (!Number.isFinite(id) || id <= 0) throw new Error('Некорректный company_id'); company = await q.getCompanyById(id); if (!company) throw new Error('Компания не найдена'); } else { company = await q.getOrCreateCompany(clientId, companyName); } // createEntry возвращает { entry, wasNormalized } // wasNormalized=true если IP был нормализован (напр. 192.168.1.1/24 → 192.168.1.0/24) const result = await q.createEntry(company.id, value, comment, email); const msg = result.wasNormalized ? `Адрес нормализован в ${result.entry.value_cidr}` : 'Добавлено'; res.redirect(backUrl(isAdmin, company.id, { message: msg, wasNormalized: result.wasNormalized ? '1' : '0', })); } catch (e) { // Ошибки: лимит исчерпан, дубликат, невалидный CIDR, etc. const cid = company ? company.id : req.body.company_id; res.redirect(backUrl(isAdmin, cid, { error: e.message })); } }); // ── POST /edit/:id ──────────────────────────────────────────────────────────── // Обновление существующей записи. // updateEntry: валидирует CIDR, проверяет принадлежность записи компании, // пишет в audit_log (транзакция). router.post('/edit/:id', mutationLimiter, doubleCsrfProtection, async (req, res) => { const { value, comment } = req.body; const { clientId, companyName, email, isAdmin } = req.user; // parseInt с radix=10 — защита от октальных и hex-значений const entryId = parseInt(req.params.id, 10); let company; try { if (isAdmin && req.body.company_id) { const id = parseInt(req.body.company_id, 10); if (!Number.isFinite(id) || id <= 0) throw new Error('Некорректный company_id'); company = await q.getCompanyById(id); if (!company) throw new Error('Компания не найдена'); } else { company = await q.getOrCreateCompany(clientId, companyName); } const result = await q.updateEntry(entryId, company.id, value, comment, email); const msg = result.wasNormalized ? `Адрес нормализован в ${result.entry.value_cidr}` : 'Сохранено'; res.redirect(backUrl(isAdmin, company.id, { message: msg, wasNormalized: result.wasNormalized ? '1' : '0', })); } catch (e) { const cid = company ? company.id : req.body.company_id; res.redirect(backUrl(isAdmin, cid, { error: e.message })); } }); // ── POST /delete/:id ───────────────────────────────────────────────────────── // Удаление записи. // deleteEntry: проверяет принадлежность записи компании, пишет в audit_log. router.post('/delete/:id', mutationLimiter, doubleCsrfProtection, async (req, res) => { const { clientId, companyName, email, isAdmin } = req.user; const entryId = parseInt(req.params.id, 10); let company; try { if (isAdmin && req.body.company_id) { const id = parseInt(req.body.company_id, 10); if (!Number.isFinite(id) || id <= 0) throw new Error('Некорректный company_id'); company = await q.getCompanyById(id); if (!company) throw new Error('Компания не найдена'); } else { company = await q.getOrCreateCompany(clientId, companyName); } await q.deleteEntry(entryId, company.id, email); res.redirect(backUrl(isAdmin, company.id, { message: 'Запись удалена' })); } catch (e) { const cid = company ? company.id : req.body.company_id; res.redirect(backUrl(isAdmin, cid, { error: e.message })); } }); return router; } module.exports = { createRouter };