Files
ipwhitelist-app/src/routes/admin.js
T

140 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* src/routes/admin.js — маршруты администратора.
*
* Маршруты:
* GET /audit — журнал аудита всех изменений
* GET /admin — страница управления лимитами компаний
* POST /admin/limit/:companyId — изменить или сбросить лимит компании
*
* Все маршруты защищены requireAdmin — возвращают 403 для не-admin.
* Дополнительно POST /admin/limit:
* - mutationLimiter: не более 30 запросов/мин с одного IP
* - doubleCsrfProtection: проверка CSRF-токена
*
* Зависимости (через deps):
* q — объект из src/queries.js (все DB-операции)
* requireAdmin — middleware из src/auth.js
* doubleCsrfProtection — middleware проверки CSRF
* mutationLimiter — middleware ограничения частоты POST
* generateCsrfToken — функция генерации CSRF-токена для шаблонов
*
* @param {{ q, requireAdmin, doubleCsrfProtection, mutationLimiter, generateCsrfToken }} deps
* @returns {import('express').Router}
*/
'use strict';
const { Router } = require('express');
function createRouter({ q, requireAdmin, doubleCsrfProtection, mutationLimiter, generateCsrfToken }) {
const router = Router();
// ── GET /audit ────────────────────────────────────────────────────────────────
// Журнал всех изменений белого списка (CREATE / UPDATE / DELETE).
// ТЗ 4.6: «журнал доступен для просмотра только администратору».
//
// ?company=<id> — фильтр по конкретной компании (необязательно).
// Максимум 500 строк (ограничение в queries.js).
router.get('/audit', requireAdmin, async (req, res) => {
try {
// Парсим фильтр: если ?company не задан — показываем все компании
const companyId = req.query.company ? parseInt(req.query.company, 10) : null;
// Грузим список компаний для dropdown-фильтра в шаблоне
const companies = await q.getAllCompanies();
// Строки аудита (JOIN с companies → company_name, client_id)
const rows = await q.getAudit(companyId || null);
res.render('audit', {
user: req.user,
rows,
companies,
selectedCompanyId: companyId,
error: req.query.error || null,
});
} catch (e) {
console.error('GET /audit error:', e);
// При ошибке рендерим страницу с пустым списком, не падаем с 500
res.render('audit', {
user: req.user,
rows: [],
companies: [],
selectedCompanyId: null,
error: 'Ошибка загрузки: ' + e.message,
});
}
});
// ── GET /admin ────────────────────────────────────────────────────────────────
// Страница управления лимитами: таблица компаний с их active_count и лимитом.
// Для каждой компании — форма изменения лимита (POST /admin/limit/:id).
//
// DEFAULT_LIMIT из env — используется как дефолт при отображении и при сбросе.
router.get('/admin', requireAdmin, async (req, res) => {
try {
const companies = await q.getAllCompanies();
// Если DEFAULT_LIMIT не задан в env — используем 15 (как в queries.js)
const defaultLimit = parseInt(process.env.DEFAULT_LIMIT, 10) || 15;
res.render('admin', {
user: req.user,
companies,
defaultLimit,
message: req.query.message || null,
error: req.query.error || null,
// CSRF-токен для форм /admin/limit/:id (по одной на каждую строку таблицы)
csrfToken: generateCsrfToken(req, res),
});
} catch (e) {
console.error('GET /admin error:', e);
res.status(500).send('Ошибка: ' + e.message);
}
});
// ── POST /admin/limit/:companyId ──────────────────────────────────────────────
// Изменить или сбросить индивидуальный лимит компании.
// ТЗ 4.5: «администратор может задать индивидуальный лимит».
//
// Логика:
// - Пустое поле limit → null → сброс на DEFAULT_LIMIT (глобальный дефолт)
// - Число ≥ 0 → custom_limit = это число
// - Отрицательное или нечисловое → ошибка
router.post('/admin/limit/:companyId', requireAdmin, mutationLimiter, doubleCsrfProtection,
async (req, res) => {
// Валидация числового PK компании из URL-параметра
const companyId = parseInt(req.params.companyId, 10);
if (!Number.isFinite(companyId) || companyId <= 0) {
return res.redirect('/admin?error=' + encodeURIComponent('Некорректный ID компании'));
}
try {
const raw = req.body.limit;
let newLimit;
if (raw === '' || raw === null || raw === undefined) {
// Пустое поле — сброс: setLimit(id, null) → queries.js удаляет custom_limit
newLimit = null;
} else {
newLimit = parseInt(raw, 10);
if (!Number.isFinite(newLimit) || newLimit < 0) {
throw new Error('Лимит должен быть целым числом ≥ 0, или пустым для сброса');
}
}
await q.setLimit(companyId, newLimit);
// Формируем человекочитаемое сообщение для flash-уведомления
const label = newLimit === null ? 'сброшен на дефолт' : `установлен: ${newLimit}`;
res.redirect('/admin?message=' + encodeURIComponent(`Лимит ${label}`));
} catch (e) {
res.redirect('/admin?error=' + encodeURIComponent(e.message));
}
}
);
return router;
}
module.exports = { createRouter };