import { useState } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { DnsRecord, InsertDnsRecord, Domain, Provider, recordTypes } from "@shared/schema"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { useToast } from "@/hooks/use-toast"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescription, } from "@/components/ui/form"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { RecentActivity } from "@/components/activity/recent-activity"; import { Pagination } from "@/components/shared/pagination"; import { Loader2, FileText, Pencil, Trash2, Plus } from "lucide-react"; import { formatDistanceToNow } from "date-fns"; // DNS Record form schema const dnsRecordSchema = z.object({ name: z.string().min(1, "Record name is required"), type: z.enum(recordTypes), content: z.string().optional().or(z.literal('')), ttl: z.number().int().min(1).default(3600), proxied: z.boolean().default(false), isActive: z.boolean().default(true), isAutoIP: z.boolean().default(false), notes: z.string().optional(), }).refine(data => { // Different validation based on record type if ((data.type === 'A' || data.type === 'AAAA') && data.isAutoIP) { // Auto IP enabled, content not required return true; } else if (data.type === 'A') { // A record - validate IPv4 return !!data.content && /^(\d{1,3}\.){3}\d{1,3}$/.test(data.content.trim()); } else if (data.type === 'AAAA') { // AAAA record - accept any non-empty content for IPv6 (simplified validation) return !!data.content && data.content.trim().length > 0; } else if (data.type === 'CNAME' || data.type === 'MX' || data.type === 'NS') { // Domain-based records - ensure content is present and looks like domain return !!data.content && data.content.trim().length > 0; } else { // For all other types, just ensure content is non-empty return !!data.content && data.content.trim().length > 0; } }, { message: "Content is required and must be valid for the selected record type", path: ["content"] }); interface DomainDetailsProps { domain: Domain; onBack?: () => void; } export function DomainDetails({ domain, onBack }: DomainDetailsProps) { const { toast } = useToast(); const [isAddRecordDialogOpen, setIsAddRecordDialogOpen] = useState(false); const [isEditRecordDialogOpen, setIsEditRecordDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [selectedRecord, setSelectedRecord] = useState(null); const [currentPage, setCurrentPage] = useState(1); const pageSize = 5; // Fetch providers for the form const { data: providers = [] } = useQuery({ queryKey: ["/api/providers"], }); // Fetch DNS Records const { data: records = [], isLoading } = useQuery({ queryKey: ["/api/dns-records", domain.id], queryFn: async () => { const res = await apiRequest("GET", `/api/dns-records?domainId=${domain.id}`); return await res.json(); }, enabled: !!domain?.id, }); // Form for adding/editing a record const form = useForm>({ resolver: zodResolver(dnsRecordSchema), defaultValues: { name: "", type: "A", content: "", ttl: 3600, proxied: false, isActive: true, isAutoIP: false, notes: "", }, }); // Add record mutation const addRecordMutation = useMutation({ mutationFn: async (data: z.infer) => { // Prepare record data const recordData: InsertDnsRecord = { ...data, domainId: domain.id, // Inherit providerId from domain providerId: domain.providerId }; const res = await apiRequest("POST", "/api/dns-records", recordData); return await res.json(); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/dns-records", domain.id] }); setIsAddRecordDialogOpen(false); form.reset(); toast({ title: "Record added", description: "The DNS record has been successfully added.", }); }, onError: (error) => { toast({ title: "Failed to add record", description: error.message, variant: "destructive", }); }, }); // Update record mutation const updateRecordMutation = useMutation({ mutationFn: async (data: z.infer & { id: string }) => { const { id, ...updateData } = data; // Always use domain's provider for the record const updatedData = { ...updateData, providerId: domain.providerId }; const res = await apiRequest("PUT", `/api/dns-records/${id}`, updatedData); return await res.json(); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/dns-records", domain.id] }); setIsEditRecordDialogOpen(false); setSelectedRecord(null); toast({ title: "Record updated", description: "The DNS record has been successfully updated.", }); }, onError: (error) => { toast({ title: "Failed to update record", description: error.message, variant: "destructive", }); }, }); // Delete record mutation const deleteRecordMutation = useMutation({ mutationFn: async (recordId: string) => { await apiRequest("DELETE", `/api/dns-records/${recordId}`); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/dns-records", domain.id] }); setIsDeleteDialogOpen(false); setSelectedRecord(null); toast({ title: "Record deleted", description: "The DNS record has been successfully deleted.", }); }, onError: (error) => { toast({ title: "Failed to delete record", description: error.message, variant: "destructive", }); }, }); // Form submission handlers const onAddSubmit = (data: z.infer) => { addRecordMutation.mutate(data); }; const onEditSubmit = (data: z.infer) => { if (selectedRecord) { updateRecordMutation.mutate({ ...data, id: selectedRecord.id }); } }; // Handle record editing const handleEditRecord = (record: DnsRecord) => { setSelectedRecord(record); // Set form values for editing form.reset({ name: record.name, type: record.type as any, // Type cast to handle compatibility content: record.content, ttl: record.ttl as number, // Type cast to handle nullable proxied: record.proxied as boolean, // Type cast to handle nullable isActive: record.isActive, isAutoIP: record.isAutoIP, notes: record.notes || "", }); setIsEditRecordDialogOpen(true); }; // Handle record deletion const handleDeleteRecord = (record: DnsRecord) => { setSelectedRecord(record); setIsDeleteDialogOpen(true); }; // Confirm deletion const confirmDeleteRecord = () => { if (selectedRecord) { deleteRecordMutation.mutate(selectedRecord.id); } }; // Get a human-readable provider name from the provider ID const getProviderName = (providerId: string | null) => { if (!providerId) return "None"; const provider = providers.find(p => p.id === providerId); return provider ? provider.name : "Unknown Provider"; }; // Calculate pagination const startIndex = (currentPage - 1) * pageSize; const endIndex = startIndex + pageSize; const paginatedRecords = records.slice(startIndex, endIndex); const totalPages = Math.ceil(records.length / pageSize); // Handle adding a new record const handleAddRecord = () => { // Reset form with defaults form.reset({ name: "", type: "A", content: "", ttl: 3600, proxied: false, isActive: true, isAutoIP: false, notes: "", }); setIsAddRecordDialogOpen(true); }; if (isLoading) { return (
); } return ( <>

DNS Records for {domain.name}

{records.length === 0 ? (

No DNS records found

Get started by adding your first DNS record.

) : (
Name Type Content TTL Provider Last Update Status Actions {paginatedRecords.map(record => ( {record.name === "@" ? ( {record.name}

Root domain: {domain.name}

) : ( record.name )}
{record.type} {record.isAutoIP ? "Auto IP" : record.content}

{record.isAutoIP ? "Auto IP" : record.content}

{record.notes && ( <>

{record.notes}

)}
{record.ttl}s {getProviderName(record.providerId)} {record.lastUpdated ? formatDistanceToNow(new Date(record.lastUpdated), { addSuffix: true }) : "Never"} {record.isActive ? ( Active ) : ( Inactive )}
))}
{records.length > pageSize && (
)}
)}
Recent Activity
{/* Add Record Dialog */} Add DNS Record Add a new DNS record to {domain.name}.
( Name Use @ for root domain or enter subdomain name )} /> ( Record Type )} /> {/* Moved proxied toggle above isAutoIP */} (
Proxied Enable proxying through CDN (Cloudflare only)
)} /> {(form.watch("type") === "A" || form.watch("type") === "AAAA") && ( (
Auto IP Address Automatically determine IP address using STUN
)} /> )} {(!form.watch("isAutoIP") || (form.watch("type") !== "A" && form.watch("type") !== "AAAA")) && ( ( Content The content of the DNS record (e.g. IP address, domain name) )} /> )} ( TTL (seconds) field.onChange(parseInt(e.target.value))} /> Time to live in seconds, how long DNS servers should cache this record )} /> (
Active Enable or disable this DNS record
)} /> ( Notes Add optional notes about this record (for internal reference only) )} />
{/* Edit Record Dialog */} Edit DNS Record Update the DNS record for {domain.name}.
( Name Use @ for root domain or enter subdomain name )} /> ( Record Type )} /> {/* Moved proxied toggle above isAutoIP */} (
Proxied Enable proxying through CDN (Cloudflare only)
)} /> {(form.watch("type") === "A" || form.watch("type") === "AAAA") && ( (
Auto IP Address Automatically determine IP address using STUN
)} /> )} {(!form.watch("isAutoIP") || (form.watch("type") !== "A" && form.watch("type") !== "AAAA")) && ( ( Content The content of the DNS record (e.g. IP address, domain name) )} /> )} ( TTL (seconds) field.onChange(parseInt(e.target.value))} /> Time to live in seconds, how long DNS servers should cache this record )} /> (
Active Enable or disable this DNS record
)} /> ( Notes Add optional notes about this record (for internal reference only) )} />
{/* Delete Confirmation Dialog */} Are you sure? This will permanently delete the DNS record "{selectedRecord?.name}" of type {selectedRecord?.type}. This action cannot be undone. Cancel {deleteRecordMutation.isPending && ( )} Delete {/* Back Button */} {onBack && (
)} ); }