165 lines
7.6 KiB
TypeScript
165 lines
7.6 KiB
TypeScript
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<T>(schema: z.ZodType<T>, 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<unknown>(baseUrl, path, { ...init, signal: AbortSignal.any([...(init?.signal ? [init.signal] : []), AbortSignal.timeout(30_000)]) })
|
|
}
|
|
export async function getTrafficFlowSettings(baseUrl: string, init?: RequestInit): Promise<TrafficFlowSettingsDto> {
|
|
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<string, unknown>)[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<TrafficFlowOverlayResult> {
|
|
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<FlowStatsDto> {
|
|
return requestJson<FlowStatsDto>(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<FlowExportersDto> {
|
|
return requestJson<FlowExportersDto>(baseUrl, `/api/traffic/flow/exporters?range=${encodeURIComponent(range)}`)
|
|
}
|
|
|
|
export async function getFlowClients(baseUrl: string, range = "5m"): Promise<FlowClientsDto> {
|
|
return requestJson<FlowClientsDto>(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<FlowMapHopsDto> {
|
|
return requestJson<FlowMapHopsDto>(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<FlowAnalyticsDto> {
|
|
return requestJson<FlowAnalyticsDto>(baseUrl, `/api/traffic/flow/analytics${flowQuery(params)}`)
|
|
}
|
|
|
|
export async function getFlowMonthly(
|
|
baseUrl: string,
|
|
params: { month: string; serverId?: string },
|
|
): Promise<FlowMonthlyDto> {
|
|
const q = new URLSearchParams()
|
|
q.set("month", params.month)
|
|
if (params.serverId) q.set("serverId", params.serverId)
|
|
return requestJson<FlowMonthlyDto>(baseUrl, `/api/traffic/flow/monthly?${q.toString()}`)
|
|
}
|
|
|
|
export async function purgeTrafficFlowData(baseUrl: string, init?: RequestInit): Promise<FlowPurgeDto> {
|
|
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 }
|