feat: Enhance server validation in server.js and improve gateway management in ServerManager and FilterManager, ensuring proper gateway configuration for jumphost and exit server types.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m2s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m2s
This commit is contained in:
@@ -306,6 +306,22 @@ const serversRoutes = createJsonDataRoutes('servers.json', (server, i) => {
|
|||||||
return `Server at index ${i} has invalid type (allowed: ${SERVER_TYPES.join(', ')})`;
|
return `Server at index ${i} has invalid type (allowed: ${SERVER_TYPES.join(', ')})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (normalizedType === 'jumphost' || normalizedType === 'exit') {
|
||||||
|
if (!Array.isArray(server.gateways) || server.gateways.length === 0) {
|
||||||
|
return `Server at index ${i} must have gateways for type ${normalizedType}`;
|
||||||
|
}
|
||||||
|
const primaries = server.gateways.filter(g => g && g.primary);
|
||||||
|
if (primaries.length !== 1) {
|
||||||
|
return `Server at index ${i} must have exactly one primary gateway`;
|
||||||
|
}
|
||||||
|
for (let j = 0; j < server.gateways.length; j++) {
|
||||||
|
const gw = server.gateways[j] || {};
|
||||||
|
if (!gw.name || String(gw.name).trim().length === 0) {
|
||||||
|
return `Server at index ${i} gateway at index ${j} is missing name`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Нормализуем тип, чтобы в S3 всегда лежали одинаковые значения
|
// Нормализуем тип, чтобы в S3 всегда лежали одинаковые значения
|
||||||
server.type = normalizedType;
|
server.type = normalizedType;
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -43,39 +43,6 @@ const countryToFlag = (code) => {
|
|||||||
return String.fromCodePoint(...codePoints);
|
return String.fromCodePoint(...codePoints);
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeGateways = (gateways = [], fallbackName = '') => {
|
|
||||||
if (!Array.isArray(gateways)) return [];
|
|
||||||
const cleaned = gateways
|
|
||||||
.map((g, idx) => {
|
|
||||||
const name = String(g?.name || '').trim();
|
|
||||||
const ip = String(g?.ip || '').trim();
|
|
||||||
const comment = String(g?.comment || '').trim();
|
|
||||||
const primary = Boolean(g?.primary);
|
|
||||||
const id = g?.id || `${Date.now()}-${idx}-${Math.random().toString(16).slice(2, 6)}`;
|
|
||||||
return { id, name, ip, comment, primary };
|
|
||||||
})
|
|
||||||
.filter(g => g.name || g.ip || g.comment);
|
|
||||||
if (cleaned.length === 0) {
|
|
||||||
return [{ id: `${Date.now()}-primary`, name: fallbackName || 'gateway', ip: '', comment: '', primary: true }];
|
|
||||||
}
|
|
||||||
if (!cleaned.some(g => g.primary)) {
|
|
||||||
cleaned[0].primary = true;
|
|
||||||
} else {
|
|
||||||
// если несколько primary, оставляем только первый
|
|
||||||
let found = false;
|
|
||||||
cleaned.forEach(g => {
|
|
||||||
if (g.primary) {
|
|
||||||
if (!found) {
|
|
||||||
found = true;
|
|
||||||
} else {
|
|
||||||
g.primary = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return cleaned;
|
|
||||||
};
|
|
||||||
|
|
||||||
function FilterManager() {
|
function FilterManager() {
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [success, setSuccess] = useState('');
|
const [success, setSuccess] = useState('');
|
||||||
@@ -90,15 +57,7 @@ function FilterManager() {
|
|||||||
const [servers, setServers] = useState([]);
|
const [servers, setServers] = useState([]);
|
||||||
const [selectedServer, setSelectedServer] = useState(null);
|
const [selectedServer, setSelectedServer] = useState(null);
|
||||||
const [serverConfigModalOpen, setServerConfigModalOpen] = useState(false);
|
const [serverConfigModalOpen, setServerConfigModalOpen] = useState(false);
|
||||||
const makeGateway = (over = {}) => ({
|
const [newServer, setNewServer] = useState({ name: '', description: '', enabled: true });
|
||||||
id: `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
|
|
||||||
name: '',
|
|
||||||
ip: '',
|
|
||||||
comment: '',
|
|
||||||
primary: false,
|
|
||||||
...over,
|
|
||||||
});
|
|
||||||
const [newServer, setNewServer] = useState({ name: '', description: '', enabled: true, gateways: [makeGateway({ primary: true })] });
|
|
||||||
|
|
||||||
// Состояния для управления фильтрами сервера
|
// Состояния для управления фильтрами сервера
|
||||||
const [serverFilters, setServerFilters] = useState([]);
|
const [serverFilters, setServerFilters] = useState([]);
|
||||||
@@ -112,63 +71,6 @@ function FilterManager() {
|
|||||||
// Новое состояние для редактирования сервера
|
// Новое состояние для редактирования сервера
|
||||||
const [editServerModalOpen, setEditServerModalOpen] = useState(false);
|
const [editServerModalOpen, setEditServerModalOpen] = useState(false);
|
||||||
const [editingServer, setEditingServer] = useState(null);
|
const [editingServer, setEditingServer] = useState(null);
|
||||||
|
|
||||||
const updateNewGateway = (idx, field, value) => {
|
|
||||||
setNewServer((prev) => {
|
|
||||||
const list = prev.gateways.map((g, i) => {
|
|
||||||
if (i !== idx) return g;
|
|
||||||
const next = { ...g, [field]: value };
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
if (field === 'primary' && value) {
|
|
||||||
list.forEach((g, i) => { g.primary = i === idx; });
|
|
||||||
}
|
|
||||||
return { ...prev, gateways: list };
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const addNewGateway = () => {
|
|
||||||
setNewServer((prev) => ({ ...prev, gateways: [...prev.gateways, makeGateway({ primary: prev.gateways.length === 0 })] }));
|
|
||||||
};
|
|
||||||
const removeNewGateway = (idx) => {
|
|
||||||
setNewServer((prev) => {
|
|
||||||
if (prev.gateways.length === 1) return prev;
|
|
||||||
const list = prev.gateways.filter((_, i) => i !== idx);
|
|
||||||
if (!list.some(g => g.primary) && list.length > 0) list[0].primary = true;
|
|
||||||
return { ...prev, gateways: list };
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateEditGateway = (idx, field, value) => {
|
|
||||||
setEditingServer((prev) => {
|
|
||||||
if (!prev) return prev;
|
|
||||||
const list = (prev.gateways || []).map((g, i) => {
|
|
||||||
if (i !== idx) return g;
|
|
||||||
const next = { ...g, [field]: value };
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
if (field === 'primary' && value) {
|
|
||||||
list.forEach((g, i) => { g.primary = i === idx; });
|
|
||||||
}
|
|
||||||
return { ...prev, gateways: list };
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const addEditGateway = () => {
|
|
||||||
setEditingServer((prev) => {
|
|
||||||
if (!prev) return prev;
|
|
||||||
const list = [...(prev.gateways || []), makeGateway({ primary: !(prev.gateways || []).length })];
|
|
||||||
return { ...prev, gateways: list };
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const removeEditGateway = (idx) => {
|
|
||||||
setEditingServer((prev) => {
|
|
||||||
if (!prev) return prev;
|
|
||||||
const list = (prev.gateways || []);
|
|
||||||
if (list.length === 1) return prev;
|
|
||||||
const next = list.filter((_, i) => i !== idx);
|
|
||||||
if (!next.some(g => g.primary) && next.length > 0) next[0].primary = true;
|
|
||||||
return { ...prev, gateways: next };
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const [editServerError, setEditServerError] = useState('');
|
const [editServerError, setEditServerError] = useState('');
|
||||||
|
|
||||||
// Состояние поиска/сортировки
|
// Состояние поиска/сортировки
|
||||||
@@ -176,19 +78,6 @@ function FilterManager() {
|
|||||||
const [sortBy, setSortBy] = useState('community'); // community | gateway | description
|
const [sortBy, setSortBy] = useState('community'); // community | gateway | description
|
||||||
const [sortDir, setSortDir] = useState('asc'); // asc | desc
|
const [sortDir, setSortDir] = useState('asc'); // asc | desc
|
||||||
|
|
||||||
// Инвентарный список servers (dns/ip) для сопоставления по имени
|
|
||||||
const [inventoryServers, setInventoryServers] = useState([]);
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const res = await api.get('/servers');
|
|
||||||
setInventoryServers(Array.isArray(res.data) ? res.data : []);
|
|
||||||
} catch (e) {
|
|
||||||
// не блокируем UI, если нет инвентаря
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Префилл из query (?community=..., ?serverId=..., ?server=...) без автоселекта по умолчанию
|
// Префилл из query (?community=..., ?serverId=..., ?server=...) без автоселекта по умолчанию
|
||||||
const queryParamsRef = useRef({ community: null, serverId: null, serverName: null });
|
const queryParamsRef = useRef({ community: null, serverId: null, serverName: null });
|
||||||
const serverPrefillDoneRef = useRef(false);
|
const serverPrefillDoneRef = useRef(false);
|
||||||
@@ -299,7 +188,6 @@ function FilterManager() {
|
|||||||
const serverToAdd = {
|
const serverToAdd = {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
...newServer,
|
...newServer,
|
||||||
gateways: normalizeGateways(newServer.gateways, newServer.name),
|
|
||||||
createdAt: new Date().toISOString()
|
createdAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -307,7 +195,7 @@ function FilterManager() {
|
|||||||
await api.post(`/server-configs`, { servers: updatedServers });
|
await api.post(`/server-configs`, { servers: updatedServers });
|
||||||
|
|
||||||
setServers(updatedServers);
|
setServers(updatedServers);
|
||||||
setNewServer({ name: '', description: '', enabled: true, gateways: [makeGateway({ primary: true })] });
|
setNewServer({ name: '', description: '', enabled: true });
|
||||||
setServerConfigModalOpen(false);
|
setServerConfigModalOpen(false);
|
||||||
setSuccess('Сервер успешно добавлен!');
|
setSuccess('Сервер успешно добавлен!');
|
||||||
setTimeout(() => setSuccess(''), 3000);
|
setTimeout(() => setSuccess(''), 3000);
|
||||||
@@ -622,7 +510,7 @@ function FilterManager() {
|
|||||||
|
|
||||||
// Открыть модалку редактирования
|
// Открыть модалку редактирования
|
||||||
const handleEditServer = (server) => {
|
const handleEditServer = (server) => {
|
||||||
setEditingServer({ ...server, gateways: normalizeGateways(server.gateways, server.name) });
|
setEditingServer({ ...server });
|
||||||
setEditServerError('');
|
setEditServerError('');
|
||||||
setEditServerModalOpen(true);
|
setEditServerModalOpen(true);
|
||||||
};
|
};
|
||||||
@@ -634,7 +522,7 @@ function FilterManager() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const edited = { ...editingServer, gateways: normalizeGateways(editingServer.gateways, editingServer.name) };
|
const edited = { ...editingServer };
|
||||||
const updatedServers = servers.map(s =>
|
const updatedServers = servers.map(s =>
|
||||||
s.id === editingServer.id ? { ...s, name: edited.name, description: edited.description, gateways: edited.gateways } : s
|
s.id === editingServer.id ? { ...s, name: edited.name, description: edited.description, gateways: edited.gateways } : s
|
||||||
);
|
);
|
||||||
@@ -1631,7 +1519,7 @@ function FilterManager() {
|
|||||||
show={serverConfigModalOpen}
|
show={serverConfigModalOpen}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setServerConfigModalOpen(false);
|
setServerConfigModalOpen(false);
|
||||||
setNewServer({ name: '', description: '', enabled: true, gateways: [makeGateway({ primary: true })] });
|
setNewServer({ name: '', description: '', enabled: true });
|
||||||
}}
|
}}
|
||||||
onSubmit={handleAddServer}
|
onSubmit={handleAddServer}
|
||||||
title="Добавить новый сервер"
|
title="Добавить новый сервер"
|
||||||
@@ -1665,80 +1553,6 @@ function FilterManager() {
|
|||||||
icon={IconFileText}
|
icon={IconFileText}
|
||||||
helpText="Краткое описание сервера (необязательно)"
|
helpText="Краткое описание сервера (необязательно)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<label className="form-label">Шлюзы</label>
|
|
||||||
<div className="list-group list-group-flush border rounded">
|
|
||||||
{newServer.gateways.map((gw, idx) => (
|
|
||||||
<div key={gw.id || idx} className="list-group-item">
|
|
||||||
<div className="row g-2 align-items-end">
|
|
||||||
<div className="col-12 col-md-4">
|
|
||||||
<label className="form-label small mb-1">Имя *</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
value={gw.name}
|
|
||||||
onChange={(e) => updateNewGateway(idx, 'name', e.target.value)}
|
|
||||||
placeholder="GW-MSK"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-4">
|
|
||||||
<label className="form-label small mb-1">IP</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
value={gw.ip}
|
|
||||||
onChange={(e) => updateNewGateway(idx, 'ip', e.target.value)}
|
|
||||||
placeholder="10.0.0.1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-3">
|
|
||||||
<label className="form-label small mb-1">Комментарий</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
value={gw.comment}
|
|
||||||
onChange={(e) => updateNewGateway(idx, 'comment', e.target.value)}
|
|
||||||
placeholder="Основной шлюз"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-1 d-flex align-items-center justify-content-center">
|
|
||||||
<div className="form-check form-switch m-0">
|
|
||||||
<input
|
|
||||||
className="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
checked={gw.primary}
|
|
||||||
onChange={(e) => updateNewGateway(idx, 'primary', e.target.checked)}
|
|
||||||
title="Основной шлюз"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="d-flex justify-content-between align-items-center mt-2">
|
|
||||||
<div className="text-muted small">{gw.primary ? 'Основной шлюз' : 'Резервный шлюз'}</div>
|
|
||||||
{newServer.gateways.length > 1 && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-link text-danger px-0"
|
|
||||||
onClick={() => removeNewGateway(idx)}
|
|
||||||
>
|
|
||||||
Удалить
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="mt-2">
|
|
||||||
<button type="button" className="btn btn-outline-primary btn-sm" onClick={addNewGateway}>
|
|
||||||
<IconPlus className="icon" />
|
|
||||||
Добавить шлюз
|
|
||||||
</button>
|
|
||||||
<div className="form-text">Один шлюз должен быть помечен как основной.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-check form-switch mb-3">
|
<div className="form-check form-switch mb-3">
|
||||||
<input
|
<input
|
||||||
className="form-check-input"
|
className="form-check-input"
|
||||||
@@ -1796,78 +1610,6 @@ function FilterManager() {
|
|||||||
rows={2}
|
rows={2}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<label className="form-label">Шлюзы</label>
|
|
||||||
<div className="list-group list-group-flush border rounded">
|
|
||||||
{(editingServer.gateways || []).map((gw, idx) => (
|
|
||||||
<div key={gw.id || idx} className="list-group-item">
|
|
||||||
<div className="row g-2 align-items-end">
|
|
||||||
<div className="col-12 col-md-4">
|
|
||||||
<label className="form-label small mb-1">Имя *</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
value={gw.name}
|
|
||||||
onChange={(e) => updateEditGateway(idx, 'name', e.target.value)}
|
|
||||||
placeholder="GW-MSK"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-4">
|
|
||||||
<label className="form-label small mb-1">IP</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
value={gw.ip}
|
|
||||||
onChange={(e) => updateEditGateway(idx, 'ip', e.target.value)}
|
|
||||||
placeholder="10.0.0.1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-3">
|
|
||||||
<label className="form-label small mb-1">Комментарий</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
value={gw.comment}
|
|
||||||
onChange={(e) => updateEditGateway(idx, 'comment', e.target.value)}
|
|
||||||
placeholder="Основной шлюз"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-1 d-flex align-items-center justify-content-center">
|
|
||||||
<div className="form-check form-switch m-0">
|
|
||||||
<input
|
|
||||||
className="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
checked={gw.primary}
|
|
||||||
onChange={(e) => updateEditGateway(idx, 'primary', e.target.checked)}
|
|
||||||
title="Основной шлюз"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="d-flex justify-content-between align-items-center mt-2">
|
|
||||||
<div className="text-muted small">{gw.primary ? 'Основной шлюз' : 'Резервный шлюз'}</div>
|
|
||||||
{(editingServer.gateways || []).length > 1 && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-link text-danger px-0"
|
|
||||||
onClick={() => removeEditGateway(idx)}
|
|
||||||
>
|
|
||||||
Удалить
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="mt-2">
|
|
||||||
<button type="button" className="btn btn-outline-primary btn-sm" onClick={addEditGateway}>
|
|
||||||
<IconPlus className="icon" />
|
|
||||||
Добавить шлюз
|
|
||||||
</button>
|
|
||||||
<div className="form-text">Один шлюз должен быть помечен как основной.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</FormModal>
|
</FormModal>
|
||||||
|
|||||||
@@ -31,6 +31,43 @@ const SERVER_TYPE_OPTIONS = [
|
|||||||
{ value: 'jumphost', label: 'Jumphost' },
|
{ value: 'jumphost', label: 'Jumphost' },
|
||||||
{ value: 'exit', label: 'Выходная нода' },
|
{ value: 'exit', label: 'Выходная нода' },
|
||||||
];
|
];
|
||||||
|
const NEEDS_GATEWAYS = (type) => ['jumphost', 'exit'].includes(String(type || '').toLowerCase());
|
||||||
|
|
||||||
|
const makeGateway = (over = {}) => ({
|
||||||
|
id: `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
|
||||||
|
name: '',
|
||||||
|
ip: '',
|
||||||
|
comment: '',
|
||||||
|
primary: false,
|
||||||
|
...over,
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalizeGateways = (gateways = [], fallbackName = '') => {
|
||||||
|
if (!Array.isArray(gateways)) return [];
|
||||||
|
const cleaned = gateways
|
||||||
|
.map((g, idx) => ({
|
||||||
|
id: g?.id || `${Date.now()}-${idx}`,
|
||||||
|
name: String(g?.name || '').trim(),
|
||||||
|
ip: String(g?.ip || '').trim(),
|
||||||
|
comment: String(g?.comment || '').trim(),
|
||||||
|
primary: Boolean(g?.primary),
|
||||||
|
}))
|
||||||
|
.filter(g => g.name || g.ip || g.comment);
|
||||||
|
if (cleaned.length === 0) {
|
||||||
|
return [{ id: `${Date.now()}-primary`, name: fallbackName || 'gateway', ip: '', comment: '', primary: true }];
|
||||||
|
}
|
||||||
|
// ensure single primary
|
||||||
|
let hasPrimary = false;
|
||||||
|
cleaned.forEach((g, i) => {
|
||||||
|
if (g.primary && !hasPrimary) {
|
||||||
|
hasPrimary = true;
|
||||||
|
} else {
|
||||||
|
g.primary = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!hasPrimary) cleaned[0].primary = true;
|
||||||
|
return cleaned;
|
||||||
|
};
|
||||||
|
|
||||||
function ServerManager() {
|
function ServerManager() {
|
||||||
const [servers, setServers] = useState([]);
|
const [servers, setServers] = useState([]);
|
||||||
@@ -44,7 +81,8 @@ function ServerManager() {
|
|||||||
provider: '',
|
provider: '',
|
||||||
tunnel: 'GRE',
|
tunnel: 'GRE',
|
||||||
type: 'jumphost',
|
type: 'jumphost',
|
||||||
gateway: ''
|
gateway: '',
|
||||||
|
gateways: [makeGateway({ primary: true })],
|
||||||
});
|
});
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [success, setSuccess] = useState('');
|
const [success, setSuccess] = useState('');
|
||||||
@@ -136,6 +174,7 @@ function ServerManager() {
|
|||||||
const normalized = (Array.isArray(response.data) ? response.data : []).map((srv) => ({
|
const normalized = (Array.isArray(response.data) ? response.data : []).map((srv) => ({
|
||||||
...srv,
|
...srv,
|
||||||
type: normalizeServerType(srv.type),
|
type: normalizeServerType(srv.type),
|
||||||
|
gateways: NEEDS_GATEWAYS(srv.type) ? normalizeGateways(srv.gateways, srv.gateway || srv.dns || srv.ip) : (Array.isArray(srv.gateways) ? srv.gateways : []),
|
||||||
}));
|
}));
|
||||||
setServers(normalized);
|
setServers(normalized);
|
||||||
setError('');
|
setError('');
|
||||||
@@ -212,10 +251,20 @@ function ServerManager() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAddServerFromModal = () => {
|
const handleAddServerFromModal = () => {
|
||||||
if (!newServer.ip.trim() || !newServer.dns.trim() || !newServer.country.trim() || !newServer.provider.trim() || !newServer.tunnel.trim() || !newServer.gateway.trim() || !newServer.type) {
|
if (!newServer.ip.trim() || !newServer.dns.trim() || !newServer.country.trim() || !newServer.provider.trim() || !newServer.tunnel.trim() || !newServer.type) {
|
||||||
setError('Все обязательные поля должны быть заполнены.');
|
setError('Все обязательные поля должны быть заполнены.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (NEEDS_GATEWAYS(newServer.type)) {
|
||||||
|
const normalizedGws = normalizeGateways(newServer.gateways, newServer.gateway || newServer.dns || newServer.ip);
|
||||||
|
if (!normalizedGws.length) {
|
||||||
|
setError('Добавьте хотя бы один gateway и отметьте основной.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newServer.gateways = normalizedGws;
|
||||||
|
} else {
|
||||||
|
newServer.gateways = [];
|
||||||
|
}
|
||||||
|
|
||||||
// Проверяем, если выбран "Другой" провайдер, то customProvider должен быть заполнен
|
// Проверяем, если выбран "Другой" провайдер, то customProvider должен быть заполнен
|
||||||
if (newServer.provider === 'Другой' && !customProvider.trim()) {
|
if (newServer.provider === 'Другой' && !customProvider.trim()) {
|
||||||
@@ -246,7 +295,8 @@ function ServerManager() {
|
|||||||
provider: '',
|
provider: '',
|
||||||
tunnel: 'GRE',
|
tunnel: 'GRE',
|
||||||
type: 'jumphost',
|
type: 'jumphost',
|
||||||
gateway: ''
|
gateway: '',
|
||||||
|
gateways: [makeGateway({ primary: true })],
|
||||||
});
|
});
|
||||||
setCustomProvider('');
|
setCustomProvider('');
|
||||||
setAddServerModalOpen(false);
|
setAddServerModalOpen(false);
|
||||||
@@ -264,7 +314,11 @@ function ServerManager() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEditModalSave = (updatedServer) => {
|
const handleEditModalSave = (updatedServer) => {
|
||||||
const normalizedServer = { ...updatedServer, type: normalizeServerType(updatedServer.type) };
|
const normalizedServer = {
|
||||||
|
...updatedServer,
|
||||||
|
type: normalizeServerType(updatedServer.type),
|
||||||
|
gateways: NEEDS_GATEWAYS(updatedServer.type) ? normalizeGateways(updatedServer.gateways, updatedServer.gateway || updatedServer.dns || updatedServer.ip) : [],
|
||||||
|
};
|
||||||
setServers(servers.map(s => {
|
setServers(servers.map(s => {
|
||||||
const sameId = s.id && updatedServer.id ? s.id === updatedServer.id : false;
|
const sameId = s.id && updatedServer.id ? s.id === updatedServer.id : false;
|
||||||
const sameIp = s.ip === updatedServer.ip;
|
const sameIp = s.ip === updatedServer.ip;
|
||||||
@@ -1022,6 +1076,100 @@ function ServerManager() {
|
|||||||
required
|
required
|
||||||
helpText="Имя основного шлюза для маршрутизации"
|
helpText="Имя основного шлюза для маршрутизации"
|
||||||
/>
|
/>
|
||||||
|
{NEEDS_GATEWAYS(newServer.type) && (
|
||||||
|
<>
|
||||||
|
<div className="form-label">Шлюзы</div>
|
||||||
|
<div className="list-group list-group-flush border rounded mb-2">
|
||||||
|
{newServer.gateways.map((gw, idx) => (
|
||||||
|
<div key={gw.id || idx} className="list-group-item">
|
||||||
|
<div className="row g-2 align-items-end">
|
||||||
|
<div className="col-12 col-md-4">
|
||||||
|
<label className="form-label small mb-1">Имя *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
value={gw.name}
|
||||||
|
onChange={(e) => {
|
||||||
|
const list = newServer.gateways.map((g, i) => i === idx ? { ...g, name: e.target.value } : g);
|
||||||
|
setNewServer({ ...newServer, gateways: list });
|
||||||
|
}}
|
||||||
|
placeholder="GW-MSK"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-md-4">
|
||||||
|
<label className="form-label small mb-1">IP</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
value={gw.ip || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const list = newServer.gateways.map((g, i) => i === idx ? { ...g, ip: e.target.value } : g);
|
||||||
|
setNewServer({ ...newServer, gateways: list });
|
||||||
|
}}
|
||||||
|
placeholder="10.0.0.1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-md-3">
|
||||||
|
<label className="form-label small mb-1">Комментарий</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
value={gw.comment || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const list = newServer.gateways.map((g, i) => i === idx ? { ...g, comment: e.target.value } : g);
|
||||||
|
setNewServer({ ...newServer, gateways: list });
|
||||||
|
}}
|
||||||
|
placeholder="Основной шлюз"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-md-1 d-flex align-items-center justify-content-center">
|
||||||
|
<div className="form-check form-switch m-0">
|
||||||
|
<input
|
||||||
|
className="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
checked={gw.primary}
|
||||||
|
onChange={(e) => {
|
||||||
|
const list = newServer.gateways.map((g, i) => ({ ...g, primary: i === idx ? e.target.checked : false }));
|
||||||
|
if (!list.some(g => g.primary)) list[idx].primary = true;
|
||||||
|
setNewServer({ ...newServer, gateways: list });
|
||||||
|
}}
|
||||||
|
title="Основной шлюз"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex justify-content-between align-items-center mt-2">
|
||||||
|
<div className="text-muted small">{gw.primary ? 'Основной шлюз' : 'Резервный шлюз'}</div>
|
||||||
|
{newServer.gateways.length > 1 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-link text-danger px-0"
|
||||||
|
onClick={() => {
|
||||||
|
if (newServer.gateways.length === 1) return;
|
||||||
|
const list = newServer.gateways.filter((_, i) => i !== idx);
|
||||||
|
if (!list.some(g => g.primary) && list.length > 0) list[0].primary = true;
|
||||||
|
setNewServer({ ...newServer, gateways: list });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Удалить
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-primary btn-sm"
|
||||||
|
onClick={() => setNewServer({ ...newServer, gateways: [...newServer.gateways, makeGateway({ primary: newServer.gateways.length === 0 })] })}
|
||||||
|
>
|
||||||
|
<IconPlus className="icon" />
|
||||||
|
Добавить шлюз
|
||||||
|
</button>
|
||||||
|
<div className="form-text">Для jumphost/exit должен быть один основной gateway.</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</FormModal>
|
</FormModal>
|
||||||
|
|
||||||
<div className="card w-100 mb-4">
|
<div className="card w-100 mb-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user