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
- 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]>
396 lines
14 KiB
TypeScript
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'
|