Docker images / prepare-release (push) Successful in 9s
Docker images / backend-image (push) Successful in 1m43s
Docker images / frontend-image (push) Successful in 2m58s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 39s
Docker images / publish-release (push) Successful in 8s
Replaced existing KPI display implementations across multiple pages with the new KpiStatGrid component for a more consistent and visually appealing presentation of statistics. Updated the Backups, BGP, Certificates, Communities, Containers, Dashboard, and Data Collection pages to utilize the KpiStatGrid, improving the overall user experience and maintainability of the codebase. Additionally, added new dependencies in package.json for required libraries.
199 lines
5.0 KiB
TypeScript
199 lines
5.0 KiB
TypeScript
import { eq } from "drizzle-orm"
|
|
import { db } from "../db/index.js"
|
|
import { servers } from "../db/schema.js"
|
|
import {
|
|
MikrotikClient,
|
|
firewallRestPath,
|
|
} from "./mikrotik.js"
|
|
import type {
|
|
FirewallFamily,
|
|
FirewallTable,
|
|
RosFirewallAddressList,
|
|
RosFirewallFilter,
|
|
} from "../types/server.js"
|
|
|
|
type ServerRow = typeof servers.$inferSelect
|
|
|
|
export interface FirewallRuleDto {
|
|
id: string
|
|
rosId: string
|
|
serverId: string
|
|
serverName: string
|
|
family: FirewallFamily
|
|
table: FirewallTable
|
|
chain: string
|
|
action: string
|
|
proto: string
|
|
src: string
|
|
dst: string
|
|
port: string
|
|
iface: string
|
|
comment: string
|
|
enabled: boolean
|
|
hits: number
|
|
log: boolean
|
|
logPrefix: string
|
|
tlsHost?: string
|
|
layer7Proto?: string
|
|
}
|
|
|
|
export interface FirewallAddressListDto {
|
|
id: string
|
|
rosId: string
|
|
serverId: string
|
|
serverName: string
|
|
family: FirewallFamily
|
|
list: string
|
|
address: string
|
|
comment: string
|
|
disabled: boolean
|
|
timeout?: string
|
|
}
|
|
|
|
const TABLES: FirewallTable[] = ["filter", "nat", "mangle", "raw"]
|
|
const FAMILIES: FirewallFamily[] = ["ip", "ip6"]
|
|
|
|
function dash(v: string | undefined): string {
|
|
const s = v?.trim() ?? ""
|
|
return s.length > 0 ? s : "—"
|
|
}
|
|
|
|
function rosDisabled(v: string | undefined): boolean {
|
|
return v === "true" || v === "yes"
|
|
}
|
|
|
|
function parseHits(raw: RosFirewallFilter): number {
|
|
const n = Number.parseInt(raw.packets ?? "0", 10)
|
|
return Number.isFinite(n) ? n : 0
|
|
}
|
|
|
|
export function ruleUiId(
|
|
serverId: string | number,
|
|
family: FirewallFamily,
|
|
table: FirewallTable,
|
|
rosId: string,
|
|
): string {
|
|
return `${serverId}:${family}:${table}:${rosId}`
|
|
}
|
|
|
|
export function addressUiId(
|
|
serverId: string | number,
|
|
family: FirewallFamily,
|
|
rosId: string,
|
|
): string {
|
|
return `${serverId}:${family}:address-list:${rosId}`
|
|
}
|
|
|
|
export function mapFirewallRule(
|
|
server: ServerRow,
|
|
family: FirewallFamily,
|
|
table: FirewallTable,
|
|
raw: RosFirewallFilter,
|
|
idx: number,
|
|
): FirewallRuleDto {
|
|
const rosId = raw[".id"] || `*${idx}`
|
|
const src = raw["src-address"] || raw["src-address-list"]
|
|
const dst = raw["dst-address"] || raw["dst-address-list"]
|
|
const port = raw["dst-port"] || raw["src-port"]
|
|
const iface = raw["in-interface"] || raw["out-interface"]
|
|
return {
|
|
id: ruleUiId(server.id, family, table, rosId),
|
|
rosId,
|
|
serverId: String(server.id),
|
|
serverName: server.name || server.host,
|
|
family,
|
|
table,
|
|
chain: raw.chain || "",
|
|
action: raw.action || "",
|
|
proto: raw.protocol || "all",
|
|
src: dash(src),
|
|
dst: dash(dst),
|
|
port: dash(port),
|
|
iface: dash(iface),
|
|
comment: raw.comment ?? "",
|
|
enabled: !rosDisabled(raw.disabled),
|
|
hits: parseHits(raw),
|
|
log: raw.log === "true" || raw.log === "yes",
|
|
logPrefix: raw["log-prefix"] ?? "",
|
|
tlsHost: raw["tls-host"],
|
|
layer7Proto: raw["layer7-protocol"],
|
|
}
|
|
}
|
|
|
|
export function mapAddressList(
|
|
server: ServerRow,
|
|
family: FirewallFamily,
|
|
raw: RosFirewallAddressList,
|
|
idx: number,
|
|
): FirewallAddressListDto {
|
|
const rosId = raw[".id"] || `*${idx}`
|
|
return {
|
|
id: addressUiId(server.id, family, rosId),
|
|
rosId,
|
|
serverId: String(server.id),
|
|
serverName: server.name || server.host,
|
|
family,
|
|
list: raw.list || "",
|
|
address: raw.address || "",
|
|
comment: raw.comment ?? "",
|
|
disabled: rosDisabled(raw.disabled),
|
|
timeout: raw.timeout,
|
|
}
|
|
}
|
|
|
|
async function safeGet<T>(fn: () => Promise<T[]>, fallback: T[] = []): Promise<T[]> {
|
|
try {
|
|
const rows = await fn()
|
|
return Array.isArray(rows) ? rows : fallback
|
|
} catch {
|
|
return fallback
|
|
}
|
|
}
|
|
|
|
export async function fetchServerFirewall(server: ServerRow): Promise<{
|
|
rules: FirewallRuleDto[]
|
|
addressLists: FirewallAddressListDto[]
|
|
}> {
|
|
const client = MikrotikClient.fromServer(server)
|
|
const ruleJobs = FAMILIES.flatMap((family) =>
|
|
TABLES.map(async (table) => {
|
|
const raw = await safeGet(() => client.getFirewallRules(family, table))
|
|
return raw.map((row, idx) => mapFirewallRule(server, family, table, row, idx))
|
|
}),
|
|
)
|
|
const listJobs = FAMILIES.map(async (family) => {
|
|
const raw = await safeGet(() => client.getFirewallAddressList(family))
|
|
return raw.map((row, idx) => mapAddressList(server, family, row, idx))
|
|
})
|
|
const [ruleChunks, listChunks] = await Promise.all([
|
|
Promise.all(ruleJobs),
|
|
Promise.all(listJobs),
|
|
])
|
|
return {
|
|
rules: ruleChunks.flat(),
|
|
addressLists: listChunks.flat(),
|
|
}
|
|
}
|
|
|
|
export async function listFirewallAll(): Promise<{
|
|
rules: FirewallRuleDto[]
|
|
addressLists: FirewallAddressListDto[]
|
|
}> {
|
|
const allServers = db.select().from(servers).where(eq(servers.enabled, true)).all()
|
|
const perServer = await Promise.all(
|
|
allServers.map(async (server) => {
|
|
try {
|
|
return await fetchServerFirewall(server)
|
|
} catch {
|
|
return { rules: [] as FirewallRuleDto[], addressLists: [] as FirewallAddressListDto[] }
|
|
}
|
|
}),
|
|
)
|
|
return {
|
|
rules: perServer.flatMap((r) => r.rules),
|
|
addressLists: perServer.flatMap((r) => r.addressLists),
|
|
}
|
|
}
|
|
|
|
export { firewallRestPath, FAMILIES, TABLES }
|