From 23d7b678132acb7f0d1d995af56aabcf9475e0ba Mon Sep 17 00:00:00 2001 From: shats Date: Tue, 7 Apr 2026 22:50:54 +0700 Subject: [PATCH] feat(integration): switch data sections to EvoBGP v1 API 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 --- frontend/src/ASNsNewManager.jsx | 58 ++----- frontend/src/CommunitiesManager.jsx | 64 +------ frontend/src/DomainsNewManager.jsx | 58 ++----- frontend/src/IPRangesManager.jsx | 56 ++----- frontend/src/lib/evobgpData.js | 251 ++++++++++++++++++++++++++++ frontend/vite.config.js | 5 + 6 files changed, 315 insertions(+), 177 deletions(-) create mode 100644 frontend/src/lib/evobgpData.js diff --git a/frontend/src/ASNsNewManager.jsx b/frontend/src/ASNsNewManager.jsx index b6e04c5..81ae9e3 100644 --- a/frontend/src/ASNsNewManager.jsx +++ b/frontend/src/ASNsNewManager.jsx @@ -43,13 +43,16 @@ import AddItemModal from './components/AddItemModal.jsx'; import FormModal from './components/FormModal.jsx'; import { getAsnName, getAsnNameSync } from './lib/asn.js'; import LastSaved from './components/LastSaved.jsx'; - -const API_URL = '/api'; +import { + listCommunities, + fetchAsnsData, + saveAsnsData, +} from './lib/evobgpData.js'; function ASNsNewManager() { const [items, setItems] = useState([]); const [originalItems, setOriginalItems] = useState([]); - const [etag, setEtag] = useState(''); + const [, setEtag] = useState(''); const [lastModified, setLastModified] = useState(''); const [, setContentLength] = useState(null); const [newItem, setNewItem] = useState({ asn: '', community: '' }); @@ -80,8 +83,8 @@ function ASNsNewManager() { useEffect(() => { (async () => { try { - const res = await api.get(`/communities`); - setCommunities(Array.isArray(res.data) ? res.data : []); + const list = await listCommunities(); + setCommunities(list); } catch (_e) { // тихо игнорируем } @@ -112,21 +115,17 @@ function ASNsNewManager() { return () => controller.abort(); }, []); - const fetchItems = useCallback(async (abortSignal) => { + const fetchItems = useCallback(async (_abortSignal) => { setLoading(true); - const opts = abortSignal ? { signal: abortSignal } : {}; try { - const response = await api.get(`/asns`, { params: { offset: 0, limit: 0, format: 'std' }, ...opts }); - const payload = Array.isArray(response.data?.items) ? response.data.items : []; - const mapped = payload.map(item => ({ asn: String(item.domain), community: String(item.type) })); + const mapped = await fetchAsnsData(); const total = mapped.length; setItems(mapped); setOriginalItems(mapped); setTotalItems(Number.isFinite(total) ? total : 0); - setEtag(response.headers?.etag || ''); - setLastModified(response.headers?.['last-modified'] || ''); - const lengthHeader = response.headers?.['content-length-source']; - setContentLength(typeof lengthHeader !== 'undefined' ? Number(lengthHeader) : null); + setEtag(''); + setLastModified(new Date().toISOString()); + setContentLength(null); setError(''); } catch (error) { 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() })); const unique = deduplicate(valid); - // Загрузим полный список, совместим и сохраним - const fullRes = await api.get(`/asns`, { params: { offset: 0, limit: 0, format: 'std' } }); - 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 saveAsnsData(unique, originalItems); + setEtag(''); await fetchItems(); setSuccess('Изменения успешно сохранены!'); setTimeout(() => setSuccess(''), 3000); @@ -352,7 +331,7 @@ function ASNsNewManager() { } finally { setLoading(false); } - }, [items, originalItems, etag, fetchItems]); + }, [items, originalItems, fetchItems]); const handleSaveChanges = useCallback(async () => { // подготовим diff и спросим подтверждение только при массовых изменениях (>10) @@ -380,10 +359,7 @@ function ASNsNewManager() { const handleExport = async () => { try { - const effectiveQ = searchTerm ? searchTerm : (filterCommunity ? filterCommunity : ''); - 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 all = filtered; const header = ['asn', 'community']; const csv = [header, ...all.map(i => [i.asn, i.community])] .map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(',')) diff --git a/frontend/src/CommunitiesManager.jsx b/frontend/src/CommunitiesManager.jsx index 87ddcc9..cbdd0c1 100644 --- a/frontend/src/CommunitiesManager.jsx +++ b/frontend/src/CommunitiesManager.jsx @@ -30,8 +30,7 @@ import ErrorAlert from './components/ErrorAlert.jsx'; import Pagination from './components/Pagination.jsx'; import IconPicker from './components/IconPicker.jsx'; import { getIconById } from './lib/brandIcons.js'; - -const API_URL = '/api'; +import { listCommunities, saveCommunitiesData } from './lib/evobgpData.js'; const normalizeCommunityValue = (value) => { const trimmed = String(value || '').trim(); @@ -90,57 +89,12 @@ function CommunitiesManager() { const fetchItems = async () => { setLoading(true); try { - const [dictRes, asnsRes, ipRes, domRes] = await Promise.all([ - api.get(`/communities`), - api.get(`/asns`, { params: { offset: 0, limit: 0 } }).catch(() => ({ data: [] })), - api.get(`/ip-ranges`, { params: { offset: 0, limit: 0 } }).catch(() => ({ data: [] })), - api.get(`/domains-new`, { params: { offset: 0, limit: 0 } }).catch(() => ({ data: [] })), - ]); - 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())); + const rows = await listCommunities(); + const normalized = rows.map((item) => ({ + ...item, + value: normalizeCommunityValue(item.value), + })); + setItems(normalized); setError(''); } catch (e) { console.error('Error fetching communities:', e); @@ -189,9 +143,7 @@ function CommunitiesManager() { const saveAll = async (data) => { setLoading(true); try { - // не отправляем служебное поле _external - const payload = data.map(({ _external: _, ...rest }) => rest); - await api.post(`/communities`, { communities: payload }); + await saveCommunitiesData(data); setSuccess('Справочник сохранён!'); setTimeout(() => setSuccess(''), 3000); } catch (e) { diff --git a/frontend/src/DomainsNewManager.jsx b/frontend/src/DomainsNewManager.jsx index 8cbd9c3..a80abbb 100644 --- a/frontend/src/DomainsNewManager.jsx +++ b/frontend/src/DomainsNewManager.jsx @@ -49,14 +49,17 @@ import AddItemModal from './components/AddItemModal.jsx'; import LastSaved from './components/LastSaved.jsx'; import FormModal from './components/FormModal.jsx'; import { useToast } from './components/ToastContainer.jsx'; - -const API_URL = '/api'; +import { + listCommunities, + fetchDomainsData, + saveDomainsData, +} from './lib/evobgpData.js'; function DomainsNewManager() { const toast = useToast(); const [items, setItems] = useState([]); const [originalItems, setOriginalItems] = useState([]); - const [etag, setEtag] = useState(''); + const [, setEtag] = useState(''); const [lastModified, setLastModified] = useState(''); const [, setContentLength] = useState(null); const [newItem, setNewItem] = useState({ domain: '', community: '' }); @@ -90,8 +93,8 @@ function DomainsNewManager() { const signal = controller.signal; (async () => { try { - const res = await api.get(`/communities`, { signal }); - setCommunities(Array.isArray(res.data) ? res.data : []); + const list = await listCommunities(); + setCommunities(list); } catch (err) { if (err?.name === 'CanceledError' || err?.name === 'AbortError' || err?.code === 'ERR_CANCELED') return; } @@ -121,20 +124,17 @@ function DomainsNewManager() { const [wsOpen, setWsOpen] = useState(false); const [wsUrl, setWsUrl] = useState(''); - const fetchItems = useCallback(async (abortSignal) => { + const fetchItems = useCallback(async (_abortSignal) => { setLoading(true); - const opts = abortSignal ? { signal: abortSignal } : {}; try { - const response = await api.get(`/domains-new`, { params: { offset: 0, limit: 0, format: 'std' }, ...opts }); - const payload = Array.isArray(response.data?.items) ? response.data.items : []; + const payload = await fetchDomainsData(); const total = payload.length; setItems(payload); setOriginalItems(payload); setTotalItems(Number.isFinite(total) ? total : 0); - setEtag(response.headers?.etag || ''); - setLastModified(response.headers?.['last-modified'] || ''); - const lengthHeader = response.headers?.['content-length-source']; - setContentLength(typeof lengthHeader !== 'undefined' ? Number(lengthHeader) : null); + setEtag(''); + setLastModified(new Date().toISOString()); + setContentLength(null); setError(''); } catch (error) { 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() })); const unique = deduplicate(valid); - // Подгружаем весь список, применяем изменения текущей страницы и сохраняем полные данные - const fullRes = await api.get(`/domains-new`, { params: { offset: 0, limit: 0, format: 'std' } }); - 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 saveDomainsData(unique, originalItems); + setEtag(''); // После сохранения перезагрузим текущую страницу для консистентности await fetchItems(); setSuccess('Изменения успешно сохранены!'); @@ -316,7 +294,7 @@ function DomainsNewManager() { } finally { setLoading(false); } - }, [items, originalItems, etag, fetchItems]); + }, [items, originalItems, fetchItems]); const handleSaveChanges = useCallback(async () => { const valid = items.filter(i => i != null && isValidDomain(i.domain) && isValidCommunity(i.community)) @@ -360,9 +338,7 @@ function DomainsNewManager() { const handleExport = async () => { try { - const effectiveQ = searchTerm ? searchTerm : (filterCommunity ? filterCommunity : ''); - 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 all = filtered; const header = ['domain', '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(',')) diff --git a/frontend/src/IPRangesManager.jsx b/frontend/src/IPRangesManager.jsx index d1ec590..638c76f 100644 --- a/frontend/src/IPRangesManager.jsx +++ b/frontend/src/IPRangesManager.jsx @@ -44,8 +44,11 @@ import Pagination from './components/Pagination.jsx'; import AddItemModal from './components/AddItemModal.jsx'; import LastSaved from './components/LastSaved.jsx'; import FormModal from './components/FormModal.jsx'; - -const API_URL = '/api'; +import { + listCommunities, + fetchIpRangesData, + saveIpRangesData, +} from './lib/evobgpData.js'; const isValidIPv4 = (ip) => { const octets = ip.split('.'); @@ -67,7 +70,7 @@ const isValidCidr = (value) => { function IPRangesManager() { const [items, setItems] = useState([]); const [originalItems, setOriginalItems] = useState([]); - const [etag, setEtag] = useState(''); + const [, setEtag] = useState(''); const [lastModified, setLastModified] = useState(''); const [, setContentLength] = useState(null); const [newItem, setNewItem] = useState({ ipRange: '', community: '' }); @@ -97,8 +100,8 @@ function IPRangesManager() { useEffect(() => { (async () => { try { - const res = await api.get(`/communities`); - setCommunities(Array.isArray(res.data) ? res.data : []); + const list = await listCommunities(); + setCommunities(list); } catch (_e) { // тихо игнорируем } @@ -126,20 +129,17 @@ function IPRangesManager() { return () => controller.abort(); }, []); - const fetchItems = useCallback(async (abortSignal) => { + const fetchItems = useCallback(async (_abortSignal) => { setLoading(true); - const opts = abortSignal ? { signal: abortSignal } : {}; try { - const response = await api.get(`/ip-ranges`, { params: { offset: 0, limit: 0, format: 'std' }, ...opts }); - const payload = Array.isArray(response.data?.items) ? response.data.items : []; + const payload = await fetchIpRangesData(); const total = payload.length; setItems(payload); setOriginalItems(payload); setTotalItems(Number.isFinite(total) ? total : 0); - setEtag(response.headers?.etag || ''); - setLastModified(response.headers?.['last-modified'] || ''); - const lengthHeader = response.headers?.['content-length-source']; - setContentLength(typeof lengthHeader !== 'undefined' ? Number(lengthHeader) : null); + setEtag(''); + setLastModified(new Date().toISOString()); + setContentLength(null); setError(''); } catch (error) { 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() })); const unique = deduplicate(valid); - // Загрузим полный список, совместим с изменениями текущей страницы и сохраним - const fullRes = await api.get(`/ip-ranges`, { params: { offset: 0, limit: 0, format: 'std' } }); - 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 saveIpRangesData(unique, originalItems); + setEtag(''); await fetchItems(); setSuccess('Изменения успешно сохранены!'); setTimeout(() => setSuccess(''), 3000); @@ -378,7 +358,7 @@ function IPRangesManager() { } finally { setLoading(false); } - }, [items, originalItems, etag, fetchItems]); + }, [items, originalItems, fetchItems]); const handleSaveChanges = useCallback(async () => { const valid = items.filter(i => i != null && isValidCidr(i.ipRange) && isValidCommunity(i.community)) @@ -400,9 +380,7 @@ function IPRangesManager() { const handleExport = async () => { try { - const effectiveQ = searchTerm ? searchTerm : (filterCommunity ? filterCommunity : ''); - 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 all = filtered; const header = ['ipRange', 'community']; const csv = [header, ...all.map(i => [i.ipRange, i.community])] .map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(',')) diff --git a/frontend/src/lib/evobgpData.js b/frontend/src/lib/evobgpData.js new file mode 100644 index 0000000..6829b03 --- /dev/null +++ b/frontend/src/lib/evobgpData.js @@ -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 }); + } + } +} diff --git a/frontend/vite.config.js b/frontend/vite.config.js index a10de3b..f2b4855 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -9,6 +9,11 @@ export default defineConfig({ }, server: { proxy: { + '/v1': { + target: 'http://localhost:8080', + changeOrigin: true, + secure: false, + }, '/api': { target: 'http://localhost:3001', changeOrigin: true,