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 vxlanRoutes from "./routes/vxlan.js" import containersRoutes from "./routes/containers.js" import firewallRoutes from "./routes/firewall.js" import greRoutes from "./routes/gre.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 { 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(vxlanRoutes, { prefix: "/api" }) await app.register(containersRoutes, { prefix: "/api" }) await app.register(firewallRoutes, { prefix: "/api" }) await app.register(greRoutes, { 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) } }