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:
Vendored
+389
-87
@@ -424,6 +424,7 @@ async function groupRoutes(app2) {
|
||||
|
||||
// src/routes/services.ts
|
||||
import { z as z3 } from "zod";
|
||||
import { reorderServicesSchema } from "@cfdm/shared";
|
||||
import { repos as repos7 } from "@cfdm/db";
|
||||
|
||||
// src/services/service-config-service.ts
|
||||
@@ -431,7 +432,8 @@ import { repos as repos6 } from "@cfdm/db";
|
||||
import {
|
||||
SYNC_ERROR as SYNC_ERROR2,
|
||||
SYNC_PENDING_PUSH as SYNC_PENDING_PUSH3,
|
||||
SYNC_SYNCED as SYNC_SYNCED3
|
||||
SYNC_SYNCED as SYNC_SYNCED3,
|
||||
dnsNameToSubdomainLabel as dnsNameToSubdomainLabel2
|
||||
} from "@cfdm/shared";
|
||||
|
||||
// src/lib/validators.ts
|
||||
@@ -874,9 +876,9 @@ async function createDomain(db, cf, groupId, zoneName) {
|
||||
"\u043D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u0437\u043E\u043D \u0432 Cloudflare \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C\u0442\u0435 CLOUDFLARE_API_TOKEN \u0438 \u043F\u0440\u0430\u0432\u0430 Zone:Read"
|
||||
);
|
||||
}
|
||||
const zone = zones.find((z9) => z9.name.toLowerCase() === trimmed.toLowerCase());
|
||||
const zone = zones.find((z8) => z8.name.toLowerCase() === trimmed.toLowerCase());
|
||||
if (!zone) {
|
||||
const names = zones.map((z9) => z9.name).join(", ");
|
||||
const names = zones.map((z8) => z8.name).join(", ");
|
||||
throw AppError.notFound(
|
||||
`\u0437\u043E\u043D\u0430 \xAB${trimmed}\xBB \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430 \u0432 Cloudflare. \u0414\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0435: ${names}`
|
||||
);
|
||||
@@ -956,13 +958,19 @@ async function buildView(db, serviceId) {
|
||||
const records = repos6.listRecordsForBinding(db, binding.id);
|
||||
const statuses = records.map((r) => r.sync_status);
|
||||
const targetIps = repos6.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" : "A",
|
||||
target_ips: targetCname ? [] : targetIps,
|
||||
target_cname: targetCname,
|
||||
sync_status: aggregateSyncStatus(statuses)
|
||||
};
|
||||
});
|
||||
@@ -1012,14 +1020,114 @@ function shouldPushDns(db, service) {
|
||||
const group = repos6.getServiceGroup(db, service.service_group_id);
|
||||
return group.enabled;
|
||||
}
|
||||
async function syncBindingDns(db, cf, bindingId, domainId, hostname, desiredIps) {
|
||||
async function syncBindingDns(db, cf, bindingId, domainId, hostname, desiredIps, cnameTarget) {
|
||||
const domain = repos6.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;
|
||||
repos6.setBindingCnameTarget(db, bindingId, effectiveCname);
|
||||
repos6.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, cf, bindingId, domainId, hostname, cnameTarget) {
|
||||
const domain = repos6.getDomain(db, domainId);
|
||||
const zoneName = domain.zone_name;
|
||||
const normalized = normalizeCnameTarget(cnameTarget, zoneName);
|
||||
const existingRecords = repos6.listRecordsForBinding(db, bindingId);
|
||||
for (const record of existingRecords) {
|
||||
if (!desiredIps.includes(record.content)) {
|
||||
if (record.record_type.toUpperCase() === "A") {
|
||||
repos6.unlinkBindingRecord(db, bindingId, record.id);
|
||||
await deleteRecord(db, cf, domainId, record.id);
|
||||
}
|
||||
}
|
||||
const refreshed = repos6.listRecordsForBinding(db, bindingId);
|
||||
const existingCname = refreshed.find(
|
||||
(record) => record.record_type.toUpperCase() === "CNAME"
|
||||
);
|
||||
let recordId;
|
||||
if (existingCname) {
|
||||
if (!cnameContentMatches(existingCname.content, normalized, zoneName) || existingCname.name !== hostname) {
|
||||
await update(db, cf, domainId, existingCname.id, {
|
||||
record_type: "CNAME",
|
||||
name: hostname,
|
||||
content: normalized,
|
||||
proxied: false
|
||||
});
|
||||
}
|
||||
recordId = existingCname.id;
|
||||
} else {
|
||||
const adopted = await findOrImportDnsRecord(
|
||||
db,
|
||||
cf,
|
||||
domainId,
|
||||
zoneName,
|
||||
hostname,
|
||||
"CNAME",
|
||||
normalized
|
||||
);
|
||||
if (adopted) {
|
||||
repos6.linkBindingRecord(db, bindingId, adopted.id);
|
||||
if (!cnameContentMatches(adopted.content, normalized, zoneName)) {
|
||||
await update(db, cf, domainId, adopted.id, {
|
||||
record_type: "CNAME",
|
||||
name: hostname,
|
||||
content: normalized,
|
||||
proxied: false
|
||||
});
|
||||
}
|
||||
recordId = adopted.id;
|
||||
} else {
|
||||
const record = await create(db, cf, domainId, {
|
||||
record_type: "CNAME",
|
||||
name: hostname,
|
||||
content: normalized,
|
||||
ttl: 1,
|
||||
proxied: false
|
||||
});
|
||||
repos6.linkBindingRecord(db, bindingId, record.id);
|
||||
recordId = record.id;
|
||||
}
|
||||
}
|
||||
repos6.setBindingDnsRecordId(db, bindingId, recordId);
|
||||
repos6.setBindingCnameTarget(db, bindingId, normalized);
|
||||
}
|
||||
async function syncBindingADns(db, cf, bindingId, domainId, hostname, desiredIps) {
|
||||
const domain = repos6.getDomain(db, domainId);
|
||||
const zoneName = domain.zone_name;
|
||||
const existingRecords = repos6.listRecordsForBinding(db, bindingId);
|
||||
for (const record of existingRecords) {
|
||||
if (record.record_type.toUpperCase() === "CNAME") {
|
||||
repos6.unlinkBindingRecord(db, bindingId, record.id);
|
||||
await deleteRecord(db, cf, domainId, record.id);
|
||||
} else if (!desiredIps.includes(record.content)) {
|
||||
repos6.unlinkBindingRecord(db, bindingId, record.id);
|
||||
await deleteRecord(db, cf, domainId, record.id);
|
||||
}
|
||||
}
|
||||
repos6.setBindingCnameTarget(db, bindingId, null);
|
||||
if (desiredIps.length === 0) {
|
||||
repos6.setBindingDnsRecordId(db, bindingId, null);
|
||||
return;
|
||||
@@ -1040,22 +1148,36 @@ async function syncBindingDns(db, cf, bindingId, domainId, hostname, desiredIps)
|
||||
}
|
||||
recordId = existing.id;
|
||||
} else {
|
||||
const record = await create(db, cf, domainId, {
|
||||
record_type: "A",
|
||||
name: hostname,
|
||||
content: ip,
|
||||
ttl: 1,
|
||||
proxied: false
|
||||
});
|
||||
repos6.linkBindingRecord(db, bindingId, record.id);
|
||||
recordId = record.id;
|
||||
const adopted = await findOrImportDnsRecord(
|
||||
db,
|
||||
cf,
|
||||
domainId,
|
||||
zoneName,
|
||||
hostname,
|
||||
"A",
|
||||
ip
|
||||
);
|
||||
if (adopted) {
|
||||
repos6.linkBindingRecord(db, bindingId, adopted.id);
|
||||
recordId = adopted.id;
|
||||
} else {
|
||||
const record = await create(db, cf, domainId, {
|
||||
record_type: "A",
|
||||
name: hostname,
|
||||
content: ip,
|
||||
ttl: 1,
|
||||
proxied: false
|
||||
});
|
||||
repos6.linkBindingRecord(db, bindingId, record.id);
|
||||
recordId = record.id;
|
||||
}
|
||||
}
|
||||
if (primaryId == null) primaryId = recordId;
|
||||
}
|
||||
repos6.setBindingDnsRecordId(db, bindingId, primaryId);
|
||||
}
|
||||
async function cleanupBindingDns(db, cf, bindingId, domainId, hostname) {
|
||||
await syncBindingDns(db, cf, bindingId, domainId, hostname, []);
|
||||
await syncBindingDns(db, cf, bindingId, domainId, hostname, [], null);
|
||||
}
|
||||
async function cleanupServiceDnsOnly(db, cf, serviceId) {
|
||||
const bindings = repos6.listBindingsByService(db, serviceId);
|
||||
@@ -1080,6 +1202,7 @@ function validateTargetIpsInPool(targetIps, ips) {
|
||||
}
|
||||
}
|
||||
function bindingTargetIps(input) {
|
||||
if (input.target_cname?.trim()) return [];
|
||||
const raw = input.target_ips ? input.target_ips : input.target_ip?.trim() ? [input.target_ip.trim()] : [];
|
||||
const normalized = normalizeIps(raw);
|
||||
if (raw.length > 0 && normalized.length === 0) {
|
||||
@@ -1087,20 +1210,150 @@ function bindingTargetIps(input) {
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
async function syncServiceBindingsToDns(db, cf, serviceId) {
|
||||
const ips = repos6.listServiceIps(db, serviceId);
|
||||
if (ips.length === 0) {
|
||||
throw AppError.validation("\u0434\u043E\u0431\u0430\u0432\u044C\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441\u0430 \u0432 \u043F\u0443\u043B \u0441\u0435\u0440\u0432\u0438\u0441\u0430");
|
||||
function bindingTargetCname(input) {
|
||||
const target = input.target_cname?.trim();
|
||||
return target ? target : null;
|
||||
}
|
||||
function normalizeCnameTarget(target, zoneName) {
|
||||
const trimmed = target.trim().toLowerCase();
|
||||
if (!trimmed) {
|
||||
throw AppError.validation("\u0443\u043A\u0430\u0436\u0438\u0442\u0435 CNAME-\u0446\u0435\u043B\u044C");
|
||||
}
|
||||
if (trimmed.includes(".")) return trimmed;
|
||||
return `${trimmed}.${zoneName.toLowerCase()}`;
|
||||
}
|
||||
function cnameContentMatches(left, right, zoneName) {
|
||||
return normalizeCnameTarget(left, zoneName) === normalizeCnameTarget(right, zoneName);
|
||||
}
|
||||
function dnsHostnameMatches(recordName, hostname, zoneName) {
|
||||
const label = dnsNameToSubdomainLabel2(recordName, zoneName);
|
||||
if (label != null) return label === hostname;
|
||||
return recordName === hostname;
|
||||
}
|
||||
function findLocalDnsRecord(db, domainId, zoneName, hostname, recordType, content) {
|
||||
const records = repos6.listDnsByDomain(db, domainId);
|
||||
return records.find(
|
||||
(record) => record.record_type.toUpperCase() === recordType && (content == null || record.content === content) && dnsHostnameMatches(record.name, hostname, zoneName)
|
||||
) ?? null;
|
||||
}
|
||||
async function findOrImportDnsRecord(db, cf, domainId, zoneName, hostname, recordType, content) {
|
||||
const local = findLocalDnsRecord(
|
||||
db,
|
||||
domainId,
|
||||
zoneName,
|
||||
hostname,
|
||||
recordType,
|
||||
content
|
||||
);
|
||||
if (local) return local;
|
||||
const domain = repos6.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 (!dnsHostnameMatches(cfRec.name, hostname, zoneName)) continue;
|
||||
if (!cfRec.id) continue;
|
||||
const existing = repos6.findDnsByCfId(db, domainId, cfRec.id);
|
||||
if (existing) return existing;
|
||||
return repos6.insertDnsRecord(
|
||||
db,
|
||||
domainId,
|
||||
cfRec.type,
|
||||
cfRec.name,
|
||||
cfRec.content,
|
||||
cfRec.ttl,
|
||||
cfRec.proxied ?? false,
|
||||
cfRec.priority ?? null,
|
||||
SYNC_SYNCED3,
|
||||
"cloudflare",
|
||||
cfRec.id
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async function findOrImportDnsARecord(db, cf, domainId, zoneName, hostname, content) {
|
||||
return findOrImportDnsRecord(
|
||||
db,
|
||||
cf,
|
||||
domainId,
|
||||
zoneName,
|
||||
hostname,
|
||||
"A",
|
||||
content
|
||||
);
|
||||
}
|
||||
async function serviceBindingsExistInDns(db, cf, serviceId) {
|
||||
const bindings = repos6.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 = repos6.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, cf, serviceId) {
|
||||
const bindings = repos6.listBindingsByService(db, serviceId);
|
||||
if (bindings.length === 0) {
|
||||
throw AppError.validation("\u043D\u0430\u0441\u0442\u0440\u043E\u0439\u0442\u0435 FQDN \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u0441\u0435\u0440\u0432\u0438\u0441\u0430");
|
||||
}
|
||||
const needsIpPool = bindings.some((binding) => {
|
||||
if (binding.cname_target?.trim()) return false;
|
||||
const targetIps = repos6.listBindingIps(db, binding.id);
|
||||
return targetIps.length > 0;
|
||||
});
|
||||
const ips = repos6.listServiceIps(db, serviceId);
|
||||
if (needsIpPool && ips.length === 0) {
|
||||
throw AppError.validation("\u0434\u043E\u0431\u0430\u0432\u044C\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441\u0430 \u0432 \u043F\u0443\u043B \u0441\u0435\u0440\u0432\u0438\u0441\u0430");
|
||||
}
|
||||
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 = repos6.listBindingIps(db, binding.id);
|
||||
if (targetIps.length === 0) {
|
||||
throw AppError.validation(
|
||||
`\u0443\u043A\u0430\u0436\u0438\u0442\u0435 IP \u0434\u043B\u044F ${fqdnToDisplay(binding.hostname, binding.zone_name)}`
|
||||
`\u0443\u043A\u0430\u0436\u0438\u0442\u0435 IP \u0438\u043B\u0438 CNAME \u0434\u043B\u044F ${fqdnToDisplay(binding.hostname, binding.zone_name)}`
|
||||
);
|
||||
}
|
||||
validateTargetIpsInPool(targetIps, ips);
|
||||
@@ -1110,7 +1363,8 @@ async function syncServiceBindingsToDns(db, cf, serviceId) {
|
||||
binding.id,
|
||||
binding.domain_id,
|
||||
binding.hostname,
|
||||
targetIps
|
||||
targetIps,
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1152,6 +1406,19 @@ async function syncGroupDomainDnsRecords(db, cf, groupId, domainId, hostname, de
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const domain = repos6.getDomain(db, domainId);
|
||||
const adopted = await findOrImportDnsARecord(
|
||||
db,
|
||||
cf,
|
||||
domainId,
|
||||
domain.zone_name,
|
||||
hostname,
|
||||
ip
|
||||
);
|
||||
if (adopted) {
|
||||
repos6.linkGroupDnsRecord(db, groupId, adopted.id);
|
||||
continue;
|
||||
}
|
||||
const record = await create(db, cf, domainId, {
|
||||
record_type: "A",
|
||||
name: hostname,
|
||||
@@ -1273,13 +1540,21 @@ async function updateConfig(db, cf, id, req) {
|
||||
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(
|
||||
`\u0443\u043A\u0430\u0436\u0438\u0442\u0435 \u043B\u0438\u0431\u043E IP, \u043B\u0438\u0431\u043E CNAME \u0434\u043B\u044F ${fqdn}`
|
||||
);
|
||||
}
|
||||
const { zoneName, hostname } = parseFqdn(fqdn, knownZones);
|
||||
const domainId = await resolveDomainId(db, cf, zoneName);
|
||||
const binding = repos6.findBinding(db, id, domainId, hostname) ?? repos6.insertBinding(db, domainId, id, hostname, null);
|
||||
keptBindingIds.push(binding.id);
|
||||
repos6.replaceBindingIps(db, binding.id, targetIps);
|
||||
repos6.replaceBindingIps(db, binding.id, targetCname ? [] : targetIps);
|
||||
repos6.setBindingCnameTarget(db, binding.id, targetCname);
|
||||
if (pushDns) {
|
||||
await syncBindingDns(
|
||||
db,
|
||||
@@ -1287,7 +1562,8 @@ async function updateConfig(db, cf, id, req) {
|
||||
binding.id,
|
||||
domainId,
|
||||
hostname,
|
||||
targetIps
|
||||
targetIps,
|
||||
targetCname
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1320,6 +1596,10 @@ async function updateConfig(db, cf, id, req) {
|
||||
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)) {
|
||||
repos6.setServiceEnabled(db, id, true);
|
||||
await syncServiceBindingsToDns(db, cf, id);
|
||||
await syncGroupDomainForService(db, cf, id);
|
||||
}
|
||||
return buildView(db, id);
|
||||
}
|
||||
@@ -1343,7 +1623,7 @@ async function updateGroup2(db, cf, id, body) {
|
||||
await cleanupGroupDomainDns(db, cf, id);
|
||||
}
|
||||
const domain = await normalizeGroupDomain(db, cf, body.domain);
|
||||
const group = repos6.updateServiceGroup(
|
||||
let group = repos6.updateServiceGroup(
|
||||
db,
|
||||
id,
|
||||
body.name,
|
||||
@@ -1351,6 +1631,10 @@ async function updateGroup2(db, cf, id, body) {
|
||||
body.icon ?? null,
|
||||
domain
|
||||
);
|
||||
if (!domain && group.enabled) {
|
||||
repos6.setServiceGroupEnabled(db, id, false);
|
||||
group = repos6.getServiceGroup(db, id);
|
||||
}
|
||||
await syncEnabledServicesInGroup(db, cf, id);
|
||||
return group;
|
||||
}
|
||||
@@ -1361,12 +1645,9 @@ async function toggleService(db, cf, serviceId, enabled) {
|
||||
const service = repos6.getService(db, serviceId);
|
||||
if (enabled && service.service_group_id) {
|
||||
const group = repos6.getServiceGroup(db, service.service_group_id);
|
||||
if (!group.enabled) {
|
||||
if (group.domain?.trim() && !group.enabled) {
|
||||
throw AppError.validation("\u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u0435 \u0433\u0440\u0443\u043F\u043F\u0443 \u0441\u0435\u0440\u0432\u0438\u0441\u043E\u0432");
|
||||
}
|
||||
if (!group.domain?.trim()) {
|
||||
throw AppError.validation("\u0443\u043A\u0430\u0436\u0438\u0442\u0435 \u0434\u043E\u043C\u0435\u043D \u0443 \u0433\u0440\u0443\u043F\u043F\u044B \u0441\u0435\u0440\u0432\u0438\u0441\u043E\u0432");
|
||||
}
|
||||
}
|
||||
repos6.setServiceEnabled(db, serviceId, enabled);
|
||||
if (!enabled) {
|
||||
@@ -1379,6 +1660,10 @@ async function toggleService(db, cf, serviceId, enabled) {
|
||||
return buildView(db, serviceId);
|
||||
}
|
||||
async function toggleGroup(db, cf, groupId, enabled) {
|
||||
const group = repos6.getServiceGroup(db, groupId);
|
||||
if (enabled && !group.domain?.trim()) {
|
||||
throw AppError.validation("\u043D\u0435\u043B\u044C\u0437\u044F \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0433\u0440\u0443\u043F\u043F\u0443 \u0431\u0435\u0437 \u0434\u043E\u043C\u0435\u043D\u0430");
|
||||
}
|
||||
repos6.setServiceGroupEnabled(db, groupId, enabled);
|
||||
if (!enabled) {
|
||||
const services = repos6.listServicesByGroup(db, groupId);
|
||||
@@ -1394,6 +1679,12 @@ async function toggleGroup(db, cf, groupId, enabled) {
|
||||
}
|
||||
return listGroupViews(db);
|
||||
}
|
||||
function reorderServices(db, groupId, serviceIds) {
|
||||
if (groupId !== null) {
|
||||
repos6.getServiceGroup(db, groupId);
|
||||
}
|
||||
repos6.reorderServices(db, groupId, serviceIds);
|
||||
}
|
||||
|
||||
// src/routes/services.ts
|
||||
async function serviceRoutes(app2) {
|
||||
@@ -1405,6 +1696,15 @@ async function serviceRoutes(app2) {
|
||||
app2.get("/services", async (request) => {
|
||||
return listViews(request.server.db);
|
||||
});
|
||||
app2.patch("/services/reorder", async (request) => {
|
||||
const body = reorderServicesSchema.parse(request.body);
|
||||
reorderServices(
|
||||
request.server.db,
|
||||
body.group_id,
|
||||
body.service_ids
|
||||
);
|
||||
return { ok: true };
|
||||
});
|
||||
app2.post("/services", async (request) => {
|
||||
const body = createSchema.parse(request.body);
|
||||
const service = repos7.createService(
|
||||
@@ -1452,14 +1752,9 @@ async function serviceRoutes(app2) {
|
||||
}
|
||||
|
||||
// src/routes/service-groups.ts
|
||||
import { z as z4 } from "zod";
|
||||
import { createServiceGroupSchema, toggleEnabledSchema } from "@cfdm/shared";
|
||||
async function serviceGroupRoutes(app2) {
|
||||
const bodySchema = z4.object({
|
||||
name: z4.string(),
|
||||
type: z4.string().optional(),
|
||||
icon: z4.string().optional(),
|
||||
domain: z4.string().optional()
|
||||
});
|
||||
const bodySchema = createServiceGroupSchema;
|
||||
app2.get("/service-groups", async (request) => {
|
||||
return listGroupViews(request.server.db);
|
||||
});
|
||||
@@ -1488,7 +1783,7 @@ async function serviceGroupRoutes(app2) {
|
||||
});
|
||||
app2.patch("/service-groups/:id/toggle", async (request) => {
|
||||
const { id } = request.params;
|
||||
const body = z4.object({ enabled: z4.boolean() }).parse(request.body);
|
||||
const body = toggleEnabledSchema.parse(request.body);
|
||||
return toggleGroup(
|
||||
request.server.db,
|
||||
request.server.cf,
|
||||
@@ -1499,18 +1794,18 @@ async function serviceGroupRoutes(app2) {
|
||||
}
|
||||
|
||||
// src/routes/service-bindings.ts
|
||||
import { z as z5 } from "zod";
|
||||
import { z as z4 } from "zod";
|
||||
async function serviceBindingRoutes(app2) {
|
||||
const createSchema = z5.object({
|
||||
domain_id: z5.number(),
|
||||
service_id: z5.number(),
|
||||
hostname: z5.string().optional(),
|
||||
target_ip: z5.string().optional()
|
||||
const createSchema = z4.object({
|
||||
domain_id: z4.number(),
|
||||
service_id: z4.number(),
|
||||
hostname: z4.string().optional(),
|
||||
target_ip: z4.string().optional()
|
||||
});
|
||||
const updateSchema = z5.object({
|
||||
service_id: z5.number().optional(),
|
||||
hostname: z5.string().optional(),
|
||||
target_ip: z5.string().optional()
|
||||
const updateSchema = z4.object({
|
||||
service_id: z4.number().optional(),
|
||||
hostname: z4.string().optional(),
|
||||
target_ip: z4.string().optional()
|
||||
});
|
||||
app2.get("/service-bindings", async (request) => {
|
||||
return listAll(request.server.db);
|
||||
@@ -1550,15 +1845,15 @@ async function serviceBindingRoutes(app2) {
|
||||
}
|
||||
|
||||
// src/routes/domains.ts
|
||||
import { z as z6 } from "zod";
|
||||
import { z as z5 } from "zod";
|
||||
async function domainRoutes(app2) {
|
||||
const createSchema = z6.object({
|
||||
zone_name: z6.string(),
|
||||
group_id: z6.number().nullable().optional()
|
||||
const createSchema = z5.object({
|
||||
zone_name: z5.string(),
|
||||
group_id: z5.number().nullable().optional()
|
||||
});
|
||||
const updateSchema = z6.object({
|
||||
group_id: z6.number().nullable().optional(),
|
||||
status: z6.string().optional()
|
||||
const updateSchema = z5.object({
|
||||
group_id: z5.number().nullable().optional(),
|
||||
status: z5.string().optional()
|
||||
});
|
||||
app2.get("/domains", async (request) => {
|
||||
const query = request.query;
|
||||
@@ -1605,7 +1900,7 @@ async function domainRoutes(app2) {
|
||||
});
|
||||
app2.put("/domains/:id/services", async (request) => {
|
||||
const { id } = request.params;
|
||||
const body = z6.object({ service_ids: z6.array(z6.number()) }).parse(request.body);
|
||||
const body = z5.object({ service_ids: z5.array(z5.number()) }).parse(request.body);
|
||||
const serviceIds = await setDomainServices2(
|
||||
request.server.db,
|
||||
Number(id),
|
||||
@@ -1616,15 +1911,15 @@ async function domainRoutes(app2) {
|
||||
}
|
||||
|
||||
// src/routes/dns.ts
|
||||
import { z as z7 } from "zod";
|
||||
import { z as z6 } from "zod";
|
||||
async function dnsRoutes(app2) {
|
||||
const createSchema = z7.object({
|
||||
record_type: z7.string(),
|
||||
name: z7.string(),
|
||||
content: z7.string(),
|
||||
ttl: z7.number().optional(),
|
||||
proxied: z7.boolean().optional(),
|
||||
priority: z7.number().optional()
|
||||
const createSchema = z6.object({
|
||||
record_type: z6.string(),
|
||||
name: z6.string(),
|
||||
content: z6.string(),
|
||||
ttl: z6.number().optional(),
|
||||
proxied: z6.boolean().optional(),
|
||||
priority: z6.number().optional()
|
||||
});
|
||||
app2.get("/domains/:id/dns", async (request) => {
|
||||
const { id } = request.params;
|
||||
@@ -1653,7 +1948,7 @@ async function dnsRoutes(app2) {
|
||||
});
|
||||
app2.post("/domains/:id/dns/bulk", async (request) => {
|
||||
const { id } = request.params;
|
||||
const body = z7.object({ operations: z7.array(z7.record(z7.unknown())) }).parse(request.body);
|
||||
const body = z6.object({ operations: z6.array(z6.record(z6.unknown())) }).parse(request.body);
|
||||
return bulk(
|
||||
request.server.db,
|
||||
request.server.cf,
|
||||
@@ -1691,7 +1986,7 @@ async function dnsRoutes(app2) {
|
||||
});
|
||||
app2.post("/domains/:id/dns/:recordId/resolve", async (request) => {
|
||||
const { id, recordId } = request.params;
|
||||
const body = z7.object({ source: z7.string() }).parse(request.body);
|
||||
const body = z6.object({ source: z6.string() }).parse(request.body);
|
||||
return resolveConflict(
|
||||
request.server.db,
|
||||
request.server.cf,
|
||||
@@ -1703,8 +1998,9 @@ async function dnsRoutes(app2) {
|
||||
}
|
||||
|
||||
// src/routes/subdomains.ts
|
||||
import { z as z8 } from "zod";
|
||||
import { updateSubdomainSchema } from "@cfdm/shared";
|
||||
import { repos as repos8 } from "@cfdm/db";
|
||||
import { z as z7 } from "zod";
|
||||
async function subdomainRoutes(app2) {
|
||||
app2.get("/domains/:id/subdomains", async (request) => {
|
||||
const { id } = request.params;
|
||||
@@ -1713,7 +2009,7 @@ async function subdomainRoutes(app2) {
|
||||
});
|
||||
app2.post("/domains/:id/subdomains", async (request) => {
|
||||
const { id } = request.params;
|
||||
const body = z8.object({ name: z8.string() }).parse(request.body);
|
||||
const body = z7.object({ name: z7.string() }).parse(request.body);
|
||||
const domain = repos8.getDomain(request.server.db, Number(id));
|
||||
const fqdn = body.name === "@" ? domain.zone_name : `${body.name}.${domain.zone_name}`;
|
||||
return repos8.createSubdomain(
|
||||
@@ -1729,16 +2025,18 @@ async function subdomainRoutes(app2) {
|
||||
});
|
||||
app2.patch("/subdomains/:id", async (request) => {
|
||||
const { id } = request.params;
|
||||
const body = z8.object({ name: z8.string() }).parse(request.body);
|
||||
const body = updateSubdomainSchema.parse(request.body);
|
||||
const sub = repos8.getSubdomain(request.server.db, Number(id));
|
||||
const domain = repos8.getDomain(request.server.db, sub.domain_id);
|
||||
const fqdn = `${body.name}.${domain.zone_name}`;
|
||||
return repos8.updateSubdomain(
|
||||
request.server.db,
|
||||
Number(id),
|
||||
body.name,
|
||||
fqdn
|
||||
);
|
||||
const patch = {};
|
||||
if (body.name !== void 0) {
|
||||
patch.name = body.name;
|
||||
patch.fqdn = body.name === "@" ? domain.zone_name : `${body.name}.${domain.zone_name}`;
|
||||
}
|
||||
if (body.enabled !== void 0) {
|
||||
patch.enabled = body.enabled;
|
||||
}
|
||||
return repos8.updateSubdomain(request.server.db, Number(id), patch);
|
||||
});
|
||||
app2.delete("/subdomains/:id", async (request) => {
|
||||
const { id } = request.params;
|
||||
@@ -1896,6 +2194,7 @@ async function syncRoutes(app2) {
|
||||
}
|
||||
|
||||
// src/app.ts
|
||||
import { AsyncTask, CronJob } from "toad-scheduler";
|
||||
async function buildApp(opts = {}) {
|
||||
const config2 = opts.config ?? loadConfig();
|
||||
const app2 = Fastify({
|
||||
@@ -1943,20 +2242,23 @@ async function buildApp(opts = {}) {
|
||||
}
|
||||
if (!opts.memory) {
|
||||
await app2.register(import("@fastify/schedule"));
|
||||
app2.scheduler.addCronJob(
|
||||
{
|
||||
cronExpression: config2.certCheckCron,
|
||||
name: "certificate-check"
|
||||
},
|
||||
const certTask = new AsyncTask(
|
||||
"certificate-check",
|
||||
async () => {
|
||||
try {
|
||||
const n = await runAllChecks(app2.db);
|
||||
app2.log.info({ checked: n }, "certificate check completed");
|
||||
} catch (err) {
|
||||
app2.log.warn({ err }, "certificate check failed");
|
||||
}
|
||||
const n = await runAllChecks(app2.db);
|
||||
app2.log.info({ checked: n }, "certificate check completed");
|
||||
},
|
||||
(err) => {
|
||||
app2.log.warn({ err }, "certificate check failed");
|
||||
}
|
||||
);
|
||||
app2.scheduler.addCronJob(
|
||||
new CronJob(
|
||||
{ cronExpression: config2.certCheckCron },
|
||||
certTask,
|
||||
{ preventOverrun: true }
|
||||
)
|
||||
);
|
||||
}
|
||||
return app2;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user