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

This commit is contained in:
Denozordec committed 2026-06-22 13:52:33 +07:00
1 parent 4a70cf5854
commit 6d1d8ca4f3
99 files changed
+6692 -1483

No files matched your search

+10
View File
@@ -9,3 +9,13 @@ export const CERT_WARNING = "warning";
export const CERT_EXPIRED = "expired";
export const CERT_ERROR = "error";
export const CERT_UNKNOWN = "unknown";
export const CERT_MONITOR_AUTO = "auto";
export const CERT_MONITOR_REQUIRED = "required";
export const CERT_MONITOR_SKIPPED = "skipped";
export const CERT_MONITORING_VALUES = [
CERT_MONITOR_AUTO,
CERT_MONITOR_REQUIRED,
CERT_MONITOR_SKIPPED,
] as const;
+1
View File
@@ -11,6 +11,7 @@ export type {
LoginResponse,
JwtClaims,
SyncJob,
ServiceBinding,
ServiceBindingView,
ServiceDomainBindingView,
Subdomain,
+90 -21
View File
@@ -1,5 +1,9 @@
import { z } from 'zod'
export const certMonitoringSchema = z.enum(['auto', 'required', 'skipped'])
export type CertMonitoring = z.infer<typeof certMonitoringSchema>
export const groupSchema = z.object({
id: z.number(),
name: z.string(),
@@ -50,8 +54,10 @@ export const serviceDomainBindingSchema = z
zone_name: z.string(),
hostname: z.string(),
fqdn: z.string(),
record_type: z.enum(['A', 'CNAME']).default('A'),
target_ips: z.array(z.string()).optional(),
target_ip: z.string().nullable().optional(),
target_cname: z.string().nullable().optional(),
sync_status: z.string().nullable(),
})
.transform((binding) => ({
@@ -62,6 +68,10 @@ export const serviceDomainBindingSchema = z
: binding.target_ip
? [binding.target_ip]
: [],
target_cname: binding.target_cname?.trim() || null,
record_type: binding.target_cname?.trim()
? 'CNAME'
: binding.record_type ?? 'A',
}))
export const serviceViewSchema = serviceSchema.extend({
@@ -86,6 +96,7 @@ export const domainSchema = z.object({
zone_name: z.string(),
cf_zone_id: z.string(),
status: z.string(),
cert_monitoring: certMonitoringSchema.default('auto'),
last_synced_at: z.string().nullable(),
created_at: z.string(),
updated_at: z.string(),
@@ -96,22 +107,33 @@ export const domainListItemSchema = domainSchema.extend({
service_count: z.number(),
})
export const serviceBindingSchema = z.object({
id: z.number(),
domain_id: z.number(),
service_id: z.number(),
hostname: z.string(),
dns_record_id: z.number().nullable(),
zone_name: z.string(),
group_id: z.number().nullable(),
group_name: z.string().nullable(),
service_name: z.string(),
service_slug: z.string(),
target_ip: z.string().nullable(),
sync_status: z.string().nullable(),
created_at: z.string(),
updated_at: z.string(),
})
export const serviceBindingSchema = z
.object({
id: z.number(),
domain_id: z.number(),
service_id: z.number(),
hostname: z.string(),
dns_record_id: z.number().nullable(),
zone_name: z.string(),
group_id: z.number().nullable(),
group_name: z.string().nullable(),
service_name: z.string(),
service_slug: z.string(),
target_ip: z.string().nullable(),
target_ips: z.array(z.string()).optional(),
sync_status: z.string().nullable(),
created_at: z.string(),
updated_at: z.string(),
})
.transform((binding) => ({
...binding,
target_ips:
binding.target_ips && binding.target_ips.length > 0
? binding.target_ips
: binding.target_ip
? [binding.target_ip]
: [],
}))
export const dnsRecordSchema = z.object({
id: z.number(),
@@ -153,7 +175,6 @@ export type ServiceGroupView = z.infer<typeof serviceGroupViewSchema>
export type ServiceGroupsResponse = z.infer<typeof serviceGroupsResponseSchema>
export type Domain = z.infer<typeof domainSchema>
export type DomainListItem = z.infer<typeof domainListItemSchema>
export type ServiceBinding = z.infer<typeof serviceBindingSchema>
export type DnsRecord = z.infer<typeof dnsRecordSchema>
export type Certificate = z.infer<typeof certificateSchema>
@@ -169,10 +190,30 @@ const ipv4Schema = z
'Некорректный IPv4',
)
const serviceDomainInputSchema = z.object({
fqdn: z.string().min(1, 'Укажите FQDN'),
target_ips: z.array(ipv4Schema).min(1, 'Выберите хотя бы один IP'),
})
const serviceDomainInputSchema = z
.object({
fqdn: z.string().min(1, 'Укажите FQDN'),
target_ips: z.array(ipv4Schema).optional(),
target_cname: z.string().min(1, 'Укажите CNAME-цель').optional(),
})
.superRefine((data, ctx) => {
const hasIps = (data.target_ips?.length ?? 0) > 0
const hasCname = Boolean(data.target_cname?.trim())
if (!hasIps && !hasCname) {
ctx.addIssue({
code: 'custom',
message: 'Укажите IP или CNAME-цель',
path: ['target_ips'],
})
}
if (hasIps && hasCname) {
ctx.addIssue({
code: 'custom',
message: 'Укажите либо IP, либо CNAME-цель',
path: ['target_cname'],
})
}
})
export const createServiceSchema = z.object({
name: z.string().min(1, 'Укажите название'),
@@ -220,12 +261,34 @@ export const subdomainSchema = z.object({
domain_id: z.number(),
name: z.string(),
fqdn: z.string(),
enabled: z.boolean(),
cert_monitoring: certMonitoringSchema.default('auto'),
created_at: z.string(),
updated_at: z.string(),
})
export type SubdomainRecord = z.infer<typeof subdomainSchema>
export const createSubdomainSchema = z.object({
name: z.string().min(1, 'Укажите имя поддомена'),
})
export const updateSubdomainSchema = z.object({
name: z.string().min(1, 'Укажите имя поддомена').optional(),
enabled: z.boolean().optional(),
cert_monitoring: certMonitoringSchema.optional(),
})
export const updateDomainSchema = z.object({
group_id: z.number().nullable().optional(),
status: z.string().optional(),
cert_monitoring: certMonitoringSchema.optional(),
})
export type CreateSubdomainInput = z.infer<typeof createSubdomainSchema>
export type UpdateSubdomainInput = z.infer<typeof updateSubdomainSchema>
export type UpdateDomainInput = z.infer<typeof updateDomainSchema>
export type CreateGroupInput = z.infer<typeof createGroupSchema>
export type CreateServiceInput = z.infer<typeof createServiceSchema>
export type CreateServiceWithConfigInput = z.infer<typeof createServiceWithConfigSchema>
@@ -253,8 +316,14 @@ export const toggleEnabledSchema = z.object({
enabled: z.boolean(),
})
export const reorderServicesSchema = z.object({
group_id: z.union([z.number(), z.null()]).optional().default(null),
service_ids: z.array(z.number().int().positive()).min(1),
})
export type CreateServiceGroupInput = z.infer<typeof createServiceGroupSchema>
export type ToggleEnabledInput = z.infer<typeof toggleEnabledSchema>
export type ReorderServicesInput = z.infer<typeof reorderServicesSchema>
export type CreateServiceBindingInput = z.infer<typeof createServiceBindingSchema>
export type CreateDomainInput = z.infer<typeof createDomainSchema>
export type LoginInput = z.infer<typeof loginSchema>
+23
View File
@@ -27,3 +27,26 @@ export function dnsNameToSubdomainLabel(
export function subdomainLabelToFqdn(label: string, zoneName: string): string {
return label === "@" ? zoneName : `${label}.${zoneName}`;
}
/** Canonical DNS record name as returned by Cloudflare for a zone. */
export function normalizeDnsRecordName(
recordName: string,
zoneName: string,
): string {
const label = dnsNameToSubdomainLabel(recordName, zoneName);
if (label == null) {
return recordName.trim().replace(/\.+$/, "");
}
return subdomainLabelToFqdn(label, zoneName);
}
export function dnsRecordNamesMatch(
left: string,
right: string,
zoneName: string,
): boolean {
return (
normalizeDnsRecordName(left, zoneName).toLowerCase() ===
normalizeDnsRecordName(right, zoneName).toLowerCase()
);
}
+8
View File
@@ -33,6 +33,7 @@ export interface Service {
service_group_id: number | null;
subdomain: string;
enabled: boolean;
sort_order: number;
created_at: string;
updated_at: string;
}
@@ -43,6 +44,7 @@ export interface Domain {
zone_name: string;
cf_zone_id: string;
status: string;
cert_monitoring: string;
last_synced_at: string | null;
created_at: string;
updated_at: string;
@@ -53,6 +55,8 @@ export interface Subdomain {
domain_id: number;
name: string;
fqdn: string;
enabled: boolean;
cert_monitoring: string;
created_at: string;
updated_at: string;
}
@@ -92,6 +96,7 @@ export interface ServiceBinding {
domain_id: number;
service_id: number;
hostname: string;
cname_target: string | null;
dns_record_id: number | null;
created_at: string;
updated_at: string;
@@ -109,6 +114,7 @@ export interface ServiceBindingView {
service_name: string;
service_slug: string;
target_ip: string | null;
target_ips: string[];
sync_status: string | null;
created_at: string;
updated_at: string;
@@ -120,7 +126,9 @@ export interface ServiceDomainBindingView {
zone_name: string;
hostname: string;
fqdn: string;
record_type: "A" | "CNAME";
target_ips: string[];
target_cname: string | null;
sync_status: string | null;
}
+10
View File
@@ -3,6 +3,7 @@ import {
CERT_OK,
CERT_WARNING,
} from "./constants.js";
import type { Service, ServiceGroup } from "./types.js";
const NAME_RE =
/^(@|\*|[a-zA-Z0-9_]([a-zA-Z0-9_-]*[a-zA-Z0-9_])?(\.[a-zA-Z0-9_]([a-zA-Z0-9_-]*[a-zA-Z0-9_])?)*)$/;
@@ -70,6 +71,15 @@ export function certStatusFromExpiry(daysLeft: number): string {
return CERT_OK;
}
export function shouldMonitorService(
service: Pick<Service, "enabled" | "service_group_id">,
group?: Pick<ServiceGroup, "enabled"> | null,
): boolean {
if (!service.enabled) return false;
if (!service.service_group_id) return true;
return group?.enabled ?? false;
}
export function isValidIpv4(ip: string): boolean {
const parts = ip.split(".");
if (parts.length !== 4) return false;