Files
MikrotikManager/backend/src/services/traffic-flow-settings.ts
T
DenozordecandCursor ec43591a99
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
feat(db): перевести хранилище с SQLite на PostgreSQL
При старте backend накатывает схему PostgreSQL 18 и, если база пустая, один раз импортирует mikrotik.db с тома. Повторный старт не копирует данные. Бэкап в UI идёт через pg_dump.

Co-authored-by: Cursor <[email protected]>
2026-09-08 01:36:48 +07:00

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()
}