"use client"; import React, { useCallback, useEffect, useState } from "react"; import { Globe, RefreshCw, Plus, KeyRound, CheckCircle2, AlertTriangle } from "lucide-react"; import { useTranslations } from "../../../../i18n/provider"; import { Card } from "../../../../components/ui/Card"; import { Badge } from "../../../../components/ui/Badge"; import { Button } from "../../../../components/ui/Button"; /** * Domains, read from the database. * * The previous version of this screen was a mockup end to end. It rendered a * fixed table of thirteen DNS records for "example.com", every one of them * marked VERIFIED; its onboarding wizard had four steps of which only the first * called anything, step three printing "Cloudflare connection validated, 13 * records created and propagated" without a request behind it; and its DKIM * rotation posted {domain_id: "default-domain"} to an endpoint whose field is * "domain", swallowed the resulting 400, and displayed a made-up TXT record as * though a key had been issued. Rotation therefore never once worked from here. * * What is shown now is what the API returns, and the one thing the backend * cannot yet do - verify individual DNS records against a resolver - says so * instead of drawing a table of green ticks. */ interface Domain { id: string; name: string; dns_status: string; dmarc_policy: string | null; mta_sts_mode: string | null; dane_enabled: boolean; is_active: boolean; dns_last_checked_at: string | null; mailbox_count: number; } function statusVariant(status: string): "success" | "warning" | "danger" { if (status === "VERIFIED") return "success"; if (status === "PENDING" || status === "PARTIAL") return "warning"; return "danger"; } export default function AdminDomainsPage() { const t = useTranslations(); const [activeTab, setActiveTab] = useState<"domains" | "onboarding" | "dkim">("domains"); const [domains, setDomains] = useState([]); const [selectedId, setSelectedId] = useState(""); const [loading, setLoading] = useState(true); const [notice, setNotice] = useState(null); const [error, setError] = useState(null); const [reconciling, setReconciling] = useState(false); const [onboardDomain, setOnboardDomain] = useState(""); const [onboarding, setOnboarding] = useState(false); const [dkimAlgorithm, setDkimAlgorithm] = useState<"ed25519" | "rsa2048">("ed25519"); const [dkimSelector, setDkimSelector] = useState("s1"); const [dkimRecord, setDkimRecord] = useState<{ name: string; value: string; overlapDays: number } | null>(null); const [rotating, setRotating] = useState(false); const load = useCallback(async () => { setLoading(true); try { const res = await fetch("/api/v1/admin/domains"); if (!res.ok) throw new Error(); const data: Domain[] = await res.json(); setDomains(data); // Keeps the current pick if it still exists, so a refresh after an action // does not silently retarget the next operation at another domain. setSelectedId((prev) => (data.some((d) => d.id === prev) ? prev : (data[0]?.id ?? ""))); setError(null); } catch { setDomains([]); setError(t("errors.loadFailed")); } finally { setLoading(false); } }, [t]); useEffect(() => { void load(); }, [load]); const selected = domains.find((d) => d.id === selectedId) ?? null; const handleReconcile = async () => { if (!selected) return; setReconciling(true); setNotice(null); setError(null); try { // The real domain id, not the literal string "default-domain" that used // to be sent here for every domain on the server. const res = await fetch("/api/v1/admin/domains/reconcile", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ domain_id: selected.id }), }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.message); setNotice(data.message ?? t("settings.settingsSaved")); await load(); } catch (err) { setError(err instanceof Error && err.message ? err.message : t("errors.serverError")); } finally { setReconciling(false); } }; const handleOnboard = async (e: React.FormEvent) => { e.preventDefault(); if (!onboardDomain.trim()) return; setOnboarding(true); setNotice(null); setError(null); try { const res = await fetch("/api/v1/admin/domains/onboard", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ domain: onboardDomain.trim() }), }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.message); setOnboardDomain(""); setNotice(`${data.name}`); await load(); setActiveTab("domains"); } catch (err) { setError(err instanceof Error && err.message ? err.message : t("errors.serverError")); } finally { setOnboarding(false); } }; const handleRotateDkim = async () => { if (!selected) return; setRotating(true); setNotice(null); setError(null); setDkimRecord(null); try { // The endpoint's field is "domain" and it wants the name. Sending the // wrong field is what made every previous rotation fail unnoticed. const res = await fetch("/api/v1/admin/dkim/rotate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ domain: selected.name, selector: dkimSelector, algorithm: dkimAlgorithm, }), }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.message); // The key that was actually generated and sealed, not a sample string. setDkimRecord({ name: `${data.selector}._domainkey.${data.domain}`, value: data.public_key_record, overlapDays: data.overlap_days, }); } catch (err) { setError(err instanceof Error && err.message ? err.message : t("errors.serverError")); } finally { setRotating(false); } }; const tabs = [ { id: "domains" as const, label: t("admin.domainsTitle"), icon: Globe }, { id: "onboarding" as const, label: t("common.add"), icon: Plus }, { id: "dkim" as const, label: "DKIM", icon: KeyRound }, ]; return (

{t("admin.domainsTitle")}

{tabs.map((tab) => { const Icon = tab.icon; return ( ); })}
{notice && (
{notice}
)} {error && (
{error}
)} {activeTab === "domains" && (
{domains.length === 0 && !loading ? ( {t("admin.noDomains")} ) : ( domains.map((d) => (
{d.name}
{t("admin.mailboxCount", { count: d.mailbox_count })} {d.dmarc_policy ? ` | DMARC: ${d.dmarc_policy}` : ""} {d.mta_sts_mode ? ` | MTA-STS: ${d.mta_sts_mode}` : ""} {d.dane_enabled ? " | DANE" : ""}
{d.dns_status}
{t("admin.lastChecked", { when: d.dns_last_checked_at ? new Date(d.dns_last_checked_at).toLocaleString() : t("common.never"), })}
)) )} {/* Says what reconciliation currently does. The old copy announced "Cloudflare sync finished: all 13 DNS records verified" on every press - including when the request failed - while the endpoint behind it only writes an audit entry. */}

{t("admin.reconcileNotImplemented")}

)} {activeTab === "onboarding" && (

{t("admin.onboardTitle")}

{t("admin.onboardIntro")}

setOnboardDomain(e.target.value)} placeholder="example.org" required className="w-full px-3.5 py-2.5 rounded-xl bg-slate-900/90 border border-slate-800 text-white placeholder-slate-500 focus:outline-none focus:border-emerald-500" />
)} {activeTab === "dkim" && (

{t("admin.dkimTitle")}

{selected ? selected.name : t("admin.noDomains")}

setDkimSelector(e.target.value)} pattern="[A-Za-z0-9-]{1,63}" className="w-full px-3.5 py-2.5 rounded-xl bg-slate-900/90 border border-slate-800 text-white font-mono focus:outline-none focus:border-emerald-500" />
{dkimRecord && (
{t("admin.dkimPublishRecord")}
{dkimRecord.name} IN TXT "{dkimRecord.value}"
{t("admin.dkimOverlap", { days: dkimRecord.overlapDays })}
)}
)}
); }