feat: Introduce server type management in ServerManager, enhancing server configuration with type selection and validation, and update example-servers.json to include server types for improved data consistency.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m27s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m27s
This commit is contained in:
+10
-1
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
+10
-5
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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() {
|
||||
<option key={provider} value={provider}>{provider}</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="form-select w-auto" value={filterType} onChange={e => { setFilterType(e.target.value); setCurrentPage(1); }}>
|
||||
<option value="">Все типы</option>
|
||||
{allTypes.map(type => (
|
||||
<option key={type} value={type}>{type === 'exit' ? 'Выходная нода' : 'Jumphost'}</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="input-icon flex-grow-1">
|
||||
<span className="input-icon-addon">
|
||||
<IconSearch size={18} />
|
||||
@@ -632,7 +669,7 @@ function ServerManager() {
|
||||
<button
|
||||
className="btn btn-outline-secondary"
|
||||
type="button"
|
||||
onClick={() => { setFilterTunnel(''); setFilterCountry(''); setFilterProvider(''); setSearchTerm(''); setCurrentPage(1); }}
|
||||
onClick={() => { setFilterTunnel(''); setFilterCountry(''); setFilterProvider(''); setFilterType(''); setSearchTerm(''); setCurrentPage(1); }}
|
||||
title="Сбросить фильтры"
|
||||
>
|
||||
Сбросить
|
||||
@@ -651,6 +688,14 @@ function ServerManager() {
|
||||
</span>
|
||||
)}
|
||||
</th>
|
||||
<th className="cursor-pointer" onClick={() => handleSort('type')}>
|
||||
Тип
|
||||
{sortField === 'type' && (
|
||||
<span className="ms-1">
|
||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
||||
</span>
|
||||
)}
|
||||
</th>
|
||||
<th>
|
||||
Внешний IP
|
||||
</th>
|
||||
@@ -704,11 +749,16 @@ function ServerManager() {
|
||||
<tbody>
|
||||
{paginatedServers.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={8} className="text-center text-muted py-4">Ничего не найдено. Измените фильтры или поиск.</td>
|
||||
<td colSpan={11} className="text-center text-muted py-4">Ничего не найдено. Измените фильтры или поиск.</td>
|
||||
</tr>
|
||||
) : paginatedServers.map((server) => (
|
||||
<tr key={server.id || server.ip} className={editingServer === server.ip ? 'table-info' : ''}>
|
||||
<td>{server.ip}</td>
|
||||
<td>
|
||||
<span className={`badge ${server.type === 'exit' ? 'bg-red-lt text-danger' : 'bg-blue-lt text-blue'}`}>
|
||||
{server.type === 'exit' ? 'Выходная нода' : 'Jumphost'}
|
||||
</span>
|
||||
</td>
|
||||
<td>{server.extIp || <span className="text-muted">—</span>}</td>
|
||||
<td>{server.internalIp || <span className="text-muted">—</span>}</td>
|
||||
<td>{server.dns}</td>
|
||||
@@ -934,6 +984,20 @@ function ServerManager() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
label="Тип сервера"
|
||||
name="type"
|
||||
type="select"
|
||||
value={newServer.type}
|
||||
onChange={(val) => setNewServer({ ...newServer, type: val })}
|
||||
required
|
||||
options={[
|
||||
{ value: '', label: 'Выберите тип', disabled: true },
|
||||
...SERVER_TYPE_OPTIONS.map(opt => ({ value: opt.value, label: opt.label }))
|
||||
]}
|
||||
helpText="Выберите роль: jumphost или выходная нода"
|
||||
/>
|
||||
|
||||
<FormField
|
||||
label="Тип туннеля"
|
||||
name="tunnel"
|
||||
@@ -1284,6 +1348,14 @@ function EditServerModal({ show, server, onChange, onSave, onClose }) {
|
||||
<label className="form-label">Провайдер</label>
|
||||
<input type="text" className="form-control" value={localServer.provider || ''} onChange={e => handleChange('provider', e.target.value)} />
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Тип сервера</label>
|
||||
<select className="form-select" value={localServer.type || 'jumphost'} onChange={e => handleChange('type', e.target.value)}>
|
||||
{SERVER_TYPE_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Тип туннеля</label>
|
||||
<input type="text" className="form-control" value={localServer.tunnel || ''} onChange={e => handleChange('tunnel', e.target.value)} />
|
||||
@@ -1651,6 +1723,20 @@ function AddServerModal({ show, newServer, customProvider, onNewServerChange, on
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Тип сервера *</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={newServer.type}
|
||||
onChange={(e) => handleChange('type', e.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="">Выберите тип</option>
|
||||
{SERVER_TYPE_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Тип туннеля *</label>
|
||||
<select
|
||||
|
||||
Reference in New Issue
Block a user