427 lines
14 KiB
JavaScript
427 lines
14 KiB
JavaScript
/**
|
|
* Серверный клиент EvoBGP API (Bearer, base /v1). Логика выборки совместима с frontend/src/lib/evobgpData.js
|
|
*/
|
|
|
|
const { normalizeEvobgpBaseUrl } = require('../routes/evobgpProxyRoutes');
|
|
|
|
const PAGE_LIMIT = 500;
|
|
const DEFAULT_TIMEOUT_MS = Number(process.env.EVOBGP_CLIENT_TIMEOUT_MS) || 30000;
|
|
|
|
function getBaseAndToken() {
|
|
const base = normalizeEvobgpBaseUrl(process.env.EVOBGP_API_URL || '');
|
|
const token = String(process.env.EVOBGP_API_TOKEN || '').trim();
|
|
return { base, token };
|
|
}
|
|
|
|
function isConfigured() {
|
|
const { base, token } = getBaseAndToken();
|
|
return Boolean(base && token);
|
|
}
|
|
|
|
async function evobgpRequest(method, pathname, { query, body } = {}) {
|
|
const { base, token } = getBaseAndToken();
|
|
if (!base || !token) {
|
|
const err = new Error('EvoBGP not configured');
|
|
err.code = 'E_EVOBGP_NOT_CONFIGURED';
|
|
throw err;
|
|
}
|
|
const u = new URL(pathname.startsWith('/') ? pathname.slice(1) : pathname, base.endsWith('/') ? base : `${base}/`);
|
|
if (query && typeof query === 'object') {
|
|
for (const [k, v] of Object.entries(query)) {
|
|
if (v !== undefined && v !== null) u.searchParams.set(k, String(v));
|
|
}
|
|
}
|
|
const ctrl = new AbortController();
|
|
const t = setTimeout(() => ctrl.abort(), DEFAULT_TIMEOUT_MS);
|
|
try {
|
|
const res = await fetch(u.toString(), {
|
|
method,
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
Accept: 'application/json',
|
|
...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
},
|
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
signal: ctrl.signal,
|
|
});
|
|
const text = await res.text();
|
|
let data = null;
|
|
try {
|
|
data = text ? JSON.parse(text) : null;
|
|
} catch (_) {
|
|
data = text;
|
|
}
|
|
if (!res.ok) {
|
|
const err = new Error(typeof data === 'object' && data?.message ? data.message : res.statusText);
|
|
err.code = 'E_EVOBGP_HTTP';
|
|
err.status = res.status;
|
|
err.body = data;
|
|
throw err;
|
|
}
|
|
return data;
|
|
} finally {
|
|
clearTimeout(t);
|
|
}
|
|
}
|
|
|
|
async function paginate(path, params = {}) {
|
|
const items = [];
|
|
let cursor = null;
|
|
while (true) {
|
|
const res = await evobgpRequest('GET', path, {
|
|
query: { ...params, limit: PAGE_LIMIT, ...(cursor ? { cursor } : {}) },
|
|
});
|
|
const chunk = Array.isArray(res?.items) ? res.items : [];
|
|
items.push(...chunk);
|
|
if (!res?.has_more || !res?.next_cursor) break;
|
|
cursor = res.next_cursor;
|
|
}
|
|
return items;
|
|
}
|
|
|
|
async function fetchRouterListsCatalog() {
|
|
return evobgpRequest('GET', '/router-lists/catalog');
|
|
}
|
|
|
|
function getCatalogItems(section) {
|
|
if (!section || typeof section !== 'object') return [];
|
|
return Array.isArray(section.items) ? section.items : [];
|
|
}
|
|
|
|
async function listCommunities() {
|
|
let communities = [];
|
|
try {
|
|
const catalog = await fetchRouterListsCatalog();
|
|
communities = getCatalogItems(catalog.communities);
|
|
} catch {
|
|
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 evobgpRequest('POST', '/modules', {
|
|
body: { type: moduleType, name: defaultName, enabled: true },
|
|
});
|
|
return created?.data || created;
|
|
}
|
|
|
|
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 evobgpRequest('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 evobgpRequest('POST', `/modules/${moduleId}/${listPath}`, { body: createBody(item, communityId) });
|
|
continue;
|
|
}
|
|
|
|
const currentCommunity = mapCommunityToValue(existingRow.community_id, byId);
|
|
if (currentCommunity !== rawCommunity) {
|
|
await evobgpRequest('PATCH', `/modules/${moduleId}/${listPath}/${existingRow.id}`, {
|
|
body: patchBody(item, communityId),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
async function fetchDomainsData() {
|
|
try {
|
|
const catalog = await fetchRouterListsCatalog();
|
|
const communities = getCatalogItems(catalog.communities);
|
|
const byId = new Map(communities.map((c) => [c.id, String(c.community || '')]));
|
|
const rows = getCatalogItems(catalog.domains);
|
|
return rows
|
|
.map((row) => ({
|
|
id: row?.entry?.id || row?.id || '',
|
|
domain: String(row?.entry?.fqdn || row?.fqdn || '')
|
|
.trim()
|
|
.toLowerCase(),
|
|
community: mapCommunityToValue(row?.entry?.community_id || row?.community_id || null, byId),
|
|
}))
|
|
.filter((row) => row.domain);
|
|
} catch {
|
|
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),
|
|
}));
|
|
}
|
|
}
|
|
|
|
async function fetchIpRangesData() {
|
|
try {
|
|
const catalog = await fetchRouterListsCatalog();
|
|
const communities = getCatalogItems(catalog.communities);
|
|
const byId = new Map(communities.map((c) => [c.id, String(c.community || '')]));
|
|
const rows = getCatalogItems(catalog.ip_ranges);
|
|
return rows
|
|
.map((row) => ({
|
|
id: row?.entry?.id || row?.id || '',
|
|
ipRange: String(row?.entry?.prefix || row?.prefix || '').trim(),
|
|
community: mapCommunityToValue(row?.entry?.community_id || row?.community_id || null, byId),
|
|
}))
|
|
.filter((row) => row.ipRange);
|
|
} catch {
|
|
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),
|
|
}));
|
|
}
|
|
}
|
|
|
|
async function fetchAsnsData() {
|
|
try {
|
|
const catalog = await fetchRouterListsCatalog();
|
|
const communities = getCatalogItems(catalog.communities);
|
|
const byId = new Map(communities.map((c) => [c.id, String(c.community || '')]));
|
|
const rows = getCatalogItems(catalog.asns);
|
|
return rows
|
|
.map((row) => ({
|
|
id: row?.entry?.id || row?.id || '',
|
|
asn: String(row?.entry?.asn || row?.asn || '').trim(),
|
|
community: mapCommunityToValue(row?.entry?.community_id || row?.community_id || null, byId),
|
|
}))
|
|
.filter((row) => row.asn);
|
|
} catch {
|
|
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),
|
|
}));
|
|
}
|
|
}
|
|
|
|
/** Auto-URL: секция каталога или модуль AUTO_URLS / URL_LIST (настраивается env). */
|
|
async function fetchAutoUrlsData() {
|
|
try {
|
|
const catalog = await fetchRouterListsCatalog();
|
|
for (const name of ['auto_urls', 'auto_url', 'urls', 'url_list']) {
|
|
const sec = catalog[name];
|
|
const items = getCatalogItems(sec);
|
|
if (items.length) {
|
|
const communities = getCatalogItems(catalog.communities);
|
|
const byId = new Map(communities.map((c) => [c.id, String(c.community || '')]));
|
|
return items.map((row) => {
|
|
const url = String(row?.entry?.url || row?.entry?.fqdn || row?.url || row?.fqdn || '').trim();
|
|
const community = mapCommunityToValue(
|
|
row?.entry?.community_id || row?.community_id || null,
|
|
byId,
|
|
);
|
|
return { url, community };
|
|
}).filter((r) => r.url);
|
|
}
|
|
}
|
|
} catch (_) {}
|
|
const modType = process.env.EVOBGP_AUTO_URL_MODULE_TYPE || 'AUTO_URLS';
|
|
try {
|
|
const module = await getOrCreateModule(modType, 'Auto URLs');
|
|
const listPath = process.env.EVOBGP_AUTO_URL_LIST_PATH || 'url-entries';
|
|
const rows = await paginate(`/modules/${module.id}/${listPath}`);
|
|
const { byId } = await buildCommunityMaps();
|
|
return rows.map((row) => ({
|
|
url: String(row.url || row.fqdn || '').trim(),
|
|
community: mapCommunityToValue(row.community_id, byId),
|
|
})).filter((r) => r.url);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
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 }),
|
|
});
|
|
}
|
|
|
|
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 }),
|
|
});
|
|
}
|
|
|
|
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 }),
|
|
});
|
|
}
|
|
|
|
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 evobgpRequest('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 evobgpRequest('POST', '/communities', { body: { community: value, title } });
|
|
continue;
|
|
}
|
|
if (String(row.title || '') !== title) {
|
|
await evobgpRequest('PATCH', `/communities/${row.id}`, { body: { community: value, title } });
|
|
}
|
|
}
|
|
}
|
|
|
|
async function saveAutoUrlsData(urls) {
|
|
const originalItems = await fetchAutoUrlsData();
|
|
const list = (urls || []).map((u) => ({
|
|
url: String(u?.url || '').trim(),
|
|
community: String(u?.community || '').trim(),
|
|
})).filter((u) => u.url && u.community);
|
|
const modType = process.env.EVOBGP_AUTO_URL_MODULE_TYPE || 'AUTO_URLS';
|
|
const listPath = process.env.EVOBGP_AUTO_URL_LIST_PATH || 'url-entries';
|
|
const keyName = process.env.EVOBGP_AUTO_URL_KEY_FIELD || 'url';
|
|
const currentItems = list.map((u) => ({ [keyName]: u.url, community: u.community }));
|
|
const origMapped = originalItems.map((u) => ({ [keyName]: u.url, community: u.community }));
|
|
await syncEntries({
|
|
moduleType: modType,
|
|
moduleName: 'Auto URLs',
|
|
listPath,
|
|
keyName,
|
|
currentItems,
|
|
originalItems: origMapped,
|
|
requireCommunity: true,
|
|
createBody: (item, communityId) => ({ url: item[keyName], community_id: communityId }),
|
|
patchBody: (item, communityId) => ({ url: item[keyName], community_id: communityId }),
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
isConfigured,
|
|
evobgpRequest,
|
|
paginate,
|
|
fetchRouterListsCatalog,
|
|
listCommunities,
|
|
fetchDomainsData,
|
|
fetchIpRangesData,
|
|
fetchAsnsData,
|
|
fetchAutoUrlsData,
|
|
saveCommunitiesData,
|
|
saveDomainsData,
|
|
saveIpRangesData,
|
|
saveAsnsData,
|
|
saveAutoUrlsData,
|
|
buildCommunityMaps,
|
|
mapCommunityToValue,
|
|
};
|