diff --git a/backend/routes/mikrotikConfigRoutes.js b/backend/routes/mikrotikConfigRoutes.js index 81cb039..edfeea1 100644 --- a/backend/routes/mikrotikConfigRoutes.js +++ b/backend/routes/mikrotikConfigRoutes.js @@ -15,7 +15,7 @@ const { buildMikrotikRecursiveRoutes, getParentGateway, } = require('../utils/mikrotikInterfaceGenerator'); -const { createRosClient, applyBlock } = require('../services/mikrotikApplyService'); +const { createRosClient, applyBlock, rosPrint } = require('../services/mikrotikApplyService'); const IPSEC_PASSWORDS_KEY = 'network-config/ipsec-passwords.json'; const NETWORK_CONFIG_KEY = 'network-config.json'; @@ -1146,6 +1146,50 @@ async function runScript(req, res) { } } +/** + * GET /api/mikrotik/address-lists?serverId=xxx + * Возвращает address-list списки ban и ban_counter с выбранного роутера (MikroTik). + */ +async function getAddressLists(req, res) { + try { + const serverId = req.query?.serverId; + if (!serverId) { + return sendError(res, 400, 'serverId is required', 'E_BAD_REQUEST'); + } + + const servers = await readServersFromS3(); + const server = servers.find((s) => (s.id || s.dns || s.ip) === serverId); + if (!server || (server.type !== 'jumphost' && server.type !== 'home')) { + return sendError(res, 400, 'Jumphost or home server not found', 'E_NOT_FOUND'); + } + + const creds = getMikrotikCredentials(server); + if (!creds) { + return sendError(res, 400, 'MikroTik credentials not configured for this server', 'E_CREDENTIALS'); + } + + const client = createRosClient(creds); + const path = 'ip/firewall/address-list'; + + const [ban, banCounter] = await Promise.all([ + rosPrint(client, path, { list: 'ban' }), + rosPrint(client, path, { list: 'ban_counter' }), + ]); + + return sendOk(res, { + serverId, + serverLabel: server.dns || server.ip || serverId, + ban: Array.isArray(ban) ? ban : [], + ban_counter: Array.isArray(banCounter) ? banCounter : [], + }); + } catch (error) { + const msg = error.response?.data?.detail || error.response?.data?.message || error.message || 'Ошибка получения address-list'; + const status = error.response?.status; + console.error('getAddressLists:', error); + return sendError(res, status && status >= 400 ? status : 502, msg, 'E_ADDRESS_LISTS'); + } +} + module.exports = { generateMikrotikConfig, generateInterfaces, @@ -1158,4 +1202,5 @@ module.exports = { runPingViaRouter, speedTestViaTunnel, loadNetworkConfig, + getAddressLists, }; diff --git a/backend/server.js b/backend/server.js index 4bf39a1..9942158 100644 --- a/backend/server.js +++ b/backend/server.js @@ -467,6 +467,7 @@ app.post('/api/mikrotik/traceroute', writeLimiter, mikrotikConfigRoutes.tracerou app.post('/api/mikrotik/speed-test', writeLimiter, mikrotikConfigRoutes.speedTestViaTunnel); app.post('/api/mikrotik/apply', mikrotikConfigRoutes.applyMikrotikConfig); app.post('/api/mikrotik/run-script', mikrotikConfigRoutes.runScript); +app.get('/api/mikrotik/address-lists', mikrotikConfigRoutes.getAddressLists); // === TRAFFIC STATS (MikroTik interfaces by jumphost) === app.get('/api/traffic/interface-stats', trafficRoutes.getInterfaceStats); diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 25b2506..cd2e0c8 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -24,7 +24,8 @@ import { IconLayoutSidebarLeftExpand, IconLayoutNavbar, IconChartPie, - IconClockPlay + IconClockPlay, + IconShield } from '@tabler/icons-react'; import ServerManager from './ServerManager'; import FilterManager from './FilterManager'; @@ -44,6 +45,7 @@ import NetworkMapDashboard from './NetworkMapDashboard.jsx'; import NetworkMapSchedulerPage from './NetworkMapSchedulerPage.jsx'; import MikrotikBackupsManager from './MikrotikBackupsManager.jsx'; import PingServicesManager from './PingServicesManager.jsx'; +import FirewallPage from './FirewallPage.jsx'; import SettingsPage from './SettingsPage.jsx'; import './App.css'; import { NotifyProvider } from './components/NotifyProvider.jsx'; @@ -64,7 +66,7 @@ function LanguageProvider({ children }) { home: 'Главная', data: 'Данные', management: 'Управление', tools: 'Инструменты', dashboard: 'Панель', trafficTraffic: 'Расход трафика', networkMap: 'Карта сети', domains: 'Домены', ipRanges: 'IP-диапазоны', asns: 'AS', communities: 'Community', servers: 'Серверы', filters: 'Фильтры', billing: 'Биллинг', autoUrls: 'Авто URL', - easySwitch: 'Easy Switch', networkConfig: 'Сетевые настройки', mikrotikBackups: 'MikroTik Бэкапы', pingServices: 'Пинг сервисов', + easySwitch: 'Easy Switch', networkConfig: 'Сетевые настройки', mikrotikBackups: 'MikroTik Бэкапы', pingServices: 'Пинг сервисов', firewall: 'Firewall', light: 'Светлая', dark: 'Тёмная', layoutSidebar: 'Сайдбар', layoutHorizontal: 'Верхнее меню' }, @@ -72,7 +74,7 @@ function LanguageProvider({ children }) { home: 'Home', data: 'Data', management: 'Management', tools: 'Tools', dashboard: 'Dashboard', trafficTraffic: 'Traffic Usage', networkMap: 'Network Map', domains: 'Domains', ipRanges: 'IP Ranges', asns: 'ASNs', communities: 'Communities', servers: 'Servers', filters: 'Filters', billing: 'Billing', autoUrls: 'Auto URLs', - easySwitch: 'Easy Switch', networkConfig: 'Network Config', mikrotikBackups: 'MikroTik Backups', pingServices: 'Ping Services', + easySwitch: 'Easy Switch', networkConfig: 'Network Config', mikrotikBackups: 'MikroTik Backups', pingServices: 'Ping Services', firewall: 'Firewall', light: 'Light', dark: 'Dark', layoutSidebar: 'Sidebar', layoutHorizontal: 'Top menu' } @@ -231,6 +233,7 @@ function MainLayout() { { id: 'auto-urls', title: t('autoUrls'), path: '/auto-urls', icon: IconDownload }, { id: 'mikrotik-backups', title: t('mikrotikBackups'), path: '/mikrotik-backups', icon: IconDatabase }, { id: 'mikrotik-tools', title: 'MikroTik Инструменты', path: '/mikrotik-tools', icon: IconNetwork }, + { id: 'firewall', title: t('firewall'), path: '/firewall', icon: IconShield }, { id: 'interface-speed', title: 'Скорость интерфейсов', path: '/interface-speed', icon: IconNetwork }, { id: 'ping-services', title: t('pingServices'), path: '/ping-services', icon: IconNetwork }, { id: 'scheduler', title: 'Планировщик карты сети', path: '/scheduler', icon: IconClockPlay } @@ -400,6 +403,7 @@ function MainLayout() { } /> } /> } /> + } /> } /> } /> } /> @@ -560,6 +564,7 @@ function MainLayout() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/FirewallPage.jsx b/frontend/src/FirewallPage.jsx new file mode 100644 index 0000000..2943f78 --- /dev/null +++ b/frontend/src/FirewallPage.jsx @@ -0,0 +1,319 @@ +import { useState, useEffect, useMemo } from 'react'; +import api from './lib/api.js'; +import PageHeader from './components/PageHeader.jsx'; +import TableSkeleton, { TableEmpty } from './components/TableSkeleton.jsx'; +import EmptyState from './components/EmptyState.jsx'; +import ErrorAlert from './components/ErrorAlert.jsx'; +import ServerAutocompleteInput from './components/ServerAutocompleteInput.jsx'; +import Pagination from './components/Pagination.jsx'; +import Tooltip from './components/Tooltip.jsx'; +import { + IconShield, + IconRefresh, + IconSearch, + IconCopy, + IconAlertCircle, + IconList, +} from '@tabler/icons-react'; + +const PAGE_SIZE = 15; + +/** Нормализация записи address-list из RouterOS (поля могут приходить в разном регистре) */ +function normalizeEntry(entry) { + if (!entry || typeof entry !== 'object') return null; + const addr = entry.address ?? entry.Address ?? ''; + const list = entry.list ?? entry.List ?? ''; + const timeout = entry.timeout ?? entry.Timeout ?? ''; + const comment = entry.comment ?? entry.Comment ?? ''; + const id = entry['.id'] ?? entry.id ?? ''; + const disabled = entry.disabled ?? entry.Disabled ?? ''; + const dynamic = entry.dynamic ?? entry.Dynamic ?? ''; + return { + id: id || addr || Math.random().toString(36).slice(2), + address: String(addr).trim(), + list: String(list).trim(), + timeout: String(timeout).trim(), + comment: String(comment).trim(), + disabled: String(disabled).trim(), + dynamic: String(dynamic).trim(), + }; +} + +function AddressListTable({ title, listKey, items, loading, emptyMessage }) { + const [search, setSearch] = useState(''); + const [page, setPage] = useState(1); + + const filtered = useMemo(() => { + const term = String(search || '').toLowerCase().trim(); + if (!term) return items; + return items.filter((e) => { + const a = (e.address || '').toLowerCase(); + const c = (e.comment || '').toLowerCase(); + return a.includes(term) || c.includes(term); + }); + }, [items, search]); + + const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)); + const paginated = useMemo( + () => filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE), + [filtered, page] + ); + + useEffect(() => { + setPage(1); + }, [search]); + + const copyToClipboard = (text) => { + navigator.clipboard.writeText(text).then(() => { + window.notify?.success?.('Скопировано в буфер обмена'); + }).catch(() => { + window.notify?.error?.('Не удалось скопировать'); + }); + }; + + return ( +
+
+
+
+
+ + {title} + {filtered.length} +
+
+
+
+
+ + + + setSearch(e.target.value)} + aria-label="Поиск" + /> +
+
+
+
+
+ {loading ? ( + + ) : paginated.length === 0 ? ( + + + + ) : ( +
+ + + + + + + + + + + + {paginated.map((entry, idx) => ( + + + + + + + + ))} + +
#IP-адресТаймаутКомментарий
{(page - 1) * PAGE_SIZE + idx + 1} +
+ {entry.address || '—'} + + + +
+
{entry.timeout || '—'}{entry.comment || '—'}
+
+ )} +
+ {!loading && filtered.length > PAGE_SIZE && ( + + )} +
+ ); +} + +export default function FirewallPage() { + const [servers, setServers] = useState([]); + const [serverId, setServerId] = useState(''); + const [serverMeta, setServerMeta] = useState(null); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const [loadingServers, setLoadingServers] = useState(true); + const [error, setError] = useState(''); + + const routerServers = useMemo( + () => + (servers || []).filter( + (s) => String(s.type || '').toLowerCase() === 'jumphost' || String(s.type || '').toLowerCase() === 'home' + ), + [servers] + ); + + useEffect(() => { + let cancelled = false; + (async () => { + setLoadingServers(true); + try { + const res = await api.get('/servers'); + const list = Array.isArray(res.data) ? res.data : []; + if (!cancelled) setServers(list); + } catch (e) { + if (!cancelled) setError('Не удалось загрузить список серверов.'); + } finally { + if (!cancelled) setLoadingServers(false); + } + })(); + return () => { cancelled = true; }; + }, []); + + const fetchAddressLists = async () => { + if (!serverId) { + setData(null); + return; + } + setLoading(true); + setError(''); + try { + const res = await api.get('/mikrotik/address-lists', { params: { serverId } }); + const payload = res?.data ?? {}; + setData({ + serverLabel: payload.serverLabel || serverId, + ban: (payload.ban || []).map(normalizeEntry).filter(Boolean), + ban_counter: (payload.ban_counter || []).map(normalizeEntry).filter(Boolean), + }); + } catch (e) { + console.error('Firewall fetch:', e); + setData(null); + setError(e?.response?.data?.message || e?.message || 'Не удалось загрузить address-list с роутера.'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (serverId) fetchAddressLists(); + else setData(null); + }, [serverId]); + + const currentServerLabel = serverMeta?.dns || serverMeta?.ip || serverId || 'Не выбран'; + + return ( +
+ {error && ( + setError('')} /> + )} + + } + meta={`Списки блокировки (address-list) с роутера: ${currentServerLabel}`} + actions={ +
+ +
+ } + /> + +
+
+

Сервер (MikroTik)

+

+ Выберите jumphost или home-роутер для просмотра списков ban и ban_counter. +

+
+
+ {loadingServers ? ( +
Загрузка списка серверов...
+ ) : ( +
+ +
+ )} +
+
+ + {!serverId && ( +
+
+ +

Выберите сервер выше, чтобы загрузить address-list (ban и ban_counter).

+
+
+ )} + + {serverId && data && ( + <> + + + + )} + + {serverId && loading && !data && ( +
+
+ + Загрузка списков с роутера... +
+
+ )} +
+ ); +} diff --git a/frontend/src/components/CommandPalette.jsx b/frontend/src/components/CommandPalette.jsx index 8b919f7..2492327 100644 --- a/frontend/src/components/CommandPalette.jsx +++ b/frontend/src/components/CommandPalette.jsx @@ -13,7 +13,8 @@ import { IconKeyboard, IconChartPie, IconSettings, - IconClockPlay + IconClockPlay, + IconShield } from '@tabler/icons-react' /** @@ -50,6 +51,7 @@ function CommandPalette() { { icon: IconFilter, label: 'Community', description: 'Справочник BGP Community', action: () => navigate('/communities'), keywords: ['community', 'справочник'] }, { icon: IconServer, label: 'Серверы', description: 'Управление серверами', action: () => navigate('/servers'), keywords: ['серверы', 'servers'] }, { icon: IconFilter, label: 'Фильтры', description: 'Filter Manager', action: () => navigate('/filters'), keywords: ['фильтры', 'filters', 'mikrotik'] }, + { icon: IconShield, label: 'Firewall', description: 'Списки блокировки ban и ban_counter с роутера', action: () => navigate('/firewall'), keywords: ['firewall', 'ban', 'address-list', 'блокировка', 'роутер'] }, { icon: IconCreditCard, label: 'Биллинг', description: 'Управление биллингом', action: () => navigate('/billing'), keywords: ['биллинг', 'billing', 'оплата'] }, { icon: IconDownload, label: 'Авто-URL', description: 'Генератор ссылок', action: () => navigate('/auto-urls'), keywords: ['url', 'ссылки', 'генератор'] }, { icon: IconSettings, label: 'Настройки интерфейса', description: 'WebSocket, DoH, пинг, PTR зона', action: () => navigate('/settings'), keywords: ['настройки', 'settings', 'интерфейс', 'doh', 'websocket'] },