import type { FastifyInstance } from "fastify"; import { z } from "zod"; import { changeDomainSchema, createServiceNodeSchema, reorderServicesSchema, toggleServiceIpSchema, updateServiceConfigSchema, updateServiceNodeSchema, } from "@cfdm/shared"; import { repos } from "@cfdm/db"; import * as serviceConfig from "../services/service-config-service.js"; import * as nodeService from "../services/node-service.js"; import * as changeDomain from "../services/change-domain-service.js"; import * as certificateService from "../services/certificate-service.js"; import { recordAudit } from "../lib/audit.js"; export async function serviceRoutes(app: FastifyInstance) { const createSchema = z.object({ name: z.string(), slug: z.string(), service_group_id: z.number().nullable().optional(), }); app.get("/services", async (request) => { return serviceConfig.listViews(request.server.db); }); app.patch("/services/reorder", async (request) => { const body = reorderServicesSchema.parse(request.body); serviceConfig.reorderServices( request.server.db, body.group_id, body.service_ids, ); return { ok: true }; }); app.post("/services", async (request) => { const body = createSchema.parse(request.body); const service = repos.createService( request.server.db, body.name, body.slug, ); if (body.service_group_id != null) { repos.setServiceGroup( request.server.db, service.id, body.service_group_id, ); } const view = await serviceConfig.getView(request.server.db, service.id); recordAudit(request.server, request, { action: "service.create", targetType: "app_resource", targetId: String(service.id), summary: `Создан сервис «${service.name}»`, details: { name: service.name, slug: service.slug }, }); return view; }); app.get("/services/:id", async (request) => { const { id } = request.params as { id: string }; return serviceConfig.getView(request.server.db, Number(id)); }); app.get("/services/:id/health-log", async (request) => { const { id } = request.params as { id: string }; repos.getService(request.server.db, Number(id)); return { items: repos.listHealthProbeLogForService( request.server.db, Number(id), 200, ), }; }); app.get("/services/:id/failover-log", async (request) => { const { id } = request.params as { id: string }; repos.getService(request.server.db, Number(id)); return { items: repos.listFailoverLogForService( request.server.db, Number(id), 200, ), }; }); app.get("/services/:id/certificates", async (request) => { const { id } = request.params as { id: string }; return certificateService.listServiceCertificates( request.server.db, Number(id), ); }); app.post("/services/:id/certificates/check", async (request) => { const { id } = request.params as { id: string }; const checked = await certificateService.runServiceChecks( request.server.db, Number(id), ); return { checked }; }); app.get("/services/:id/overview", async (request) => { const { id } = request.params as { id: string }; return nodeService.getOverview(request.server.db, Number(id)); }); app.get("/services/:id/nodes", async (request) => { const { id } = request.params as { id: string }; return nodeService.listNodes(request.server.db, Number(id)); }); app.post("/services/:id/nodes", async (request) => { const { id } = request.params as { id: string }; const body = createServiceNodeSchema.parse(request.body); const node = nodeService.createNode(request.server.db, Number(id), body); recordAudit(request.server, request, { action: "node.create", targetType: "app_resource", targetId: String(node.id), summary: `Добавлена нода ${node.address}`, details: { service_id: Number(id), address: node.address }, }); return node; }); app.patch("/services/:id/nodes/:nodeId", async (request) => { const { id, nodeId } = request.params as { id: string; nodeId: string }; const body = updateServiceNodeSchema.parse(request.body); const node = nodeService.updateNode( request.server.db, Number(id), Number(nodeId), body, ); recordAudit(request.server, request, { action: "node.update", targetType: "app_resource", targetId: String(node.id), summary: `Обновлена нода ${node.address}`, details: body, }); return node; }); app.delete("/services/:id/nodes/:nodeId", async (request) => { const { id, nodeId } = request.params as { id: string; nodeId: string }; const node = repos.getNode(request.server.db, Number(nodeId)); nodeService.deleteNode(request.server.db, Number(id), Number(nodeId)); recordAudit(request.server, request, { action: "node.delete", severity: "warning", targetType: "app_resource", targetId: nodeId, summary: `Удалена нода ${node.address}`, }); return { deleted: true }; }); app.post("/services/:id/change-domain", async (request) => { const { id } = request.params as { id: string }; const body = changeDomainSchema.parse(request.body); const result = await changeDomain.changeServiceDomain( request.server.db, request.server.cf, Number(id), body, ); if (result.applied) { recordAudit(request.server, request, { action: "service.change_domain", targetType: "app_resource", targetId: id, summary: result.message, details: result, }); } return result; }); app.get("/ops-summary", async (request) => { return nodeService.opsSummary(request.server.db); }); app.patch("/services/:id", async (request) => { const { id } = request.params as { id: string }; const body = updateServiceConfigSchema.parse(request.body); const view = await serviceConfig.updateConfig( request.server.db, request.server.cf, Number(id), body, ); recordAudit(request.server, request, { action: "service.update", targetType: "app_resource", targetId: String(id), summary: `Обновлён сервис «${view.name}»`, details: body, }); return view; }); app.delete("/services/:id", async (request) => { const { id } = request.params as { id: string }; const view = await serviceConfig.getView(request.server.db, Number(id)); repos.deleteService(request.server.db, Number(id)); recordAudit(request.server, request, { action: "service.delete", severity: "warning", targetType: "app_resource", targetId: String(id), summary: `Удалён сервис «${view.name}»`, details: { name: view.name, slug: view.slug }, }); return { deleted: true }; }); app.patch("/services/:id/toggle", async (request) => { const { id } = request.params as { id: string }; const body = z.object({ enabled: z.boolean() }).parse(request.body); return serviceConfig.toggleService( request.server.db, request.server.cf, Number(id), body.enabled, ); }); app.patch("/services/:id/ips/toggle", async (request) => { const { id } = request.params as { id: string }; const body = toggleServiceIpSchema.parse(request.body); const view = await serviceConfig.toggleServiceIp( request.server.db, request.server.cf, Number(id), body.ip, body.enabled, ); recordAudit(request.server, request, { action: "service.ip.toggle", targetType: "app_resource", targetId: String(id), summary: body.enabled ? `Включён IP ${body.ip} сервиса «${view.name}»` : `Выключен IP ${body.ip} сервиса «${view.name}»`, details: { ip: body.ip, enabled: body.enabled }, }); return view; }); }