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

This commit is contained in:
2025-12-06 02:16:37 +07:00
parent 7ccb3118aa
commit f8f8be44ca
3 changed files with 116 additions and 16 deletions
+10 -1
View File
@@ -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
View File
@@ -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"
}
]
+96 -10
View File
@@ -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