Files
cloudflare-domain-manager/apps/web/src/lib/schemas.ts
T
DenozordecandCursor 64585ccd47
Build, Test, and Push CFDM Docker Image / test (push) Successful in 3m3s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 1m48s
Build, Test, and Push CFDM Docker Image / update-wiki (push) Successful in 6s
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped
feat(api, web): enhance health check and domain management features
- Integrated domain monitoring routes and bulk update functionality for domains in the API.
- Improved health check service to include domain monitoring and logging of health status changes.
- Updated web components to reflect health status with new HealthCheckBadge and enhanced domain filtering options.
- Refactored domain service to support bulk updates and improved domain management capabilities.

Co-authored-by: Cursor <[email protected]>
2026-07-17 02:25:12 +07:00

396 lines
14 KiB
TypeScript

import { z } from 'zod'
export const groupSchema = z.object({
id: z.number(),
name: z.string(),
slug: z.string(),
created_at: z.string(),
updated_at: z.string(),
})
export const groupWithStatsSchema = groupSchema.extend({
domain_count: z.number(),
})
export const serviceGroupTypeSchema = z.enum([
'vpn',
'network',
'internet',
'bgp',
'custom',
])
export const serviceGroupSchema = z.object({
id: z.number(),
name: z.string(),
type: serviceGroupTypeSchema.catch('custom'),
icon: z.string().nullable(),
domain: z.string().nullable(),
enabled: z.coerce.boolean(),
lb_mode: z.enum(['round_robin', 'failover', 'weighted']).catch('round_robin'),
health_check_enabled: z.coerce.boolean().default(false),
health_check_type: z.enum(['tcp', 'http']).catch('tcp'),
health_check_port: z.number().nullable(),
health_check_path: z.string().nullable(),
health_check_expected_status: z.number().nullable(),
health_check_interval_sec: z.number().default(30),
health_check_timeout_ms: z.number().default(3000),
created_at: z.string(),
updated_at: z.string(),
})
export const serviceSchema = z.object({
id: z.number(),
name: z.string(),
slug: z.string(),
service_group_id: z.number().nullable().optional(),
subdomain: z.string().optional(),
enabled: z.coerce.boolean().optional(),
computed_fqdn: z.string().nullable().optional(),
lb_weight: z.number().default(1),
lb_priority: z.number().default(1),
created_at: z.string(),
updated_at: z.string(),
})
export const serviceDomainBindingSchema = z
.object({
binding_id: z.number(),
domain_id: z.number(),
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_ip_weights: z.record(z.string(), z.number()).optional(),
target_ip_priorities: z.record(z.string(), z.number()).optional(),
target_cname: z.string().nullable().optional(),
lb_mode: z.enum(['round_robin', 'failover', 'weighted']).catch('round_robin'),
health_check_enabled: z.coerce.boolean().default(false),
health_check_type: z.enum(['tcp', 'http']).catch('tcp'),
health_check_port: z.number().nullable(),
health_check_path: z.string().nullable(),
health_check_expected_status: z.number().nullable(),
health_check_interval_sec: z.number().default(30),
health_check_timeout_ms: z.number().default(3000),
sync_status: z.string().nullable(),
})
.transform((binding) => ({
...binding,
target_ips:
binding.target_ips && binding.target_ips.length > 0
? binding.target_ips
: binding.target_ip
? [binding.target_ip]
: [],
target_ip_weights: binding.target_ip_weights ?? {},
target_ip_priorities: binding.target_ip_priorities ?? {},
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({
subdomain: z.string().default(''),
enabled: z.coerce.boolean().default(false),
ips: z.array(z.string()).default([]),
domains: z.array(serviceDomainBindingSchema).default([]),
})
export const serviceGroupViewSchema = serviceGroupSchema.extend({
services: z.array(serviceViewSchema).default([]),
})
export const serviceGroupsResponseSchema = z.object({
groups: z.array(serviceGroupViewSchema).default([]),
ungrouped: z.array(serviceViewSchema).default([]),
})
export const domainSchema = z.object({
id: z.number(),
group_id: z.number().nullable(),
zone_name: z.string(),
cf_zone_id: z.string(),
status: z.string(),
cert_monitoring: z.enum(['auto', 'required', 'skipped']).default('auto'),
environment: z.enum(['prod', 'staging', 'dev']).nullable().optional().default(null),
last_synced_at: z.string().nullable(),
created_at: z.string(),
updated_at: z.string(),
})
export const domainListItemSchema = domainSchema.extend({
group_name: z.string().nullable(),
service_count: z.number(),
health_status: z.enum(['up', 'down', 'degraded', 'unknown']).default('unknown'),
health_latency_ms: z.number().nullable().default(null),
tags: z.array(z.string()).default([]),
})
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(),
target_ip_weights: z.record(z.string(), z.number()).optional(),
target_ip_priorities: z.record(z.string(), z.number()).optional(),
lb_mode: z.enum(['round_robin', 'failover', 'weighted']).catch('round_robin'),
health_check_enabled: z.coerce.boolean().default(false),
health_check_type: z.enum(['tcp', 'http']).catch('tcp'),
health_check_port: z.number().nullable(),
health_check_path: z.string().nullable(),
health_check_expected_status: z.number().nullable(),
health_check_interval_sec: z.number().default(30),
health_check_timeout_ms: z.number().default(3000),
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]
: [],
target_ip_weights: binding.target_ip_weights ?? {},
target_ip_priorities: binding.target_ip_priorities ?? {},
}))
export const dnsRecordSchema = z.object({
id: z.number(),
domain_id: z.number(),
cf_record_id: z.string().nullable(),
record_type: z.string(),
name: z.string(),
content: z.string(),
ttl: z.number(),
proxied: z.boolean(),
priority: z.number().nullable(),
sync_status: z.string(),
origin: z.string(),
last_error: z.string().nullable(),
created_at: z.string(),
updated_at: z.string(),
})
export const certificateSchema = z.object({
id: z.number(),
domain_id: z.number(),
subdomain_id: z.number().nullable(),
hostname: z.string(),
expires_at: z.string().nullable(),
last_checked_at: z.string().nullable(),
last_error: z.string().nullable(),
status: z.string(),
created_at: z.string(),
updated_at: z.string(),
})
export type Group = z.infer<typeof groupSchema>
export type GroupWithStats = z.infer<typeof groupWithStatsSchema>
export type Service = z.infer<typeof serviceSchema>
export type ServiceDomainBinding = z.infer<typeof serviceDomainBindingSchema>
export type ServiceView = z.infer<typeof serviceViewSchema>
export type ServiceGroup = z.infer<typeof serviceGroupSchema>
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>
export const createGroupSchema = z.object({
name: z.string().min(1, '╨г╨║╨░╨╢╨╕╤В╨╡ ╨╜╨░╨╖╨▓╨░╨╜╨╕╨╡'),
slug: z.string().min(1, '╨г╨║╨░╨╢╨╕╤В╨╡ slug'),
})
const ipv4Schema = z
.string()
.regex(
/^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/,
'╨Э╨╡╨║╨╛╤А╤А╨╡╨║╤В╨╜╤Л╨╣ IPv4',
)
const lbModeSchema = z.enum(['round_robin', 'failover', 'weighted'])
const healthCheckTypeSchema = z.enum(['tcp', 'http'])
const healthCheckConfigFields = {
health_check_enabled: z.boolean().optional(),
health_check_type: healthCheckTypeSchema.optional(),
health_check_port: z.number().int().min(1).max(65535).nullable().optional(),
health_check_path: z.string().nullable().optional(),
health_check_expected_status: z.number().int().min(100).max(599).nullable().optional(),
health_check_interval_sec: z.number().int().min(5).max(3600).optional(),
health_check_timeout_ms: z.number().int().min(100).max(30000).optional(),
}
const serviceDomainInputSchema = z
.object({
fqdn: z.string().min(1, 'Укажите FQDN'),
target_ips: z.array(ipv4Schema).optional(),
target_cname: z.string().min(1, 'Укажите CNAME-цель').optional(),
target_ip_weights: z.record(ipv4Schema, z.number().int().min(1).max(100)).optional(),
target_ip_priorities: z.record(ipv4Schema, z.number().int().min(1).max(100)).optional(),
lb_mode: lbModeSchema.optional(),
...healthCheckConfigFields,
})
.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, '╨г╨║╨░╨╢╨╕╤В╨╡ ╨╜╨░╨╖╨▓╨░╨╜╨╕╨╡'),
slug: z.string().min(1, '╨г╨║╨░╨╢╨╕╤В╨╡ slug'),
})
export const createServiceWithConfigSchema = createServiceSchema.extend({
service_group_id: z.number().nullable().optional(),
ips: z.array(ipv4Schema).default([]),
lb_weight: z.number().int().min(1).max(100).optional(),
lb_priority: z.number().int().min(1).max(100).optional(),
domains: z.array(serviceDomainInputSchema).default([]),
})
export const createServiceBindingSchema = z.object({
domain_id: z.string().min(1, '╨Т╤Л╨▒╨╡╤А╨╕╤В╨╡ ╨┤╨╛╨╝╨╡╨╜'),
service_id: z.string().min(1, '╨Т╤Л╨▒╨╡╤А╨╕╤В╨╡ ╤Б╨╡╤А╨▓╨╕╤Б'),
hostname: z.string().optional(),
target_ip: z.string().optional(),
})
export const updateDomainGroupSchema = z.object({
group_id: z.number().nullable(),
status: z.string().optional(),
})
export const createDomainSchema = z.object({
zone_name: z.string().min(1, '╨г╨║╨░╨╢╨╕╤В╨╡ ╨╕╨╝╤П ╨╖╨╛╨╜╤Л'),
group_id: z.string(),
})
export const loginSchema = z.object({
username: z.string().min(1, '╨г╨║╨░╨╢╨╕╤В╨╡ ╨╕╨╝╤П ╨┐╨╛╨╗╤М╨╖╨╛╨▓╨░╤В╨╡╨╗╤П'),
password: z.string().min(1, '╨г╨║╨░╨╢╨╕╤В╨╡ ╨┐╨░╤А╨╛╨╗╤М'),
})
export const createDnsRecordSchema = z.object({
record_type: z.enum(['A', 'AAAA', 'CNAME', 'TXT', 'MX']),
name: z.string().min(1, '╨г╨║╨░╨╢╨╕╤В╨╡ ╨╕╨╝╤П'),
content: z.string().min(1, '╨г╨║╨░╨╢╨╕╤В╨╡ ╨╖╨╜╨░╤З╨╡╨╜╨╕╨╡'),
ttl: z.number().int().min(1),
proxied: z.boolean(),
})
export type CreateGroupInput = z.infer<typeof createGroupSchema>
export type CreateServiceInput = z.infer<typeof createServiceSchema>
export type CreateServiceWithConfigInput = z.infer<typeof createServiceWithConfigSchema>
export const updateServiceConfigSchema = z.object({
name: z.string().min(1, 'name').optional(),
slug: z.string().min(1, 'slug').optional(),
service_group_id: z.number().nullable().optional(),
ips: z.array(ipv4Schema).optional(),
lb_weight: z.number().int().min(1).max(100).optional(),
lb_priority: z.number().int().min(1).max(100).optional(),
domains: z
.array(serviceDomainInputSchema)
.optional(),
})
export type UpdateServiceConfigInput = z.infer<typeof updateServiceConfigSchema>
export const updateServiceGroupSchema = z.object({
name: z.string().min(1, 'name').optional(),
type: serviceGroupTypeSchema.optional(),
icon: z.string().nullable().optional(),
domain: z.string().nullable().optional(),
lb_mode: lbModeSchema.optional(),
...healthCheckConfigFields,
})
export type UpdateServiceGroupInput = z.infer<typeof updateServiceGroupSchema>
export const createServiceGroupSchema = z.object({
name: z.string().min(1, 'name'),
type: serviceGroupTypeSchema.default('custom'),
icon: z.string().nullable().optional(),
domain: z.string().nullable().optional(),
lb_mode: lbModeSchema.optional(),
...healthCheckConfigFields,
})
export const ipHealthStatusSchema = z.object({
scope: z.enum(['binding', 'group']),
ref_id: z.number(),
ip: z.string(),
status: z.enum(['up', 'down', 'degraded', 'unknown']),
latency_ms: z.number().nullable(),
consecutive_failures: z.number(),
last_checked_at: z.string().nullable(),
last_error: z.string().nullable(),
})
export type IpHealthStatus = z.infer<typeof ipHealthStatusSchema>
export const toggleEnabledSchema = z.object({
enabled: z.boolean(),
})
export type CreateServiceGroupInput = z.infer<typeof createServiceGroupSchema>
export type ToggleEnabledInput = z.infer<typeof toggleEnabledSchema>
export type CreateServiceBindingInput = z.infer<typeof createServiceBindingSchema>
export type CreateDomainInput = z.infer<typeof createDomainSchema>
export type LoginInput = z.infer<typeof loginSchema>
export type CreateDnsRecordInput = z.infer<typeof createDnsRecordSchema>
export {
bulkUpdateDomainsSchema,
createDomainMonitorSchema,
createSubdomainSchema,
domainMonitorResultSchema,
domainMonitorSchema,
notificationLogSchema,
subdomainSchema,
updateDomainSchema,
updateSubdomainSchema,
type BulkUpdateDomainsInput,
type CreateDomainMonitorInput,
type CreateSubdomainInput,
type DomainMonitor,
type DomainMonitorResult,
type DomainMonitorType,
type NotificationLog,
type SubdomainRecord,
type UpdateDomainInput,
type UpdateSubdomainInput,
} from '@cfdm/shared'