From b9f430de169e5e6ccc3bfd5e47842a346247c9ef Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sat, 5 Sep 2026 01:29:58 +0700 Subject: [PATCH] refactor(acme-cloudflare): enhance upsertARecord and syncCertificateDomainRecords functions Updated the upsertARecord function to return status messages ("updated", "created", "skipped_cname") instead of void, improving clarity on record handling. Modified syncCertificateDomainRecords to collect and return skipped CNAME records, enhancing error handling and feedback during DNS operations. --- backend/src/services/acme-cloudflare.ts | 38 +++++++++++++++++++------ 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/backend/src/services/acme-cloudflare.ts b/backend/src/services/acme-cloudflare.ts index c0baafb..f79e764 100644 --- a/backend/src/services/acme-cloudflare.ts +++ b/backend/src/services/acme-cloudflare.ts @@ -161,11 +161,11 @@ async function listDnsRecordsByName(token: string, zoneId: string, fqdn: string) ) } -async function upsertARecord(token: string, zoneId: string, fqdn: string, ip: string): Promise { +async function upsertARecord(token: string, zoneId: string, fqdn: string, ip: string): Promise<"updated" | "created" | "skipped_cname"> { const records = await listDnsRecordsByName(token, zoneId, fqdn) const existingA = records.find((record) => record.type === "A") if (existingA) { - if (existingA.content === ip) return + if (existingA.content === ip) return "updated" await cloudflareRequest(token, `/zones/${zoneId}/dns_records/${existingA.id}`, { method: "PATCH", body: JSON.stringify({ @@ -176,11 +176,12 @@ async function upsertARecord(token: string, zoneId: string, fqdn: string, ip: st proxied: false, }), }) - return + return "updated" } + // CNAME на CN/SAN (алиас на канонический хост) — норма; A конфликтует с CNAME и для DNS-01 не нужен if (records.some((record) => record.type === "CNAME")) { - throw new Error(`Для ${fqdn} уже есть CNAME в Cloudflare — A-запись не создана`) + return "skipped_cname" } await cloudflareRequest<{ id: string }>(token, `/zones/${zoneId}/dns_records`, { @@ -193,6 +194,7 @@ async function upsertARecord(token: string, zoneId: string, fqdn: string, ip: st proxied: false, }), }) + return "created" } async function syncCertificateDomainRecords( @@ -200,11 +202,14 @@ async function syncCertificateDomainRecords( domains: string[], serverIp: string, defaultZoneId?: string, -): Promise { +): Promise<{ skippedCname: string[] }> { + const skippedCname: string[] = [] for (const domain of domains) { const zoneId = await resolveZoneId(token, domain, defaultZoneId) - await upsertARecord(token, zoneId, domain, serverIp) + const result = await upsertARecord(token, zoneId, domain, serverIp) + if (result === "skipped_cname") skippedCname.push(domain) } + return { skippedCname } } async function sleep(ms: number) { @@ -296,9 +301,26 @@ export async function issueCertificateWithCloudflareDns(params: { const finalized = await client.finalizeOrder(order, csr) const certPem = await client.getCertificate(finalized) + // A-sync опционален: DNS-01 уже завершён. CNAME на CN (msk2 → msk-gw02) не должен валить импорт. const clientRos = MikrotikClient.fromServer(params.server) - const serverIp = await resolveServerPublicIp(params.server, clientRos) - await syncCertificateDomainRecords(token, domains, serverIp, settings.defaultZoneId) + try { + params.onStep?.("dns_a_sync") + const serverIp = await resolveServerPublicIp(params.server, clientRos) + const { skippedCname } = await syncCertificateDomainRecords( + token, + domains, + serverIp, + settings.defaultZoneId, + ) + if (skippedCname.length > 0) { + params.onStep?.( + `dns_a_sync_skip_cname:${skippedCname.join(",")}`, + ) + } + } catch (e) { + const msg = e instanceof Error ? e.message : "ошибка DNS A-sync" + params.onStep?.(`dns_a_sync_warn:${msg}`) + } const trustStores = params.trustStore.filter(Boolean) const effectiveTrustStores = trustStores.length > 0 ? trustStores : ["www", "api"]