"use client"; import { useMemo, useState, useEffect } from "react"; import { Alert, Box, Button, Card, CardContent, Checkbox, Chip, Collapse, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, IconButton, MenuItem, Stack, Switch, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Typography, Tooltip } from "@mui/material"; import AddIcon from "@mui/icons-material/Add"; import EditIcon from "@mui/icons-material/Edit"; import DeleteIcon from "@mui/icons-material/Delete"; import CloseIcon from "@mui/icons-material/Close"; import { useFormState } from "react-dom"; import type { AccessList } from "@/src/lib/models/access-lists"; import type { Certificate } from "@/src/lib/models/certificates"; import type { ProxyHost } from "@/src/lib/models/proxy-hosts"; import { INITIAL_ACTION_STATE, type ActionState } from "@/src/lib/actions"; import { createProxyHostAction, deleteProxyHostAction, updateProxyHostAction } from "./actions"; type Props = { hosts: ProxyHost[]; certificates: Certificate[]; accessLists: AccessList[]; }; const AUTHENTIK_DEFAULT_HEADERS = [ "X-Authentik-Username", "X-Authentik-Groups", "X-Authentik-Entitlements", "X-Authentik-Email", "X-Authentik-Name", "X-Authentik-Uid", "X-Authentik-Jwt", "X-Authentik-Meta-Jwks", "X-Authentik-Meta-Outpost", "X-Authentik-Meta-Provider", "X-Authentik-Meta-App", "X-Authentik-Meta-Version" ]; const AUTHENTIK_DEFAULT_TRUSTED_PROXIES = ["private_ranges"]; export default function ProxyHostsClient({ hosts, certificates, accessLists }: Props) { const [createOpen, setCreateOpen] = useState(false); const [editHost, setEditHost] = useState(null); const [deleteHost, setDeleteHost] = useState(null); return ( Proxy Hosts Define HTTP(S) reverse proxies orchestrated by Caddy with automated certificates. Name Domains Upstreams Certificate Status Actions {hosts.length === 0 ? ( No proxy hosts configured. Click "Create Host" to add one. ) : ( hosts.map((host) => { const certificate = host.certificate_id ? certificates.find(c => c.id === host.certificate_id) : null; const certName = certificate?.name ?? "Managed by Caddy (Auto)"; return ( {host.name} {host.domains.slice(0, 2).join(", ")} {host.domains.length > 2 && ` +${host.domains.length - 2} more`} {host.upstreams.slice(0, 2).join(", ")} {host.upstreams.length > 2 && ` +${host.upstreams.length - 2} more`} {certName} setEditHost(host)} sx={{ color: "rgba(99, 102, 241, 0.8)", "&:hover": { bgcolor: "rgba(99, 102, 241, 0.1)" } }} > setDeleteHost(host)} sx={{ color: "rgba(239, 68, 68, 0.8)", "&:hover": { bgcolor: "rgba(239, 68, 68, 0.1)" } }} > ); }) )}
setCreateOpen(false)} certificates={certificates} accessLists={accessLists} /> {editHost && ( setEditHost(null)} certificates={certificates} accessLists={accessLists} /> )} {deleteHost && ( setDeleteHost(null)} /> )}
); } function CreateHostDialog({ open, onClose, certificates, accessLists }: { open: boolean; onClose: () => void; certificates: Certificate[]; accessLists: AccessList[]; }) { const [state, formAction] = useFormState(createProxyHostAction, INITIAL_ACTION_STATE); useEffect(() => { if (state.status === "success") { // revalidatePath in server action already handles the refresh setTimeout(onClose, 1000); } }, [state.status, onClose]); return ( Create Proxy Host {state.status !== "idle" && state.message && ( {state.message} )} Managed by Caddy (Auto) {certificates.map((cert) => ( {cert.name} ))} None {accessLists.map((list) => ( {list.name} ))} ); } function EditHostDialog({ open, host, onClose, certificates, accessLists }: { open: boolean; host: ProxyHost; onClose: () => void; certificates: Certificate[]; accessLists: AccessList[]; }) { const [state, formAction] = useFormState(updateProxyHostAction.bind(null, host.id), INITIAL_ACTION_STATE); useEffect(() => { if (state.status === "success") { // revalidatePath in server action already handles the refresh setTimeout(onClose, 1000); } }, [state.status, onClose]); return ( Edit Proxy Host {state.status !== "idle" && state.message && ( {state.message} )} Managed by Caddy (Auto) {certificates.map((cert) => ( {cert.name} ))} None {accessLists.map((list) => ( {list.name} ))} ); } function DeleteHostDialog({ open, host, onClose }: { open: boolean; host: ProxyHost; onClose: () => void; }) { const [state, formAction] = useFormState(deleteProxyHostAction.bind(null, host.id), INITIAL_ACTION_STATE); useEffect(() => { if (state.status === "success") { // revalidatePath in server action already handles the refresh setTimeout(onClose, 1000); } }, [state.status, onClose]); return ( Delete Proxy Host {state.status !== "idle" && state.message && ( {state.message} )} Are you sure you want to delete the proxy host {host.name}? This will remove the configuration for: • Domains: {host.domains.join(", ")} • Upstreams: {host.upstreams.join(", ")} This action cannot be undone.
); } function AuthentikFields({ authentik }: { authentik?: ProxyHost["authentik"] | null }) { const initial = authentik ?? null; const [enabled, setEnabled] = useState(initial?.enabled ?? false); const copyHeadersValue = initial && initial.copyHeaders.length > 0 ? initial.copyHeaders.join("\n") : AUTHENTIK_DEFAULT_HEADERS.join("\n"); const trustedProxiesValue = initial && initial.trustedProxies.length > 0 ? initial.trustedProxies.join("\n") : AUTHENTIK_DEFAULT_TRUSTED_PROXIES.join("\n"); const setHostHeaderDefault = initial?.setOutpostHostHeader ?? true; return ( Authentik Forward Auth Proxy authentication via Authentik outpost setEnabled(checked)} /> ); } function HiddenCheckboxField({ name, defaultChecked, label, disabled }: { name: string; defaultChecked: boolean; label: string; disabled?: boolean; }) { return ( } label={{label}} disabled={disabled} /> ); }