refactor: шаг 4 — GET /export вынесен в src/routes/export.js

This commit is contained in:
2026-05-30 13:38:03 +03:00
parent b25f346d97
commit f91a882379
2 changed files with 64 additions and 19 deletions
+8 -19
View File
@@ -40,30 +40,19 @@ async function start() {
// ── Публичные маршруты (до auth.middleware) ────────────────────────────────── // ── Публичные маршруты (до auth.middleware) ──────────────────────────────────
// k8s liveness probe // k8s liveness probe — простая проверка что сервер жив
app.get('/healthz', (req, res) => res.send('OK')); app.get('/healthz', (req, res) => res.send('OK'));
// JWKS endpoint для валидации токенов внешними сервисами (только в mock-режиме) // JWKS endpoint для валидации токенов внешними сервисами (только в mock-режиме)
if (auth.jwksHandler) app.get('/.well-known/jwks.json', auth.jwksHandler); if (auth.jwksHandler) app.get('/.well-known/jwks.json', auth.jwksHandler);
// /export агрегированный список всех активных CIDR по всем компаниям. // GET /export: агрегированный список CIDR, без авторизации.
// ТЗ: «авторизация не требуется, доступ ограничивается на сетевом уровне». // src/routes/export.js содержит логику и комментарии.
// Маршрут регистрируется ДО auth.middleware чтобы не требовать jwt-cookie. app.use(require('./src/routes/export').createRouter({
// exportLimiter: не более 20 запросов/мин с одного IP (защита от scraping). q,
app.get('/export', exportLimiter, async (req, res) => { exportLimiter,
try { aggregateCIDRs,
// Получаем все активные CIDR без фильтра по компании }));
const cidrs = await q.getExportCIDRs();
// Суммаризация: объединяем пересекающиеся диапазоны → минимальный набор
const aggregated = aggregateCIDRs(cidrs);
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.setHeader('Content-Disposition', 'inline; filename="whitelist.txt"');
res.send(aggregated.join('\n') + (aggregated.length ? '\n' : ''));
} catch (e) {
console.error('Export error:', e);
res.status(500).send('Export error');
}
});
// Страница входа — отдаём форму выбора мок-пользователя. // Страница входа — отдаём форму выбора мок-пользователя.
// Генерируем CSRF-токен и передаём в шаблон для скрытого поля формы. // Генерируем CSRF-токен и передаём в шаблон для скрытого поля формы.
+56
View File
@@ -0,0 +1,56 @@
/**
* src/routes/export.js — публичный маршрут GET /export.
*
* Назначение: отдаёт агрегированный список всех активных CIDR (все компании)
* в виде текстового файла (один адрес/подсеть на строку).
*
* Особенности:
* - НЕ требует авторизации (ТЗ: «доступ ограничивается на сетевом уровне»).
* - Регистрируется ДО auth.middleware в server.js.
* - Ограничен exportLimiter: не более 20 запросов/мин с одного IP.
* - aggregateCIDRs объединяет перекрывающиеся диапазоны → минимальный набор.
*
* Паттерн «фабрика»:
* Вместо прямых require(зависимостей) получаем их через параметр deps.
* Это позволяет легко подменять зависимости в тестах (mock q, mock aggregateCIDRs).
*
* @param {{ q: object, exportLimiter: Function, aggregateCIDRs: Function }} deps
* @returns {import('express').Router}
*/
'use strict';
const { Router } = require('express');
function createRouter({ q, exportLimiter, aggregateCIDRs }) {
const router = Router();
// GET /export — агрегированный whitelist всех компаний в текстовом виде.
// exportLimiter ограничивает частоту запросов (20/мин) до проверки авторизации —
// это важно, так как маршрут публичный.
router.get('/export', exportLimiter, async (req, res) => {
try {
// Получаем все активные CIDR без фильтра по компании
const cidrs = await q.getExportCIDRs();
// Суммаризация: объединяем пересекающиеся и смежные диапазоны
// Например: 192.168.0.0/25 + 192.168.0.128/25 → 192.168.0.0/24
const aggregated = aggregateCIDRs(cidrs);
// Content-Type: text/plain — чтобы браузер не пытался разбирать как HTML.
// Content-Disposition: inline — открывается в браузере, а не скачивается.
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.setHeader('Content-Disposition', 'inline; filename="whitelist.txt"');
// Один CIDR на строку. Если список пустой — возвращаем пустую строку (не ошибку).
res.send(aggregated.join('\n') + (aggregated.length ? '\n' : ''));
} catch (e) {
console.error('Export error:', e);
res.status(500).send('Export error');
}
});
return router;
}
module.exports = { createRouter };