feat(integrations): добавить полный sync bindings в VPS Tracker
Build and Push CFDM Docker Image / build-and-push (push) Successful in 1m56s
Build and Push CFDM Docker Image / create-release (push) Skipped
Build and Push CFDM Docker Image / update-wiki (push) Successful in 6s

Endpoint для запроса полной синхронизации по integration token.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-31 23:24:04 +07:00
co-authored by Cursor
parent 3e4cacab42
commit 60da2b9032
5 changed files with 110 additions and 2 deletions
@@ -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 };
});
}
+71
View File
@@ -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;
+1
View File
@@ -1480,6 +1480,7 @@ declare const cfdmSyncBindingsBodySchema: z.ZodObject<{
ips: z.ZodArray<z.ZodString>;
deleted: z.ZodOptional<z.ZodBoolean>;
}, z.core.$strip>>;
fullSync: z.ZodOptional<z.ZodBoolean>;
}, z.core.$strip>;
type CfdmBindingSyncItem = z.infer<typeof cfdmBindingSyncItemSchema>;
declare const appSettingsPatchSchema: z.ZodObject<{
+6 -1
View File
@@ -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(),
@@ -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<typeof cfdmBindingSyncItemSchema>;