fix(evolution): проверить серверный каталог и завершить локальную приёмку
This commit is contained in:
1 parent
d272ef57f0
commit
2b6d3ab2e0
8 files changed
+169
-129
No files matched your search
@@ -28,7 +28,7 @@
|
|||||||
"test:backups": "tsx src/services/backup-scheduler-run.test.ts && tsx src/services/backup-restore.test.ts && tsx src/services/backup-deletion.test.ts && tsx src/services/backup-dto.test.ts && tsx src/services/backup-storage-update.test.ts && tsx src/services/backup-job-execution.test.ts && tsx src/routes/backups-operations.test.ts && tsx src/services/s3-backup-client.test.ts",
|
"test:backups": "tsx src/services/backup-scheduler-run.test.ts && tsx src/services/backup-restore.test.ts && tsx src/services/backup-deletion.test.ts && tsx src/services/backup-dto.test.ts && tsx src/services/backup-storage-update.test.ts && tsx src/services/backup-job-execution.test.ts && tsx src/routes/backups-operations.test.ts && tsx src/services/s3-backup-client.test.ts",
|
||||||
"test:live-maps": "tsx src/services/ospf-route-parse.test.ts && tsx src/services/vxlan-live.test.ts && tsx src/services/containers-live.test.ts && tsx src/services/firewall-poll.test.ts && tsx src/services/gre-catalog-poll.test.ts && tsx src/routes/filters-gre-catalog.test.ts && tsx src/routes/bgp-catalog.test.ts && tsx src/services/bgp-catalog-parse.test.ts && tsx src/routes/ospf-catalog.test.ts && tsx src/services/ospf-observed-values.test.ts",
|
"test:live-maps": "tsx src/services/ospf-route-parse.test.ts && tsx src/services/vxlan-live.test.ts && tsx src/services/containers-live.test.ts && tsx src/services/firewall-poll.test.ts && tsx src/services/gre-catalog-poll.test.ts && tsx src/routes/filters-gre-catalog.test.ts && tsx src/routes/bgp-catalog.test.ts && tsx src/services/bgp-catalog-parse.test.ts && tsx src/routes/ospf-catalog.test.ts && tsx src/services/ospf-observed-values.test.ts",
|
||||||
"test": "npm run test:probes && npm run test:exec && npm run test:alert-engine && npm run test:auth && npm run test:wireguard && npm run test:traffic-rate && npm run test:traffic-flow && npm run test:users && npm run test:pg && npm run test:backups && npm run test:scheduler && npm run test:live-maps && npm run test:ospf-optimizer && npm run test:recursive-routes && npm run test:config-sync && npm run test:ros-file && npm run test:containers && npm run test:certificates && npm run test:netflow-settings && npm run test:netflow-purge && npm run test:evolution",
|
"test": "npm run test:probes && npm run test:exec && npm run test:alert-engine && npm run test:auth && npm run test:wireguard && npm run test:traffic-rate && npm run test:traffic-flow && npm run test:users && npm run test:pg && npm run test:backups && npm run test:scheduler && npm run test:live-maps && npm run test:ospf-optimizer && npm run test:recursive-routes && npm run test:config-sync && npm run test:ros-file && npm run test:containers && npm run test:certificates && npm run test:netflow-settings && npm run test:netflow-purge && npm run test:evolution",
|
||||||
"test:evolution": "tsx src/services/evobgp-catalog.test.ts && tsx src/routes/evobgp-settings.test.ts && tsx src/plugins/auth.evolution-settings.test.ts",
|
"test:evolution": "tsx src/services/evobgp-catalog.test.ts && tsx src/routes/evobgp-catalog.test.ts && tsx src/routes/evobgp-settings.test.ts && tsx src/plugins/auth.evolution-settings.test.ts",
|
||||||
"test:netflow-purge": "tsx src/db/operation-lock.test.ts && tsx src/services/traffic-flow-purge-execution.test.ts && tsx src/routes/traffic-flow-purge.test.ts",
|
"test:netflow-purge": "tsx src/db/operation-lock.test.ts && tsx src/services/traffic-flow-purge-execution.test.ts && tsx src/routes/traffic-flow-purge.test.ts",
|
||||||
"test:netflow-settings": "tsx src/services/traffic-flow-host-file-builders.test.ts && tsx src/services/traffic-flow-key-execution.test.ts && tsx src/routes/traffic-flow-settings.test.ts && tsx src/plugins/auth.netflow-settings.test.ts",
|
"test:netflow-settings": "tsx src/services/traffic-flow-host-file-builders.test.ts && tsx src/services/traffic-flow-key-execution.test.ts && tsx src/routes/traffic-flow-settings.test.ts && tsx src/plugins/auth.netflow-settings.test.ts",
|
||||||
"test:geoip": "tsx src/services/traffic-flow-geoip.test.ts && tsx src/plugins/auth.geoip.test.ts",
|
"test:geoip": "tsx src/services/traffic-flow-geoip.test.ts && tsx src/plugins/auth.geoip.test.ts",
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import assert from "node:assert/strict"
|
import assert from "node:assert/strict"
|
||||||
import Fastify from "fastify"
|
import Fastify from "fastify"
|
||||||
import {SignJWT} from "jose"
|
import {SignJWT} from "jose"
|
||||||
|
import {createEvoBgpCatalogRoutes} from "../routes/evobgp-catalog.js"
|
||||||
import {createEvoBgpSettingsRoutes} from "../routes/evobgp-settings.js"
|
import {createEvoBgpSettingsRoutes} from "../routes/evobgp-settings.js"
|
||||||
process.env.AUTH_REQUIRED="true";process.env.AUTH_JWT_SECRET="evolution-fixture-secret";process.env.AUTH_ISSUER="https://auth.test.local";process.env.NODE_ENV="test"
|
process.env.AUTH_REQUIRED="true";process.env.AUTH_JWT_SECRET="evolution-fixture-secret";process.env.AUTH_ISSUER="https://auth.test.local";process.env.NODE_ENV="test"
|
||||||
const {default:authPlugin,requireAuth}=await import("./auth.js")
|
const {default:authPlugin,requireAuth}=await import("./auth.js")
|
||||||
const app=Fastify();await app.register(authPlugin);app.addHook("preHandler",requireAuth)
|
const app=Fastify();await app.register(authPlugin);app.addHook("preHandler",requireAuth)
|
||||||
let writes=0,probes=0
|
let writes=0,probes=0,catalogs=0
|
||||||
await app.register(createEvoBgpSettingsRoutes({read:async()=>({baseUrl:"https://evo.example.test",enabled:false,apiKey:"fixture-only"}),write:async()=>{writes++;return {baseUrl:"https://evo.example.test",enabled:false,apiKey:"fixture-only"}},probe:async()=>{probes++;return {items:[]}}}),{prefix:"/api"})
|
await app.register(createEvoBgpSettingsRoutes({read:async()=>({baseUrl:"https://evo.example.test",enabled:false,apiKey:"fixture-only"}),write:async()=>{writes++;return {baseUrl:"https://evo.example.test",enabled:false,apiKey:"fixture-only"}},probe:async()=>{probes++;return {items:[]}}}),{prefix:"/api"})
|
||||||
|
await app.register(createEvoBgpCatalogRoutes({read:async()=>({baseUrl:"https://evo.example.test",enabled:true,apiKey:"fixture-only"}),fetchCatalog:async()=>{catalogs++;return {modules:{items:[]},domains:{items:[]},ip_ranges:{items:[]},asns:{items:[]},communities:{items:[]}}},serversByCommunity:async()=>new Map()}),{prefix:"/api"})
|
||||||
for(const permission of ["mm:settings:admin","mm:settings:write","mm:traffic:admin","mm:dashboard:read"]){
|
for(const permission of ["mm:settings:admin","mm:settings:write","mm:traffic:admin","mm:dashboard:read"]){
|
||||||
const token=await new SignJWT({sub:"fixture",apps:["mm"],permissions:[permission]}).setProtectedHeader({alg:"HS256"}).setIssuer("https://auth.test.local").setExpirationTime("1h").sign(new TextEncoder().encode("evolution-fixture-secret"))
|
const token=await new SignJWT({sub:"fixture",apps:["mm"],permissions:[permission]}).setProtectedHeader({alg:"HS256"}).setIssuer("https://auth.test.local").setExpirationTime("1h").sign(new TextEncoder().encode("evolution-fixture-secret"))
|
||||||
for(const [method,url] of [["GET","/api/evobgp/settings"],["PUT","/api/evobgp/settings"],["POST","/api/evobgp/test"]] as const){const response=await app.inject({method,url,headers:{authorization:"Bearer "+token},...(method==="PUT"?{payload:{enabled:false}}:{})});assert.equal(response.statusCode,permission==="mm:settings:admin"?200:403);assert.equal(response.body.includes("fixture-only"),false)}
|
for(const [method,url] of [["GET","/api/evobgp/settings"],["PUT","/api/evobgp/settings"],["POST","/api/evobgp/test"],["POST","/api/evobgp/catalog"]] as const){const response=await app.inject({method,url,headers:{authorization:"Bearer "+token},...(method==="PUT"?{payload:{enabled:false}}:{})});assert.equal(response.statusCode,permission==="mm:settings:admin"?200:403);assert.equal(response.body.includes("fixture-only"),false)}
|
||||||
}
|
}
|
||||||
assert.equal(writes,1);assert.equal(probes,1);assert.equal((await app.inject({method:"GET",url:"/api/evobgp/settings"})).statusCode,401)
|
assert.equal(writes,1);assert.equal(probes,1);assert.equal(catalogs,1);assert.equal((await app.inject({method:"POST",url:"/api/evobgp/catalog"})).statusCode,401);assert.equal((await app.inject({method:"GET",url:"/api/evobgp/settings"})).statusCode,401)
|
||||||
await app.close();console.log("Evolution JWT gate: actual settings/test handlers require settings admin, no key echo")
|
await app.close();console.log("Evolution JWT gate: actual settings/test/catalog handlers require settings admin, no key echo")
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import assert from "node:assert/strict"
|
||||||
|
import Fastify from "fastify"
|
||||||
|
import {createEvoBgpCatalogRoutes} from "./evobgp-catalog.js"
|
||||||
|
const empty={modules:{items:[]},domains:{items:[]},ip_ranges:{items:[]},asns:{items:[]},communities:{items:[]}}
|
||||||
|
let settings={enabled:true,baseUrl:"EVO.EXAMPLE.test/",apiKey:"Bearer fixture-secret"},reads=0,fetches=0,serverReads=0,failFetch=false,failLocal=false
|
||||||
|
let catalog:unknown=empty
|
||||||
|
const app=Fastify();await app.register(createEvoBgpCatalogRoutes({read:async()=>{reads++;return {...settings}},fetchCatalog:async(root,key)=>{fetches++;assert.equal(root,"https://evo.example.test");assert.equal(key,"fixture-secret");if(failFetch)throw Error("upstream unavailable");return catalog},serversByCommunity:async()=>{serverReads++;if(failLocal)throw Error("local associations unavailable");return new Map([["64512:1",3]])},now:()=>new Date("2026-10-03T00:00:00Z")}),{prefix:"/api"})
|
||||||
|
const load=()=>app.inject({method:"POST",url:"/api/evobgp/catalog",payload:{baseUrl:"https://untrusted.test",apiKey:"untrusted"}})
|
||||||
|
let response=await load();assert.equal(response.statusCode,200);assert.deepEqual(response.json(),{fetchedAt:"2026-10-03T00:00:00.000Z",modules:[],domains:[],ipRanges:[],asns:[],communities:[]});assert.equal(reads,1);assert.equal(fetches,1)
|
||||||
|
catalog={...empty,modules:{items:[{id:"m",name:"Example module",type:"custom"}]},domains:{items:[{module_id:"m",entry:{id:"d",fqdn:"example.test",community_id:"c"}}]},asns:{items:[{module_id:"m",entry:{id:"a",asn:64512,community_id:"c"}},{module_id:"m",entry:{id:"z",asn:64513,community_id:"z",prefix_count:0}}]},communities:{items:[{id:"c",community:"64512:1",title:"Example community"},{id:"z",community:"64512:2"},{id:"empty",community:"64512:3"}]}}
|
||||||
|
response=await load();assert.equal(response.statusCode,200);const mapped=response.json();assert.equal(mapped.asns[0].prefixes,null);assert.equal(mapped.asns[1].prefixes,0);assert.equal(mapped.communities[0].prefixCount,null);assert.equal(mapped.communities[0].serverCount,3);assert.equal(mapped.communities[1].prefixCount,0);assert.equal(mapped.communities[2].prefixCount,0);assert.equal(mapped.domains[0].purpose,"Example module");assert.equal(response.body.includes("fixture-secret"),false)
|
||||||
|
const serverBefore=serverReads;for(const malformed of [{},null,{...empty,domains:undefined},{...empty,asns:{items:[{module_id:"m",entry:{id:"a",asn:1,prefix_count:1.2}}]}}]){catalog=malformed;response=await load();assert.equal(response.statusCode,502);assert.match(response.json().error,/не подтверждён/);assert.equal(response.json().domains,undefined)}assert.equal(serverReads,serverBefore)
|
||||||
|
settings={...settings,enabled:false};const before=fetches;response=await load();assert.equal(response.statusCode,400);assert.equal(fetches,before)
|
||||||
|
settings={...settings,enabled:true,apiKey:""};response=await load();assert.equal(response.statusCode,400);assert.equal(fetches,before)
|
||||||
|
settings={...settings,apiKey:"fixture-secret"};catalog=empty;failFetch=true;response=await load();assert.equal(response.statusCode,502);assert.equal(fetches,before+1);assert.equal(response.json().domains,undefined)
|
||||||
|
failFetch=false;failLocal=true;response=await load();assert.equal(response.statusCode,502);assert.equal(response.json().domains,undefined)
|
||||||
|
await app.close();console.log("Evolution catalog HTTP: one settings snapshot, DB credentials only, known empty/nullable counts, malformed/upstream/local errors without fabricated success")
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
||||||
|
import { normalizeEvoBgpBaseUrl as normalizeBaseUrl, normalizeEvoBgpApiKey as normalizeApiKey } from "@mmapp/contracts/evobgp"
|
||||||
|
import { parseEvoCatalogRaw, aggregateCommunityRouteCounts } from "../services/evobgp-catalog.js"
|
||||||
|
interface Dependencies {
|
||||||
|
read: () => Promise<{enabled:boolean;baseUrl:string;apiKey:string}>
|
||||||
|
fetchCatalog: (root:string,key:string) => Promise<unknown>
|
||||||
|
serversByCommunity: () => Promise<Map<string,number>>
|
||||||
|
now?: () => Date
|
||||||
|
}
|
||||||
|
export function createEvoBgpCatalogRoutes(deps:Dependencies):FastifyPluginAsyncZod {
|
||||||
|
return async app => {
|
||||||
|
app.post("/evobgp/catalog", async (_req, reply) => {
|
||||||
|
const row = await deps.read()
|
||||||
|
if (!row.enabled) {
|
||||||
|
return reply.status(400).send({ error: "Интеграция EvoBGP выключена в настройках" })
|
||||||
|
}
|
||||||
|
const root = normalizeBaseUrl(row.baseUrl)
|
||||||
|
const apiKey = normalizeApiKey(row.apiKey)
|
||||||
|
const cred = root && apiKey ? {root,apiKey} : null
|
||||||
|
if (!cred) {
|
||||||
|
return reply.status(400).send({ error: "Не заданы базовый URL или API-ключ в БД" })
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const catalog = parseEvoCatalogRaw(await deps.fetchCatalog(cred.root, cred.apiKey))
|
||||||
|
const modules = catalog.modules.items
|
||||||
|
const modMap = new Map(modules.map((m) => [m.id, m]))
|
||||||
|
const commItems = catalog.communities.items
|
||||||
|
const commLabel = new Map<string, string>()
|
||||||
|
for (const c of commItems) {
|
||||||
|
const label = (c.community && c.title)
|
||||||
|
? `${c.title} (${c.community})`
|
||||||
|
: (c.title ?? c.community ?? c.id)
|
||||||
|
commLabel.set(c.id, label)
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = deps.now?.() ?? new Date()
|
||||||
|
const fmtTime = now.toLocaleString("ru-RU")
|
||||||
|
const fetchedAt = now.toISOString()
|
||||||
|
|
||||||
|
const domains = catalog.domains.items.map((rowItem) => {
|
||||||
|
const mod = modMap.get(rowItem.module_id)
|
||||||
|
const cid = rowItem.entry.community_id
|
||||||
|
return {
|
||||||
|
id: `${rowItem.module_id}:${rowItem.entry.id}`,
|
||||||
|
domain: rowItem.entry.fqdn,
|
||||||
|
resolvedIp: "—",
|
||||||
|
asn: "—",
|
||||||
|
purpose: mod?.name ?? "—",
|
||||||
|
filter: cid ? (commLabel.get(cid) ?? cid) : "—",
|
||||||
|
updated: fmtTime,
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const ipRanges = catalog.ip_ranges.items.map((rowItem) => {
|
||||||
|
const mod = modMap.get(rowItem.module_id)
|
||||||
|
const cid = rowItem.entry.community_id
|
||||||
|
return {
|
||||||
|
id: `${rowItem.module_id}:${rowItem.entry.id}`,
|
||||||
|
cidr: rowItem.entry.prefix,
|
||||||
|
asn: "—",
|
||||||
|
country: "—",
|
||||||
|
purpose: mod?.name ?? "—",
|
||||||
|
filter: commLabel.get(cid) ?? cid,
|
||||||
|
updated: fmtTime,
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const asns = catalog.asns.items.map((rowItem) => {
|
||||||
|
const cid = rowItem.entry.community_id
|
||||||
|
const e = rowItem.entry
|
||||||
|
const name = (e.asn_name ?? "").trim()
|
||||||
|
const prefixes = e.prefix_count ?? null
|
||||||
|
let updated = fmtTime
|
||||||
|
const rawResolved = (e.asn_resolved_at ?? "").trim()
|
||||||
|
if (rawResolved) {
|
||||||
|
const d = new Date(rawResolved)
|
||||||
|
if (!Number.isNaN(d.getTime())) updated = d.toLocaleString("ru-RU")
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: `${rowItem.module_id}:${e.id}`,
|
||||||
|
asn: `AS${e.asn}`,
|
||||||
|
org: name || "—",
|
||||||
|
country: "—",
|
||||||
|
prefixes,
|
||||||
|
filter: cid ? (commLabel.get(cid) ?? cid) : "—",
|
||||||
|
updated,
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const routeCountsByCommUuid = aggregateCommunityRouteCounts(catalog)
|
||||||
|
const serversByCommunityStr = await deps.serversByCommunity()
|
||||||
|
|
||||||
|
const communities = commItems.map((c) => {
|
||||||
|
const valueStr = (c.community ?? "").trim()
|
||||||
|
return {
|
||||||
|
id: c.id,
|
||||||
|
value: c.community ?? c.id,
|
||||||
|
name: c.title ?? c.community ?? c.id,
|
||||||
|
description: "",
|
||||||
|
type: "custom" as const,
|
||||||
|
filterIds: [] as string[],
|
||||||
|
serverCount: valueStr ? (serversByCommunityStr.get(valueStr) ?? 0) : 0,
|
||||||
|
prefixCount: routeCountsByCommUuid.has(c.id) ? routeCountsByCommUuid.get(c.id)! : 0,
|
||||||
|
action: "permit" as const,
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return reply.send({
|
||||||
|
fetchedAt,
|
||||||
|
modules: modules.map((m) => ({ id: m.id, name: m.name, type: m.type })),
|
||||||
|
domains,
|
||||||
|
ipRanges,
|
||||||
|
asns,
|
||||||
|
communities,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e instanceof Error ? e.message : "Ошибка EvoBGP"
|
||||||
|
return reply.status(502).send({ error: msg })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { eq, sql } from "drizzle-orm"
|
import { eq, sql } from "drizzle-orm"
|
||||||
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
||||||
import { normalizeEvoBgpBaseUrl as normalizeBaseUrl, normalizeEvoBgpApiKey as normalizeApiKey } from "@mmapp/contracts/evobgp"
|
import { createEvoBgpCatalogRoutes } from "./evobgp-catalog.js"
|
||||||
import { parseEvoCatalogRaw, aggregateCommunityRouteCounts } from "../services/evobgp-catalog.js"
|
|
||||||
import { createEvoBgpSettingsRoutes } from "./evobgp-settings.js"
|
import { createEvoBgpSettingsRoutes } from "./evobgp-settings.js"
|
||||||
import { db } from "../db/index.js"
|
import { db } from "../db/index.js"
|
||||||
import { evobgpSettings, filterRules } from "../db/schema.js"
|
import { evobgpSettings, filterRules } from "../db/schema.js"
|
||||||
@@ -71,14 +70,6 @@ async function fetchEvoJson<T>(root: string, path: string, token: string): Promi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function credentialsFromDb(): Promise<{ root: string; apiKey: string } | null> {
|
|
||||||
const row = await ensureEvobgpRow()
|
|
||||||
const root = normalizeBaseUrl(row.baseUrl)
|
|
||||||
const apiKey = normalizeApiKey(row.apiKey)
|
|
||||||
if (!root || !apiKey) return null
|
|
||||||
return { root, apiKey }
|
|
||||||
}
|
|
||||||
|
|
||||||
const evobgpRoutes: FastifyPluginAsyncZod = async (app) => {
|
const evobgpRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||||
await app.register(createEvoBgpSettingsRoutes({
|
await app.register(createEvoBgpSettingsRoutes({
|
||||||
read: ensureEvobgpRow,
|
read: ensureEvobgpRow,
|
||||||
@@ -95,119 +86,11 @@ const evobgpRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
/**
|
await app.register(createEvoBgpCatalogRoutes({
|
||||||
* Прокси к GET /v1/router-lists/catalog — учётные данные только из БД.
|
read: ensureEvobgpRow,
|
||||||
*/
|
fetchCatalog: (root,key) => fetchEvoJson<unknown>(root,"/v1/router-lists/catalog",key),
|
||||||
app.post("/evobgp/catalog", async (_req, reply) => {
|
serversByCommunity: distinctServersByFilterCommunityValue,
|
||||||
const row = await ensureEvobgpRow()
|
}))
|
||||||
if (!row.enabled) {
|
|
||||||
return reply.status(400).send({ error: "Интеграция EvoBGP выключена в настройках" })
|
|
||||||
}
|
|
||||||
const cred = await credentialsFromDb()
|
|
||||||
if (!cred) {
|
|
||||||
return reply.status(400).send({ error: "Не заданы базовый URL или API-ключ в БД" })
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const catalog = parseEvoCatalogRaw(await fetchEvoJson<unknown>(cred.root, "/v1/router-lists/catalog", cred.apiKey))
|
|
||||||
const modules = catalog.modules.items
|
|
||||||
const modMap = new Map(modules.map((m) => [m.id, m]))
|
|
||||||
const commItems = catalog.communities.items
|
|
||||||
const commLabel = new Map<string, string>()
|
|
||||||
for (const c of commItems) {
|
|
||||||
const label = (c.community && c.title)
|
|
||||||
? `${c.title} (${c.community})`
|
|
||||||
: (c.title ?? c.community ?? c.id)
|
|
||||||
commLabel.set(c.id, label)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fmtTime = new Date().toLocaleString("ru-RU")
|
|
||||||
const fetchedAt = new Date().toISOString()
|
|
||||||
|
|
||||||
const domains = catalog.domains.items.map((rowItem) => {
|
|
||||||
const mod = modMap.get(rowItem.module_id)
|
|
||||||
const cid = rowItem.entry.community_id
|
|
||||||
return {
|
|
||||||
id: `${rowItem.module_id}:${rowItem.entry.id}`,
|
|
||||||
domain: rowItem.entry.fqdn,
|
|
||||||
resolvedIp: "—",
|
|
||||||
asn: "—",
|
|
||||||
purpose: mod?.name ?? "—",
|
|
||||||
filter: cid ? (commLabel.get(cid) ?? cid) : "—",
|
|
||||||
updated: fmtTime,
|
|
||||||
enabled: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const ipRanges = catalog.ip_ranges.items.map((rowItem) => {
|
|
||||||
const mod = modMap.get(rowItem.module_id)
|
|
||||||
const cid = rowItem.entry.community_id
|
|
||||||
return {
|
|
||||||
id: `${rowItem.module_id}:${rowItem.entry.id}`,
|
|
||||||
cidr: rowItem.entry.prefix,
|
|
||||||
asn: "—",
|
|
||||||
country: "—",
|
|
||||||
purpose: mod?.name ?? "—",
|
|
||||||
filter: commLabel.get(cid) ?? cid,
|
|
||||||
updated: fmtTime,
|
|
||||||
enabled: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const asns = catalog.asns.items.map((rowItem) => {
|
|
||||||
const cid = rowItem.entry.community_id
|
|
||||||
const e = rowItem.entry
|
|
||||||
const name = (e.asn_name ?? "").trim()
|
|
||||||
const prefixes = e.prefix_count ?? null
|
|
||||||
let updated = fmtTime
|
|
||||||
const rawResolved = (e.asn_resolved_at ?? "").trim()
|
|
||||||
if (rawResolved) {
|
|
||||||
const d = new Date(rawResolved)
|
|
||||||
if (!Number.isNaN(d.getTime())) updated = d.toLocaleString("ru-RU")
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
id: `${rowItem.module_id}:${e.id}`,
|
|
||||||
asn: `AS${e.asn}`,
|
|
||||||
org: name || "—",
|
|
||||||
country: "—",
|
|
||||||
prefixes,
|
|
||||||
filter: cid ? (commLabel.get(cid) ?? cid) : "—",
|
|
||||||
updated,
|
|
||||||
enabled: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const routeCountsByCommUuid = aggregateCommunityRouteCounts(catalog)
|
|
||||||
const serversByCommunityStr = await distinctServersByFilterCommunityValue()
|
|
||||||
|
|
||||||
const communities = commItems.map((c) => {
|
|
||||||
const valueStr = (c.community ?? "").trim()
|
|
||||||
return {
|
|
||||||
id: c.id,
|
|
||||||
value: c.community ?? c.id,
|
|
||||||
name: c.title ?? c.community ?? c.id,
|
|
||||||
description: "",
|
|
||||||
type: "custom" as const,
|
|
||||||
filterIds: [] as string[],
|
|
||||||
serverCount: valueStr ? (serversByCommunityStr.get(valueStr) ?? 0) : 0,
|
|
||||||
prefixCount: routeCountsByCommUuid.has(c.id) ? routeCountsByCommUuid.get(c.id)! : 0,
|
|
||||||
action: "permit" as const,
|
|
||||||
enabled: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return reply.send({
|
|
||||||
fetchedAt,
|
|
||||||
modules: modules.map((m) => ({ id: m.id, name: m.name, type: m.type })),
|
|
||||||
domains,
|
|
||||||
ipRanges,
|
|
||||||
asns,
|
|
||||||
communities,
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
const msg = e instanceof Error ? e.message : "Ошибка EvoBGP"
|
|
||||||
return reply.status(502).send({ error: msg })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default evobgpRoutes
|
export default evobgpRoutes
|
||||||
@@ -282,3 +282,5 @@ REST запрос → ожидание → подтверждённый output /
|
|||||||
Завершён 11Б.3.5.4.1: отдельная форма Evolution dirty/rights/lock/clear confirmation/source; 17 основных, 8 source и 3 responsive E2E, typecheck/lint/build прошли. 6 снимков, 320/ПК/clear просмотрены. Общие Settings и live consumer mocks ещё не приняты. Далее 11Б.3.5.4.2 — consumers/sidebar unknown/disabled/no fallback.
|
Завершён 11Б.3.5.4.1: отдельная форма Evolution dirty/rights/lock/clear confirmation/source; 17 основных, 8 source и 3 responsive E2E, typecheck/lint/build прошли. 6 снимков, 320/ПК/clear просмотрены. Общие Settings и live consumer mocks ещё не приняты. Далее 11Б.3.5.4.2 — consumers/sidebar unknown/disabled/no fallback.
|
||||||
|
|
||||||
Завершён 11Б.3.5.4.2: live catalog consumers/sidebar/Filters без demo fallback, unknown/disabled/last snapshot и current details; 25 E2E, typecheck/профильный lint/build прошли. 3 community screenshots, 320/ПК просмотрены. Sidebar sync-effect baseline error и Filters unused warnings описаны в журнале. Далее 11Б.3.5.5 — HTTP/JWT catalog и приёмка Evolution.
|
Завершён 11Б.3.5.4.2: live catalog consumers/sidebar/Filters без demo fallback, unknown/disabled/last snapshot и current details; 25 E2E, typecheck/профильный lint/build прошли. 3 community screenshots, 320/ПК просмотрены. Sidebar sync-effect baseline error и Filters unused warnings описаны в журнале. Далее 11Б.3.5.5 — HTTP/JWT catalog и приёмка Evolution.
|
||||||
|
|
||||||
|
Завершён 11Б.3.5.5: Evolution catalog DI HTTP/JWT, один settings snapshot, backend test/build/lint прошли. Evolution принят локально в границах предыдущих form/provider/source/consumer/adaptive проверок; реальный upstream/PG/Portal/RouterOS не принят. Далее 11Б.3.6.1 — достоверные общие Settings без фиктивных сохранений/API keys/MFA; затем system DB, Alerts/Releases/probes.
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
|
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
|
||||||
| 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 |
|
| 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 |
|
||||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 |
|
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 |
|
||||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.3.5.4.2; scheduler и GeoIP приняты локально; NetFlow принят локально; далее Evolution и общие settings |
|
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.3.5.5; scheduler и GeoIP приняты локально; NetFlow принят локально; Evolution принят локально; далее общие Settings |
|
||||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||||
|
|
||||||
|
|
||||||
@@ -36,6 +36,14 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Блок 11Б.3.5.5 — серверная и локальная приёмка Evolution (2026-10-03)
|
||||||
|
|
||||||
|
- Catalog handler вынесен в DI plugin, production route использует те же функции. Enabled/URL/key читаются одним снимком настроек (раньше двумя GET БД), credentials поступают только из сохранённых данных, request body не переопределяет URL/key. Mapping сохраняет null/0 и local server associations; timestamps берутся из одного now.
|
||||||
|
- HTTP injection проверил known empty, реальный mapping modules/domain/ASN/community, unknown/zero/known sums, malformed upstream (502, без empty receipt и без чтения local associations), disabled/missing key (400, upstream не вызывается), upstream и local-association failures. JWT реального auth plugin и реальных catalog/settings/test handlers: admin 200, остальные 403 до upstream, без токена 401, ключ не возвращается.
|
||||||
|
- Backend Evolution parser/HTTP/settings/JWT, backend build и lint прошли; prefer-const в новом тесте исправлен. Frontend 17 form/provider, 8 source, 18 consumer и 3 responsive сценария, count E2E, typecheck/production build и снимки описаны в предыдущих блоках; повторять без нового frontend изменения не требуется.
|
||||||
|
- Evolution принят в границах локальных HTTP-фикстур и DI. Реальный upstream format/version и nullable counts, PG selective writes/associations, Portal, доступность внешнего EvoBGP и фактическое применение сетевых списков требуют тестового окружения. Read-only catalog POST может продолжать работу backend после browser abort; автоматического повтора mutations нет.
|
||||||
|
- Следующий блок 11Б.3.6.1 — общие Settings: убрать фиктивное сохранение/demo API keys/MFA и параметры без реализации; оставить реальные source/интеграции/переходы, отдельная приёмка system DB операций затем. Alerts/Releases/probe scheduling/приёмка 11Б остаются впереди.
|
||||||
|
|
||||||
## Блок 11Б.3.5.4.2 — live потребители каталога (2026-10-03)
|
## Блок 11Б.3.5.4.2 — live потребители каталога (2026-10-03)
|
||||||
|
|
||||||
- Domains/IP ranges/ASN/communities используют mock rows только в mock mode. В live unknown/disabled/no access не становится демонстрационными строками или подтверждённым нулём. Общий status разделяет unread/disabled/error/last snapshot/freshness; retry может перечитать настройки отключённой интеграции. Подтверждённый empty catalogue и search no-results имеют отдельные подсказки.
|
- Domains/IP ranges/ASN/communities используют mock rows только в mock mode. В live unknown/disabled/no access не становится демонстрационными строками или подтверждённым нулём. Общий status разделяет unread/disabled/error/last snapshot/freshness; retry может перечитать настройки отключённой интеграции. Подтверждённый empty catalogue и search no-results имеют отдельные подсказки.
|
||||||
|
|||||||
@@ -214,3 +214,5 @@ ReUI PRO/base-nova; существующие Frame, Data Grid v9, KpiStatGrid, S
|
|||||||
| 11Б.3.5.4.1 Evolution | [Settings 16](https://reui.io/preview/base/settings-16?ref=mcp) | Статус и свежесть → поля → сохранить / проверить → подтверждённый результат; удаление ключа → подтверждение → результат |
|
| 11Б.3.5.4.1 Evolution | [Settings 16](https://reui.io/preview/base/settings-16?ref=mcp) | Статус и свежесть → поля → сохранить / проверить → подтверждённый результат; удаление ключа → подтверждение → результат |
|
||||||
|
|
||||||
Завершён 11Б.3.5.4.2: live catalog consumers/sidebar/Filters без demo fallback, unknown/disabled/last snapshot и current details; 25 E2E, typecheck/профильный lint/build прошли. 3 community screenshots, 320/ПК просмотрены. Sidebar sync-effect baseline error и Filters unused warnings описаны в журнале. Далее 11Б.3.5.5 — HTTP/JWT catalog и приёмка Evolution.
|
Завершён 11Б.3.5.4.2: live catalog consumers/sidebar/Filters без demo fallback, unknown/disabled/last snapshot и current details; 25 E2E, typecheck/профильный lint/build прошли. 3 community screenshots, 320/ПК просмотрены. Sidebar sync-effect baseline error и Filters unused warnings описаны в журнале. Далее 11Б.3.5.5 — HTTP/JWT catalog и приёмка Evolution.
|
||||||
|
|
||||||
|
Завершён 11Б.3.5.5: Evolution catalog DI HTTP/JWT, один settings snapshot, backend test/build/lint прошли. Evolution принят локально в границах предыдущих form/provider/source/consumer/adaptive проверок; реальный upstream/PG/Portal/RouterOS не принят. Далее 11Б.3.6.1 — достоверные общие Settings без фиктивных сохранений/API keys/MFA; затем system DB, Alerts/Releases/probes.
|
||||||
Reference in new issue
Block a user