diff --git a/app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx b/app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx index 328acf5b..33b20aff 100644 --- a/app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx +++ b/app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx @@ -3,6 +3,7 @@ import { useMemo, useState, useEffect } from "react"; import { Alert, + Autocomplete, Box, Button, Card, @@ -16,6 +17,7 @@ import { DialogTitle, FormControlLabel, IconButton, + InputAdornment, MenuItem, Stack, Switch, @@ -34,6 +36,9 @@ import EditIcon from "@mui/icons-material/Edit"; import DeleteIcon from "@mui/icons-material/Delete"; import CloseIcon from "@mui/icons-material/Close"; import SearchIcon from "@mui/icons-material/Search"; +import RemoveCircleOutlineIcon from "@mui/icons-material/RemoveCircleOutline"; +import PlayArrowRoundedIcon from "@mui/icons-material/PlayArrowRounded"; +import PauseRoundedIcon from "@mui/icons-material/PauseRounded"; import { useFormState } from "react-dom"; import type { AccessList } from "@/src/lib/models/access-lists"; import type { Certificate } from "@/src/lib/models/certificates"; @@ -351,6 +356,7 @@ function CreateHostDialog({ {state.message} )} + - + Managed by Caddy (Auto) {certificates.map((cert) => ( @@ -388,11 +385,6 @@ function CreateHostDialog({ ))} - - - - - )} + - + Managed by Caddy (Auto) {certificates.map((cert) => ( @@ -512,11 +501,6 @@ function EditHostDialog({ ))} - - - - - ); } + +type ToggleSetting = { + name: string; + label: string; + description: string; + defaultChecked: boolean; + color?: "success" | "warning" | "default"; +}; + +function SettingsToggles({ + hstsSubdomains = false, + skipHttpsValidation = false, + enabled = true +}: { + hstsSubdomains?: boolean; + skipHttpsValidation?: boolean; + enabled?: boolean; +}) { + const [values, setValues] = useState({ + hsts_subdomains: hstsSubdomains, + skip_https_hostname_validation: skipHttpsValidation, + enabled: enabled + }); + + const handleChange = (name: keyof typeof values) => (event: React.ChangeEvent) => { + setValues(prev => ({ ...prev, [name]: event.target.checked })); + }; + + const toggleEnabled = () => { + setValues(prev => ({ ...prev, enabled: !prev.enabled })); + }; + + const settings: ToggleSetting[] = [ + { + name: "hsts_subdomains", + label: "HSTS Subdomains", + description: "Include subdomains in the Strict-Transport-Security header", + defaultChecked: values.hsts_subdomains, + color: "default" + }, + { + name: "skip_https_hostname_validation", + label: "Skip HTTPS Validation", + description: "Skip SSL certificate hostname verification for backend connections", + defaultChecked: values.skip_https_hostname_validation, + color: "warning" + } + ]; + + return ( + + {/* Prominent Enabled/Paused Control */} + + + + + + {values.enabled ? ( + + ) : ( + + )} + + + + {values.enabled ? "Active" : "Paused"} + + + {values.enabled + ? "This proxy host is enabled and routing traffic" + : "This proxy host is paused and not routing traffic"} + + + + Click to {values.enabled ? "pause" : "activate"} + + + + + {/* Other Options */} + + + + Advanced Options + + + }> + {settings.map((setting) => ( + + + + + + {setting.label} + + + {setting.description} + + + + + + ))} + + + + ); +} + +const PROTOCOL_OPTIONS = ["http://", "https://"]; + +type UpstreamEntry = { + protocol: string; + address: string; +}; + +function parseUpstream(upstream: string): UpstreamEntry { + if (upstream.startsWith("https://")) { + return { protocol: "https://", address: upstream.slice(8) }; + } + if (upstream.startsWith("http://")) { + return { protocol: "http://", address: upstream.slice(7) }; + } + // Default to http:// if no protocol specified + return { protocol: "http://", address: upstream }; +} + +function UpstreamInput({ + defaultUpstreams = [], + name = "upstreams" +}: { + defaultUpstreams?: string[]; + name?: string; +}) { + const initialEntries: UpstreamEntry[] = defaultUpstreams.length > 0 + ? defaultUpstreams.map(parseUpstream) + : [{ protocol: "http://", address: "" }]; + + const [entries, setEntries] = useState(initialEntries); + + const handleProtocolChange = (index: number, newProtocol: string | null) => { + const updated = [...entries]; + updated[index].protocol = newProtocol || "http://"; + setEntries(updated); + }; + + const handleAddressChange = (index: number, newAddress: string) => { + const updated = [...entries]; + updated[index].address = newAddress; + setEntries(updated); + }; + + const handleAdd = () => { + setEntries([...entries, { protocol: "http://", address: "" }]); + }; + + const handleRemove = (index: number) => { + if (entries.length === 1) return; + setEntries(entries.filter((_, i) => i !== index)); + }; + + // Serialize entries to a single string for form submission + const serializedValue = entries + .filter(e => e.address.trim() !== "") + .map(e => `${e.protocol}${e.address}`) + .join("\n"); + + return ( + + + + Upstreams + + + {entries.map((entry, index) => ( + + handleProtocolChange(index, newValue)} + onInputChange={(_, newInputValue) => { + if (newInputValue) { + handleProtocolChange(index, newInputValue); + } + }} + disableClearable + sx={{ width: 140 }} + renderInput={(params) => ( + + )} + /> + handleAddressChange(index, e.target.value)} + placeholder="10.0.0.5:8080" + size="small" + fullWidth + required={index === 0} + sx={{ + "& .MuiOutlinedInput-root": { + bgcolor: "rgba(20, 20, 22, 0.6)", + } + }} + /> + + + handleRemove(index)} + disabled={entries.length === 1} + sx={{ + color: entries.length === 1 ? "rgba(255, 255, 255, 0.2)" : "rgba(239, 68, 68, 0.7)", + "&:hover": { bgcolor: "rgba(239, 68, 68, 0.1)" }, + mt: 0.5 + }} + > + + + + + + ))} + + + + Backend servers to proxy requests to (supports load balancing with multiple upstreams) + + + ); +}