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

219 lines
11 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/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=<id>
* - В 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 };