feat:Update pnpm-lock.yaml to link shared package; modify API routes to utilize new schemas for domain and service management; enhance DNS record handling with CNAME support; refactor service and subdomain routes for improved functionality; implement confirm dialog for domain deletion in the frontend; clean up unused components and improve UI consistency.
Build, Test, and Push CFDM Docker Image / test (push) Failing after 51s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Has been skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Has been skipped
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped
Build, Test, and Push CFDM Docker Image / test (push) Failing after 51s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Has been skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Has been skipped
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { updateDomainSchema } from "@cfdm/shared";
|
||||
import { z } from "zod";
|
||||
import * as domainService from "../services/domain-service.js";
|
||||
|
||||
@@ -8,11 +9,6 @@ export async function domainRoutes(app: FastifyInstance) {
|
||||
group_id: z.number().nullable().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = z.object({
|
||||
group_id: z.number().nullable().optional(),
|
||||
status: z.string().optional(),
|
||||
});
|
||||
|
||||
app.get("/domains", async (request) => {
|
||||
const query = request.query as { group_id?: string };
|
||||
const groupId = query.group_id ? Number(query.group_id) : undefined;
|
||||
@@ -36,13 +32,14 @@ export async function domainRoutes(app: FastifyInstance) {
|
||||
|
||||
app.patch("/domains/:id", async (request) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const body = updateSchema.parse(request.body);
|
||||
const body = updateDomainSchema.parse(request.body);
|
||||
const existing = domainService.getDomain(request.server.db, Number(id));
|
||||
return domainService.updateDomain(
|
||||
request.server.db,
|
||||
Number(id),
|
||||
body.group_id !== undefined ? body.group_id : existing.group_id,
|
||||
body.status ?? existing.status,
|
||||
body.cert_monitoring,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { createServiceGroupSchema, toggleEnabledSchema } from "@cfdm/shared";
|
||||
import * as serviceConfig from "../services/service-config-service.js";
|
||||
|
||||
export async function serviceGroupRoutes(app: FastifyInstance) {
|
||||
const bodySchema = z.object({
|
||||
name: z.string(),
|
||||
type: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
domain: z.string().optional(),
|
||||
});
|
||||
const bodySchema = createServiceGroupSchema;
|
||||
|
||||
app.get("/service-groups", async (request) => {
|
||||
return serviceConfig.listGroupViews(request.server.db);
|
||||
@@ -42,7 +37,7 @@ export async function serviceGroupRoutes(app: FastifyInstance) {
|
||||
|
||||
app.patch("/service-groups/:id/toggle", async (request) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const body = z.object({ enabled: z.boolean() }).parse(request.body);
|
||||
const body = toggleEnabledSchema.parse(request.body);
|
||||
return serviceConfig.toggleGroup(
|
||||
request.server.db,
|
||||
request.server.cf,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { reorderServicesSchema } from "@cfdm/shared";
|
||||
import { repos } from "@cfdm/db";
|
||||
import * as serviceConfig from "../services/service-config-service.js";
|
||||
|
||||
@@ -14,6 +15,16 @@ export async function serviceRoutes(app: FastifyInstance) {
|
||||
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(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { updateSubdomainSchema } from "@cfdm/shared";
|
||||
import { repos } from "@cfdm/db";
|
||||
import { z } from "zod";
|
||||
import type { UpdateSubdomainPatch } from "@cfdm/db";
|
||||
|
||||
export async function subdomainRoutes(app: FastifyInstance) {
|
||||
app.get("/domains/:id/subdomains", async (request) => {
|
||||
@@ -32,16 +34,26 @@ export async function subdomainRoutes(app: FastifyInstance) {
|
||||
|
||||
app.patch("/subdomains/:id", async (request) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const body = z.object({ name: z.string() }).parse(request.body);
|
||||
const body = updateSubdomainSchema.parse(request.body);
|
||||
const sub = repos.getSubdomain(request.server.db, Number(id));
|
||||
const domain = repos.getDomain(request.server.db, sub.domain_id);
|
||||
const fqdn = `${body.name}.${domain.zone_name}`;
|
||||
return repos.updateSubdomain(
|
||||
request.server.db,
|
||||
Number(id),
|
||||
body.name,
|
||||
fqdn,
|
||||
);
|
||||
|
||||
const patch: UpdateSubdomainPatch = {};
|
||||
if (body.name !== undefined) {
|
||||
patch.name = body.name;
|
||||
patch.fqdn =
|
||||
body.name === "@"
|
||||
? domain.zone_name
|
||||
: `${body.name}.${domain.zone_name}`;
|
||||
}
|
||||
if (body.enabled !== undefined) {
|
||||
patch.enabled = body.enabled;
|
||||
}
|
||||
if (body.cert_monitoring !== undefined) {
|
||||
patch.cert_monitoring = body.cert_monitoring;
|
||||
}
|
||||
|
||||
return repos.updateSubdomain(request.server.db, Number(id), patch);
|
||||
});
|
||||
|
||||
app.delete("/subdomains/:id", async (request) => {
|
||||
|
||||
@@ -2,13 +2,25 @@ import { connect } from "node:net";
|
||||
import { connect as tlsConnect } from "node:tls";
|
||||
import type { Db } from "@cfdm/db";
|
||||
import { repos } from "@cfdm/db";
|
||||
import type { Certificate } from "@cfdm/shared";
|
||||
import type { Certificate, Domain, Subdomain } from "@cfdm/shared";
|
||||
import {
|
||||
CERT_ERROR,
|
||||
CERT_MONITOR_AUTO,
|
||||
CERT_MONITOR_REQUIRED,
|
||||
CERT_MONITOR_SKIPPED,
|
||||
CERT_UNKNOWN,
|
||||
certStatusFromExpiry,
|
||||
fqdnToDisplay,
|
||||
parseFqdn,
|
||||
shouldMonitorService,
|
||||
} from "@cfdm/shared";
|
||||
|
||||
export interface CertificateTarget {
|
||||
domainId: number;
|
||||
subdomainId: number | null;
|
||||
hostname: string;
|
||||
}
|
||||
|
||||
export function listCertificates(
|
||||
db: Db,
|
||||
status?: string,
|
||||
@@ -98,17 +110,137 @@ export async function checkAndStore(
|
||||
);
|
||||
}
|
||||
|
||||
export async function runAllChecks(db: Db): Promise<number> {
|
||||
let count = 0;
|
||||
function resolveMonitoringMode(
|
||||
domain: Domain,
|
||||
subdomain: Subdomain | null,
|
||||
fqdn: string,
|
||||
): string {
|
||||
if (subdomain) {
|
||||
return subdomain.cert_monitoring;
|
||||
}
|
||||
if (fqdn === domain.zone_name) {
|
||||
return domain.cert_monitoring;
|
||||
}
|
||||
return CERT_MONITOR_AUTO;
|
||||
}
|
||||
|
||||
function bindingSubdomain(
|
||||
db: Db,
|
||||
domainId: number,
|
||||
hostname: string,
|
||||
): Subdomain | null {
|
||||
if (hostname === "@") return null;
|
||||
return repos.findSubdomainByDomainAndName(db, domainId, hostname);
|
||||
}
|
||||
|
||||
export function buildServiceCertificateFqdns(
|
||||
db: Db,
|
||||
): Map<string, CertificateTarget> {
|
||||
const result = new Map<string, CertificateTarget>();
|
||||
|
||||
for (const binding of repos.listAllBindings(db)) {
|
||||
const service = repos.getService(db, binding.service_id);
|
||||
const group = service.service_group_id
|
||||
? repos.getServiceGroup(db, service.service_group_id)
|
||||
: null;
|
||||
if (!shouldMonitorService(service, group)) continue;
|
||||
|
||||
const subdomain = bindingSubdomain(db, binding.domain_id, binding.hostname);
|
||||
if (subdomain && !subdomain.enabled) continue;
|
||||
|
||||
const fqdn = fqdnToDisplay(binding.hostname, binding.zone_name);
|
||||
result.set(fqdn, {
|
||||
domainId: binding.domain_id,
|
||||
subdomainId: subdomain?.id ?? null,
|
||||
hostname: fqdn,
|
||||
});
|
||||
}
|
||||
|
||||
const knownZones = repos.listAllDomains(db).map((d) => d.zone_name);
|
||||
for (const group of repos.listServiceGroups(db)) {
|
||||
if (!group.enabled || !group.domain?.trim()) continue;
|
||||
|
||||
const parsed = parseFqdn(group.domain, knownZones);
|
||||
if (!parsed) continue;
|
||||
|
||||
const domain = repos.findDomainByZoneName(db, parsed.zoneName);
|
||||
if (!domain) continue;
|
||||
|
||||
const subdomain =
|
||||
parsed.hostname === "@"
|
||||
? null
|
||||
: bindingSubdomain(db, domain.id, parsed.hostname);
|
||||
if (subdomain && !subdomain.enabled) continue;
|
||||
|
||||
result.set(parsed.fqdn, {
|
||||
domainId: domain.id,
|
||||
subdomainId: subdomain?.id ?? null,
|
||||
hostname: parsed.fqdn,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function resolveCertificateTargets(db: Db): CertificateTarget[] {
|
||||
const serviceFqdns = buildServiceCertificateFqdns(db);
|
||||
const targets = new Map<string, CertificateTarget>();
|
||||
|
||||
for (const domain of repos.listAllDomains(db)) {
|
||||
await checkAndStore(db, domain.id, null, domain.zone_name);
|
||||
count += 1;
|
||||
if (domain.cert_monitoring === CERT_MONITOR_SKIPPED) continue;
|
||||
if (domain.cert_monitoring === CERT_MONITOR_REQUIRED) {
|
||||
targets.set(domain.zone_name, {
|
||||
domainId: domain.id,
|
||||
subdomainId: null,
|
||||
hostname: domain.zone_name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const sub of repos.listAllSubdomains(db)) {
|
||||
await checkAndStore(db, sub.domain_id, sub.id, sub.fqdn);
|
||||
count += 1;
|
||||
if (sub.cert_monitoring === CERT_MONITOR_SKIPPED) continue;
|
||||
if (sub.cert_monitoring === CERT_MONITOR_REQUIRED) {
|
||||
targets.set(sub.fqdn, {
|
||||
domainId: sub.domain_id,
|
||||
subdomainId: sub.id,
|
||||
hostname: sub.fqdn,
|
||||
});
|
||||
}
|
||||
}
|
||||
return count;
|
||||
|
||||
for (const [fqdn, meta] of serviceFqdns) {
|
||||
const domain = repos.getDomain(db, meta.domainId);
|
||||
const subdomain = meta.subdomainId
|
||||
? repos.getSubdomain(db, meta.subdomainId)
|
||||
: null;
|
||||
const monitoring = resolveMonitoringMode(domain, subdomain, fqdn);
|
||||
if (monitoring === CERT_MONITOR_SKIPPED) continue;
|
||||
if (
|
||||
monitoring === CERT_MONITOR_AUTO ||
|
||||
monitoring === CERT_MONITOR_REQUIRED
|
||||
) {
|
||||
targets.set(fqdn, meta);
|
||||
}
|
||||
}
|
||||
|
||||
return [...targets.values()];
|
||||
}
|
||||
|
||||
export async function runAllChecks(db: Db): Promise<number> {
|
||||
const targets = resolveCertificateTargets(db);
|
||||
for (const target of targets) {
|
||||
await checkAndStore(
|
||||
db,
|
||||
target.domainId,
|
||||
target.subdomainId,
|
||||
target.hostname,
|
||||
);
|
||||
}
|
||||
repos.deleteCertificatesNotIn(
|
||||
db,
|
||||
targets.map((t) => t.hostname),
|
||||
);
|
||||
return targets.length;
|
||||
}
|
||||
|
||||
export function statusSummary(db: Db): Array<[string, number]> {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
SYNC_ERROR,
|
||||
SYNC_PENDING_PUSH,
|
||||
SYNC_SYNCED,
|
||||
normalizeDnsRecordName,
|
||||
} from "@cfdm/shared";
|
||||
import type { CloudflareClient } from "../lib/cf-client.js";
|
||||
import { AppError } from "../errors.js";
|
||||
@@ -87,12 +88,12 @@ async function pushRecord(
|
||||
repos.updateDnsFields(
|
||||
db,
|
||||
record.id,
|
||||
record.record_type,
|
||||
record.name,
|
||||
record.content,
|
||||
record.ttl,
|
||||
record.proxied,
|
||||
record.priority,
|
||||
cfRec.type ?? record.record_type,
|
||||
cfRec.name,
|
||||
cfRec.content,
|
||||
cfRec.ttl,
|
||||
cfRec.proxied ?? false,
|
||||
cfRec.priority ?? null,
|
||||
SYNC_SYNCED,
|
||||
cfRec.id ?? null,
|
||||
null,
|
||||
@@ -119,13 +120,14 @@ export async function create(
|
||||
const domain = repos.getDomain(db, domainId);
|
||||
const ttl = req.ttl ?? 1;
|
||||
const proxied = req.proxied ?? false;
|
||||
validateDnsRecord(req.record_type, req.name, req.content, ttl, proxied);
|
||||
const name = normalizeDnsRecordName(req.name, domain.zone_name);
|
||||
validateDnsRecord(req.record_type, name, req.content, ttl, proxied);
|
||||
|
||||
const record = repos.insertDnsRecord(
|
||||
db,
|
||||
domainId,
|
||||
req.record_type,
|
||||
req.name,
|
||||
name,
|
||||
req.content,
|
||||
ttl,
|
||||
proxied,
|
||||
@@ -149,7 +151,10 @@ export async function update(
|
||||
const existing = repos.getDnsRecord(db, domainId, recordId);
|
||||
|
||||
const recordType = req.record_type ?? existing.record_type;
|
||||
const name = req.name ?? existing.name;
|
||||
const name = normalizeDnsRecordName(
|
||||
req.name ?? existing.name,
|
||||
domain.zone_name,
|
||||
);
|
||||
const content = req.content ?? existing.content;
|
||||
const ttl = req.ttl ?? existing.ttl;
|
||||
const proxied = req.proxied ?? existing.proxied;
|
||||
|
||||
@@ -45,8 +45,9 @@ export function updateDomain(
|
||||
id: number,
|
||||
groupId: number | null,
|
||||
status: string,
|
||||
certMonitoring?: string,
|
||||
): Domain {
|
||||
return repos.updateDomain(db, id, groupId, status);
|
||||
return repos.updateDomain(db, id, groupId, status, certMonitoring);
|
||||
}
|
||||
|
||||
export function deleteDomain(db: Db, id: number): void {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Db } from "@cfdm/db";
|
||||
import { repos } from "@cfdm/db";
|
||||
import type {
|
||||
DnsRecord,
|
||||
Service,
|
||||
ServiceGroup,
|
||||
ServiceGroupsResponse,
|
||||
@@ -10,6 +11,8 @@ import {
|
||||
SYNC_ERROR,
|
||||
SYNC_PENDING_PUSH,
|
||||
SYNC_SYNCED,
|
||||
dnsRecordNamesMatch,
|
||||
normalizeDnsRecordName,
|
||||
} from "@cfdm/shared";
|
||||
import type { CloudflareClient } from "../lib/cf-client.js";
|
||||
import { AppError } from "../errors.js";
|
||||
@@ -21,6 +24,7 @@ export interface ServiceDomainInput {
|
||||
fqdn: string;
|
||||
target_ips?: string[];
|
||||
target_ip?: string;
|
||||
target_cname?: string;
|
||||
}
|
||||
|
||||
export interface ToggleRequest {
|
||||
@@ -30,8 +34,8 @@ export interface ToggleRequest {
|
||||
export interface ServiceGroupBody {
|
||||
name: string;
|
||||
type?: string;
|
||||
icon?: string;
|
||||
domain?: string;
|
||||
icon?: string | null;
|
||||
domain?: string | null;
|
||||
}
|
||||
|
||||
export interface UpdateServiceConfigRequest {
|
||||
@@ -114,13 +118,20 @@ async function buildView(db: Db, serviceId: number): Promise<ServiceView> {
|
||||
const records = repos.listRecordsForBinding(db, binding.id);
|
||||
const statuses = records.map((r) => r.sync_status);
|
||||
const targetIps = repos.listBindingIps(db, binding.id);
|
||||
const linkedCname = records.find(
|
||||
(record) => record.record_type.toUpperCase() === "CNAME",
|
||||
);
|
||||
const targetCname =
|
||||
binding.cname_target?.trim() || linkedCname?.content?.trim() || null;
|
||||
return {
|
||||
binding_id: binding.id,
|
||||
domain_id: binding.domain_id,
|
||||
zone_name: binding.zone_name,
|
||||
hostname: binding.hostname,
|
||||
fqdn: fqdnToDisplay(binding.hostname, binding.zone_name),
|
||||
target_ips: targetIps,
|
||||
record_type: targetCname ? ("CNAME" as const) : ("A" as const),
|
||||
target_ips: targetCname ? [] : targetIps,
|
||||
target_cname: targetCname,
|
||||
sync_status: aggregateSyncStatus(statuses),
|
||||
};
|
||||
});
|
||||
@@ -185,16 +196,144 @@ async function syncBindingDns(
|
||||
domainId: number,
|
||||
hostname: string,
|
||||
desiredIps: string[],
|
||||
cnameTarget: string | null,
|
||||
): Promise<void> {
|
||||
const domain = repos.getDomain(db, domainId);
|
||||
const zoneName = domain.zone_name;
|
||||
let effectiveCname = cnameTarget?.trim() || null;
|
||||
|
||||
if (!effectiveCname) {
|
||||
const existingCname = await findOrImportDnsRecord(
|
||||
db,
|
||||
cf,
|
||||
domainId,
|
||||
zoneName,
|
||||
hostname,
|
||||
"CNAME",
|
||||
);
|
||||
if (existingCname) {
|
||||
effectiveCname = existingCname.content;
|
||||
repos.setBindingCnameTarget(db, bindingId, effectiveCname);
|
||||
repos.replaceBindingIps(db, bindingId, []);
|
||||
}
|
||||
}
|
||||
|
||||
if (effectiveCname) {
|
||||
await syncBindingCnameDns(
|
||||
db,
|
||||
cf,
|
||||
bindingId,
|
||||
domainId,
|
||||
hostname,
|
||||
effectiveCname,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await syncBindingADns(db, cf, bindingId, domainId, hostname, desiredIps);
|
||||
}
|
||||
|
||||
async function syncBindingCnameDns(
|
||||
db: Db,
|
||||
cf: CloudflareClient,
|
||||
bindingId: number,
|
||||
domainId: number,
|
||||
hostname: string,
|
||||
cnameTarget: string,
|
||||
): Promise<void> {
|
||||
const domain = repos.getDomain(db, domainId);
|
||||
const zoneName = domain.zone_name;
|
||||
const normalized = normalizeCnameTarget(cnameTarget, zoneName);
|
||||
const existingRecords = repos.listRecordsForBinding(db, bindingId);
|
||||
|
||||
for (const record of existingRecords) {
|
||||
if (!desiredIps.includes(record.content)) {
|
||||
if (record.record_type.toUpperCase() === "A") {
|
||||
repos.unlinkBindingRecord(db, bindingId, record.id);
|
||||
await dnsService.deleteRecord(db, cf, domainId, record.id);
|
||||
}
|
||||
}
|
||||
|
||||
const refreshed = repos.listRecordsForBinding(db, bindingId);
|
||||
const existingCname = refreshed.find(
|
||||
(record) => record.record_type.toUpperCase() === "CNAME",
|
||||
);
|
||||
|
||||
let recordId: number;
|
||||
if (existingCname) {
|
||||
if (
|
||||
!cnameContentMatches(existingCname.content, normalized, zoneName) ||
|
||||
!dnsRecordNamesMatch(existingCname.name, hostname, zoneName)
|
||||
) {
|
||||
await dnsService.update(db, cf, domainId, existingCname.id, {
|
||||
record_type: "CNAME",
|
||||
name: dnsNameForBinding(hostname, zoneName),
|
||||
content: normalized,
|
||||
proxied: false,
|
||||
});
|
||||
}
|
||||
recordId = existingCname.id;
|
||||
} else {
|
||||
const adopted = await findOrImportDnsRecord(
|
||||
db,
|
||||
cf,
|
||||
domainId,
|
||||
zoneName,
|
||||
hostname,
|
||||
"CNAME",
|
||||
normalized,
|
||||
);
|
||||
if (adopted) {
|
||||
repos.linkBindingRecord(db, bindingId, adopted.id);
|
||||
if (!cnameContentMatches(adopted.content, normalized, zoneName)) {
|
||||
await dnsService.update(db, cf, domainId, adopted.id, {
|
||||
record_type: "CNAME",
|
||||
name: dnsNameForBinding(hostname, zoneName),
|
||||
content: normalized,
|
||||
proxied: false,
|
||||
});
|
||||
}
|
||||
recordId = adopted.id;
|
||||
} else {
|
||||
const record = await dnsService.create(db, cf, domainId, {
|
||||
record_type: "CNAME",
|
||||
name: dnsNameForBinding(hostname, zoneName),
|
||||
content: normalized,
|
||||
ttl: 1,
|
||||
proxied: false,
|
||||
});
|
||||
repos.linkBindingRecord(db, bindingId, record.id);
|
||||
recordId = record.id;
|
||||
}
|
||||
}
|
||||
|
||||
repos.setBindingDnsRecordId(db, bindingId, recordId);
|
||||
repos.setBindingCnameTarget(db, bindingId, normalized);
|
||||
}
|
||||
|
||||
async function syncBindingADns(
|
||||
db: Db,
|
||||
cf: CloudflareClient,
|
||||
bindingId: number,
|
||||
domainId: number,
|
||||
hostname: string,
|
||||
desiredIps: string[],
|
||||
): Promise<void> {
|
||||
const domain = repos.getDomain(db, domainId);
|
||||
const zoneName = domain.zone_name;
|
||||
const existingRecords = repos.listRecordsForBinding(db, bindingId);
|
||||
|
||||
for (const record of existingRecords) {
|
||||
if (record.record_type.toUpperCase() === "CNAME") {
|
||||
repos.unlinkBindingRecord(db, bindingId, record.id);
|
||||
await dnsService.deleteRecord(db, cf, domainId, record.id);
|
||||
} else if (!desiredIps.includes(record.content)) {
|
||||
repos.unlinkBindingRecord(db, bindingId, record.id);
|
||||
await dnsService.deleteRecord(db, cf, domainId, record.id);
|
||||
}
|
||||
}
|
||||
|
||||
repos.setBindingCnameTarget(db, bindingId, null);
|
||||
|
||||
if (desiredIps.length === 0) {
|
||||
repos.setBindingDnsRecordId(db, bindingId, null);
|
||||
return;
|
||||
@@ -207,25 +346,39 @@ async function syncBindingDns(
|
||||
const existing = refreshed.find((r) => r.content === ip);
|
||||
let recordId: number;
|
||||
if (existing) {
|
||||
if (existing.name !== hostname) {
|
||||
if (!dnsRecordNamesMatch(existing.name, hostname, zoneName)) {
|
||||
await dnsService.update(db, cf, domainId, existing.id, {
|
||||
record_type: "A",
|
||||
name: hostname,
|
||||
name: dnsNameForBinding(hostname, zoneName),
|
||||
content: ip,
|
||||
proxied: false,
|
||||
});
|
||||
}
|
||||
recordId = existing.id;
|
||||
} else {
|
||||
const record = await dnsService.create(db, cf, domainId, {
|
||||
record_type: "A",
|
||||
name: hostname,
|
||||
content: ip,
|
||||
ttl: 1,
|
||||
proxied: false,
|
||||
});
|
||||
repos.linkBindingRecord(db, bindingId, record.id);
|
||||
recordId = record.id;
|
||||
const adopted = await findOrImportDnsRecord(
|
||||
db,
|
||||
cf,
|
||||
domainId,
|
||||
zoneName,
|
||||
hostname,
|
||||
"A",
|
||||
ip,
|
||||
);
|
||||
if (adopted) {
|
||||
repos.linkBindingRecord(db, bindingId, adopted.id);
|
||||
recordId = adopted.id;
|
||||
} else {
|
||||
const record = await dnsService.create(db, cf, domainId, {
|
||||
record_type: "A",
|
||||
name: dnsNameForBinding(hostname, zoneName),
|
||||
content: ip,
|
||||
ttl: 1,
|
||||
proxied: false,
|
||||
});
|
||||
repos.linkBindingRecord(db, bindingId, record.id);
|
||||
recordId = record.id;
|
||||
}
|
||||
}
|
||||
if (primaryId == null) primaryId = recordId;
|
||||
}
|
||||
@@ -240,7 +393,7 @@ async function cleanupBindingDns(
|
||||
domainId: number,
|
||||
hostname: string,
|
||||
): Promise<void> {
|
||||
await syncBindingDns(db, cf, bindingId, domainId, hostname, []);
|
||||
await syncBindingDns(db, cf, bindingId, domainId, hostname, [], null);
|
||||
}
|
||||
|
||||
async function cleanupServiceDnsOnly(
|
||||
@@ -272,6 +425,7 @@ function validateTargetIpsInPool(targetIps: string[], ips: string[]): void {
|
||||
}
|
||||
|
||||
function bindingTargetIps(input: ServiceDomainInput): string[] {
|
||||
if (input.target_cname?.trim()) return [];
|
||||
const raw = input.target_ips
|
||||
? input.target_ips
|
||||
: input.target_ip?.trim()
|
||||
@@ -284,26 +438,209 @@ function bindingTargetIps(input: ServiceDomainInput): string[] {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function bindingTargetCname(input: ServiceDomainInput): string | null {
|
||||
const target = input.target_cname?.trim();
|
||||
return target ? target : null;
|
||||
}
|
||||
|
||||
function normalizeCnameTarget(target: string, zoneName: string): string {
|
||||
const trimmed = target.trim().toLowerCase();
|
||||
if (!trimmed) {
|
||||
throw AppError.validation("укажите CNAME-цель");
|
||||
}
|
||||
if (trimmed.includes(".")) return trimmed;
|
||||
return `${trimmed}.${zoneName.toLowerCase()}`;
|
||||
}
|
||||
|
||||
function dnsNameForBinding(hostname: string, zoneName: string): string {
|
||||
return normalizeDnsRecordName(hostname, zoneName);
|
||||
}
|
||||
|
||||
function cnameContentMatches(
|
||||
left: string,
|
||||
right: string,
|
||||
zoneName: string,
|
||||
): boolean {
|
||||
return (
|
||||
normalizeCnameTarget(left, zoneName) ===
|
||||
normalizeCnameTarget(right, zoneName)
|
||||
);
|
||||
}
|
||||
|
||||
function findLocalDnsRecord(
|
||||
db: Db,
|
||||
domainId: number,
|
||||
zoneName: string,
|
||||
hostname: string,
|
||||
recordType: "A" | "CNAME",
|
||||
content?: string,
|
||||
): DnsRecord | null {
|
||||
const records = repos.listDnsByDomain(db, domainId);
|
||||
return (
|
||||
records.find(
|
||||
(record) =>
|
||||
record.record_type.toUpperCase() === recordType &&
|
||||
(content == null || record.content === content) &&
|
||||
dnsRecordNamesMatch(record.name, hostname, zoneName),
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
async function findOrImportDnsRecord(
|
||||
db: Db,
|
||||
cf: CloudflareClient,
|
||||
domainId: number,
|
||||
zoneName: string,
|
||||
hostname: string,
|
||||
recordType: "A" | "CNAME",
|
||||
content?: string,
|
||||
): Promise<DnsRecord | null> {
|
||||
const local = findLocalDnsRecord(
|
||||
db,
|
||||
domainId,
|
||||
zoneName,
|
||||
hostname,
|
||||
recordType,
|
||||
content,
|
||||
);
|
||||
if (local) return local;
|
||||
|
||||
const domain = repos.getDomain(db, domainId);
|
||||
const remote = await cf.listDnsRecords(domain.cf_zone_id);
|
||||
for (const cfRec of remote) {
|
||||
if (cfRec.type.toUpperCase() !== recordType) continue;
|
||||
if (content != null) {
|
||||
if (recordType === "CNAME") {
|
||||
if (!cnameContentMatches(cfRec.content, content, zoneName)) continue;
|
||||
} else if (cfRec.content !== content) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!dnsRecordNamesMatch(cfRec.name, hostname, zoneName)) continue;
|
||||
if (!cfRec.id) continue;
|
||||
|
||||
const existing = repos.findDnsByCfId(db, domainId, cfRec.id);
|
||||
if (existing) return existing;
|
||||
|
||||
return repos.insertDnsRecord(
|
||||
db,
|
||||
domainId,
|
||||
cfRec.type,
|
||||
cfRec.name,
|
||||
cfRec.content,
|
||||
cfRec.ttl,
|
||||
cfRec.proxied ?? false,
|
||||
cfRec.priority ?? null,
|
||||
SYNC_SYNCED,
|
||||
"cloudflare",
|
||||
cfRec.id,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function findOrImportDnsARecord(
|
||||
db: Db,
|
||||
cf: CloudflareClient,
|
||||
domainId: number,
|
||||
zoneName: string,
|
||||
hostname: string,
|
||||
content: string,
|
||||
): Promise<DnsRecord | null> {
|
||||
return findOrImportDnsRecord(
|
||||
db,
|
||||
cf,
|
||||
domainId,
|
||||
zoneName,
|
||||
hostname,
|
||||
"A",
|
||||
content,
|
||||
);
|
||||
}
|
||||
|
||||
async function serviceBindingsExistInDns(
|
||||
db: Db,
|
||||
cf: CloudflareClient,
|
||||
serviceId: number,
|
||||
): Promise<boolean> {
|
||||
const bindings = repos.listBindingsByService(db, serviceId);
|
||||
if (bindings.length === 0) return false;
|
||||
|
||||
for (const binding of bindings) {
|
||||
const cnameTarget = binding.cname_target?.trim() || null;
|
||||
if (cnameTarget) {
|
||||
const record = await findOrImportDnsRecord(
|
||||
db,
|
||||
cf,
|
||||
binding.domain_id,
|
||||
binding.zone_name,
|
||||
binding.hostname,
|
||||
"CNAME",
|
||||
cnameTarget,
|
||||
);
|
||||
if (!record) return false;
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetIps = repos.listBindingIps(db, binding.id);
|
||||
if (targetIps.length === 0) return false;
|
||||
|
||||
for (const ip of targetIps) {
|
||||
const record = await findOrImportDnsARecord(
|
||||
db,
|
||||
cf,
|
||||
binding.domain_id,
|
||||
binding.zone_name,
|
||||
binding.hostname,
|
||||
ip,
|
||||
);
|
||||
if (!record) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function syncServiceBindingsToDns(
|
||||
db: Db,
|
||||
cf: CloudflareClient,
|
||||
serviceId: number,
|
||||
): Promise<void> {
|
||||
const ips = repos.listServiceIps(db, serviceId);
|
||||
if (ips.length === 0) {
|
||||
throw AppError.validation("добавьте IP-адреса в пул сервиса");
|
||||
}
|
||||
|
||||
const bindings = repos.listBindingsByService(db, serviceId);
|
||||
if (bindings.length === 0) {
|
||||
throw AppError.validation("настройте FQDN в редакторе сервиса");
|
||||
}
|
||||
|
||||
const needsIpPool = bindings.some((binding) => {
|
||||
if (binding.cname_target?.trim()) return false;
|
||||
const targetIps = repos.listBindingIps(db, binding.id);
|
||||
return targetIps.length > 0;
|
||||
});
|
||||
const ips = repos.listServiceIps(db, serviceId);
|
||||
if (needsIpPool && ips.length === 0) {
|
||||
throw AppError.validation("добавьте IP-адреса в пул сервиса");
|
||||
}
|
||||
|
||||
for (const binding of bindings) {
|
||||
const cnameTarget = binding.cname_target?.trim() || null;
|
||||
if (cnameTarget) {
|
||||
await syncBindingDns(
|
||||
db,
|
||||
cf,
|
||||
binding.id,
|
||||
binding.domain_id,
|
||||
binding.hostname,
|
||||
[],
|
||||
cnameTarget,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetIps = repos.listBindingIps(db, binding.id);
|
||||
if (targetIps.length === 0) {
|
||||
throw AppError.validation(
|
||||
`укажите IP для ${fqdnToDisplay(binding.hostname, binding.zone_name)}`,
|
||||
`укажите IP или CNAME для ${fqdnToDisplay(binding.hostname, binding.zone_name)}`,
|
||||
);
|
||||
}
|
||||
validateTargetIpsInPool(targetIps, ips);
|
||||
@@ -314,6 +651,7 @@ async function syncServiceBindingsToDns(
|
||||
binding.domain_id,
|
||||
binding.hostname,
|
||||
targetIps,
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -345,6 +683,8 @@ async function syncGroupDomainDnsRecords(
|
||||
hostname: string,
|
||||
desiredIps: string[],
|
||||
): Promise<void> {
|
||||
const domain = repos.getDomain(db, domainId);
|
||||
const zoneName = domain.zone_name;
|
||||
const existingRecords = repos.listGroupDnsRecords(db, groupId);
|
||||
|
||||
for (const record of existingRecords) {
|
||||
@@ -360,19 +700,31 @@ async function syncGroupDomainDnsRecords(
|
||||
for (const ip of desiredIps) {
|
||||
const existing = refreshed.find((r) => r.content === ip);
|
||||
if (existing) {
|
||||
if (existing.name !== hostname) {
|
||||
if (!dnsRecordNamesMatch(existing.name, hostname, zoneName)) {
|
||||
await dnsService.update(db, cf, domainId, existing.id, {
|
||||
record_type: "A",
|
||||
name: hostname,
|
||||
name: dnsNameForBinding(hostname, zoneName),
|
||||
content: ip,
|
||||
proxied: false,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const adopted = await findOrImportDnsARecord(
|
||||
db,
|
||||
cf,
|
||||
domainId,
|
||||
zoneName,
|
||||
hostname,
|
||||
ip,
|
||||
);
|
||||
if (adopted) {
|
||||
repos.linkGroupDnsRecord(db, groupId, adopted.id);
|
||||
continue;
|
||||
}
|
||||
const record = await dnsService.create(db, cf, domainId, {
|
||||
record_type: "A",
|
||||
name: hostname,
|
||||
name: dnsNameForBinding(hostname, zoneName),
|
||||
content: ip,
|
||||
ttl: 1,
|
||||
proxied: false,
|
||||
@@ -466,7 +818,7 @@ async function syncEnabledServicesInGroup(
|
||||
async function normalizeGroupDomain(
|
||||
db: Db,
|
||||
cf: CloudflareClient,
|
||||
domain?: string,
|
||||
domain?: string | null,
|
||||
): Promise<string | null> {
|
||||
const raw = domain?.trim();
|
||||
if (!raw) return null;
|
||||
@@ -543,8 +895,15 @@ export async function updateConfig(
|
||||
for (const input of req.domains) {
|
||||
const fqdn = input.fqdn.trim();
|
||||
if (!fqdn) continue;
|
||||
const targetCname = bindingTargetCname(input);
|
||||
const targetIps = bindingTargetIps(input);
|
||||
validateTargetIpsInPool(targetIps, ips);
|
||||
if (!targetCname) {
|
||||
validateTargetIpsInPool(targetIps, ips);
|
||||
} else if (targetIps.length > 0) {
|
||||
throw AppError.validation(
|
||||
`укажите либо IP, либо CNAME для ${fqdn}`,
|
||||
);
|
||||
}
|
||||
|
||||
const { zoneName, hostname } = parseFqdn(fqdn, knownZones);
|
||||
const domainId = await resolveDomainId(db, cf, zoneName);
|
||||
@@ -554,7 +913,8 @@ export async function updateConfig(
|
||||
repos.insertBinding(db, domainId, id, hostname, null);
|
||||
|
||||
keptBindingIds.push(binding.id);
|
||||
repos.replaceBindingIps(db, binding.id, targetIps);
|
||||
repos.replaceBindingIps(db, binding.id, targetCname ? [] : targetIps);
|
||||
repos.setBindingCnameTarget(db, binding.id, targetCname);
|
||||
|
||||
if (pushDns) {
|
||||
await syncBindingDns(
|
||||
@@ -564,6 +924,7 @@ export async function updateConfig(
|
||||
domainId,
|
||||
hostname,
|
||||
targetIps,
|
||||
targetCname,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -598,6 +959,15 @@ export async function updateConfig(
|
||||
if (shouldPushDns(db, service)) {
|
||||
await syncServiceBindingsToDns(db, cf, id);
|
||||
await syncGroupDomainForService(db, cf, id);
|
||||
} else if (
|
||||
req.domains &&
|
||||
req.domains.length > 0 &&
|
||||
!service.enabled &&
|
||||
(await serviceBindingsExistInDns(db, cf, id))
|
||||
) {
|
||||
repos.setServiceEnabled(db, id, true);
|
||||
await syncServiceBindingsToDns(db, cf, id);
|
||||
await syncGroupDomainForService(db, cf, id);
|
||||
}
|
||||
|
||||
return buildView(db, id);
|
||||
@@ -633,7 +1003,7 @@ export async function updateGroup(
|
||||
await cleanupGroupDomainDns(db, cf, id);
|
||||
}
|
||||
const domain = await normalizeGroupDomain(db, cf, body.domain);
|
||||
const group = repos.updateServiceGroup(
|
||||
let group = repos.updateServiceGroup(
|
||||
db,
|
||||
id,
|
||||
body.name,
|
||||
@@ -641,6 +1011,10 @@ export async function updateGroup(
|
||||
body.icon ?? null,
|
||||
domain,
|
||||
);
|
||||
if (!domain && group.enabled) {
|
||||
repos.setServiceGroupEnabled(db, id, false);
|
||||
group = repos.getServiceGroup(db, id);
|
||||
}
|
||||
await syncEnabledServicesInGroup(db, cf, id);
|
||||
return group;
|
||||
}
|
||||
@@ -659,12 +1033,9 @@ export async function toggleService(
|
||||
|
||||
if (enabled && service.service_group_id) {
|
||||
const group = repos.getServiceGroup(db, service.service_group_id);
|
||||
if (!group.enabled) {
|
||||
if (group.domain?.trim() && !group.enabled) {
|
||||
throw AppError.validation("сначала включите группу сервисов");
|
||||
}
|
||||
if (!group.domain?.trim()) {
|
||||
throw AppError.validation("укажите домен у группы сервисов");
|
||||
}
|
||||
}
|
||||
|
||||
repos.setServiceEnabled(db, serviceId, enabled);
|
||||
@@ -686,6 +1057,11 @@ export async function toggleGroup(
|
||||
groupId: number,
|
||||
enabled: boolean,
|
||||
): Promise<ServiceGroupsResponse> {
|
||||
const group = repos.getServiceGroup(db, groupId);
|
||||
if (enabled && !group.domain?.trim()) {
|
||||
throw AppError.validation("нельзя включить группу без домена");
|
||||
}
|
||||
|
||||
repos.setServiceGroupEnabled(db, groupId, enabled);
|
||||
|
||||
if (!enabled) {
|
||||
@@ -703,3 +1079,14 @@ export async function toggleGroup(
|
||||
|
||||
return listGroupViews(db);
|
||||
}
|
||||
|
||||
export function reorderServices(
|
||||
db: Db,
|
||||
groupId: number | null,
|
||||
serviceIds: number[],
|
||||
): void {
|
||||
if (groupId !== null) {
|
||||
repos.getServiceGroup(db, groupId);
|
||||
}
|
||||
repos.reorderServices(db, groupId, serviceIds);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,93 @@
|
||||
import type { Db } from "@cfdm/db";
|
||||
import { repos } from "@cfdm/db";
|
||||
import type { Domain, SyncJob } from "@cfdm/shared";
|
||||
import type { CfDnsRecord, Domain, DnsRecord, SyncJob } from "@cfdm/shared";
|
||||
import {
|
||||
SYNC_CONFLICT,
|
||||
SYNC_PENDING_PUSH,
|
||||
SYNC_SYNCED,
|
||||
dnsNameToSubdomainLabel,
|
||||
dnsRecordNamesMatch,
|
||||
subdomainLabelToFqdn,
|
||||
} from "@cfdm/shared";
|
||||
import type { CloudflareClient } from "../lib/cf-client.js";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
function findLocalByRemote(
|
||||
local: DnsRecord[],
|
||||
cfRec: CfDnsRecord,
|
||||
zoneName: string,
|
||||
): DnsRecord | null {
|
||||
return (
|
||||
local.find(
|
||||
(record) =>
|
||||
record.record_type.toUpperCase() === cfRec.type.toUpperCase() &&
|
||||
dnsRecordNamesMatch(record.name, cfRec.name, zoneName),
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
function dnsRecordsEquivalent(
|
||||
existing: DnsRecord,
|
||||
cfRec: CfDnsRecord,
|
||||
zoneName: string,
|
||||
): boolean {
|
||||
const proxied = cfRec.proxied ?? false;
|
||||
return (
|
||||
existing.content === cfRec.content &&
|
||||
existing.ttl === cfRec.ttl &&
|
||||
existing.proxied === proxied &&
|
||||
dnsRecordNamesMatch(existing.name, cfRec.name, zoneName) &&
|
||||
existing.record_type.toUpperCase() === cfRec.type.toUpperCase()
|
||||
);
|
||||
}
|
||||
|
||||
function applyRemoteRecord(
|
||||
db: Db,
|
||||
domainId: number,
|
||||
cfRec: CfDnsRecord,
|
||||
existing: DnsRecord,
|
||||
zoneName: string,
|
||||
): boolean {
|
||||
const cfId = cfRec.id;
|
||||
if (!cfId) return false;
|
||||
|
||||
const proxied = cfRec.proxied ?? false;
|
||||
const equivalent = dnsRecordsEquivalent(existing, cfRec, zoneName);
|
||||
|
||||
if (!equivalent && existing.sync_status !== SYNC_PENDING_PUSH) {
|
||||
repos.setDnsSyncStatus(db, existing.id, SYNC_CONFLICT, cfId, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!equivalent) return false;
|
||||
|
||||
if (
|
||||
existing.name !== cfRec.name ||
|
||||
existing.sync_status !== SYNC_SYNCED ||
|
||||
existing.cf_record_id !== cfId ||
|
||||
existing.content !== cfRec.content ||
|
||||
existing.ttl !== cfRec.ttl ||
|
||||
existing.proxied !== proxied
|
||||
) {
|
||||
repos.updateDnsFields(
|
||||
db,
|
||||
existing.id,
|
||||
cfRec.type,
|
||||
cfRec.name,
|
||||
cfRec.content,
|
||||
cfRec.ttl,
|
||||
proxied,
|
||||
cfRec.priority ?? null,
|
||||
SYNC_SYNCED,
|
||||
cfId,
|
||||
null,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function pullSync(
|
||||
db: Db,
|
||||
cf: CloudflareClient,
|
||||
@@ -27,22 +104,14 @@ export async function pullSync(
|
||||
for (const cfRec of remote) {
|
||||
const cfId = cfRec.id;
|
||||
if (!cfId) continue;
|
||||
const proxied = cfRec.proxied ?? false;
|
||||
|
||||
const existing = repos.findDnsByCfId(db, domain.id, cfId);
|
||||
let existing = repos.findDnsByCfId(db, domain.id, cfId);
|
||||
if (!existing) {
|
||||
existing = findLocalByRemote(local, cfRec, domain.zone_name);
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
const contentMatch =
|
||||
existing.content === cfRec.content &&
|
||||
existing.ttl === cfRec.ttl &&
|
||||
existing.proxied === proxied &&
|
||||
existing.name === cfRec.name &&
|
||||
existing.record_type.toUpperCase() === cfRec.type.toUpperCase();
|
||||
|
||||
if (!contentMatch && existing.sync_status !== SYNC_PENDING_PUSH) {
|
||||
repos.setDnsSyncStatus(db, existing.id, SYNC_CONFLICT, cfId, null);
|
||||
changed += 1;
|
||||
} else if (contentMatch && existing.sync_status === SYNC_CONFLICT) {
|
||||
repos.setDnsSyncStatus(db, existing.id, SYNC_SYNCED, cfId, null);
|
||||
if (applyRemoteRecord(db, domain.id, cfRec, existing, domain.zone_name)) {
|
||||
changed += 1;
|
||||
}
|
||||
} else {
|
||||
@@ -53,7 +122,7 @@ export async function pullSync(
|
||||
cfRec.name,
|
||||
cfRec.content,
|
||||
cfRec.ttl,
|
||||
proxied,
|
||||
cfRec.proxied ?? false,
|
||||
cfRec.priority ?? null,
|
||||
SYNC_SYNCED,
|
||||
"cloudflare",
|
||||
@@ -63,7 +132,9 @@ export async function pullSync(
|
||||
}
|
||||
}
|
||||
|
||||
for (const rec of local) {
|
||||
const refreshedLocal = repos.listDnsByDomain(db, domain.id);
|
||||
|
||||
for (const rec of refreshedLocal) {
|
||||
if (rec.cf_record_id && !remoteIds.has(rec.cf_record_id)) {
|
||||
if (rec.sync_status !== "pending_delete") {
|
||||
repos.setDnsSyncStatus(
|
||||
@@ -75,6 +146,41 @@ export async function pullSync(
|
||||
);
|
||||
changed += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rec.sync_status === SYNC_PENDING_PUSH) continue;
|
||||
|
||||
const remoteSameType = remote.find(
|
||||
(r) =>
|
||||
r.id &&
|
||||
dnsRecordNamesMatch(r.name, rec.name, domain.zone_name) &&
|
||||
r.type.toUpperCase() === rec.record_type.toUpperCase(),
|
||||
);
|
||||
|
||||
if (remoteSameType?.id) {
|
||||
if (applyRemoteRecord(db, domain.id, remoteSameType, rec, domain.zone_name)) {
|
||||
changed += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const remoteSameHost = remote.find((r) =>
|
||||
dnsRecordNamesMatch(r.name, rec.name, domain.zone_name),
|
||||
);
|
||||
|
||||
if (
|
||||
remoteSameHost &&
|
||||
remoteSameHost.type.toUpperCase() !== rec.record_type.toUpperCase()
|
||||
) {
|
||||
repos.setDnsSyncStatus(
|
||||
db,
|
||||
rec.id,
|
||||
SYNC_CONFLICT,
|
||||
rec.cf_record_id,
|
||||
"type mismatch with cloudflare",
|
||||
);
|
||||
changed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user