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
2026-06-22 13:52:33 +07:00
parent 4a70cf5854
commit 6d1d8ca4f3
99 changed files with 6692 additions and 1483 deletions
+27
View File
@@ -0,0 +1,27 @@
import type { CertMonitoring } from '@cfdm/shared'
export const certMonitoringOptions: Array<{
value: CertMonitoring
label: string
description: string
}> = [
{
value: 'auto',
label: 'Авто',
description: 'Проверять, если хост обслуживается активным сервисом',
},
{
value: 'required',
label: 'Обязательно',
description: 'Всегда проверять SSL, даже без привязок',
},
{
value: 'skipped',
label: 'Не проверять',
description: 'Исключить из мониторинга сертификатов',
},
]
export function certMonitoringLabel(value: string): string {
return certMonitoringOptions.find((o) => o.value === value)?.label ?? value
}
+11 -6
View File
@@ -1,9 +1,14 @@
import type { ServiceBinding } from '@/lib/schemas'
function bindingIps(binding: ServiceBinding): string[] {
if (binding.target_ips.length > 0) return binding.target_ips
return binding.target_ip ? [binding.target_ip] : []
}
export function getDomainIps(bindings: ServiceBinding[], domainId: number): string[] {
const ips = bindings
.filter((b) => b.domain_id === domainId && b.target_ip)
.map((b) => b.target_ip as string)
.filter((b) => b.domain_id === domainId)
.flatMap(bindingIps)
return [...new Set(ips)]
}
@@ -22,11 +27,11 @@ export function groupBindingsByHostname(
export function buildIpsByDomainId(bindings: ServiceBinding[]): Map<number, string[]> {
const map = new Map<number, string[]>()
for (const binding of bindings) {
if (!binding.target_ip) continue
const ips = bindingIps(binding)
if (ips.length === 0) continue
const existing = map.get(binding.domain_id) ?? []
if (!existing.includes(binding.target_ip)) {
map.set(binding.domain_id, [...existing, binding.target_ip])
}
const merged = [...new Set([...existing, ...ips])]
map.set(binding.domain_id, merged)
}
return map
}
+8 -12
View File
@@ -1,12 +1,8 @@
import { z } from 'zod'
export const subdomainSchema = z.object({
id: z.number(),
domain_id: z.number(),
name: z.string(),
fqdn: z.string(),
created_at: z.string(),
updated_at: z.string(),
})
export type Subdomain = z.infer<typeof subdomainSchema>
export {
createSubdomainSchema,
subdomainSchema,
updateSubdomainSchema,
type CreateSubdomainInput,
type SubdomainRecord,
type UpdateSubdomainInput,
} from '@cfdm/shared'
+69 -20
View File
@@ -50,8 +50,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 +64,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' as const)
: (binding.record_type ?? 'A'),
}))
export const serviceViewSchema = serviceSchema.extend({
@@ -86,6 +92,7 @@ export const domainSchema = z.object({
zone_name: z.string(),
cf_zone_id: z.string(),
status: z.string(),
cert_monitoring: z.enum(['auto', 'required', 'skipped']).default('auto'),
last_synced_at: z.string().nullable(),
created_at: z.string(),
updated_at: z.string(),
@@ -96,22 +103,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(),
@@ -169,10 +187,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, '╨г╨║╨░╨╢╨╕╤В╨╡ ╨╜╨░╨╖╨▓╨░╨╜╨╕╨╡'),
@@ -248,3 +286,14 @@ export type CreateServiceBindingInput = z.infer<typeof createServiceBindingSchem
export type CreateDomainInput = z.infer<typeof createDomainSchema>
export type LoginInput = z.infer<typeof loginSchema>
export type CreateDnsRecordInput = z.infer<typeof createDnsRecordSchema>
export {
createSubdomainSchema,
subdomainSchema,
updateDomainSchema,
updateSubdomainSchema,
type CreateSubdomainInput,
type SubdomainRecord,
type UpdateDomainInput,
type UpdateSubdomainInput,
} from '@cfdm/shared'
+24 -1
View File
@@ -1,5 +1,28 @@
import { bindingToFqdn } from '@/lib/parse-fqdn'
import type { ServiceView } from '@/lib/schemas'
import type { ServiceGroupsResponse, ServiceView } from '@/lib/schemas'
export function formatServiceGroupLabel(
groupName: string | null | undefined,
serviceName: string,
): string {
const group = groupName?.trim() || 'Без группы'
return `${group} / ${serviceName}`
}
export function buildServiceGroupNameById(
data: ServiceGroupsResponse,
): Map<number, string | null> {
const map = new Map<number, string | null>()
for (const group of data.groups) {
for (const service of group.services) {
map.set(service.id, group.name)
}
}
for (const service of data.ungrouped) {
map.set(service.id, null)
}
return map
}
export function serviceDisplayFqdn(service: ServiceView): string {
const first = service.domains?.[0]