"use client"; import { useState } from "react"; import { useFormState } from "react-dom"; import { Alert, Box, Button, Card, CardContent, Checkbox, FormControlLabel, MenuItem, Stack, TextField, Typography } from "@mui/material"; import type { GeneralSettings, AuthentikSettings, MetricsSettings, LoggingSettings, DnsSettings, UpstreamDnsResolutionSettings, GeoBlockSettings } from "@/src/lib/settings"; import { GeoBlockFields } from "@/src/components/proxy-hosts/GeoBlockFields"; import { updateCloudflareSettingsAction, updateGeneralSettingsAction, updateAuthentikSettingsAction, updateMetricsSettingsAction, updateLoggingSettingsAction, updateDnsSettingsAction, updateUpstreamDnsResolutionSettingsAction, updateInstanceModeAction, updateSlaveMasterTokenAction, createSlaveInstanceAction, deleteSlaveInstanceAction, toggleSlaveInstanceAction, syncSlaveInstancesAction, updateGeoBlockSettingsAction } from "./actions"; type Props = { general: GeneralSettings | null; cloudflare: { hasToken: boolean; zoneId?: string; accountId?: string; }; authentik: AuthentikSettings | null; metrics: MetricsSettings | null; logging: LoggingSettings | null; dns: DnsSettings | null; upstreamDnsResolution: UpstreamDnsResolutionSettings | null; globalGeoBlock?: GeoBlockSettings | null; instanceSync: { mode: "standalone" | "master" | "slave"; modeFromEnv: boolean; tokenFromEnv: boolean; overrides: { general: boolean; cloudflare: 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; base_url: string; enabled: boolean; last_sync_at: string | null; last_sync_error: string | null; }>; envInstances: Array<{ name: string; url: string; }>; } | null; }; }; export default function SettingsClient({ general, cloudflare, authentik, metrics, logging, dns, upstreamDnsResolution, globalGeoBlock, instanceSync }: Props) { const [generalState, generalFormAction] = useFormState(updateGeneralSettingsAction, null); const [cloudflareState, cloudflareFormAction] = useFormState(updateCloudflareSettingsAction, null); 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 [cloudflareOverride, setCloudflareOverride] = useState(instanceSync.overrides.cloudflare); 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 Choose whether this instance acts independently, pushes configuration to slave nodes, or pulls configuration from a master. {instanceSync.modeFromEnv && ( Instance mode is configured via INSTANCE_MODE environment variable and cannot be changed at runtime. )} {instanceModeState?.message && ( {instanceModeState.message} )} Standalone Master Slave {isSlave && ( Master Connection {instanceSync.tokenFromEnv && ( Sync token is configured via INSTANCE_SYNC_TOKEN environment variable and cannot be changed at runtime. )} {slaveTokenState?.message && ( {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. )} } label="Remove existing token" disabled={!instanceSync.slave?.hasToken || instanceSync.tokenFromEnv} /> {instanceSync.slave?.lastSyncAt ? `Last sync: ${instanceSync.slave.lastSyncAt}${instanceSync.slave.lastSyncError ? ` (${instanceSync.slave.lastSyncError})` : ""}` : "No sync payload has been received yet."} )} {isMaster && ( Slave Instances {slaveInstanceState?.message && ( {slaveInstanceState.message} )} {syncState?.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 instances (via INSTANCE_SLAVES) {instanceSync.master.envInstances.map((instance, index) => ( {instance.name} {instance.url} Configured via environment variable ))} )} {instanceSync.master?.instances && instanceSync.master.instances.length > 0 && ( UI-configured instances )} {instanceSync.master?.instances.map((instance) => ( {instance.name} {instance.base_url} {instance.last_sync_at ? `Last sync: ${instance.last_sync_at}` : "No sync yet"} {instance.last_sync_error && ( {instance.last_sync_error} )} ))} )} General {generalState?.message && ( {generalState.message} )} {isSlave && ( setGeneralOverride(event.target.checked)} /> } label="Override master settings" /> )} Cloudflare DNS Configure a Cloudflare API token with Zone.DNS Edit permissions to enable DNS-01 challenges for wildcard certificates. {cloudflare.hasToken && ( A Cloudflare API token is already configured. Leave the token field blank to keep it, or select “Remove existing token” to delete it. )} {cloudflareState?.message && ( {cloudflareState.message} )} {isSlave && ( setCloudflareOverride(event.target.checked)} /> } label="Override master settings" /> )} } label="Remove existing token" disabled={!cloudflare.hasToken || (isSlave && !cloudflareOverride)} /> DNS Resolvers Configure custom DNS resolvers for ACME DNS-01 challenges. These resolvers will be used to verify DNS records during certificate issuance. {dnsState?.message && ( {dnsState.message} )} {isSlave && ( setDnsOverride(event.target.checked)} /> } label="Override master settings" /> )} } label="Enable custom DNS resolvers" /> Custom DNS resolvers are useful when your DNS provider has slow propagation or when using split-horizon DNS. Common public resolvers: 1.1.1.1 (Cloudflare), 8.8.8.8 (Google), 9.9.9.9 (Quad9). Upstream DNS Pinning Optionally resolve upstream hostnames when applying config and pin reverse proxy upstream dials to IP addresses. This can avoid runtime DNS churn and lets you force IPv6, IPv4, or both (IPv6 preferred). {upstreamDnsResolutionState?.message && ( {upstreamDnsResolutionState.message} )} {isSlave && ( setUpstreamDnsResolutionOverride(event.target.checked)} /> } label="Override master settings" /> )} } label="Enable upstream DNS pinning during config apply" /> Both (Prefer IPv6) IPv6 only IPv4 only Host-level settings can override this default. Resolution happens at config save/reload time and resolved IPs are written into Caddy's active config. If one handler has multiple different HTTPS upstream hostnames, HTTPS pinning is skipped for those HTTPS upstreams to avoid SNI mismatch. Authentik Defaults Set default Authentik forward authentication values. These will be pre-filled when creating new proxy hosts but can be customized per host. {authentikState?.message && ( {authentikState.message} )} {isSlave && ( setAuthentikOverride(event.target.checked)} /> } label="Override master settings" /> )} Metrics & Monitoring Enable Caddy metrics exposure for monitoring with Prometheus, Grafana, or other observability tools. Metrics will be available at http://caddy:{metrics?.port ?? 9090}/metrics on a separate port (NOT the admin API port for security). {metricsState?.message && ( {metricsState.message} )} {isSlave && ( setMetricsOverride(event.target.checked)} /> } label="Override master settings" /> )} } label="Enable metrics endpoint" /> After enabling metrics, configure your monitoring tool to scrape http://caddy-proxy-manager-caddy:{metrics?.port ?? 9090}/metrics from within the Docker network. To expose metrics externally, add a port mapping like "{metrics?.port ?? 9090}:{metrics?.port ?? 9090}" in docker-compose.yml. Access Logging Enable HTTP access logging to track all requests going through your proxy hosts. Logs will be stored in the caddy-logs directory and mounted at /logs/access.log inside the container. {loggingState?.message && ( {loggingState.message} )} {isSlave && ( setLoggingOverride(event.target.checked)} /> } label="Override master settings" /> )} } label="Enable access logging" /> JSON Console (Common Log Format) Access logs are stored in the caddy-logs Docker volume. You can view them with: docker exec caddy-proxy-manager-caddy tail -f /logs/access.log Global Geoblocking Configure default geoblocking rules applied to all proxy hosts. Per-host rules can merge with or override these global defaults. {geoBlockState?.message && ( {geoBlockState.message} )} ); }