import type { FlowAnalyticsDto, FlowClientsDto, FlowExportersDto, FlowMapHopsDto, FlowMonthlyDto, FlowPurgeDto, FlowStatsDto, TrafficFlowHostFile, TrafficFlowOverlayResult, TrafficFlowSettingsDto, TrafficFlowSettingsPatch, } from "@mmapp/contracts/traffic-flow" import { z } from "zod" import { trafficFlowSettingsDtoSchema, trafficFlowSettingsPatchSchema, trafficFlowHostFileSchema, flowPurgeDtoSchema } from "@mmapp/contracts/traffic-flow" import { requestJson } from "@/shared/api/http-client" export type { TrafficFlowHostFile } const flowSettingsSchema = trafficFlowSettingsDtoSchema.extend({ flowListenPort: z.number().int().min(1).max(65535), wgListenPort: z.number().int().min(1).max(65535) }).refine(data => (!data.hasHostPrivateKey || data.hostPublicKey.length > 0) && (!data.listenerBound || !!data.listenerAddress) && (data.lastDatagramAt === null || Number.isFinite(Date.parse(data.lastDatagramAt))) && new Set(data.peers.map(peer => peer.serverId)).size === data.peers.length, "Несогласованные настройки NetFlow") const flowPatchSchema = trafficFlowSettingsPatchSchema.extend({ flowListenPort: z.number().int().min(1).max(65535).optional(), wgListenPort: z.number().int().min(1).max(65535).optional() }).strict() function flowConfirmed(schema: z.ZodType, raw: unknown): T { const parsed = schema.safeParse(raw) if (!parsed.success) throw new Error("Данные NetFlow не подтверждены: получен некорректный ответ сервера") return parsed.data } function flowJson(baseUrl: string, path: string, init?: RequestInit) { return requestJson(baseUrl, path, { ...init, signal: AbortSignal.any([...(init?.signal ? [init.signal] : []), AbortSignal.timeout(30_000)]) }) } export async function getTrafficFlowSettings(baseUrl: string, init?: RequestInit): Promise { return flowConfirmed(flowSettingsSchema, await flowJson(baseUrl, "/api/traffic/flow/settings", { ...init, method: "GET" })) } export async function putTrafficFlowSettings(baseUrl: string, patch: TrafficFlowSettingsPatch, init?: RequestInit) { const valid = flowPatchSchema.safeParse(patch) if (!valid.success || !Object.values(valid.data).some(value => value !== undefined)) throw new Error("Укажите допустимые настройки NetFlow") const body = valid.data const response = flowConfirmed(z.object({ ok: z.literal(true), settings: flowSettingsSchema, listenerError: z.string().optional() }), await flowJson(baseUrl, "/api/traffic/flow/settings", { ...init, method: "PUT", body: JSON.stringify(body) })) if (Object.entries(body).some(([field, value]) => value !== undefined && (response.settings as Record)[field] !== value)) throw new Error("Сохранение NetFlow не подтверждено: сервер вернул другие настройки") return response } export async function generateTrafficFlowKeys(baseUrl: string, init?: RequestInit) { const schema = z.object({ ok: z.literal(true), created: z.boolean(), publicKey: z.string().min(1), settings: flowSettingsSchema }).refine(data => data.settings.hasHostPrivateKey && data.publicKey === data.settings.hostPublicKey) return flowConfirmed(schema, await flowJson(baseUrl, "/api/traffic/flow/settings/generate-keys", { ...init, method: "POST" })) } export async function getTrafficFlowHostFiles(baseUrl: string, init?: RequestInit): Promise<{ files: TrafficFlowHostFile[] }> { const file = trafficFlowHostFileSchema.extend({ id: z.string().min(1), label: z.string().min(1), filename: z.string().min(1).refine(name => !/[\\/\r\n]/.test(name)), code: z.string().min(1) }) const schema = z.object({ files: z.array(file).min(1).refine(files => new Set(files.map(item => item.id)).size === files.length && new Set(files.map(item => item.filename)).size === files.length) }) return flowConfirmed(schema, await flowJson(baseUrl, "/api/traffic/flow/host-files", { ...init, method: "GET" })) } export async function applyTrafficFlowOverlay( baseUrl: string, serverId: string | number, publicEndpoint?: string, opts?: { disableGreFastPath?: boolean }, ): Promise { return requestJson(baseUrl, "/api/traffic/flow-overlay", { method: "POST", body: JSON.stringify({ serverId, publicEndpoint, ...(opts?.disableGreFastPath ? { disableGreFastPath: true } : {}), }), }) } export async function getTrafficFlows(baseUrl: string, range = "5m"): Promise { return requestJson(baseUrl, `/api/traffic/flows?range=${encodeURIComponent(range)}`) } function flowQuery(params: { range?: string serverId?: string userId?: string iface?: string dedup?: boolean excludeMesh?: boolean excludeOverlay?: boolean }): string { const q = new URLSearchParams() if (params.range) q.set("range", params.range) if (params.serverId) q.set("serverId", params.serverId) if (params.userId) q.set("userId", params.userId) if (params.iface && params.iface !== "__all__") q.set("iface", params.iface) if (params.dedup === false) q.set("dedup", "0") else if (params.dedup === true) q.set("dedup", "1") if (params.excludeMesh === false) q.set("excludeMesh", "0") else if (params.excludeMesh === true) q.set("excludeMesh", "1") if (params.excludeOverlay === false) q.set("excludeOverlay", "0") else if (params.excludeOverlay === true) q.set("excludeOverlay", "1") const s = q.toString() return s ? `?${s}` : "" } export async function getFlowExporters(baseUrl: string, range = "5m"): Promise { return requestJson(baseUrl, `/api/traffic/flow/exporters?range=${encodeURIComponent(range)}`) } export async function getFlowClients(baseUrl: string, range = "5m"): Promise { return requestJson(baseUrl, `/api/traffic/flow/clients?range=${encodeURIComponent(range)}`) } export async function getFlowMapHops( baseUrl: string, params: { range?: string serverId?: string userId?: string iface?: string dedup?: boolean excludeMesh?: boolean excludeOverlay?: boolean } = {}, ): Promise { return requestJson(baseUrl, `/api/traffic/flow/map-hops${flowQuery({ range: params.range ?? "5m", serverId: params.serverId, userId: params.userId, iface: params.iface, dedup: params.dedup, excludeMesh: params.excludeMesh, excludeOverlay: params.excludeOverlay, })}`) } export async function getFlowAnalytics( baseUrl: string, params: { range?: string serverId?: string userId?: string iface?: string dedup?: boolean excludeMesh?: boolean excludeOverlay?: boolean }, ): Promise { return requestJson(baseUrl, `/api/traffic/flow/analytics${flowQuery(params)}`) } export async function getFlowMonthly( baseUrl: string, params: { month: string; serverId?: string }, ): Promise { const q = new URLSearchParams() q.set("month", params.month) if (params.serverId) q.set("serverId", params.serverId) return requestJson(baseUrl, `/api/traffic/flow/monthly?${q.toString()}`) } export async function purgeTrafficFlowData(baseUrl: string, init?: RequestInit): Promise { return flowConfirmed(flowPurgeDtoSchema, await flowJson(baseUrl, "/api/traffic/flow/purge", { ...init, method: "POST" })) } export async function rebuildTrafficFlowFacts(baseUrl: string): Promise<{ ok: true buckets: number facts: number days: string[] }> { return requestJson(baseUrl, "/api/traffic/flow/rebuild-facts", { method: "POST" }) } export { flowQuery }