"use client"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { InfoSection, InfoSectionContent, InfoSections, InfoSectionTitle } from "@app/components/InfoSection"; import { useTranslations } from "next-intl"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useDomainContext } from "@app/hooks/useDomainContext"; import { SettingsContainer, SettingsSection, SettingsSectionBody, SettingsSectionDescription, SettingsSectionFooter, SettingsSectionForm, SettingsSectionHeader, SettingsSectionTitle } from "./Settings"; import { Button } from "./ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescription } from "@app/components/ui/form"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; import { Input } from "./ui/input"; import { useForm } from "react-hook-form"; import z from "zod"; import { toASCII } from "punycode"; import { zodResolver } from "@hookform/resolvers/zod"; import { build } from "@server/build"; import { Switch } from "./ui/switch"; import { useEffect, useState } from "react"; import DNSRecordsTable, { DNSRecordRow } from "./DNSRecordTable"; import { createApiClient } from "@app/lib/api"; import { useToast } from "@app/hooks/useToast"; import { formatAxiosError } from "@app/lib/api"; import { Badge } from "./ui/badge"; type DomainInfoCardProps = { orgId?: string; domainId?: string; }; // Helper functions for Unicode domain handling function toPunycode(domain: string): string { try { const parts = toASCII(domain); return parts; } catch (error) { return domain.toLowerCase(); } } function isValidDomainFormat(domain: string): boolean { const unicodeRegex = /^(?!:\/\/)([^\s.]+\.)*[^\s.]+$/; if (!unicodeRegex.test(domain)) { return false; } const parts = domain.split("."); for (const part of parts) { if (part.length === 0 || part.startsWith("-") || part.endsWith("-")) { return false; } if (part.length > 63) { return false; } } if (domain.length > 253) { return false; } return true; } const formSchema = z.object({ baseDomain: z .string() .min(1, "Domain is required") .refine((val) => isValidDomainFormat(val), "Invalid domain format") .transform((val) => toPunycode(val)), type: z.enum(["ns", "cname", "wildcard"]), certResolver: z.string().nullable().optional(), preferWildcardCert: z.boolean().optional() }); type FormValues = z.infer; const certResolverOptions = [ { id: "default", title: "Default" }, { id: "custom", title: "Custom Resolver" } ]; export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps) { const { domain, updateDomain } = useDomainContext(); const t = useTranslations(); const { env } = useEnvContext(); const api = createApiClient(useEnvContext()); const { toast } = useToast(); const [dnsRecords, setDnsRecords] = useState([]); const [loadingRecords, setLoadingRecords] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); const [saveLoading, setSaveLoading] = useState(false); const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { baseDomain: "", type: build == "oss" || !env.flags.usePangolinDns ? "wildcard" : "ns", certResolver: domain.certResolver, preferWildcardCert: false } }); useEffect(() => { if (domain.domainId) { const certResolverValue = domain.certResolver && domain.certResolver.trim() !== "" ? domain.certResolver : null; form.reset({ baseDomain: domain.baseDomain || "", type: (domain.type as "ns" | "cname" | "wildcard") || "wildcard", certResolver: certResolverValue, preferWildcardCert: domain.preferWildcardCert || false }); } }, [domain]); const fetchDNSRecords = async (showRefreshing = false) => { if (showRefreshing) { setIsRefreshing(true); } else { setLoadingRecords(true); } try { const response = await api.get<{ data: DNSRecordRow[] }>( `/org/${orgId}/domain/${domainId}/dns-records` ); setDnsRecords(response.data.data); } catch (error) { // Only show error if records exist (not a 404) const err = error as any; if (err?.response?.status !== 404) { toast({ title: t("error"), description: formatAxiosError(error), variant: "destructive" }); } } finally { setLoadingRecords(false); setIsRefreshing(false); } }; useEffect(() => { if (domain.domainId) { fetchDNSRecords(); } }, [domain.domainId]); const onSubmit = async (values: FormValues) => { if (!orgId || !domainId) { toast({ title: t("error"), description: t("orgOrDomainIdMissing", { fallback: "Organization or Domain ID is missing" }), variant: "destructive" }); return; } setSaveLoading(true); try { if (!values.certResolver) { values.certResolver = null; } await api.patch( `/org/${orgId}/domain/${domainId}`, { certResolver: values.certResolver, preferWildcardCert: values.preferWildcardCert } ); updateDomain({ ...domain, certResolver: values.certResolver || null, preferWildcardCert: values.preferWildcardCert || false }); toast({ title: t("success"), description: t("domainSettingsUpdated", { fallback: "Domain settings updated successfully" }), variant: "default" }); } catch (error) { toast({ title: t("error"), description: formatAxiosError(error), variant: "destructive" }); } finally { setSaveLoading(false); } }; const getTypeDisplay = (type: string) => { switch (type) { case "ns": return t("selectDomainTypeNsName"); case "cname": return t("selectDomainTypeCnameName"); case "wildcard": return t("selectDomainTypeWildcardName"); default: return type; } }; return ( <> {t("type")} {getTypeDisplay( domain.type ? domain.type : "" )} {t("status")} {domain.verified ? ( domain.type === "wildcard" ? ( {t("manual", { fallback: "Manual" })} ) : ( {t("verified")} ) ) : ( {t("pending", { fallback: "Pending" })} )} {domain.type === "wildcard" && ( {t("domainSetting")}
<> ( {t("certResolver")} )} /> {form.watch("certResolver") !== null && form.watch("certResolver") !== "default" && ( ( field.onChange( e .target .value ) } /> )} /> )} {form.watch("certResolver") !== null && form.watch("certResolver") !== "default" && ( (
{t( "preferWildcardCert" )}
{t( "preferWildcardCertDescription" )}
)} /> )}
)} ); }