diff --git a/apps/api/src/routes/integrations-vps-tracker.ts b/apps/api/src/routes/integrations-vps-tracker.ts index 6bd8bc5..9b5c974 100644 --- a/apps/api/src/routes/integrations-vps-tracker.ts +++ b/apps/api/src/routes/integrations-vps-tracker.ts @@ -3,6 +3,7 @@ import type { FastifyInstance } from "fastify"; import { vpsTrackerEventSchema } from "@cfdm/shared"; import { getAppSettingsSecrets } from "@cfdm/db"; import * as vpsTrackerEvents from "../services/vps-tracker-events.js"; +import { syncAllToVpsTracker } from "../services/vps-tracker-sync.js"; function verifyBearer(authHeader: string | undefined, expected: string): boolean { if (!authHeader?.startsWith("Bearer ")) return false; @@ -41,4 +42,29 @@ export async function integrationsVpsTrackerRoutes(app: FastifyInstance) { ); return { ok: true, reconciled }; }); + + /** Запрос полной синхронизации bindings → VPS Tracker (вызывает VPS Tracker). */ + app.post("/integrations/vps-tracker/sync", async (req, reply) => { + const secrets = getAppSettingsSecrets(app.db); + const token = secrets.vpsTrackerIntegrationToken; + if (!token) { + return reply.status(503).send({ + ok: false, + error: "Integration not configured", + }); + } + if (!verifyBearer(req.headers.authorization, token)) { + return reply.status(401).send({ ok: false, error: "Unauthorized" }); + } + + const result = await syncAllToVpsTracker(app.db); + if (!result.ok) { + return reply.status(502).send({ + ok: false, + error: result.error ?? "Sync failed", + count: result.count, + }); + } + return { ok: true, count: result.count }; + }); } diff --git a/apps/api/src/services/vps-tracker-sync.ts b/apps/api/src/services/vps-tracker-sync.ts index e4ccc63..f4e867a 100644 --- a/apps/api/src/services/vps-tracker-sync.ts +++ b/apps/api/src/services/vps-tracker-sync.ts @@ -52,6 +52,29 @@ export function buildServiceSyncBindings( return items; } +export function buildAllSyncBindings(db: Db): CfdmBindingSyncItem[] { + const bindings = repos.listAllBindings(db); + return bindings.map((binding) => { + const serviceIps = repos.listServiceIps(db, binding.service_id); + const ips = + binding.target_ips.length > 0 + ? binding.target_ips + : serviceIps.length > 0 + ? serviceIps + : []; + return { + bindingId: binding.id, + serviceId: binding.service_id, + serviceName: binding.service_name, + serviceSlug: binding.service_slug, + fqdn: fqdnToDisplay(binding.hostname, binding.zone_name), + zoneName: binding.zone_name, + hostname: binding.hostname, + ips, + }; + }); +} + export async function syncServiceToVpsTracker( db: Db, serviceId: number, @@ -91,6 +114,54 @@ export async function syncServiceToVpsTracker( } } +export async function syncAllToVpsTracker(db: Db): Promise<{ + ok: boolean; + count: number; + error?: string; +}> { + const config = getAppSettingsSecrets(db); + if (!config.vpsTrackerSyncEnabled) { + return { ok: false, count: 0, error: "Синхронизация выключена в CFDM" }; + } + + const baseUrl = config.vpsTrackerUrl.replace(/\/$/, ""); + const token = config.vpsTrackerIntegrationToken; + if (!baseUrl) { + return { ok: false, count: 0, error: "Укажите URL VPS Tracker" }; + } + if (!token) { + return { ok: false, count: 0, error: "Укажите integration token" }; + } + + const bindings = buildAllSyncBindings(db); + + try { + const res = await fetch(`${baseUrl}/api/integrations/cfdm/sync-bindings`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ bindings, fullSync: true }), + }); + if (!res.ok) { + return { + ok: false, + count: 0, + error: `HTTP ${res.status}: ${await res.text()}`, + }; + } + touchVpsTrackerSync(db); + return { ok: true, count: bindings.length }; + } catch (err) { + return { + ok: false, + count: 0, + error: err instanceof Error ? err.message : "Ошибка сети", + }; + } +} + export async function pingVpsTracker(db: Db): Promise<{ ok: boolean; error?: string; diff --git a/packages/shared/dist/index.d.ts b/packages/shared/dist/index.d.ts index 50f6469..c885fe2 100644 --- a/packages/shared/dist/index.d.ts +++ b/packages/shared/dist/index.d.ts @@ -1480,6 +1480,7 @@ declare const cfdmSyncBindingsBodySchema: z.ZodObject<{ ips: z.ZodArray; deleted: z.ZodOptional; }, z.core.$strip>>; + fullSync: z.ZodOptional; }, z.core.$strip>; type CfdmBindingSyncItem = z.infer; declare const appSettingsPatchSchema: z.ZodObject<{ diff --git a/packages/shared/dist/index.js b/packages/shared/dist/index.js index eff9434..68f3816 100644 --- a/packages/shared/dist/index.js +++ b/packages/shared/dist/index.js @@ -582,7 +582,12 @@ var cfdmBindingSyncItemSchema = z3.object({ deleted: z3.boolean().optional() }); var cfdmSyncBindingsBodySchema = z3.object({ - bindings: z3.array(cfdmBindingSyncItemSchema).min(1) + bindings: z3.array(cfdmBindingSyncItemSchema), + /** Полная пересинхронизация: удалить CFDM-домены, которых нет в payload. */ + fullSync: z3.boolean().optional() +}).refine((data) => data.fullSync === true || data.bindings.length >= 1, { + message: "bindings \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D, \u0435\u0441\u043B\u0438 fullSync \u043D\u0435 \u0437\u0430\u0434\u0430\u043D", + path: ["bindings"] }); var appSettingsPatchSchema = z3.object({ vpsTrackerUrl: z3.string().url().or(z3.literal("")).optional(), diff --git a/packages/shared/src/integration-vps-tracker.ts b/packages/shared/src/integration-vps-tracker.ts index be1180b..ad661e0 100644 --- a/packages/shared/src/integration-vps-tracker.ts +++ b/packages/shared/src/integration-vps-tracker.ts @@ -13,7 +13,12 @@ export const cfdmBindingSyncItemSchema = z.object({ }); export const cfdmSyncBindingsBodySchema = z.object({ - bindings: z.array(cfdmBindingSyncItemSchema).min(1), + bindings: z.array(cfdmBindingSyncItemSchema), + /** Полная пересинхронизация: удалить CFDM-домены, которых нет в payload. */ + fullSync: z.boolean().optional(), +}).refine((data) => data.fullSync === true || data.bindings.length >= 1, { + message: "bindings обязателен, если fullSync не задан", + path: ["bindings"], }); export type CfdmBindingSyncItem = z.infer;