diff --git a/backend/server.js b/backend/server.js index 17b60c3..bba58a6 100644 --- a/backend/server.js +++ b/backend/server.js @@ -295,10 +295,19 @@ app.post('/api/ip-ranges', writeLimiter, ipRangesRoutes.post); // === JSON DATA ROUTES (используют фабрики) === // Servers +const SERVER_TYPES = ['jumphost', 'exit']; const serversRoutes = createJsonDataRoutes('servers.json', (server, i) => { - if (!server.ip || !server.dns || !server.country || !server.provider || !server.tunnel) { + if (!server.ip || !server.dns || !server.country || !server.provider || !server.tunnel) { return `Server at index ${i} is missing required fields`; } + + const normalizedType = String(server.type || '').toLowerCase(); + if (!SERVER_TYPES.includes(normalizedType)) { + return `Server at index ${i} has invalid type (allowed: ${SERVER_TYPES.join(', ')})`; + } + + // Нормализуем тип, чтобы в S3 всегда лежали одинаковые значения + server.type = normalizedType; return null; }); diff --git a/example-servers.json b/example-servers.json index 57dd170..9876409 100644 --- a/example-servers.json +++ b/example-servers.json @@ -4,34 +4,39 @@ "dns": "srv1.shx.su", "country": "RU", "provider": "Yandex", - "tunnel": "GRE" + "tunnel": "GRE", + "type": "jumphost" }, { "ip": "10.0.0.5", "dns": "srv2.shx.su", "country": "US", "provider": "AWS", - "tunnel": "IPSec" + "tunnel": "IPSec", + "type": "exit" }, { "ip": "172.16.0.10", "dns": "srv3.shx.su", "country": "DE", "provider": "Hetzner", - "tunnel": "GRE" + "tunnel": "GRE", + "type": "jumphost" }, { "ip": "8.8.8.8", "dns": "dns.google", "country": "US", "provider": "Google", - "tunnel": "IPSec" + "tunnel": "IPSec", + "type": "exit" }, { "ip": "1.1.1.1", "dns": "cloudflare-dns.com", "country": "US", "provider": "Cloudflare", - "tunnel": "GRE" + "tunnel": "GRE", + "type": "jumphost" } ] \ No newline at end of file diff --git a/frontend/src/ServerManager.jsx b/frontend/src/ServerManager.jsx index a65b454..2743569 100644 --- a/frontend/src/ServerManager.jsx +++ b/frontend/src/ServerManager.jsx @@ -27,6 +27,10 @@ import { import GraphView from './GraphView'; const API_URL = '/api'; +const SERVER_TYPE_OPTIONS = [ + { value: 'jumphost', label: 'Jumphost' }, + { value: 'exit', label: 'Выходная нода' }, +]; function ServerManager() { const [servers, setServers] = useState([]); @@ -39,6 +43,7 @@ function ServerManager() { country: '', provider: '', tunnel: 'GRE', + type: 'jumphost', gateway: '' }); const [error, setError] = useState(''); @@ -57,6 +62,7 @@ function ServerManager() { const [filterTunnel, setFilterTunnel] = useState(''); const [filterCountry, setFilterCountry] = useState(''); const [filterProvider, setFilterProvider] = useState(''); + const [filterType, setFilterType] = useState(''); const editInputRef = useRef(null); const pageSize = 10; @@ -100,6 +106,11 @@ function ServerManager() { const [graphProviderFilter, setGraphProviderFilter] = useState(''); const [confirmState, setConfirmState] = useState({ open: false, text: '', onConfirm: null }); + const normalizeServerType = (type) => { + const value = String(type || '').toLowerCase(); + return SERVER_TYPE_OPTIONS.some((opt) => opt.value === value) ? value : 'jumphost'; + }; + useEffect(() => { fetchServers(); fetchConnections(); @@ -122,7 +133,11 @@ function ServerManager() { setLoading(true); try { const response = await api.get(`/servers`); - setServers(response.data); + const normalized = (Array.isArray(response.data) ? response.data : []).map((srv) => ({ + ...srv, + type: normalizeServerType(srv.type), + })); + setServers(normalized); setError(''); } catch (error) { console.error('Error fetching servers:', error); @@ -147,7 +162,7 @@ function ServerManager() { }; const handleAddServer = () => { - if (!newServer.ip.trim() || !newServer.dns.trim() || !newServer.country.trim() || !newServer.provider.trim() || !newServer.tunnel.trim()) { + if (!newServer.ip.trim() || !newServer.dns.trim() || !newServer.country.trim() || !newServer.provider.trim() || !newServer.tunnel.trim() || !newServer.type) { setError('Все обязательные поля должны быть заполнены.'); return; } @@ -156,6 +171,7 @@ function ServerManager() { const serverToAdd = { ...newServer, id: newServer.id || nextId, + type: normalizeServerType(newServer.type), }; setServers([...servers, serverToAdd]); setNewServer({ @@ -167,6 +183,7 @@ function ServerManager() { country: '', provider: '', tunnel: 'GRE', + type: 'jumphost', gateway: '' }); }; @@ -187,6 +204,7 @@ function ServerManager() { country: '', provider: '', tunnel: 'GRE', + type: 'jumphost', gateway: '' }); setCustomProvider(''); @@ -194,7 +212,7 @@ function ServerManager() { }; const handleAddServerFromModal = () => { - if (!newServer.ip.trim() || !newServer.dns.trim() || !newServer.country.trim() || !newServer.provider.trim() || !newServer.tunnel.trim() || !newServer.gateway.trim()) { + if (!newServer.ip.trim() || !newServer.dns.trim() || !newServer.country.trim() || !newServer.provider.trim() || !newServer.tunnel.trim() || !newServer.gateway.trim() || !newServer.type) { setError('Все обязательные поля должны быть заполнены.'); return; } @@ -214,6 +232,7 @@ function ServerManager() { ...newServer, provider: finalProvider, id: newServer.id || nextId, + type: normalizeServerType(newServer.type), }; setServers([...servers, serverToAdd]); @@ -226,6 +245,7 @@ function ServerManager() { country: '', provider: '', tunnel: 'GRE', + type: 'jumphost', gateway: '' }); setCustomProvider(''); @@ -244,10 +264,11 @@ function ServerManager() { }; const handleEditModalSave = (updatedServer) => { + const normalizedServer = { ...updatedServer, type: normalizeServerType(updatedServer.type) }; setServers(servers.map(s => { const sameId = s.id && updatedServer.id ? s.id === updatedServer.id : false; const sameIp = s.ip === updatedServer.ip; - return (sameId || sameIp) ? { ...s, ...updatedServer } : s; + return (sameId || sameIp) ? { ...s, ...normalizedServer } : s; })); setEditModalOpen(false); setEditModalServer(null); @@ -334,9 +355,10 @@ function ServerManager() { const filtered = servers .filter(s => (!filterTunnel || s.tunnel === filterTunnel)) .filter(s => (!filterCountry || s.country === filterCountry)) - .filter(s => (!filterProvider || s.provider === filterProvider)); + .filter(s => (!filterProvider || s.provider === filterProvider)) + .filter(s => (!filterType || s.type === filterType)); const csvContent = [ - ['ID', 'IP', 'Ext IP', 'Internal IP', 'DNS', 'Country', 'Provider', 'Tunnel', 'Generated URL'], + ['ID', 'IP', 'Ext IP', 'Internal IP', 'DNS', 'Country', 'Provider', 'Type', 'Tunnel', 'Generated URL'], ...filtered.map(server => [ server.id || '', server.ip, @@ -345,6 +367,7 @@ function ServerManager() { server.dns, server.country, server.provider, + server.type, server.tunnel, generateServerUrl(server) ]) @@ -385,7 +408,9 @@ function ServerManager() { const handleSaveChanges = async () => { setLoading(true); try { - await api.post(`/servers`, { domains: servers }); + const normalizedServers = servers.map((s) => ({ ...s, type: normalizeServerType(s.type) })); + setServers(normalizedServers); + await api.post(`/servers`, { domains: normalizedServers }); await api.post(`/server-connections`, { domains: connections }); setSuccess('Изменения по серверам и связям успешно сохранены!'); setTimeout(() => setSuccess(''), 3000); @@ -440,12 +465,17 @@ function ServerManager() { filteredServers = filteredServers.filter(s => s.provider === filterProvider); } + if (filterType) { + filteredServers = filteredServers.filter(s => s.type === filterType); + } + // Поиск filteredServers = filteredServers.filter(s => s.ip.toLowerCase().includes(searchTerm.toLowerCase()) || s.dns.toLowerCase().includes(searchTerm.toLowerCase()) || s.country.toLowerCase().includes(searchTerm.toLowerCase()) || - s.provider.toLowerCase().includes(searchTerm.toLowerCase()) + s.provider.toLowerCase().includes(searchTerm.toLowerCase()) || + (s.type || '').toLowerCase().includes(searchTerm.toLowerCase()) ); // Пагинация @@ -456,6 +486,7 @@ function ServerManager() { const allTunnels = Array.from(new Set(servers.map(s => s.tunnel))); const allCountries = Array.from(new Set(servers.map(s => s.country))); const allProviders = Array.from(new Set(servers.map(s => s.provider))); + const allTypes = Array.from(new Set(servers.map(s => s.type).filter(Boolean))); // Сортировка по клику const handleSort = (field) => { @@ -617,6 +648,12 @@ function ServerManager() { ))} +
@@ -632,7 +669,7 @@ function ServerManager() {
+
+ + +
handleChange('tunnel', e.target.value)} /> @@ -1651,6 +1723,20 @@ function AddServerModal({ show, newServer, customProvider, onNewServerChange, on /> )}
+
+ + +