Docker images / prepare-release (push) Successful in 10s
Docker images / backend-test (push) Successful in 2m19s
Docker images / frontend-image (push) Successful in 4m47s
Docker images / updater-image (push) Successful in 48s
Docker images / backend-image (push) Successful in 2m53s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 13s
Куб IPFIX час+день с AND-слайсами и экраном отчётности /statistics, живой /traffic не меняем. Co-authored-by: Cursor <[email protected]>
196 lines
7.0 KiB
TypeScript
196 lines
7.0 KiB
TypeScript
import Fastify, { type FastifyError, type FastifyInstance } from "fastify"
|
|
import cors from "@fastify/cors"
|
|
import { serializerCompiler, validatorCompiler } from "@fastify/type-provider-zod"
|
|
import { monitorEventLoopDelay } from "node:perf_hooks"
|
|
import { env } from "./config.js"
|
|
import { initDatabase } from "./db/bootstrap.js"
|
|
import { closePool } from "./db/index.js"
|
|
import authPlugin, { requireAuth } from "./plugins/auth.js"
|
|
import serversRoutes from "./routes/servers.js"
|
|
import bgpRoutes from "./routes/bgp.js"
|
|
import ospfRoutes from "./routes/ospf.js"
|
|
import execRoutes from "./routes/exec.js"
|
|
import filtersRoutes from "./routes/filters.js"
|
|
import recursiveRoutes from "./routes/recursive-routes.js"
|
|
import trafficRoutes from "./routes/traffic.js"
|
|
import trafficFlowRoutes from "./routes/traffic-flow.js"
|
|
import geoipRoutes from "./routes/geoip.js"
|
|
import serversApiPingRoutes from "./routes/servers-api-ping.js"
|
|
import uptimeRoutes from "./routes/uptime.js"
|
|
import networkRoutes from "./routes/network.js"
|
|
import internetPathRoutes from "./routes/internet-path.js"
|
|
import evobgpRoutes from "./routes/evobgp.js"
|
|
import probesRoutes from "./routes/probes.js"
|
|
import schedulerRoutes from "./routes/scheduler.js"
|
|
import sidebarCountsRoutes from "./routes/sidebar-counts.js"
|
|
import alertsRoutes from "./routes/alerts.js"
|
|
import backupsRoutes from "./routes/backups.js"
|
|
import certificatesRoutes from "./routes/certificates.js"
|
|
import systemDatabaseRoutes from "./routes/system-database.js"
|
|
import eventsRoutes from "./routes/events.js"
|
|
import wireguardRoutes from "./routes/wireguard.js"
|
|
import firewallRoutes from "./routes/firewall.js"
|
|
import usersRoutes from "./routes/users.js"
|
|
import statisticsRoutes from "./routes/statistics.js"
|
|
import { refreshScheduler, stopScheduler } from "./services/scheduler.js"
|
|
import { getFlowWorkerHealth, startTrafficFlowListener, stopTrafficFlowListener } from "./services/traffic-flow-ingest.js"
|
|
import { initGeoip } from "./services/traffic-flow-geoip.js"
|
|
|
|
const eventLoopDelay = monitorEventLoopDelay({ resolution: 20 })
|
|
eventLoopDelay.enable()
|
|
|
|
export async function buildApp(opts?: {
|
|
logger?: boolean
|
|
startScheduler?: boolean
|
|
}): Promise<FastifyInstance> {
|
|
const usePrettyLogger =
|
|
opts?.logger !== false && process.env.NODE_ENV !== "production"
|
|
const app = Fastify({
|
|
bodyLimit: 2 * 1024 * 1024,
|
|
requestTimeout: 10 * 60 * 1000,
|
|
logger:
|
|
opts?.logger === false
|
|
? false
|
|
: usePrettyLogger
|
|
? {
|
|
transport: {
|
|
target: "pino-pretty",
|
|
options: {
|
|
colorize: true,
|
|
translateTime: "HH:MM:ss",
|
|
ignore: "pid,hostname",
|
|
},
|
|
},
|
|
}
|
|
: true,
|
|
})
|
|
|
|
app.setValidatorCompiler(validatorCompiler)
|
|
app.setSerializerCompiler(serializerCompiler)
|
|
|
|
app.setErrorHandler((error: FastifyError, request, reply) => {
|
|
const status = typeof error.statusCode === "number" && error.statusCode >= 400
|
|
? error.statusCode
|
|
: 500
|
|
if (status >= 500) {
|
|
request.log.error(error)
|
|
return reply.status(status).send({ error: "Внутренняя ошибка сервера" })
|
|
}
|
|
const message = error instanceof Error ? error.message : "Ошибка запроса"
|
|
return reply.status(status).send({ error: message })
|
|
})
|
|
|
|
await app.register(cors, {
|
|
origin: env.CORS_ORIGIN,
|
|
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
})
|
|
|
|
await app.register(authPlugin)
|
|
|
|
app.get("/health", async () => ({
|
|
status: "ok",
|
|
timestamp: new Date().toISOString(),
|
|
version: process.env.APP_VERSION ?? "dev",
|
|
eventLoopDelayMs: Math.round(eventLoopDelay.mean / 1e6),
|
|
flowWorker: getFlowWorkerHealth(),
|
|
}))
|
|
|
|
app.get("/api/auth/config", async () => ({
|
|
required: env.authRequired,
|
|
portal_url: env.authPortalUrl,
|
|
issuer: env.authIssuer,
|
|
}))
|
|
|
|
if (env.authRequired) {
|
|
app.addHook("preHandler", async (request, reply) => {
|
|
const pathname = request.url.split("?")[0] ?? request.url
|
|
if (!pathname.startsWith("/api/")) return
|
|
if (pathname === "/api/auth/config") return
|
|
await requireAuth(request, reply)
|
|
if (reply.sent) return
|
|
})
|
|
}
|
|
|
|
await app.register(serversRoutes, { prefix: "/api/servers" })
|
|
await app.register(bgpRoutes, { prefix: "/api" })
|
|
await app.register(ospfRoutes, { prefix: "/api" })
|
|
await app.register(execRoutes, { prefix: "/api" })
|
|
await app.register(filtersRoutes, { prefix: "/api" })
|
|
await app.register(recursiveRoutes, { prefix: "/api" })
|
|
await app.register(trafficRoutes, { prefix: "/api" })
|
|
await app.register(trafficFlowRoutes, { prefix: "/api" })
|
|
await app.register(geoipRoutes, { prefix: "/api" })
|
|
await app.register(serversApiPingRoutes, { prefix: "/api" })
|
|
await app.register(uptimeRoutes, { prefix: "/api" })
|
|
await app.register(networkRoutes, { prefix: "/api" })
|
|
await app.register(internetPathRoutes, { prefix: "/api" })
|
|
await app.register(evobgpRoutes, { prefix: "/api" })
|
|
await app.register(probesRoutes, { prefix: "/api" })
|
|
await app.register(schedulerRoutes, { prefix: "/api" })
|
|
await app.register(sidebarCountsRoutes, { prefix: "/api" })
|
|
await app.register(alertsRoutes, { prefix: "/api" })
|
|
await app.register(backupsRoutes, { prefix: "/api" })
|
|
await app.register(certificatesRoutes, { prefix: "/api" })
|
|
await app.register(systemDatabaseRoutes, { prefix: "/api" })
|
|
await app.register(eventsRoutes, { prefix: "/api" })
|
|
await app.register(wireguardRoutes, { prefix: "/api" })
|
|
await app.register(firewallRoutes, { prefix: "/api" })
|
|
await app.register(usersRoutes, { prefix: "/api" })
|
|
await app.register(statisticsRoutes, { prefix: "/api" })
|
|
|
|
if (opts?.startScheduler !== false) {
|
|
await refreshScheduler()
|
|
await initGeoip()
|
|
await startTrafficFlowListener()
|
|
app.addHook("onClose", async () => {
|
|
stopScheduler()
|
|
stopTrafficFlowListener()
|
|
})
|
|
}
|
|
|
|
return app
|
|
}
|
|
|
|
const isMain =
|
|
process.argv[1] &&
|
|
(process.argv[1].endsWith("index.ts") || process.argv[1].endsWith("index.js"))
|
|
|
|
if (isMain) {
|
|
try {
|
|
await initDatabase()
|
|
const app = await buildApp()
|
|
let shuttingDown = false
|
|
const shutdown = async (code: number) => {
|
|
if (shuttingDown) return
|
|
shuttingDown = true
|
|
try {
|
|
stopTrafficFlowListener()
|
|
await app.close()
|
|
await closePool()
|
|
} catch (err) {
|
|
console.error(err)
|
|
} finally {
|
|
process.exit(code)
|
|
}
|
|
}
|
|
process.on("SIGTERM", () => { void shutdown(0) })
|
|
process.on("SIGINT", () => { void shutdown(0) })
|
|
process.on("uncaughtException", (err) => {
|
|
console.error(err)
|
|
void shutdown(1)
|
|
})
|
|
process.on("unhandledRejection", (reason) => {
|
|
console.error(reason)
|
|
void shutdown(1)
|
|
})
|
|
await app.listen({ port: env.PORT, host: "0.0.0.0" })
|
|
console.log(
|
|
`\n🚀 MikroTik Manager Backend running at http://localhost:${env.PORT}`,
|
|
)
|
|
console.log(` Docs / test: http://localhost:${env.PORT}/health`)
|
|
} catch (err) {
|
|
console.error(err)
|
|
process.exit(1)
|
|
}
|
|
}
|