feat(integration): switch data sections to EvoBGP v1 API
Publish Docker image / build-and-push (push) Successful in 2m12s

Move Domains, IP ranges, ASNs and Communities managers from legacy /api endpoints to direct EvoBGP /v1 modules/communities endpoints with a dedicated adapter layer.

Made-with: Cursor
This commit is contained in:
2026-04-07 22:50:54 +07:00
parent 0703d2eccd
commit 23d7b67813
6 changed files with 315 additions and 177 deletions
+17 -41
View File
@@ -43,13 +43,16 @@ import AddItemModal from './components/AddItemModal.jsx';
import FormModal from './components/FormModal.jsx'; import FormModal from './components/FormModal.jsx';
import { getAsnName, getAsnNameSync } from './lib/asn.js'; import { getAsnName, getAsnNameSync } from './lib/asn.js';
import LastSaved from './components/LastSaved.jsx'; import LastSaved from './components/LastSaved.jsx';
import {
const API_URL = '/api'; listCommunities,
fetchAsnsData,
saveAsnsData,
} from './lib/evobgpData.js';
function ASNsNewManager() { function ASNsNewManager() {
const [items, setItems] = useState([]); const [items, setItems] = useState([]);
const [originalItems, setOriginalItems] = useState([]); const [originalItems, setOriginalItems] = useState([]);
const [etag, setEtag] = useState(''); const [, setEtag] = useState('');
const [lastModified, setLastModified] = useState(''); const [lastModified, setLastModified] = useState('');
const [, setContentLength] = useState(null); const [, setContentLength] = useState(null);
const [newItem, setNewItem] = useState({ asn: '', community: '' }); const [newItem, setNewItem] = useState({ asn: '', community: '' });
@@ -80,8 +83,8 @@ function ASNsNewManager() {
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
const res = await api.get(`/communities`); const list = await listCommunities();
setCommunities(Array.isArray(res.data) ? res.data : []); setCommunities(list);
} catch (_e) { } catch (_e) {
// тихо игнорируем // тихо игнорируем
} }
@@ -112,21 +115,17 @@ function ASNsNewManager() {
return () => controller.abort(); return () => controller.abort();
}, []); }, []);
const fetchItems = useCallback(async (abortSignal) => { const fetchItems = useCallback(async (_abortSignal) => {
setLoading(true); setLoading(true);
const opts = abortSignal ? { signal: abortSignal } : {};
try { try {
const response = await api.get(`/asns`, { params: { offset: 0, limit: 0, format: 'std' }, ...opts }); const mapped = await fetchAsnsData();
const payload = Array.isArray(response.data?.items) ? response.data.items : [];
const mapped = payload.map(item => ({ asn: String(item.domain), community: String(item.type) }));
const total = mapped.length; const total = mapped.length;
setItems(mapped); setItems(mapped);
setOriginalItems(mapped); setOriginalItems(mapped);
setTotalItems(Number.isFinite(total) ? total : 0); setTotalItems(Number.isFinite(total) ? total : 0);
setEtag(response.headers?.etag || ''); setEtag('');
setLastModified(response.headers?.['last-modified'] || ''); setLastModified(new Date().toISOString());
const lengthHeader = response.headers?.['content-length-source']; setContentLength(null);
setContentLength(typeof lengthHeader !== 'undefined' ? Number(lengthHeader) : null);
setError(''); setError('');
} catch (error) { } catch (error) {
if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return; if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return;
@@ -320,28 +319,8 @@ function ASNsNewManager() {
.map(i => ({ asn: String(i.asn).trim(), community: String(i.community).trim() })); .map(i => ({ asn: String(i.asn).trim(), community: String(i.community).trim() }));
const unique = deduplicate(valid); const unique = deduplicate(valid);
// Загрузим полный список, совместим и сохраним await saveAsnsData(unique, originalItems);
const fullRes = await api.get(`/asns`, { params: { offset: 0, limit: 0, format: 'std' } }); setEtag('');
const fullPayload = Array.isArray(fullRes.data?.items) ? fullRes.data.items : (Array.isArray(fullRes.data) ? fullRes.data : []);
const full = fullPayload.map(item => ({ asn: String(item.domain).trim(), community: String(item.type || '').trim() }));
const fullMap = new Map(full.map(i => [i.asn, { asn: i.asn, community: i.community }]));
const originalPageMap = new Map(originalItems.map(i => [String(i.asn).trim(), true]));
const uniqueMap = new Map(unique.map(i => [String(i.asn).trim(), { asn: i.asn, community: i.community }]));
for (const key of originalPageMap.keys()) {
if (!uniqueMap.has(key)) fullMap.delete(key);
}
for (const [key, val] of uniqueMap.entries()) fullMap.set(key, val);
const fullToSave = Array.from(fullMap.values()).map(i => ({ domain: i.asn, type: i.community }));
const et = fullRes?.headers?.etag || etag;
const payload = { domains: fullToSave, etag: et };
const response = await api.post(`/asns`, payload, { validateStatus: () => true });
if (response.status === 412) {
setError('Данные изменились в S3 (ETag mismatch). Обновите список и попробуйте снова.');
return;
}
if (response.status >= 400) throw new Error(`Save failed with status ${response.status}`);
setEtag(response.headers?.etag || et);
await fetchItems(); await fetchItems();
setSuccess('Изменения успешно сохранены!'); setSuccess('Изменения успешно сохранены!');
setTimeout(() => setSuccess(''), 3000); setTimeout(() => setSuccess(''), 3000);
@@ -352,7 +331,7 @@ function ASNsNewManager() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [items, originalItems, etag, fetchItems]); }, [items, originalItems, fetchItems]);
const handleSaveChanges = useCallback(async () => { const handleSaveChanges = useCallback(async () => {
// подготовим diff и спросим подтверждение только при массовых изменениях (>10) // подготовим diff и спросим подтверждение только при массовых изменениях (>10)
@@ -380,10 +359,7 @@ function ASNsNewManager() {
const handleExport = async () => { const handleExport = async () => {
try { try {
const effectiveQ = searchTerm ? searchTerm : (filterCommunity ? filterCommunity : ''); const all = filtered;
const response = await api.get(`/asns`, { params: { q: effectiveQ, offset: 0, limit: 0, format: 'std' } });
const payload = Array.isArray(response.data?.items) ? response.data.items : (Array.isArray(response.data) ? response.data : []);
const all = payload.map(item => ({ asn: item.domain, community: item.type }));
const header = ['asn', 'community']; const header = ['asn', 'community'];
const csv = [header, ...all.map(i => [i.asn, i.community])] const csv = [header, ...all.map(i => [i.asn, i.community])]
.map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(',')) .map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(','))
+8 -56
View File
@@ -30,8 +30,7 @@ import ErrorAlert from './components/ErrorAlert.jsx';
import Pagination from './components/Pagination.jsx'; import Pagination from './components/Pagination.jsx';
import IconPicker from './components/IconPicker.jsx'; import IconPicker from './components/IconPicker.jsx';
import { getIconById } from './lib/brandIcons.js'; import { getIconById } from './lib/brandIcons.js';
import { listCommunities, saveCommunitiesData } from './lib/evobgpData.js';
const API_URL = '/api';
const normalizeCommunityValue = (value) => { const normalizeCommunityValue = (value) => {
const trimmed = String(value || '').trim(); const trimmed = String(value || '').trim();
@@ -90,57 +89,12 @@ function CommunitiesManager() {
const fetchItems = async () => { const fetchItems = async () => {
setLoading(true); setLoading(true);
try { try {
const [dictRes, asnsRes, ipRes, domRes] = await Promise.all([ const rows = await listCommunities();
api.get(`/communities`), const normalized = rows.map((item) => ({
api.get(`/asns`, { params: { offset: 0, limit: 0 } }).catch(() => ({ data: [] })), ...item,
api.get(`/ip-ranges`, { params: { offset: 0, limit: 0 } }).catch(() => ({ data: [] })), value: normalizeCommunityValue(item.value),
api.get(`/domains-new`, { params: { offset: 0, limit: 0 } }).catch(() => ({ data: [] })), }));
]); setItems(normalized);
const dict = Array.isArray(dictRes.data) ? dictRes.data : [];
const asnsPayload = Array.isArray(asnsRes.data?.items) ? asnsRes.data.items : (Array.isArray(asnsRes.data) ? asnsRes.data : []);
const ipPayload = Array.isArray(ipRes.data?.items) ? ipRes.data.items : (Array.isArray(ipRes.data) ? ipRes.data : []);
const domPayload = Array.isArray(domRes.data?.items) ? domRes.data.items : (Array.isArray(domRes.data) ? domRes.data : []);
const usedCommunities = new Set();
// ASNs: fields domain/type
for (const it of asnsPayload) {
const v = normalizeCommunityValue(it.type);
if (v) usedCommunities.add(v);
}
// IP ranges: fields ipRange/community
for (const it of ipPayload) {
const v = normalizeCommunityValue(it.community);
if (v) usedCommunities.add(v);
}
// Domains: fields domain/community
for (const it of domPayload) {
const v = normalizeCommunityValue(it.community);
if (v) usedCommunities.add(v);
}
const map = new Map();
// put dictionary items first
for (const d of dict) {
const raw = String(d.value || '').trim();
if (!raw) continue;
const value = normalizeCommunityValue(raw);
map.set(value, {
value,
name: String(d.name || ''),
description: String(d.description || ''),
tags: Array.isArray(d.tags) ? d.tags : [],
color: String(d.color || ''),
icon: String(d.icon || ''),
_external: false,
});
}
// add unknown ones from usage
for (const v of usedCommunities) {
if (!map.has(v)) {
map.set(v, { value: v, name: '', description: '', tags: [], color: '', icon: '', _external: true });
}
}
setItems(Array.from(map.values()));
setError(''); setError('');
} catch (e) { } catch (e) {
console.error('Error fetching communities:', e); console.error('Error fetching communities:', e);
@@ -189,9 +143,7 @@ function CommunitiesManager() {
const saveAll = async (data) => { const saveAll = async (data) => {
setLoading(true); setLoading(true);
try { try {
// не отправляем служебное поле _external await saveCommunitiesData(data);
const payload = data.map(({ _external: _, ...rest }) => rest);
await api.post(`/communities`, { communities: payload });
setSuccess('Справочник сохранён!'); setSuccess('Справочник сохранён!');
setTimeout(() => setSuccess(''), 3000); setTimeout(() => setSuccess(''), 3000);
} catch (e) { } catch (e) {
+17 -41
View File
@@ -49,14 +49,17 @@ import AddItemModal from './components/AddItemModal.jsx';
import LastSaved from './components/LastSaved.jsx'; import LastSaved from './components/LastSaved.jsx';
import FormModal from './components/FormModal.jsx'; import FormModal from './components/FormModal.jsx';
import { useToast } from './components/ToastContainer.jsx'; import { useToast } from './components/ToastContainer.jsx';
import {
const API_URL = '/api'; listCommunities,
fetchDomainsData,
saveDomainsData,
} from './lib/evobgpData.js';
function DomainsNewManager() { function DomainsNewManager() {
const toast = useToast(); const toast = useToast();
const [items, setItems] = useState([]); const [items, setItems] = useState([]);
const [originalItems, setOriginalItems] = useState([]); const [originalItems, setOriginalItems] = useState([]);
const [etag, setEtag] = useState(''); const [, setEtag] = useState('');
const [lastModified, setLastModified] = useState(''); const [lastModified, setLastModified] = useState('');
const [, setContentLength] = useState(null); const [, setContentLength] = useState(null);
const [newItem, setNewItem] = useState({ domain: '', community: '' }); const [newItem, setNewItem] = useState({ domain: '', community: '' });
@@ -90,8 +93,8 @@ function DomainsNewManager() {
const signal = controller.signal; const signal = controller.signal;
(async () => { (async () => {
try { try {
const res = await api.get(`/communities`, { signal }); const list = await listCommunities();
setCommunities(Array.isArray(res.data) ? res.data : []); setCommunities(list);
} catch (err) { } catch (err) {
if (err?.name === 'CanceledError' || err?.name === 'AbortError' || err?.code === 'ERR_CANCELED') return; if (err?.name === 'CanceledError' || err?.name === 'AbortError' || err?.code === 'ERR_CANCELED') return;
} }
@@ -121,20 +124,17 @@ function DomainsNewManager() {
const [wsOpen, setWsOpen] = useState(false); const [wsOpen, setWsOpen] = useState(false);
const [wsUrl, setWsUrl] = useState(''); const [wsUrl, setWsUrl] = useState('');
const fetchItems = useCallback(async (abortSignal) => { const fetchItems = useCallback(async (_abortSignal) => {
setLoading(true); setLoading(true);
const opts = abortSignal ? { signal: abortSignal } : {};
try { try {
const response = await api.get(`/domains-new`, { params: { offset: 0, limit: 0, format: 'std' }, ...opts }); const payload = await fetchDomainsData();
const payload = Array.isArray(response.data?.items) ? response.data.items : [];
const total = payload.length; const total = payload.length;
setItems(payload); setItems(payload);
setOriginalItems(payload); setOriginalItems(payload);
setTotalItems(Number.isFinite(total) ? total : 0); setTotalItems(Number.isFinite(total) ? total : 0);
setEtag(response.headers?.etag || ''); setEtag('');
setLastModified(response.headers?.['last-modified'] || ''); setLastModified(new Date().toISOString());
const lengthHeader = response.headers?.['content-length-source']; setContentLength(null);
setContentLength(typeof lengthHeader !== 'undefined' ? Number(lengthHeader) : null);
setError(''); setError('');
} catch (error) { } catch (error) {
if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return; if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return;
@@ -281,30 +281,8 @@ function DomainsNewManager() {
.map(i => ({ domain: i.domain.trim().toLowerCase(), community: String(i.community).trim() })); .map(i => ({ domain: i.domain.trim().toLowerCase(), community: String(i.community).trim() }));
const unique = deduplicate(valid); const unique = deduplicate(valid);
// Подгружаем весь список, применяем изменения текущей страницы и сохраняем полные данные await saveDomainsData(unique, originalItems);
const fullRes = await api.get(`/domains-new`, { params: { offset: 0, limit: 0, format: 'std' } }); setEtag('');
const full = Array.isArray(fullRes.data?.items) ? fullRes.data.items : (Array.isArray(fullRes.data) ? fullRes.data : []);
const fullMap = new Map(full.filter(i => i != null).map(i => [String(i.domain || '').trim().toLowerCase(), { domain: String(i.domain || '').trim().toLowerCase(), community: String(i.community || '').trim() }]));
const originalPageMap = new Map(originalItems.map(i => [String(i.domain).trim().toLowerCase(), true]));
const uniqueMap = new Map(unique.map(i => [String(i.domain).trim().toLowerCase(), { domain: String(i.domain).trim().toLowerCase(), community: String(i.community).trim() }]));
// Удаления: всё, что было на странице, но отсутствует в изменённом наборе
for (const key of originalPageMap.keys()) {
if (!uniqueMap.has(key)) fullMap.delete(key);
}
// Добавления/изменения
for (const [key, val] of uniqueMap.entries()) fullMap.set(key, val);
const fullToSave = Array.from(fullMap.values());
const et = fullRes?.headers?.etag || etag;
const response = await api.post(`/domains-new`, { domains: fullToSave, etag: et }, { validateStatus: () => true });
if (response.status === 412) {
setError('Данные изменились в S3 (ETag mismatch). Обновите список и попробуйте снова.');
return;
}
if (response.status >= 400) {
throw new Error(`Save failed with status ${response.status}`);
}
setEtag(response.headers?.etag || et);
// После сохранения перезагрузим текущую страницу для консистентности // После сохранения перезагрузим текущую страницу для консистентности
await fetchItems(); await fetchItems();
setSuccess('Изменения успешно сохранены!'); setSuccess('Изменения успешно сохранены!');
@@ -316,7 +294,7 @@ function DomainsNewManager() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [items, originalItems, etag, fetchItems]); }, [items, originalItems, fetchItems]);
const handleSaveChanges = useCallback(async () => { const handleSaveChanges = useCallback(async () => {
const valid = items.filter(i => i != null && isValidDomain(i.domain) && isValidCommunity(i.community)) const valid = items.filter(i => i != null && isValidDomain(i.domain) && isValidCommunity(i.community))
@@ -360,9 +338,7 @@ function DomainsNewManager() {
const handleExport = async () => { const handleExport = async () => {
try { try {
const effectiveQ = searchTerm ? searchTerm : (filterCommunity ? filterCommunity : ''); const all = filtered;
const response = await api.get(`/domains-new`, { params: { q: effectiveQ, offset: 0, limit: 0, format: 'std' } });
const all = Array.isArray(response.data?.items) ? response.data.items : (Array.isArray(response.data) ? response.data : []);
const header = ['domain', 'community']; const header = ['domain', 'community'];
const csv = [header, ...all.filter(i => i != null).map(i => [i.domain || '', i.community || ''])] const csv = [header, ...all.filter(i => i != null).map(i => [i.domain || '', i.community || ''])]
.map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(',')) .map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(','))
+17 -39
View File
@@ -44,8 +44,11 @@ import Pagination from './components/Pagination.jsx';
import AddItemModal from './components/AddItemModal.jsx'; import AddItemModal from './components/AddItemModal.jsx';
import LastSaved from './components/LastSaved.jsx'; import LastSaved from './components/LastSaved.jsx';
import FormModal from './components/FormModal.jsx'; import FormModal from './components/FormModal.jsx';
import {
const API_URL = '/api'; listCommunities,
fetchIpRangesData,
saveIpRangesData,
} from './lib/evobgpData.js';
const isValidIPv4 = (ip) => { const isValidIPv4 = (ip) => {
const octets = ip.split('.'); const octets = ip.split('.');
@@ -67,7 +70,7 @@ const isValidCidr = (value) => {
function IPRangesManager() { function IPRangesManager() {
const [items, setItems] = useState([]); const [items, setItems] = useState([]);
const [originalItems, setOriginalItems] = useState([]); const [originalItems, setOriginalItems] = useState([]);
const [etag, setEtag] = useState(''); const [, setEtag] = useState('');
const [lastModified, setLastModified] = useState(''); const [lastModified, setLastModified] = useState('');
const [, setContentLength] = useState(null); const [, setContentLength] = useState(null);
const [newItem, setNewItem] = useState({ ipRange: '', community: '' }); const [newItem, setNewItem] = useState({ ipRange: '', community: '' });
@@ -97,8 +100,8 @@ function IPRangesManager() {
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
const res = await api.get(`/communities`); const list = await listCommunities();
setCommunities(Array.isArray(res.data) ? res.data : []); setCommunities(list);
} catch (_e) { } catch (_e) {
// тихо игнорируем // тихо игнорируем
} }
@@ -126,20 +129,17 @@ function IPRangesManager() {
return () => controller.abort(); return () => controller.abort();
}, []); }, []);
const fetchItems = useCallback(async (abortSignal) => { const fetchItems = useCallback(async (_abortSignal) => {
setLoading(true); setLoading(true);
const opts = abortSignal ? { signal: abortSignal } : {};
try { try {
const response = await api.get(`/ip-ranges`, { params: { offset: 0, limit: 0, format: 'std' }, ...opts }); const payload = await fetchIpRangesData();
const payload = Array.isArray(response.data?.items) ? response.data.items : [];
const total = payload.length; const total = payload.length;
setItems(payload); setItems(payload);
setOriginalItems(payload); setOriginalItems(payload);
setTotalItems(Number.isFinite(total) ? total : 0); setTotalItems(Number.isFinite(total) ? total : 0);
setEtag(response.headers?.etag || ''); setEtag('');
setLastModified(response.headers?.['last-modified'] || ''); setLastModified(new Date().toISOString());
const lengthHeader = response.headers?.['content-length-source']; setContentLength(null);
setContentLength(typeof lengthHeader !== 'undefined' ? Number(lengthHeader) : null);
setError(''); setError('');
} catch (error) { } catch (error) {
if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return; if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return;
@@ -346,28 +346,8 @@ function IPRangesManager() {
.map(i => ({ ipRange: i.ipRange.trim(), community: String(i.community).trim() })); .map(i => ({ ipRange: i.ipRange.trim(), community: String(i.community).trim() }));
const unique = deduplicate(valid); const unique = deduplicate(valid);
// Загрузим полный список, совместим с изменениями текущей страницы и сохраним await saveIpRangesData(unique, originalItems);
const fullRes = await api.get(`/ip-ranges`, { params: { offset: 0, limit: 0, format: 'std' } }); setEtag('');
const full = Array.isArray(fullRes.data?.items) ? fullRes.data.items : (Array.isArray(fullRes.data) ? fullRes.data : []);
const fullMap = new Map(full.map(i => [String(i.ipRange).trim(), { ipRange: String(i.ipRange).trim(), community: String(i.community || '').trim() }]));
const originalPageMap = new Map(originalItems.map(i => [String(i.ipRange).trim(), true]));
const uniqueMap = new Map(unique.map(i => [String(i.ipRange).trim(), { ipRange: String(i.ipRange).trim(), community: String(i.community).trim() }]));
for (const key of originalPageMap.keys()) {
if (!uniqueMap.has(key)) fullMap.delete(key);
}
for (const [key, val] of uniqueMap.entries()) fullMap.set(key, val);
const fullToSave = Array.from(fullMap.values());
const et = fullRes?.headers?.etag || etag;
const response = await api.post(`/ip-ranges`, { ipRanges: fullToSave, etag: et }, { validateStatus: () => true });
if (response.status === 412) {
setError('Данные изменились в S3 (ETag mismatch). Обновите список и попробуйте снова.');
return;
}
if (response.status >= 400) {
throw new Error(`Save failed with status ${response.status}`);
}
setEtag(response.headers?.etag || et);
await fetchItems(); await fetchItems();
setSuccess('Изменения успешно сохранены!'); setSuccess('Изменения успешно сохранены!');
setTimeout(() => setSuccess(''), 3000); setTimeout(() => setSuccess(''), 3000);
@@ -378,7 +358,7 @@ function IPRangesManager() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [items, originalItems, etag, fetchItems]); }, [items, originalItems, fetchItems]);
const handleSaveChanges = useCallback(async () => { const handleSaveChanges = useCallback(async () => {
const valid = items.filter(i => i != null && isValidCidr(i.ipRange) && isValidCommunity(i.community)) const valid = items.filter(i => i != null && isValidCidr(i.ipRange) && isValidCommunity(i.community))
@@ -400,9 +380,7 @@ function IPRangesManager() {
const handleExport = async () => { const handleExport = async () => {
try { try {
const effectiveQ = searchTerm ? searchTerm : (filterCommunity ? filterCommunity : ''); const all = filtered;
const response = await api.get(`/ip-ranges`, { params: { q: effectiveQ, offset: 0, limit: 0, format: 'std' } });
const all = Array.isArray(response.data?.items) ? response.data.items : (Array.isArray(response.data) ? response.data : []);
const header = ['ipRange', 'community']; const header = ['ipRange', 'community'];
const csv = [header, ...all.map(i => [i.ipRange, i.community])] const csv = [header, ...all.map(i => [i.ipRange, i.community])]
.map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(',')) .map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(','))
+251
View File
@@ -0,0 +1,251 @@
import axios from 'axios';
const DEFAULT_TIMEOUT_MS = 30000;
const PAGE_LIMIT = 500;
function getAuthToken() {
const envToken = import.meta?.env?.VITE_EVOBGP_API_TOKEN;
if (envToken) return String(envToken);
if (typeof window === 'undefined') return '';
return String(window.localStorage?.getItem('evobgp_api_token') || '');
}
export const evobgpApi = axios.create({
baseURL: import.meta?.env?.VITE_EVOBGP_API_URL || '/v1',
timeout: DEFAULT_TIMEOUT_MS,
});
evobgpApi.interceptors.request.use((config) => {
const token = getAuthToken();
if (token) {
config.headers = config.headers || {};
if (!config.headers.Authorization) {
config.headers.Authorization = `Bearer ${token}`;
}
}
return config;
});
async function paginate(path, params = {}) {
const items = [];
let cursor = null;
while (true) {
const res = await evobgpApi.get(path, {
params: { ...params, limit: PAGE_LIMIT, ...(cursor ? { cursor } : {}) },
});
const chunk = Array.isArray(res.data?.items) ? res.data.items : [];
items.push(...chunk);
if (!res.data?.has_more || !res.data?.next_cursor) break;
cursor = res.data.next_cursor;
}
return items;
}
export async function listCommunities() {
const communities = await paginate('/communities');
return communities.map((c) => ({
id: c.id,
value: String(c.community || ''),
name: String(c.title || ''),
description: '',
tags: [],
color: '',
icon: '',
}));
}
async function getOrCreateModule(moduleType, defaultName) {
const modules = await paginate('/modules', { type: moduleType });
if (modules.length > 0) return modules[0];
const created = await evobgpApi.post('/modules', {
type: moduleType,
name: defaultName,
enabled: true,
});
return created.data;
}
function normalizeList(items, keyName) {
const out = [];
const seen = new Set();
for (const item of items || []) {
const key = String(item?.[keyName] || '').trim().toLowerCase();
if (!key || seen.has(key)) continue;
seen.add(key);
out.push({ ...item, [keyName]: key });
}
return out;
}
async function buildCommunityMaps() {
const communities = await listCommunities();
const byId = new Map(communities.map((c) => [c.id, c.value]));
const byValue = new Map(communities.map((c) => [c.value, c.id]));
return { communities, byId, byValue };
}
function mapCommunityToValue(communityId, byId) {
if (!communityId) return '';
return byId.get(communityId) || String(communityId);
}
async function syncEntries({
moduleType,
moduleName,
listPath,
keyName,
createBody,
patchBody,
currentItems,
originalItems,
requireCommunity = false,
}) {
const module = await getOrCreateModule(moduleType, moduleName);
const moduleId = module.id;
const { byId, byValue } = await buildCommunityMaps();
const existing = await paginate(`/modules/${moduleId}/${listPath}`);
const existingByKey = new Map();
for (const row of existing) {
const entryKey = String(row?.[keyName] || '').trim().toLowerCase();
if (!entryKey) continue;
existingByKey.set(entryKey, row);
}
const normalizedCurrent = normalizeList(currentItems, keyName);
const normalizedOriginal = normalizeList(originalItems, keyName);
const currentMap = new Map(normalizedCurrent.map((i) => [String(i[keyName]).toLowerCase(), i]));
const originalKeys = new Set(normalizedOriginal.map((i) => String(i[keyName]).toLowerCase()));
for (const key of originalKeys) {
if (!currentMap.has(key) && existingByKey.has(key)) {
const row = existingByKey.get(key);
await evobgpApi.delete(`/modules/${moduleId}/${listPath}/${row.id}`);
existingByKey.delete(key);
}
}
for (const [key, item] of currentMap.entries()) {
const rawCommunity = String(item.community || '').trim();
const communityId = byValue.get(rawCommunity) || null;
if (requireCommunity && !communityId) {
throw new Error(`Community "${rawCommunity}" не найдена в справочнике`);
}
const existingRow = existingByKey.get(key);
if (!existingRow) {
await evobgpApi.post(`/modules/${moduleId}/${listPath}`, createBody(item, communityId));
continue;
}
const currentCommunity = mapCommunityToValue(existingRow.community_id, byId);
if (currentCommunity !== rawCommunity) {
await evobgpApi.patch(
`/modules/${moduleId}/${listPath}/${existingRow.id}`,
patchBody(item, communityId),
);
}
}
}
export async function fetchDomainsData() {
const module = await getOrCreateModule('DOMAINS', 'Domains');
const { byId } = await buildCommunityMaps();
const rows = await paginate(`/modules/${module.id}/domain-entries`);
return rows.map((row) => ({
id: row.id,
domain: String(row.fqdn || '').trim().toLowerCase(),
community: mapCommunityToValue(row.community_id, byId),
}));
}
export async function saveDomainsData(currentItems, originalItems) {
await syncEntries({
moduleType: 'DOMAINS',
moduleName: 'Domains',
listPath: 'domain-entries',
keyName: 'domain',
currentItems,
originalItems,
createBody: (item, communityId) => ({ fqdn: item.domain, community_id: communityId }),
patchBody: (item, communityId) => ({ fqdn: item.domain, community_id: communityId }),
});
}
export async function fetchIpRangesData() {
const module = await getOrCreateModule('IP_RANGES', 'IP ranges');
const { byId } = await buildCommunityMaps();
const rows = await paginate(`/modules/${module.id}/ip-range-entries`);
return rows.map((row) => ({
id: row.id,
ipRange: String(row.prefix || '').trim(),
community: mapCommunityToValue(row.community_id, byId),
}));
}
export async function saveIpRangesData(currentItems, originalItems) {
await syncEntries({
moduleType: 'IP_RANGES',
moduleName: 'IP ranges',
listPath: 'ip-range-entries',
keyName: 'ipRange',
currentItems,
originalItems,
requireCommunity: true,
createBody: (item, communityId) => ({ prefix: item.ipRange, community_id: communityId }),
patchBody: (item, communityId) => ({ prefix: item.ipRange, community_id: communityId }),
});
}
export async function fetchAsnsData() {
const module = await getOrCreateModule('AS_PREFIXES', 'ASNs');
const { byId } = await buildCommunityMaps();
const rows = await paginate(`/modules/${module.id}/as-entries`);
return rows.map((row) => ({
id: row.id,
asn: String(row.asn || '').trim(),
community: mapCommunityToValue(row.community_id, byId),
}));
}
export async function saveAsnsData(currentItems, originalItems) {
await syncEntries({
moduleType: 'AS_PREFIXES',
moduleName: 'ASNs',
listPath: 'as-entries',
keyName: 'asn',
currentItems,
originalItems,
createBody: (item, communityId) => ({ asn: Number(item.asn), community_id: communityId }),
patchBody: (item, communityId) => ({ asn: Number(item.asn), community_id: communityId }),
});
}
export async function saveCommunitiesData(currentItems) {
const existing = await paginate('/communities');
const existingByValue = new Map(existing.map((c) => [String(c.community || ''), c]));
const nextByValue = new Map();
for (const item of currentItems || []) {
const value = String(item?.value || '').trim();
if (!value) continue;
nextByValue.set(value, item);
}
for (const [value, row] of existingByValue.entries()) {
if (!nextByValue.has(value)) {
await evobgpApi.delete(`/communities/${row.id}`);
}
}
for (const [value, item] of nextByValue.entries()) {
const title = String(item?.name || '').trim();
const row = existingByValue.get(value);
if (!row) {
await evobgpApi.post('/communities', { community: value, title });
continue;
}
if (String(row.title || '') !== title) {
await evobgpApi.patch(`/communities/${row.id}`, { community: value, title });
}
}
}
+5
View File
@@ -9,6 +9,11 @@ export default defineConfig({
}, },
server: { server: {
proxy: { proxy: {
'/v1': {
target: 'http://localhost:8080',
changeOrigin: true,
secure: false,
},
'/api': { '/api': {
target: 'http://localhost:3001', target: 'http://localhost:3001',
changeOrigin: true, changeOrigin: true,