"use client"; import { useState } from "react"; import { useFormState } from "react-dom"; import { Cloud, Globe, Network, Pin, Activity, ScrollText, Settings2, UserCheck, MapPin, KeyRound, } from "lucide-react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { StatusChip } from "@/components/ui/StatusChip"; import type { GeneralSettings, AuthentikSettings, MetricsSettings, LoggingSettings, DnsSettings, DnsProviderSettings, UpstreamDnsResolutionSettings, GeoBlockSettings, } from "@/lib/settings"; import type { DnsProviderDefinition } from "@/src/lib/dns-providers"; import { GeoBlockFields } from "@/components/proxy-hosts/GeoBlockFields"; import OAuthProvidersSection from "./OAuthProvidersSection"; import type { OAuthProvider } from "@/src/lib/models/oauth-providers"; import { updateDnsProviderSettingsAction, updateGeneralSettingsAction, updateAuthentikSettingsAction, updateMetricsSettingsAction, updateLoggingSettingsAction, updateDnsSettingsAction, updateUpstreamDnsResolutionSettingsAction, updateInstanceModeAction, updateSlaveMasterTokenAction, createSlaveInstanceAction, deleteSlaveInstanceAction, toggleSlaveInstanceAction, syncSlaveInstancesAction, updateGeoBlockSettingsAction, } from "./actions"; import { ReactNode } from "react"; // ─── Alert helpers ──────────────────────────────────────────────────────────── function StatusAlert({ message, success }: { message: string; success: boolean }) { return ( {message} ); } function InfoAlert({ children }: { children: ReactNode }) { return ( {children} ); } function WarnAlert({ children }: { children: ReactNode }) { return ( {children} ); } // ─── Section card ───────────────────────────────────────────────────────────── type AccentConfig = { border: string; icon: string }; function SettingSection({ icon, title, description, accent, children, }: { icon: ReactNode; title: string; description?: string; accent: AccentConfig; children: ReactNode; }) { return (
{icon}

{title}

{description && (

{description}

)}
{children}
); } // ─── Accents ────────────────────────────────────────────────────────────────── const A: Record = { sync: { border: "border-l-violet-500", icon: "border-violet-500/30 bg-violet-500/10 text-violet-500" }, general: { border: "border-l-zinc-400", icon: "border-zinc-500/30 bg-zinc-500/10 text-zinc-500" }, dnsProvider:{ border: "border-l-orange-500", icon: "border-orange-500/30 bg-orange-500/10 text-orange-500" }, dns: { border: "border-l-cyan-500", icon: "border-cyan-500/30 bg-cyan-500/10 text-cyan-500" }, upstreamDns:{ border: "border-l-emerald-500", icon: "border-emerald-500/30 bg-emerald-500/10 text-emerald-500" }, authentik: { border: "border-l-purple-500", icon: "border-purple-500/30 bg-purple-500/10 text-purple-500" }, metrics: { border: "border-l-rose-500", icon: "border-rose-500/30 bg-rose-500/10 text-rose-500" }, logging: { border: "border-l-amber-500", icon: "border-amber-500/30 bg-amber-500/10 text-amber-500" }, geoblock: { border: "border-l-teal-500", icon: "border-teal-500/30 bg-teal-500/10 text-teal-500" }, oauth: { border: "border-l-indigo-500", icon: "border-indigo-500/30 bg-indigo-500/10 text-indigo-500" }, }; // ─── Props ──────────────────────────────────────────────────────────────────── type Props = { general: GeneralSettings | null; dnsProvider: DnsProviderSettings | null; dnsProviderDefinitions: DnsProviderDefinition[]; authentik: AuthentikSettings | null; metrics: MetricsSettings | null; logging: LoggingSettings | null; dns: DnsSettings | null; upstreamDnsResolution: UpstreamDnsResolutionSettings | null; globalGeoBlock?: GeoBlockSettings | null; oauthProviders: OAuthProvider[]; baseUrl: string; instanceSync: { mode: "standalone" | "master" | "slave"; modeFromEnv: boolean; tokenFromEnv: boolean; overrides: { general: boolean; dnsProvider: boolean; authentik: boolean; metrics: boolean; logging: boolean; dns: boolean; upstreamDnsResolution: boolean; }; slave: { hasToken: boolean; lastSyncAt: string | null; lastSyncError: string | null; } | null; master: { instances: Array<{ id: number; name: string; baseUrl: string; enabled: boolean; lastSyncAt: string | null; lastSyncError: string | null; }>; envInstances: Array<{ name: string; url: string; }>; } | null; }; }; // ─── Component ──────────────────────────────────────────────────────────────── export default function SettingsClient({ general, dnsProvider, dnsProviderDefinitions, authentik, metrics, logging, dns, upstreamDnsResolution, globalGeoBlock, oauthProviders, baseUrl, instanceSync }: Props) { const [generalState, generalFormAction] = useFormState(updateGeneralSettingsAction, null); const [dnsProviderState, dnsProviderFormAction] = useFormState(updateDnsProviderSettingsAction, null); const [selectedProvider, setSelectedProvider] = useState("none"); const configuredProviders = dnsProvider?.providers ? Object.keys(dnsProvider.providers) : []; const [authentikState, authentikFormAction] = useFormState(updateAuthentikSettingsAction, null); const [metricsState, metricsFormAction] = useFormState(updateMetricsSettingsAction, null); const [loggingState, loggingFormAction] = useFormState(updateLoggingSettingsAction, null); const [dnsState, dnsFormAction] = useFormState(updateDnsSettingsAction, null); const [upstreamDnsResolutionState, upstreamDnsResolutionFormAction] = useFormState( updateUpstreamDnsResolutionSettingsAction, null ); const [instanceModeState, instanceModeFormAction] = useFormState(updateInstanceModeAction, null); const [slaveTokenState, slaveTokenFormAction] = useFormState(updateSlaveMasterTokenAction, null); const [slaveInstanceState, slaveInstanceFormAction] = useFormState(createSlaveInstanceAction, null); const [syncState, syncFormAction] = useFormState(syncSlaveInstancesAction, null); const [geoBlockState, geoBlockFormAction] = useFormState(updateGeoBlockSettingsAction, null); const isSlave = instanceSync.mode === "slave"; const isMaster = instanceSync.mode === "master"; const [generalOverride, setGeneralOverride] = useState(instanceSync.overrides.general); const [dnsProviderOverride, setDnsProviderOverride] = useState(instanceSync.overrides.dnsProvider); const [authentikOverride, setAuthentikOverride] = useState(instanceSync.overrides.authentik); const [metricsOverride, setMetricsOverride] = useState(instanceSync.overrides.metrics); const [loggingOverride, setLoggingOverride] = useState(instanceSync.overrides.logging); const [dnsOverride, setDnsOverride] = useState(instanceSync.overrides.dns); const [upstreamDnsResolutionOverride, setUpstreamDnsResolutionOverride] = useState( instanceSync.overrides.upstreamDnsResolution ); return (

Settings

Configure organization-wide defaults and DNS automation.

{/* ── Instance Sync ── */} } title="Instance Sync" description="Choose whether this instance acts independently, pushes configuration to slave nodes, or pulls configuration from a master." accent={A.sync} >
{instanceSync.modeFromEnv && ( Instance mode is configured via INSTANCE_MODE environment variable and cannot be changed at runtime. )} {instanceModeState?.message && ( )}
{isSlave && (

Master Connection

{instanceSync.tokenFromEnv && ( Sync token is configured via INSTANCE_SYNC_TOKEN environment variable and cannot be changed at runtime. )} {slaveTokenState?.message && ( )} {instanceSync.slave?.hasToken && !instanceSync.tokenFromEnv && ( A master sync token is configured. Leave the token field blank to keep it, or select “Remove existing token” to delete it. )}
{instanceSync.slave?.lastSyncError ? ( {instanceSync.slave?.lastSyncAt ? `Last sync: ${instanceSync.slave.lastSyncAt} (${instanceSync.slave.lastSyncError})` : "No sync payload has been received yet."} ) : ( {instanceSync.slave?.lastSyncAt ? `Last sync: ${instanceSync.slave.lastSyncAt}` : "No sync payload has been received yet."} )}
)} {isMaster && (

Slave Instances

{slaveInstanceState?.message && ( )}
{syncState?.message && ( )}
{instanceSync.master?.instances.length === 0 && instanceSync.master?.envInstances.length === 0 && ( No slave instances configured yet. )} {instanceSync.master?.envInstances && instanceSync.master.envInstances.length > 0 && ( <>

Environment-configured (INSTANCE_SLAVES)

{instanceSync.master.envInstances.map((instance, index) => (

{instance.name}

{instance.url}

))} )} {instanceSync.master?.instances && instanceSync.master.instances.length > 0 && (

UI-configured instances

)} {instanceSync.master?.instances.map((instance) => (

{instance.name}

{instance.baseUrl}

{instance.lastSyncAt ? `Last sync: ${instance.lastSyncAt}` : "No sync yet"} {instance.lastSyncError && ( {instance.lastSyncError} )}
))}
)}
{/* ── General ── */} } title="General" accent={A.general} >
{generalState?.message && ( )} {isSlave && (
setGeneralOverride(!!v)} />
)}
{/* ── DNS Providers ── */} } title="DNS Providers" description="Configure DNS providers for ACME DNS-01 challenges (required for wildcard certificates). You can add multiple providers and select a default." accent={A.dnsProvider} > {dnsProviderState?.message && ( )} {isSlave && (
setDnsProviderOverride(!!v)} />
)} {/* Configured providers list */} {configuredProviders.length > 0 && (
{configuredProviders.map((name) => { const def = dnsProviderDefinitions.find((p) => p.name === name); const isDefault = dnsProvider?.default === name; return (
{def?.displayName ?? name} {isDefault && }
{!isDefault && (
{isSlave && }
)}
{isSlave && }
); })} {dnsProvider?.default && (
{isSlave && }
)}
)} {/* Add / update provider form */}
{/* Dynamic credential fields */} {selectedProvider && selectedProvider !== "none" && (() => { const providerDef = dnsProviderDefinitions.find((p) => p.name === selectedProvider); if (!providerDef) return null; const isUpdate = configuredProviders.includes(selectedProvider); return (
{providerDef.description && (

{providerDef.description}

)} {providerDef.fields.map((field) => (
{field.description && (

{field.description}

)}
))} {isUpdate && ( Credentials are already configured. Leave fields blank to keep existing values. )} {providerDef.docsUrl && (

Provider documentation

)}
); })()} {isSlave && }
{/* ── DNS Resolvers ── */} } title="DNS Resolvers" description="Configure custom DNS resolvers for ACME DNS-01 challenges. These resolvers will be used to verify DNS records during certificate issuance." accent={A.dns} >
{dnsState?.message && ( )} {isSlave && (
setDnsOverride(!!v)} />
)}