diff --git a/backend/routes/jsonDataRoutes.js b/backend/routes/jsonDataRoutes.js index cc5a09f..de61278 100644 --- a/backend/routes/jsonDataRoutes.js +++ b/backend/routes/jsonDataRoutes.js @@ -9,8 +9,15 @@ const { streamToString } = require('../services/s3Service'); /** * Создать GET эндпоинт для JSON данных + * @param {string} s3Key - Ключ в S3 + * @param {Object} options - Опции + * @param {boolean} options.singleObject - Если true, возвращает объект вместо массива + * @param {any} options.defaultValue - Значение по умолчанию ([] для массива, {} для объекта) */ -function createJsonDataGET(s3Key) { +function createJsonDataGET(s3Key, options = {}) { + const { singleObject = false, defaultValue } = options; + const fallback = defaultValue !== undefined ? defaultValue : (singleObject ? {} : []); + return async (req, res) => { try { const head = await s3.send(new HeadObjectCommand({ Bucket: BUCKET_NAME, Key: s3Key })).catch(() => null); @@ -22,22 +29,26 @@ function createJsonDataGET(s3Key) { const data = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: s3Key })); const fileContent = await streamToString(data.Body); - let items = []; + let items = fallback; try { - items = JSON.parse(fileContent); - if (!Array.isArray(items)) { - items = []; + const parsed = JSON.parse(fileContent); + if (singleObject) { + // Для одиночного объекта + items = (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) ? parsed : fallback; + } else { + // Для массива + items = Array.isArray(parsed) ? parsed : fallback; } } catch (parseError) { console.error(`Error parsing ${s3Key}:`, parseError); - items = []; + items = fallback; } res.json(items); } catch (error) { if (error?.code === 'NoSuchKey' || error?.$metadata?.httpStatusCode === 404) { - res.json([]); + res.json(fallback); } else { console.error(error); return sendError(res, 500, 'Error reading from S3', 'E_S3'); @@ -48,23 +59,45 @@ function createJsonDataGET(s3Key) { /** * Создать POST эндпоинт для JSON данных + * @param {string} s3Key - Ключ в S3 + * @param {Function} validateItem - Функция валидации (для массива - валидирует каждый элемент, для объекта - весь объект) + * @param {Object} options - Опции + * @param {boolean} options.singleObject - Если true, ожидает объект вместо массива */ -function createJsonDataPOST(s3Key, validateItem = null) { +function createJsonDataPOST(s3Key, validateItem = null, options = {}) { + const { singleObject = false } = options; + return async (req, res) => { const { domains: items } = req.body; // Используем 'domains' для обратной совместимости - if (!Array.isArray(items)) { - return sendError(res, 400, 'Data must be an array', 'E_BAD_REQUEST'); - } - - // Валидация элементов, если предоставлена - if (validateItem) { - for (let i = 0; i < items.length; i++) { - const error = validateItem(items[i], i); + if (singleObject) { + // Для одиночного объекта + if (typeof items !== 'object' || items === null || Array.isArray(items)) { + return sendError(res, 400, 'Data must be an object', 'E_BAD_REQUEST'); + } + + // Валидация объекта целиком + if (validateItem) { + const error = validateItem(items); if (error) { return sendError(res, 400, error, 'E_SCHEMA'); } } + } else { + // Для массива + if (!Array.isArray(items)) { + return sendError(res, 400, 'Data must be an array', 'E_BAD_REQUEST'); + } + + // Валидация каждого элемента массива + if (validateItem) { + for (let i = 0; i < items.length; i++) { + const error = validateItem(items[i], i); + if (error) { + return sendError(res, 400, error, 'E_SCHEMA'); + } + } + } } try { @@ -79,11 +112,16 @@ function createJsonDataPOST(s3Key, validateItem = null) { /** * Создать роуты для JSON данных (GET + POST) + * @param {string} s3Key - Ключ в S3 + * @param {Function} validateItem - Функция валидации + * @param {Object} options - Опции + * @param {boolean} options.singleObject - Если true, работает с объектом вместо массива + * @param {any} options.defaultValue - Значение по умолчанию */ -function createJsonDataRoutes(s3Key, validateItem = null) { +function createJsonDataRoutes(s3Key, validateItem = null, options = {}) { return { - get: createJsonDataGET(s3Key), - post: createJsonDataPOST(s3Key, validateItem) + get: createJsonDataGET(s3Key, options), + post: createJsonDataPOST(s3Key, validateItem, options) }; } diff --git a/backend/server.js b/backend/server.js index 8e28d5e..f9dbd3a 100644 --- a/backend/server.js +++ b/backend/server.js @@ -393,6 +393,30 @@ const simpleFiltersRoutes = createJsonDataRoutes('filter-manager/simple-filters. app.get('/api/simple-filters', simpleFiltersRoutes.get); app.post('/api/simple-filters', simpleFiltersRoutes.post); +// Network Config (справочник IP, интерфейсов и gateway) +const networkConfigRoutes = createJsonDataRoutes('network-config.json', (config) => { + // Валидация структуры конфига + if (typeof config !== 'object' || config === null) { + return 'Network config must be an object'; + } + // Валидация gateways + if (config.gateways && !Array.isArray(config.gateways)) { + return 'gateways must be an array'; + } + // Валидация tunnelInterfaces + if (config.tunnelInterfaces && !Array.isArray(config.tunnelInterfaces)) { + return 'tunnelInterfaces must be an array'; + } + // Валидация ipPools + if (config.ipPools && !Array.isArray(config.ipPools)) { + return 'ipPools must be an array'; + } + return null; +}, { singleObject: true }); + +app.get('/api/network-config', networkConfigRoutes.get); +app.post('/api/network-config', networkConfigRoutes.post); + // === COMMUNITIES ROUTES === app.get('/api/communities', communitiesRoutes.getCommunities); app.post('/api/communities', writeLimiter, communitiesRoutes.postCommunities); diff --git a/example/s3/network-config.json b/example/s3/network-config.json new file mode 100644 index 0000000..09b9e4d --- /dev/null +++ b/example/s3/network-config.json @@ -0,0 +1,107 @@ +{ + "gateways": [ + { + "id": "gw-cloudflare-swe", + "name": "SWE-IHOR", + "ip": "94.142.140.1", + "provider": "cloudflare", + "country": "SWE", + "description": "Cloudflare Sweden - основной выход" + }, + { + "id": "gw-bunny-swe", + "name": "SWE-BUNNY", + "ip": "94.142.140.2", + "provider": "bunny", + "country": "SWE", + "description": "Bunny CDN Sweden" + }, + { + "id": "gw-telegram-1", + "name": "TG-PROXY-1", + "ip": "149.154.167.50", + "provider": "telegram", + "country": "NL", + "description": "Telegram DC2" + }, + { + "id": "gw-hetzner-de", + "name": "DE-HETZNER", + "ip": "195.100.30.21", + "provider": "hetzner", + "country": "DE", + "description": "Hetzner Frankfurt" + }, + { + "id": "gw-fastly-us", + "name": "US-FASTLY", + "ip": "151.101.1.1", + "provider": "fastly", + "country": "US", + "description": "Fastly US East" + } + ], + "tunnelInterfaces": [ + { + "id": "if-gre-swe-de", + "name": "gre-swe-de", + "type": "GRE", + "localIp": "10.10.0.1", + "remoteIp": "10.10.0.2", + "port": "", + "serverId": "SWE-HIPHOST" + }, + { + "id": "if-wg-swe-nl", + "name": "wg0", + "type": "WireGuard", + "localIp": "10.20.0.1", + "remoteIp": "10.20.0.2", + "port": "51820", + "serverId": "SWE-HIPHOST" + }, + { + "id": "if-gre-de-us", + "name": "gre-de-us", + "type": "GRE", + "localIp": "10.10.1.1", + "remoteIp": "10.10.1.2", + "port": "", + "serverId": "DE-FRANKFURT" + }, + { + "id": "if-ipsec-nl-jp", + "name": "ipsec-nl-jp", + "type": "IPSec", + "localIp": "10.30.0.1", + "remoteIp": "10.30.0.2", + "port": "", + "serverId": "NL-AMSTERDAM" + } + ], + "ipPools": [ + { + "id": "pool-gre", + "name": "GRE Tunnels", + "cidr": "10.10.0.0/16", + "description": "Пул для GRE туннелей между серверами" + }, + { + "id": "pool-wg", + "name": "WireGuard", + "cidr": "10.20.0.0/16", + "description": "Пул для WireGuard интерфейсов" + }, + { + "id": "pool-ipsec", + "name": "IPSec VPN", + "cidr": "10.30.0.0/16", + "description": "Пул для IPSec туннелей" + } + ], + "defaults": { + "baseUrl": "https://functions.yandexcloud.net/d4eno3im0qgsr4tj5hdo", + "type": "routes", + "version": "v4.rsc" + } +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 676847a..f6ec100 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -31,6 +31,7 @@ import ASNsNewManager from './ASNsNewManager'; import AutoUrlManager from './AutoUrlManager'; import BillingManager from './BillingManager'; import CommunitiesManager from './CommunitiesManager'; +import NetworkConfigManager from './NetworkConfigManager'; import Dashboard from './Dashboard'; import './App.css'; import { NotifyProvider } from './components/NotifyProvider.jsx'; @@ -51,14 +52,14 @@ function LanguageProvider({ children }) { home: 'Главная', data: 'Данные', management: 'Управление', tools: 'Инструменты', dashboard: 'Панель', domains: 'Домены', ipRanges: 'IP-диапазоны', asns: 'AS', communities: 'Community', servers: 'Серверы', filters: 'Фильтры', billing: 'Биллинг', autoUrls: 'Авто URL', - easySwitch: 'Easy Switch', + easySwitch: 'Easy Switch', networkConfig: 'Сетевые настройки', light: 'Светлая', dark: 'Тёмная' }, en: { home: 'Home', data: 'Data', management: 'Management', tools: 'Tools', dashboard: 'Dashboard', domains: 'Domains', ipRanges: 'IP Ranges', asns: 'ASNs', communities: 'Communities', servers: 'Servers', filters: 'Filters', billing: 'Billing', autoUrls: 'Auto URLs', - easySwitch: 'Easy Switch', + easySwitch: 'Easy Switch', networkConfig: 'Network Config', light: 'Light', dark: 'Dark' } }; @@ -190,6 +191,7 @@ function MainLayout() { items: [ { id: 'servers', title: t('servers'), path: '/servers', icon: IconServer }, { id: 'filters', title: t('filters'), path: '/filters', icon: IconFilter }, + { id: 'network-config', title: t('networkConfig'), path: '/network-config', icon: IconNetwork }, { id: 'easy-switch', title: t('easySwitch'), path: '/easy-switch', icon: IconArrowsExchange }, { id: 'billing', title: t('billing'), path: '/billing', icon: IconCreditCard } ] @@ -397,6 +399,7 @@ function MainLayout() { } /> } /> } /> + } /> } /> } /> diff --git a/frontend/src/NetworkConfigManager.jsx b/frontend/src/NetworkConfigManager.jsx new file mode 100644 index 0000000..f03df00 --- /dev/null +++ b/frontend/src/NetworkConfigManager.jsx @@ -0,0 +1,1029 @@ +import { useState, useEffect, useMemo } from 'react'; +import api from './lib/api.js'; +import { useNotify } from './components/NotifyProvider.jsx'; +import FormModal from './components/FormModal.jsx'; +import FormField from './components/FormField.jsx'; +import ConfirmModal from './components/ConfirmModal.jsx'; +import { + IconNetwork, + IconPlus, + IconEdit, + IconTrash, + IconDatabase, + IconServer, + IconWorld, + IconRouter, + IconCopy, + IconSearch, + IconRefresh, + IconSettings +} from '@tabler/icons-react'; + +/** + * Менеджер сетевых настроек + * Справочник IP-адресов, интерфейсов и gateway для серверов + */ + +// Типы провайдеров gateway +const GATEWAY_PROVIDERS = [ + { value: 'cloudflare', label: 'Cloudflare', color: 'orange' }, + { value: 'bunny', label: 'Bunny CDN', color: 'yellow' }, + { value: 'fastly', label: 'Fastly', color: 'red' }, + { value: 'telegram', label: 'Telegram', color: 'blue' }, + { value: 'hetzner', label: 'Hetzner', color: 'green' }, + { value: 'yandex', label: 'Yandex Cloud', color: 'cyan' }, + { value: 'custom', label: 'Другой', color: 'secondary' }, +]; + +// Типы туннельных интерфейсов +const INTERFACE_TYPES = [ + { value: 'GRE', label: 'GRE' }, + { value: 'WireGuard', label: 'WireGuard' }, + { value: 'IPSec', label: 'IPSec' }, + { value: 'VXLAN', label: 'VXLAN' }, + { value: 'OpenVPN', label: 'OpenVPN' }, +]; + +// Пустые объекты +const getEmptyGateway = () => ({ + id: `gw-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`, + name: '', + ip: '', + provider: 'custom', + country: '', + description: '', +}); + +const getEmptyInterface = () => ({ + id: `if-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`, + name: '', + type: 'GRE', + localIp: '', + remoteIp: '', + port: '', + serverId: '', +}); + +const getEmptyIpPool = () => ({ + id: `pool-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`, + name: '', + cidr: '', + description: '', +}); + +const getDefaultConfig = () => ({ + gateways: [], + tunnelInterfaces: [], + ipPools: [], + defaults: { + baseUrl: 'https://functions.yandexcloud.net/d4eno3im0qgsr4tj5hdo', + type: 'routes', + version: 'v4.rsc', + }, +}); + +function NetworkConfigManager() { + const notify = useNotify(); + + // === Основные данные === + const [config, setConfig] = useState(getDefaultConfig()); + const [servers, setServers] = useState([]); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + + // === UI State === + const [activeTab, setActiveTab] = useState('gateways'); // 'gateways' | 'interfaces' | 'pools' | 'defaults' + const [searchTerm, setSearchTerm] = useState(''); + const [providerFilter, setProviderFilter] = useState(''); + + // === Modals === + const [gatewayModalOpen, setGatewayModalOpen] = useState(false); + const [gatewayModalMode, setGatewayModalMode] = useState('add'); + const [editingGateway, setEditingGateway] = useState(null); + + const [interfaceModalOpen, setInterfaceModalOpen] = useState(false); + const [interfaceModalMode, setInterfaceModalMode] = useState('add'); + const [editingInterface, setEditingInterface] = useState(null); + + const [poolModalOpen, setPoolModalOpen] = useState(false); + const [poolModalMode, setPoolModalMode] = useState('add'); + const [editingPool, setEditingPool] = useState(null); + + const [deleteModalOpen, setDeleteModalOpen] = useState(false); + const [itemToDelete, setItemToDelete] = useState(null); + const [deleteType, setDeleteType] = useState(''); + + // === Загрузка данных === + useEffect(() => { + fetchConfig(); + fetchServers(); + }, []); + + const fetchConfig = async () => { + setLoading(true); + try { + const response = await api.get('/network-config'); + const data = response.data || getDefaultConfig(); + setConfig({ + gateways: Array.isArray(data.gateways) ? data.gateways : [], + tunnelInterfaces: Array.isArray(data.tunnelInterfaces) ? data.tunnelInterfaces : [], + ipPools: Array.isArray(data.ipPools) ? data.ipPools : [], + defaults: data.defaults || getDefaultConfig().defaults, + }); + } catch (error) { + if (error.response?.status !== 404) { + console.error('Error fetching network config:', error); + notify.error('Не удалось загрузить сетевые настройки'); + } + // Если файла нет - используем дефолтные значения + setConfig(getDefaultConfig()); + } finally { + setLoading(false); + } + }; + + const fetchServers = async () => { + try { + const response = await api.get('/servers'); + setServers(Array.isArray(response.data) ? response.data : []); + } catch (error) { + console.error('Error fetching servers:', error); + } + }; + + // === Сохранение === + const handleSave = async () => { + setSaving(true); + try { + await api.post('/network-config', { domains: config }); + notify.success('Сетевые настройки сохранены'); + } catch (error) { + console.error('Error saving network config:', error); + notify.error('Не удалось сохранить настройки'); + } finally { + setSaving(false); + } + }; + + // === Фильтрация === + const filteredGateways = useMemo(() => { + let result = [...(config.gateways || [])]; + + if (providerFilter) { + result = result.filter(g => g.provider === providerFilter); + } + + if (searchTerm) { + const term = searchTerm.toLowerCase(); + result = result.filter(g => + g.name?.toLowerCase().includes(term) || + g.ip?.toLowerCase().includes(term) || + g.country?.toLowerCase().includes(term) || + g.description?.toLowerCase().includes(term) + ); + } + + return result; + }, [config.gateways, providerFilter, searchTerm]); + + const filteredInterfaces = useMemo(() => { + let result = [...(config.tunnelInterfaces || [])]; + + if (searchTerm) { + const term = searchTerm.toLowerCase(); + result = result.filter(i => + i.name?.toLowerCase().includes(term) || + i.localIp?.toLowerCase().includes(term) || + i.remoteIp?.toLowerCase().includes(term) || + i.type?.toLowerCase().includes(term) + ); + } + + return result; + }, [config.tunnelInterfaces, searchTerm]); + + const filteredPools = useMemo(() => { + let result = [...(config.ipPools || [])]; + + if (searchTerm) { + const term = searchTerm.toLowerCase(); + result = result.filter(p => + p.name?.toLowerCase().includes(term) || + p.cidr?.toLowerCase().includes(term) || + p.description?.toLowerCase().includes(term) + ); + } + + return result; + }, [config.ipPools, searchTerm]); + + // === CRUD для Gateways === + const handleAddGateway = () => { + setEditingGateway(getEmptyGateway()); + setGatewayModalMode('add'); + setGatewayModalOpen(true); + }; + + const handleEditGateway = (gateway) => { + setEditingGateway({ ...gateway }); + setGatewayModalMode('edit'); + setGatewayModalOpen(true); + }; + + const handleSaveGateway = (gatewayData) => { + if (gatewayModalMode === 'add') { + setConfig(prev => ({ + ...prev, + gateways: [...prev.gateways, gatewayData], + })); + notify.success('Gateway добавлен'); + } else { + setConfig(prev => ({ + ...prev, + gateways: prev.gateways.map(g => g.id === gatewayData.id ? gatewayData : g), + })); + notify.success('Gateway обновлён'); + } + setGatewayModalOpen(false); + setEditingGateway(null); + }; + + const handleDeleteGateway = (gateway) => { + setItemToDelete(gateway); + setDeleteType('gateway'); + setDeleteModalOpen(true); + }; + + // === CRUD для Interfaces === + const handleAddInterface = () => { + setEditingInterface(getEmptyInterface()); + setInterfaceModalMode('add'); + setInterfaceModalOpen(true); + }; + + const handleEditInterface = (iface) => { + setEditingInterface({ ...iface }); + setInterfaceModalMode('edit'); + setInterfaceModalOpen(true); + }; + + const handleSaveInterface = (ifaceData) => { + if (interfaceModalMode === 'add') { + setConfig(prev => ({ + ...prev, + tunnelInterfaces: [...prev.tunnelInterfaces, ifaceData], + })); + notify.success('Интерфейс добавлен'); + } else { + setConfig(prev => ({ + ...prev, + tunnelInterfaces: prev.tunnelInterfaces.map(i => i.id === ifaceData.id ? ifaceData : i), + })); + notify.success('Интерфейс обновлён'); + } + setInterfaceModalOpen(false); + setEditingInterface(null); + }; + + const handleDeleteInterface = (iface) => { + setItemToDelete(iface); + setDeleteType('interface'); + setDeleteModalOpen(true); + }; + + // === CRUD для IP Pools === + const handleAddPool = () => { + setEditingPool(getEmptyIpPool()); + setPoolModalMode('add'); + setPoolModalOpen(true); + }; + + const handleEditPool = (pool) => { + setEditingPool({ ...pool }); + setPoolModalMode('edit'); + setPoolModalOpen(true); + }; + + const handleSavePool = (poolData) => { + if (poolModalMode === 'add') { + setConfig(prev => ({ + ...prev, + ipPools: [...prev.ipPools, poolData], + })); + notify.success('IP пул добавлен'); + } else { + setConfig(prev => ({ + ...prev, + ipPools: prev.ipPools.map(p => p.id === poolData.id ? poolData : p), + })); + notify.success('IP пул обновлён'); + } + setPoolModalOpen(false); + setEditingPool(null); + }; + + const handleDeletePool = (pool) => { + setItemToDelete(pool); + setDeleteType('pool'); + setDeleteModalOpen(true); + }; + + // === Подтверждение удаления === + const executeDelete = () => { + if (!itemToDelete) return; + + if (deleteType === 'gateway') { + setConfig(prev => ({ + ...prev, + gateways: prev.gateways.filter(g => g.id !== itemToDelete.id), + })); + notify.success('Gateway удалён'); + } else if (deleteType === 'interface') { + setConfig(prev => ({ + ...prev, + tunnelInterfaces: prev.tunnelInterfaces.filter(i => i.id !== itemToDelete.id), + })); + notify.success('Интерфейс удалён'); + } else if (deleteType === 'pool') { + setConfig(prev => ({ + ...prev, + ipPools: prev.ipPools.filter(p => p.id !== itemToDelete.id), + })); + notify.success('IP пул удалён'); + } + + setDeleteModalOpen(false); + setItemToDelete(null); + setDeleteType(''); + }; + + // === Обновление defaults === + const handleDefaultsChange = (field, value) => { + setConfig(prev => ({ + ...prev, + defaults: { + ...prev.defaults, + [field]: value, + }, + })); + }; + + // === Copy to clipboard === + const copyToClipboard = async (text) => { + try { + await navigator.clipboard.writeText(text); + notify.success('Скопировано'); + } catch { + notify.error('Не удалось скопировать'); + } + }; + + // === Получение имени сервера по ID === + const getServerName = (serverId) => { + const server = servers.find(s => s.id === serverId || s.ip === serverId); + return server?.dns || server?.ip || serverId || '—'; + }; + + // === Получение цвета провайдера === + const getProviderColor = (provider) => { + const p = GATEWAY_PROVIDERS.find(gp => gp.value === provider); + return p?.color || 'secondary'; + }; + + return ( +
+ {/* Header */} +
+
+
+

+ + Сетевые настройки +

+
+ Справочник IP-адресов, интерфейсов и gateway для серверов +
+
+
+
+ + +
+
+
+
+ + {/* Tabs */} +
+
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+ + {/* Search & Filters */} + {activeTab !== 'defaults' && ( +
+
+
+
+
+ + + + setSearchTerm(e.target.value)} + /> +
+
+ + {activeTab === 'gateways' && ( +
+ +
+ )} + +
+ {activeTab === 'gateways' && ( + + )} + {activeTab === 'interfaces' && ( + + )} + {activeTab === 'pools' && ( + + )} +
+
+
+
+ )} + + {/* Content */} + {loading ? ( +
+
+
+
Загрузка...
+
+
+ ) : ( + <> + {/* Gateways Tab */} + {activeTab === 'gateways' && ( +
+ {filteredGateways.length === 0 ? ( +
+ +

Gateways не найдены

+

Добавьте gateway для использования в конфигурациях

+ +
+ ) : ( +
+ + + + + + + + + + + + + {filteredGateways.map(gateway => ( + + + + + + + + + ))} + +
ИмяIP адресПровайдерСтранаОписаниеДействия
+ {gateway.name || '—'} + + + {gateway.ip || '—'} + {gateway.ip && ( + + )} + + + + {GATEWAY_PROVIDERS.find(p => p.value === gateway.provider)?.label || gateway.provider} + + {gateway.country || '—'}{gateway.description || '—'} +
+ + +
+
+
+ )} +
+ )} + + {/* Interfaces Tab */} + {activeTab === 'interfaces' && ( +
+ {filteredInterfaces.length === 0 ? ( +
+ +

Интерфейсы не найдены

+

Добавьте туннельные интерфейсы для серверов

+ +
+ ) : ( +
+ + + + + + + + + + + + + + {filteredInterfaces.map(iface => ( + + + + + + + + + + ))} + +
ИмяТипLocal IPRemote IPПортСерверДействия
{iface.name || '—'} + {iface.type} + + + {iface.localIp || '—'} + {iface.localIp && ( + + )} + + + {iface.remoteIp || '—'} + {iface.port || '—'}{getServerName(iface.serverId)} +
+ + +
+
+
+ )} +
+ )} + + {/* IP Pools Tab */} + {activeTab === 'pools' && ( +
+ {filteredPools.length === 0 ? ( +
+ +

IP пулы не найдены

+

Добавьте пулы IP-адресов для автоматического назначения

+ +
+ ) : ( +
+ + + + + + + + + + + {filteredPools.map(pool => ( + + + + + + + ))} + +
НазваниеCIDRОписаниеДействия
{pool.name || '—'} + + {pool.cidr || '—'} + {pool.cidr && ( + + )} + + {pool.description || '—'} +
+ + +
+
+
+ )} +
+ )} + + {/* Defaults Tab */} + {activeTab === 'defaults' && ( +
+
+

Настройки по умолчанию для генерации URL

+
+
+
+
+ + handleDefaultsChange('baseUrl', e.target.value)} + placeholder="https://functions.yandexcloud.net/..." + /> +
Базовый URL для генерации ссылок
+
+
+ + +
+
+ + +
+
+
+
+ )} + + )} + + {/* Gateway Modal */} + { setGatewayModalOpen(false); setEditingGateway(null); }} + onSubmit={() => editingGateway && handleSaveGateway(editingGateway)} + title={gatewayModalMode === 'add' ? 'Добавить Gateway' : 'Редактировать Gateway'} + submitLabel={gatewayModalMode === 'add' ? 'Добавить' : 'Сохранить'} + submitIcon={gatewayModalMode === 'add' ? IconPlus : IconEdit} + > + {editingGateway && ( +
+
+ setEditingGateway({ ...editingGateway, name: val })} + placeholder="SWE-IHOR" + required + /> +
+
+ setEditingGateway({ ...editingGateway, ip: val })} + placeholder="94.142.140.1" + /> +
+
+ setEditingGateway({ ...editingGateway, provider: val })} + options={GATEWAY_PROVIDERS.map(p => ({ value: p.value, label: p.label }))} + /> +
+
+ setEditingGateway({ ...editingGateway, country: val })} + placeholder="SWE" + /> +
+
+ setEditingGateway({ ...editingGateway, description: val })} + placeholder="Основной выход через Cloudflare" + /> +
+
+ )} +
+ + {/* Interface Modal */} + { setInterfaceModalOpen(false); setEditingInterface(null); }} + onSubmit={() => editingInterface && handleSaveInterface(editingInterface)} + title={interfaceModalMode === 'add' ? 'Добавить интерфейс' : 'Редактировать интерфейс'} + submitLabel={interfaceModalMode === 'add' ? 'Добавить' : 'Сохранить'} + submitIcon={interfaceModalMode === 'add' ? IconPlus : IconEdit} + > + {editingInterface && ( +
+
+ setEditingInterface({ ...editingInterface, name: val })} + placeholder="gre-tunnel1" + required + /> +
+
+ setEditingInterface({ ...editingInterface, type: val })} + options={INTERFACE_TYPES} + /> +
+
+ setEditingInterface({ ...editingInterface, localIp: val })} + placeholder="10.10.0.1" + /> +
+
+ setEditingInterface({ ...editingInterface, remoteIp: val })} + placeholder="10.10.0.2" + /> +
+
+ setEditingInterface({ ...editingInterface, port: val })} + placeholder="51820" + /> +
+
+ setEditingInterface({ ...editingInterface, serverId: val })} + options={[ + { value: '', label: 'Выберите сервер' }, + ...servers.map(s => ({ value: s.id || s.ip, label: s.dns || s.ip })) + ]} + /> +
+
+ )} +
+ + {/* Pool Modal */} + { setPoolModalOpen(false); setEditingPool(null); }} + onSubmit={() => editingPool && handleSavePool(editingPool)} + title={poolModalMode === 'add' ? 'Добавить IP пул' : 'Редактировать IP пул'} + submitLabel={poolModalMode === 'add' ? 'Добавить' : 'Сохранить'} + submitIcon={poolModalMode === 'add' ? IconPlus : IconEdit} + > + {editingPool && ( +
+
+ setEditingPool({ ...editingPool, name: val })} + placeholder="GRE Tunnels" + required + /> +
+
+ setEditingPool({ ...editingPool, cidr: val })} + placeholder="10.10.0.0/24" + required + /> +
+
+ setEditingPool({ ...editingPool, description: val })} + placeholder="Пул для GRE туннелей между серверами" + /> +
+
+ )} +
+ + {/* Delete Confirmation Modal */} + { setDeleteModalOpen(false); setItemToDelete(null); }} + onConfirm={executeDelete} + title="Подтверждение удаления" + message={ + itemToDelete ? ( + <> +

Вы уверены, что хотите удалить {itemToDelete.name || itemToDelete.cidr || 'этот элемент'}?

+

Это действие нельзя отменить.

+ + ) : null + } + confirmLabel="Удалить" + variant="danger" + icon={IconTrash} + /> +
+ ); +} + +export default NetworkConfigManager;