From f91a8823794263e46d32d2832d20f135db4adc9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 30 May 2026 13:38:03 +0300 Subject: [PATCH] =?UTF-8?q?refactor:=20=D1=88=D0=B0=D0=B3=204=20=E2=80=94?= =?UTF-8?q?=20GET=20/export=20=D0=B2=D1=8B=D0=BD=D0=B5=D1=81=D0=B5=D0=BD?= =?UTF-8?q?=20=D0=B2=20src/routes/export.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server.js | 27 +++++++-------------- src/routes/export.js | 56 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 19 deletions(-) create mode 100644 src/routes/export.js diff --git a/server.js b/server.js index 14982a1..9599884 100644 --- a/server.js +++ b/server.js @@ -40,30 +40,19 @@ async function start() { // ── Публичные маршруты (до auth.middleware) ────────────────────────────────── - // k8s liveness probe + // k8s liveness probe — простая проверка что сервер жив app.get('/healthz', (req, res) => res.send('OK')); // JWKS endpoint для валидации токенов внешними сервисами (только в mock-режиме) if (auth.jwksHandler) app.get('/.well-known/jwks.json', auth.jwksHandler); - // /export — агрегированный список всех активных CIDR по всем компаниям. - // ТЗ: «авторизация не требуется, доступ ограничивается на сетевом уровне». - // Маршрут регистрируется ДО auth.middleware чтобы не требовать jwt-cookie. - // exportLimiter: не более 20 запросов/мин с одного IP (защита от scraping). - app.get('/export', exportLimiter, async (req, res) => { - try { - // Получаем все активные 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'); - } - }); + // GET /export: агрегированный список CIDR, без авторизации. + // src/routes/export.js содержит логику и комментарии. + app.use(require('./src/routes/export').createRouter({ + q, + exportLimiter, + aggregateCIDRs, + })); // Страница входа — отдаём форму выбора мок-пользователя. // Генерируем CSRF-токен и передаём в шаблон для скрытого поля формы. diff --git a/src/routes/export.js b/src/routes/export.js new file mode 100644 index 0000000..6fba891 --- /dev/null +++ b/src/routes/export.js @@ -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 };