Docker images / prepare-release (push) Successful in 12s
Docker images / backend-test (push) Successful in 4m9s
Docker images / frontend-image (push) Successful in 4m28s
Docker images / updater-image (push) Successful in 58s
Docker images / backend-image (push) Successful in 2m49s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 11s
При старте backend накатывает схему PostgreSQL 18 и, если база пустая, один раз импортирует mikrotik.db с тома. Повторный старт не копирует данные. Бэкап в UI идёт через pg_dump. Co-authored-by: Cursor <[email protected]>
169 lines
5.6 KiB
TypeScript
169 lines
5.6 KiB
TypeScript
import { eq } from "drizzle-orm"
|
|
import { db } from "../db/index.js"
|
|
import { parseJsonArray } from "../db/json.js"
|
|
import { trafficFlowSettings } from "../db/schema.js"
|
|
import type { FlowHostPeer, TrafficFlowSettingsDto, TrafficFlowSettingsPatch } from "@mmapp/contracts/traffic-flow"
|
|
import { generateWireGuardKeyPair } from "./wg-keys.js"
|
|
|
|
type SettingsRow = NonNullable<Awaited<ReturnType<typeof readSettingsRow>>>
|
|
|
|
let settingsRowCache: SettingsRow | null = null
|
|
|
|
function nowIso() {
|
|
return new Date().toISOString()
|
|
}
|
|
|
|
export function invalidateTrafficFlowSettingsCache(): void {
|
|
settingsRowCache = null
|
|
}
|
|
|
|
async function readSettingsRow() {
|
|
return (await db.select().from(trafficFlowSettings).where(eq(trafficFlowSettings.id, 1)).limit(1))[0]
|
|
}
|
|
|
|
export async function getTrafficFlowSettingsRow(): Promise<SettingsRow> {
|
|
if (settingsRowCache) return settingsRowCache
|
|
let row = await readSettingsRow()
|
|
if (row) {
|
|
settingsRowCache = row
|
|
return row
|
|
}
|
|
const now = nowIso()
|
|
await db.insert(trafficFlowSettings).values({
|
|
id: 1,
|
|
enabled: false,
|
|
collectorIp: "10.255.254.1",
|
|
flowListenPort: 4739,
|
|
wgListenPort: 51821,
|
|
prefix: "10.255.254.0/24",
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
})
|
|
row = await readSettingsRow()
|
|
settingsRowCache = row!
|
|
return settingsRowCache
|
|
}
|
|
|
|
function parsePeers(raw: unknown): FlowHostPeer[] {
|
|
const arr = parseJsonArray(raw)
|
|
return arr.filter((p): p is FlowHostPeer =>
|
|
p != null && typeof p === "object" && typeof (p as FlowHostPeer).publicKey === "string",
|
|
)
|
|
}
|
|
|
|
export async function toTrafficFlowSettingsDto(
|
|
listener: { bound: boolean; address: string | null },
|
|
): Promise<TrafficFlowSettingsDto> {
|
|
const row = await getTrafficFlowSettingsRow()
|
|
return {
|
|
enabled: row.enabled,
|
|
collectorIp: row.collectorIp,
|
|
flowListenPort: row.flowListenPort,
|
|
wgListenPort: row.wgListenPort,
|
|
prefix: row.prefix,
|
|
publicEndpoint: row.publicEndpoint,
|
|
hostPublicKey: row.hostPublicKey,
|
|
hasHostPrivateKey: Boolean(row.hostPrivateKey),
|
|
hubServerId: row.hubServerId ?? null,
|
|
retentionHours: row.retentionHours,
|
|
topN: row.topN,
|
|
mapServiceMinSharePct: Number(row.mapServiceMinSharePct ?? 5),
|
|
lastDatagramAt: row.lastDatagramAt ?? null,
|
|
lastExporterIp: row.lastExporterIp ?? null,
|
|
lastError: row.lastError || null,
|
|
packetsReceived: row.packetsReceived,
|
|
listenerBound: listener.bound,
|
|
listenerAddress: listener.address,
|
|
peers: parsePeers(row.peersJson),
|
|
}
|
|
}
|
|
|
|
export async function updateTrafficFlowSettings(patch: TrafficFlowSettingsPatch) {
|
|
const row = await getTrafficFlowSettingsRow()
|
|
await db.update(trafficFlowSettings).set({
|
|
enabled: patch.enabled ?? row.enabled,
|
|
collectorIp: patch.collectorIp ?? row.collectorIp,
|
|
flowListenPort: patch.flowListenPort ?? row.flowListenPort,
|
|
wgListenPort: patch.wgListenPort ?? row.wgListenPort,
|
|
prefix: patch.prefix ?? row.prefix,
|
|
publicEndpoint: patch.publicEndpoint ?? row.publicEndpoint,
|
|
hubServerId: patch.hubServerId === undefined ? row.hubServerId : patch.hubServerId,
|
|
retentionHours: patch.retentionHours ?? row.retentionHours,
|
|
topN: patch.topN ?? row.topN,
|
|
mapServiceMinSharePct: patch.mapServiceMinSharePct == null
|
|
? row.mapServiceMinSharePct
|
|
: Math.min(100, Math.max(0, patch.mapServiceMinSharePct)),
|
|
updatedAt: nowIso(),
|
|
}).where(eq(trafficFlowSettings.id, 1))
|
|
invalidateTrafficFlowSettingsCache()
|
|
return await getTrafficFlowSettingsRow()
|
|
}
|
|
|
|
export async function ensureHostKeys(): Promise<{ publicKey: string; created: boolean }> {
|
|
const row = await getTrafficFlowSettingsRow()
|
|
if (row.hostPublicKey && row.hostPrivateKey) {
|
|
return { publicKey: row.hostPublicKey, created: false }
|
|
}
|
|
const keys = generateWireGuardKeyPair()
|
|
await db.update(trafficFlowSettings).set({
|
|
hostPublicKey: keys.publicKey,
|
|
hostPrivateKey: keys.privateKey,
|
|
updatedAt: nowIso(),
|
|
}).where(eq(trafficFlowSettings.id, 1))
|
|
invalidateTrafficFlowSettingsCache()
|
|
return { publicKey: keys.publicKey, created: true }
|
|
}
|
|
|
|
export async function upsertHostPeer(peer: FlowHostPeer) {
|
|
const row = await getTrafficFlowSettingsRow()
|
|
const peers = parsePeers(row.peersJson).filter((p) => p.serverId !== peer.serverId)
|
|
peers.push(peer)
|
|
await db.update(trafficFlowSettings).set({
|
|
peersJson: peers,
|
|
updatedAt: nowIso(),
|
|
}).where(eq(trafficFlowSettings.id, 1))
|
|
invalidateTrafficFlowSettingsCache()
|
|
}
|
|
|
|
export async function recordFlowPacket(exporterIp: string) {
|
|
const row = await getTrafficFlowSettingsRow()
|
|
await db.update(trafficFlowSettings).set({
|
|
lastDatagramAt: nowIso(),
|
|
lastExporterIp: exporterIp,
|
|
packetsReceived: row.packetsReceived + 1,
|
|
updatedAt: nowIso(),
|
|
}).where(eq(trafficFlowSettings.id, 1))
|
|
invalidateTrafficFlowSettingsCache()
|
|
}
|
|
|
|
export async function recordFlowListenerError(message: string) {
|
|
await db.update(trafficFlowSettings).set({
|
|
lastError: message,
|
|
updatedAt: nowIso(),
|
|
}).where(eq(trafficFlowSettings.id, 1))
|
|
invalidateTrafficFlowSettingsCache()
|
|
}
|
|
|
|
export async function enableTrafficFlowIngest() {
|
|
await db.update(trafficFlowSettings).set({
|
|
enabled: true,
|
|
updatedAt: nowIso(),
|
|
}).where(eq(trafficFlowSettings.id, 1))
|
|
invalidateTrafficFlowSettingsCache()
|
|
}
|
|
|
|
export async function listHostPeers(): Promise<FlowHostPeer[]> {
|
|
return parsePeers((await getTrafficFlowSettingsRow()).peersJson)
|
|
}
|
|
|
|
export async function resetFlowIngestCounters(): Promise<void> {
|
|
await db.update(trafficFlowSettings).set({
|
|
packetsReceived: 0,
|
|
lastDatagramAt: null,
|
|
lastExporterIp: null,
|
|
lastError: "",
|
|
updatedAt: nowIso(),
|
|
}).where(eq(trafficFlowSettings.id, 1))
|
|
invalidateTrafficFlowSettingsCache()
|
|
}
|