quality / commitlint (push) Skipped
quality / changes (push) Successful in 11s
quality / docker-check (push) Skipped
CD / update-wiki (push) Successful in 5s
quality / web (push) Successful in 1m11s
quality / api (push) Successful in 1m1s
CD / quality (push) Successful in 2m30s
CD / publish (push) Successful in 2m4s
Сервис up, если жив хотя бы один IP. Failover показывает Down по FQDN и журнал add/remove A-записей. Co-authored-by: Cursor <cursoragent@cursor.com>
260 lines
7.9 KiB
TypeScript
260 lines
7.9 KiB
TypeScript
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;
|
|
});
|
|
}
|