diff --git a/src/components/proxy-hosts/AuthentikFields.tsx b/src/components/proxy-hosts/AuthentikFields.tsx
index 5a3c0b90..ed1c5abe 100644
--- a/src/components/proxy-hosts/AuthentikFields.tsx
+++ b/src/components/proxy-hosts/AuthentikFields.tsx
@@ -1,8 +1,11 @@
-
-import { Box, Checkbox, Collapse, FormControlLabel, Stack, Switch, TextField, Typography } from "@mui/material";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Input } from "@/components/ui/input";
+import { Switch } from "@/components/ui/switch";
+import { Textarea } from "@/components/ui/textarea";
+import { cn } from "@/lib/utils";
import { useState } from "react";
-import { AuthentikSettings } from "@/src/lib/settings";
-import { ProxyHost } from "@/src/lib/models/proxy-hosts";
+import { AuthentikSettings } from "@/lib/settings";
+import { ProxyHost } from "@/lib/models/proxy-hosts";
const AUTHENTIK_DEFAULT_HEADERS = [
"X-Authentik-Username",
@@ -35,26 +38,28 @@ function HiddenCheckboxField({
helperText?: string;
}) {
return (
-
+
-
- }
- label={
{label} }
- disabled={disabled}
- />
+
+
+
+ {label}
+
+
{helperText && (
-
+
{helperText}
-
+
)}
-
+
);
}
@@ -77,90 +82,86 @@ export function AuthentikFields({
const setHostHeaderDefault = initial?.setOutpostHostHeader ?? true;
return (
-
+
-
-
-
-
- Authentik Forward Auth
-
-
- Proxy authentication via Authentik outpost
-
-
+
+
+
+
Authentik Forward Auth
+
Proxy authentication via Authentik outpost
+
setEnabled(checked)}
+ onCheckedChange={setEnabled}
/>
-
+
-
-
-
-
- {/* ... other fields ... */}
-
-
-
-
+
+
+
+ Outpost Domain
+
+
+
+ Outpost Upstream URL
+
+
+
+ Auth Endpoint (Optional)
+
+
+
+ Headers to Copy
+
+
+
+ Trusted Proxies
+
+
+
+
Protected Paths (Optional)
+
+
+ Leave empty to protect entire domain. Specify paths to protect specific routes only.
+
+
-
-
-
-
+
+
+
+
);
}
diff --git a/src/components/proxy-hosts/DnsResolverFields.tsx b/src/components/proxy-hosts/DnsResolverFields.tsx
index 82ef9c5f..44468b89 100644
--- a/src/components/proxy-hosts/DnsResolverFields.tsx
+++ b/src/components/proxy-hosts/DnsResolverFields.tsx
@@ -1,6 +1,10 @@
-import { Box, Collapse, Stack, Switch, TextField, Typography, Alert } from "@mui/material";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Switch } from "@/components/ui/switch";
+import { Textarea } from "@/components/ui/textarea";
+import { Input } from "@/components/ui/input";
+import { cn } from "@/lib/utils";
import { useState } from "react";
-import { ProxyHost } from "@/src/lib/models/proxy-hosts";
+import { ProxyHost } from "@/lib/models/proxy-hosts";
export function DnsResolverFields({
dnsResolver
@@ -11,75 +15,71 @@ export function DnsResolverFields({
const [enabled, setEnabled] = useState(initial?.enabled ?? false);
return (
-
+
-
-
-
-
- Custom DNS Resolvers
-
-
+
+
+
+
Custom DNS Resolvers
+
Configure per-host DNS resolution for upstream discovery and health checks
-
-
+
+
setEnabled(checked)}
+ onCheckedChange={setEnabled}
/>
-
+
-
-
-
-
-
-
- Per-host DNS resolvers override global settings for this specific proxy host.
- Useful for upstream services that require specific DNS resolution (e.g., internal DNS, service discovery).
- Common resolvers: 1.1.1.1 (Cloudflare), 8.8.8.8 (Google), 9.9.9.9 (Quad9).
+
+
+
+
DNS Resolvers
+
+
+ One resolver per line (e.g., 1.1.1.1, 8.8.8.8). Used for dynamic upstream DNS resolution.
+
+
+
+
Fallback DNS Resolvers (Optional)
+
+
Fallback resolvers if primary fails. One per line.
+
+
+
DNS Query Timeout
+
+
Timeout for DNS queries (e.g., 5s, 10s)
+
+
+
+ Per-host DNS resolvers override global settings for this specific proxy host.
+ Useful for upstream services that require specific DNS resolution (e.g., internal DNS, service discovery).
+ Common resolvers: 1.1.1.1 (Cloudflare), 8.8.8.8 (Google), 9.9.9.9 (Quad9).
+
-
-
-
-
+
+
+
+
);
}
diff --git a/src/components/proxy-hosts/GeoBlockFields.tsx b/src/components/proxy-hosts/GeoBlockFields.tsx
index 179b3300..3cae7448 100644
--- a/src/components/proxy-hosts/GeoBlockFields.tsx
+++ b/src/components/proxy-hosts/GeoBlockFields.tsx
@@ -2,39 +2,21 @@
import {
Accordion,
- AccordionDetails,
- AccordionSummary,
- Box,
- Button,
- Checkbox,
- Chip,
- CircularProgress,
- Collapse,
- Divider,
- FormControlLabel,
- Grid,
- IconButton,
- InputAdornment,
- Stack,
- Switch,
- Tab,
- Tabs,
- TextField,
- Tooltip,
- Typography
-} from "@mui/material";
-import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
-import AddIcon from "@mui/icons-material/Add";
-import DeleteIcon from "@mui/icons-material/Delete";
-import CheckCircleIcon from "@mui/icons-material/CheckCircle";
-import ErrorIcon from "@mui/icons-material/Error";
-import WarningAmberIcon from "@mui/icons-material/WarningAmber";
-import PublicIcon from "@mui/icons-material/Public";
-import SearchIcon from "@mui/icons-material/Search";
-import CloseIcon from "@mui/icons-material/Close";
-import { useState, useEffect, useMemo, useCallback, SyntheticEvent } from "react";
-import { GeoBlockSettings } from "@/src/lib/settings";
-import { GeoBlockMode } from "@/src/lib/models/proxy-hosts";
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "@/components/ui/accordion";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Input } from "@/components/ui/input";
+import { Switch } from "@/components/ui/switch";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { cn } from "@/lib/utils";
+import { Globe, X } from "lucide-react";
+import { useState, useEffect, useMemo, useCallback } from "react";
+import { GeoBlockSettings } from "@/lib/settings";
+import { GeoBlockMode } from "@/lib/models/proxy-hosts";
import { COUNTRIES, flagEmoji } from "./countries";
// ─── GeoIpStatus ─────────────────────────────────────────────────────────────
@@ -54,14 +36,12 @@ function GeoIpStatus() {
}, []);
if (loading) {
- return ;
+ return ;
}
const allLoaded = status?.country && status?.asn;
const noneLoaded = !status?.country && !status?.asn;
- const color = allLoaded ? "success" : noneLoaded ? "error" : "warning";
- const Icon = allLoaded ? CheckCircleIcon : noneLoaded ? ErrorIcon : WarningAmberIcon;
const label = allLoaded ? "GeoIP ready" : noneLoaded ? "GeoIP missing" : "GeoIP partial";
const tooltip = noneLoaded
? "GeoIP databases not found — country/continent/ASN blocking will not work. Enable the geoipupdate service."
@@ -72,16 +52,17 @@ function GeoIpStatus() {
: "GeoLite2-Country and GeoLite2-ASN databases loaded";
return (
-
- }
- label={label}
- color={color}
- variant="outlined"
- sx={{ height: 22, fontSize: "0.7rem", fontWeight: 600, letterSpacing: 0.3, cursor: "default", "& .MuiChip-icon": { ml: "6px" } }}
- />
-
+
+
+ {label}
+
+
);
}
@@ -145,162 +126,131 @@ function CountryPicker({ name, initialValues = [], accentColor = "warning" }: Co
const selectedInFiltered = filtered.filter((c) => selected.has(c.code)).length;
const allFilteredSelected = filtered.length > 0 && selectedInFiltered === filtered.length;
+ const isWarning = accentColor === "warning";
+
return (
-
+
{/* Search */}
-
setSearch(e.target.value)}
- slotProps={{
- input: {
- startAdornment: (
-
-
-
- ),
- endAdornment: search ? (
-
- setSearch("")} edge="end" sx={{ p: 0.25 }}>
-
-
-
- ) : null,
- },
- }}
- sx={{ mb: 0.75 }}
- />
+
+ setSearch(e.target.value)}
+ className="h-8 text-sm pr-8"
+ />
+ {search && (
+ setSearch("")}
+ className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
+ >
+
+
+ )}
+
{/* Toolbar */}
-
-
+
+
{selected.size > 0 ? (
<>{selected.size} selected{search && `, ${selectedInFiltered} shown`}>
) : (
- None selected
+ None selected
)}
-
-
+
+
{search ? "Select matching" : "Select all"}
- ·
+ ·
{search ? "Clear matching" : "Clear all"}
-
-
+
+
{/* Grid */}
- t.palette.mode === "dark" ? "rgba(255,255,255,0.02)" : "rgba(0,0,0,0.015)",
- "&::-webkit-scrollbar": { width: 5 },
- "&::-webkit-scrollbar-thumb": { borderRadius: 3, bgcolor: "divider" },
- "&::-webkit-scrollbar-track": { bgcolor: "transparent" },
- }}
- >
+
{filtered.length === 0 ? (
-
+
No countries match “{search}”
-
+
) : (
filtered.map((country) => {
const isSelected = selected.has(country.code);
return (
-
-
- {flagEmoji(country.code)}
-
- {country.name}
-
- {country.code}
-
-
- }
- size="small"
+ type="button"
onClick={() => toggle(country.code)}
- color={isSelected ? accentColor : "default"}
- variant={isSelected ? "filled" : "outlined"}
- sx={{
- cursor: "pointer",
- fontSize: "0.72rem",
- height: 26,
- transition: "all 0.1s ease",
- "& .MuiChip-label": { px: 0.75 },
- ...(!isSelected && {
- borderColor: "divider",
- "&:hover": { borderColor: "text.disabled", bgcolor: "action.hover" },
- }),
- ...(isSelected && {
- fontWeight: 600,
- boxShadow: accentColor === "warning"
- ? "0 0 0 1px rgba(237,108,2,0.3)"
- : "0 0 0 1px rgba(46,125,50,0.3)",
- }),
- }}
- />
+ className={cn(
+ "inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs border transition-all duration-100 cursor-pointer h-[26px]",
+ isSelected
+ ? isWarning
+ ? "border-yellow-500 bg-yellow-500/10 font-semibold text-yellow-700 dark:text-yellow-400"
+ : "border-green-500 bg-green-500/10 font-semibold text-green-700 dark:text-green-400"
+ : "border-border hover:border-muted-foreground hover:bg-accent"
+ )}
+ >
+ {flagEmoji(country.code)}
+ {country.name}
+ {country.code}
+
);
})
)}
-
+
{/* Selected summary chips (shown when search is active and selected items are hidden) */}
{search && selected.size > 0 && (
-
-
- All selected ({selected.size}):
-
-
+
+
All selected ({selected.size}):
+
{[...selected].map((code) => {
const country = COUNTRIES.find((c) => c.code === code);
return (
-
- {flagEmoji(code)}
- {country?.name ?? code}
-
- }
- size="small"
- onDelete={() => toggle(code)}
- color={accentColor}
- variant="filled"
- sx={{ fontSize: "0.7rem", height: 22, fontWeight: 600, "& .MuiChip-label": { px: 0.6 } }}
- />
+ variant="secondary"
+ className={cn(
+ "gap-1 pr-1 text-[0.7rem] h-[22px] font-semibold",
+ isWarning
+ ? "bg-yellow-500/10 text-yellow-700 dark:text-yellow-400"
+ : "bg-green-500/10 text-green-700 dark:text-green-400"
+ )}
+ >
+ {flagEmoji(code)}
+ {country?.name ?? code}
+ toggle(code)}
+ className="rounded-full hover:bg-destructive/20 p-0.5"
+ >
+
+
+
);
})}
-
-
+
+
)}
-
+
);
}
@@ -329,102 +279,70 @@ function ContinentPicker({ name, initialValues = [], accentColor = "warning" }:
const isWarning = accentColor === "warning";
return (
-
+
-
-
- {selected.size > 0 ? `${selected.size} selected` : None selected }
-
-
+
+
+ {selected.size > 0 ? `${selected.size} selected` : None selected }
+
+
setSelected(new Set(CONTINENTS.map((c) => c.code)))}
disabled={selected.size === CONTINENTS.length}
- sx={{ fontSize: "0.7rem", py: 0.25, px: 0.75, minWidth: 0, textTransform: "none" }}
+ className="text-[0.7rem] py-0.5 px-1.5 h-auto"
>
Select all
- ·
+ ·
setSelected(new Set())}
disabled={selected.size === 0}
- sx={{ fontSize: "0.7rem", py: 0.25, px: 0.75, minWidth: 0, textTransform: "none" }}
+ className="text-[0.7rem] py-0.5 px-1.5 h-auto"
>
Clear all
-
-
-
+
+
+
{CONTINENTS.map((c) => {
const isSelected = selected.has(c.code);
return (
-
toggle(c.code)}
- sx={{
- display: "flex",
- alignItems: "center",
- gap: 0.75,
- px: 1.25,
- py: 0.75,
- borderRadius: 1.5,
- border: "1.5px solid",
- borderColor: isSelected
- ? isWarning ? "warning.main" : "success.main"
- : "divider",
- bgcolor: isSelected
- ? (t) => t.palette.mode === "dark"
- ? isWarning ? "rgba(237,108,2,0.15)" : "rgba(46,125,50,0.15)"
- : isWarning ? "rgba(237,108,2,0.08)" : "rgba(46,125,50,0.08)"
- : "transparent",
- cursor: "pointer",
- userSelect: "none",
- transition: "all 0.12s ease",
- "&:hover": {
- borderColor: isSelected
- ? isWarning ? "warning.dark" : "success.dark"
- : "text.disabled",
- bgcolor: isSelected
- ? (t) => t.palette.mode === "dark"
- ? isWarning ? "rgba(237,108,2,0.22)" : "rgba(46,125,50,0.22)"
- : isWarning ? "rgba(237,108,2,0.13)" : "rgba(46,125,50,0.13)"
- : "action.hover",
- },
- ...(isSelected && {
- boxShadow: isWarning
- ? "0 0 0 1px rgba(237,108,2,0.25)"
- : "0 0 0 1px rgba(46,125,50,0.25)",
- }),
- }}
+ className={cn(
+ "flex items-center gap-1.5 px-2.5 py-1.5 rounded-xl border-[1.5px] cursor-pointer select-none transition-all duration-100",
+ isSelected
+ ? isWarning
+ ? "border-yellow-500 bg-yellow-500/10 shadow-[0_0_0_1px_rgba(237,108,2,0.25)]"
+ : "border-green-500 bg-green-500/10 shadow-[0_0_0_1px_rgba(46,125,50,0.25)]"
+ : "border-border hover:border-muted-foreground hover:bg-accent"
+ )}
>
- {c.emoji}
-
-
+ {c.emoji}
+
+
{c.name}
-
-
- {c.code}
-
-
-
+
+ {c.code}
+
+
);
})}
-
-
+
+
);
}
@@ -461,47 +379,47 @@ function TagInput({ name, label, initialValues = [], placeholder, helperText, va
}
return (
-
+
-
setInputValue(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === "," || e.key === " " || e.key === "Enter") {
- e.preventDefault();
- commitInput(inputValue);
- }
- if (e.key === "Backspace" && !inputValue && tags.length > 0) {
- setTags((prev) => prev.slice(0, -1));
- }
- }}
- onBlur={() => {
- if (inputValue.trim()) commitInput(inputValue);
- }}
- slotProps={{
- input: {
- startAdornment: tags.length > 0 ? (
-
- {tags.map((tag) => (
- setTags((prev) => prev.filter((t) => t !== tag))}
- sx={{ height: 20, fontSize: "0.68rem", "& .MuiChip-label": { px: 0.6 }, "& .MuiChip-deleteIcon": { fontSize: 12 } }}
- />
- ))}
-
- ) : undefined,
- },
- }}
- />
-
+ {label}
+ 0 && "pb-1"
+ )}>
+ {tags.map((tag) => (
+
+ {tag}
+ setTags((prev) => prev.filter((t) => t !== tag))}
+ className="rounded-full hover:bg-destructive/20 p-0.5"
+ >
+
+
+
+ ))}
+ setInputValue(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "," || e.key === " " || e.key === "Enter") {
+ e.preventDefault();
+ commitInput(inputValue);
+ }
+ if (e.key === "Backspace" && !inputValue && tags.length > 0) {
+ setTags((prev) => prev.slice(0, -1));
+ }
+ }}
+ onBlur={() => {
+ if (inputValue.trim()) commitInput(inputValue);
+ }}
+ className="flex-1 min-w-[80px] bg-transparent outline-none text-sm placeholder:text-muted-foreground"
+ />
+
+ {helperText && {helperText}
}
+
);
}
@@ -515,51 +433,55 @@ function ResponseHeadersEditor({ initialHeaders }: { initialHeaders: Record
- 0 ? 1 : 0}>
-
- Custom Response Headers
-
-
- setRows((prev) => [...prev, { key: "", value: "" }])}>
-
-
-
-
+
+
+
Custom Response Headers
+
setRows((prev) => [...prev, { key: "", value: "" }])}
+ title="Add header"
+ >
+ +
+
+
{rows.length === 0 ? (
-
- No custom headers — click + to add one.
-
+
No custom headers — click + to add one.
) : (
-
+
{rows.map((row, i) => (
-
+
- setRows((prev) => prev.map((r, j) => j === i ? { ...r, key: e.target.value } : r))}
- size="small"
- fullWidth
+ className="h-8 text-sm"
/>
- setRows((prev) => prev.map((r, j) => j === i ? { ...r, value: e.target.value } : r))}
- size="small"
- fullWidth
+ className="h-8 text-sm"
/>
-
- setRows((prev) => prev.filter((_, j) => j !== i))} sx={{ mt: 0.5 }}>
-
-
-
-
+ setRows((prev) => prev.filter((_, j) => j !== i))}
+ title="Remove"
+ >
+
+
+
))}
-
+
)}
-
+
);
}
@@ -579,34 +501,30 @@ function RulesPanel({ prefix, initial }: RulesPanelProps) {
const ips = prefix === "block" ? (initial?.block_ips ?? []) : (initial?.allow_ips ?? []);
return (
-
+
{/* Countries */}
-
-
- Countries
-
+
-
+
{/* Continents */}
-
-
- Continents
-
+
-
+
{/* ASNs */}
{/* CIDRs + IPs */}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
);
}
@@ -657,138 +571,97 @@ export function GeoBlockFields({ initialValues, showModeSelector = true }: GeoBl
const initial = initialValues?.geoblock ?? null;
const [enabled, setEnabled] = useState(initial?.enabled ?? false);
const [mode, setMode] = useState(initialValues?.geoblock_mode ?? "merge");
- const [rulesTab, setRulesTab] = useState(0);
return (
- theme.palette.mode === "dark" ? "rgba(237,108,2,0.06)" : "rgba(237,108,2,0.04)",
- p: 2
- }}
- >
+
{/* Header */}
-
-
-
-
-
-
-
-
- Geo Blocking
-
+
+
+
+
+
+
+
+
Block or allow traffic by country, continent, ASN, CIDR, or IP
-
-
-
+
+
+
setEnabled(checked)}
- sx={{ flexShrink: 0 }}
+ onCheckedChange={setEnabled}
+ className="shrink-0"
/>
-
+
{/* Mode selector */}
{/* Detail fields */}
-
-
- {showModeSelector && (
- <>
-
- {(["merge", "override"] as GeoBlockMode[]).map((v) => (
- setMode(v)}
- sx={{
- flex: 1,
- py: 0.75,
- px: 1.5,
- borderRadius: 1.5,
- border: "1.5px solid",
- borderColor: mode === v ? "warning.main" : "divider",
- bgcolor: mode === v
- ? (theme) => theme.palette.mode === "dark" ? "rgba(237,108,2,0.12)" : "rgba(237,108,2,0.08)"
- : "transparent",
- cursor: "pointer",
- textAlign: "center",
- transition: "all 0.15s ease",
- userSelect: "none",
- "&:hover": {
- borderColor: mode === v ? "warning.main" : "text.disabled",
- },
- }}
- >
-
- {v === "merge" ? "Merge with global" : "Override global"}
-
-
- ))}
-
-
- >
- )}
- {!showModeSelector && }
+
+ {showModeSelector && (
+ <>
+
+ {(["merge", "override"] as GeoBlockMode[]).map((v) => (
+
setMode(v)}
+ className={cn(
+ "flex-1 py-2 px-3 rounded-xl border-[1.5px] cursor-pointer text-center transition-all duration-150 select-none",
+ mode === v
+ ? "border-yellow-500 bg-yellow-500/10"
+ : "border-border hover:border-muted-foreground"
+ )}
+ >
+
+ {v === "merge" ? "Merge with global" : "Override global"}
+
+
+ ))}
+
+
+ >
+ )}
+ {!showModeSelector &&
}
- {/* Block / Allow tabs */}
-
setRulesTab(v)}
- variant="fullWidth"
- sx={{ mb: 2.5, "& .MuiTab-root": { textTransform: "none", fontWeight: 500 } }}
- >
-
-
-
-
-
+ {/* Block / Allow tabs */}
+
+
+ Block Rules
+ Allow Rules
+
+
-
-
-
-
-
- Allow rules take precedence over block rules.
-
-
+
+
+
+ Allow rules take precedence over block rules.
+
-
+
+
- {/* Advanced: Trusted Proxies + Block Response */}
-
-
- } sx={{ minHeight: 44, "& .MuiAccordionSummary-content": { my: 0.5 } }}>
- Trusted Proxies & Block Response
-
-
-
+ {/* Advanced: Trusted Proxies + Block Response */}
+
+
+
+
+ Trusted Proxies & Block Response
+
+
+
-
-
- }
- label={Fail closed (block indeterminate IPs) }
+
+
-
+
+ Fail closed (block indeterminate IPs)
+
+
-
+
-
-
-
+
+ Status Code
+
-
-
- HTTP status when blocked
+
+
+ Response Body
+
-
-
- Body text returned to blocked clients
+
+
+
Redirect URL
+
-
-
+
If set, sends a 302 redirect instead of status/body above
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
);
}
diff --git a/src/components/proxy-hosts/HostDialogs.tsx b/src/components/proxy-hosts/HostDialogs.tsx
index 62d77aa2..9968bfba 100644
--- a/src/components/proxy-hosts/HostDialogs.tsx
+++ b/src/components/proxy-hosts/HostDialogs.tsx
@@ -1,5 +1,7 @@
-
-import { Alert, Box, MenuItem, Stack, TextField, Typography } from "@mui/material";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Input } from "@/components/ui/input";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Textarea } from "@/components/ui/textarea";
import { useFormState } from "react-dom";
import { useEffect } from "react";
import {
@@ -7,12 +9,12 @@ import {
deleteProxyHostAction,
updateProxyHostAction
} from "@/app/(dashboard)/proxy-hosts/actions";
-import { INITIAL_ACTION_STATE } from "@/src/lib/actions";
-import { AccessList } from "@/src/lib/models/access-lists";
-import { Certificate } from "@/src/lib/models/certificates";
-import { ProxyHost } from "@/src/lib/models/proxy-hosts";
-import { AuthentikSettings } from "@/src/lib/settings";
-import { AppDialog } from "@/src/components/ui/AppDialog";
+import { INITIAL_ACTION_STATE } from "@/lib/actions";
+import { AccessList } from "@/lib/models/access-lists";
+import { Certificate } from "@/lib/models/certificates";
+import { ProxyHost } from "@/lib/models/proxy-hosts";
+import { AuthentikSettings } from "@/lib/settings";
+import { AppDialog } from "@/components/ui/AppDialog";
import { AuthentikFields } from "./AuthentikFields";
import { DnsResolverFields } from "./DnsResolverFields";
import { LoadBalancerFields } from "./LoadBalancerFields";
@@ -24,7 +26,7 @@ import { WafFields } from "./WafFields";
import { MtlsFields } from "./MtlsConfig";
import { RedirectsFields } from "./RedirectsFields";
import { RewriteFields } from "./RewriteFields";
-import type { CaCertificate } from "@/src/lib/models/ca-certificates";
+import type { CaCertificate } from "@/lib/models/ca-certificates";
export function CreateHostDialog({
open,
@@ -59,14 +61,13 @@ export function CreateHostDialog({
maxWidth="md"
submitLabel="Create"
onSubmit={() => {
- // Trigger generic form submit
(document.getElementById("create-host-form") as HTMLFormElement)?.requestSubmit();
}}
>
-
+
+
);
}
@@ -178,10 +200,10 @@ export function EditHostDialog({
(document.getElementById("edit-host-form") as HTMLFormElement)?.requestSubmit();
}}
>
-
+
{state.status !== "idle" && state.message && (
-
- {state.message}
+
+ {state.message}
)}
-
-
+
+ Name
+
+
+
+
Domains
+
+
+ One per line or comma-separated. Wildcards like *.example.com are supported.
+
+
-
- Managed by Caddy (Auto)
- {certificates.map((cert) => (
-
- {cert.name}
-
- ))}
-
-
- None
- {accessLists.map((list) => (
-
- {list.name}
-
- ))}
-
+
+ Certificate
+
+
+
+
+
+ Managed by Caddy (Auto)
+ {certificates.map((cert) => (
+
+ {cert.name}
+
+ ))}
+
+
+
+
+ Access List
+
+
+
+
+
+ None
+ {accessLists.map((list) => (
+
+ {list.name}
+
+ ))}
+
+
+
-
-
+
+
Custom Pre-Handlers (JSON)
+
+
Optional JSON array of Caddy handlers
+
+
+
Custom Reverse Proxy (JSON)
+
+
+ Deep-merge into reverse_proxy handler (only applies in proxy mode)
+
+
-
+
);
}
@@ -281,30 +326,26 @@ export function DeleteHostDialog({
(document.getElementById("delete-host-form") as HTMLFormElement)?.requestSubmit();
}}
>
-
+
{state.status !== "idle" && state.message && (
-
- {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(", ")}
-
-
-
+
+
+
• Domains: {host.domains.join(", ")}
+
• Upstreams: {host.upstreams.join(", ")}
+
+
This action cannot be undone.
-
-
+
+
);
}
diff --git a/src/components/proxy-hosts/LoadBalancerFields.tsx b/src/components/proxy-hosts/LoadBalancerFields.tsx
index 979793bb..0065ced6 100644
--- a/src/components/proxy-hosts/LoadBalancerFields.tsx
+++ b/src/components/proxy-hosts/LoadBalancerFields.tsx
@@ -1,7 +1,9 @@
-
-import { Box, Collapse, FormControlLabel, Stack, Switch, TextField, Typography, MenuItem } from "@mui/material";
+import { Input } from "@/components/ui/input";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Switch } from "@/components/ui/switch";
+import { cn } from "@/lib/utils";
import { useState } from "react";
-import { ProxyHost, LoadBalancingPolicy } from "@/src/lib/models/proxy-hosts";
+import { ProxyHost, LoadBalancingPolicy } from "@/lib/models/proxy-hosts";
const LOAD_BALANCING_POLICIES = [
{ value: "random", label: "Random", description: "Random selection (default)" },
@@ -29,311 +31,245 @@ export function LoadBalancerFields({
const showCookieFields = policy === "cookie";
return (
-
+
-
-
-
-
- Load Balancer
-
-
+
+
+
+
Load Balancer
+
Configure load balancing and health checks for multiple upstreams
-
-
+
+
setEnabled(checked)}
+ onCheckedChange={setEnabled}
/>
-
+
-
-
+
+
{/* Policy Selection */}
-
-
- Selection Policy
-
- setPolicy(e.target.value as LoadBalancingPolicy)}
- fullWidth
- size="small"
- >
- {LOAD_BALANCING_POLICIES.map((p) => (
-
- {p.label} - {p.description}
-
- ))}
-
-
+
+
Selection Policy
+
+
setPolicy(v as LoadBalancingPolicy)}>
+
+
+
+
+ {LOAD_BALANCING_POLICIES.map((p) => (
+
+ {p.label} - {p.description}
+
+ ))}
+
+
+
{/* Header-based policy fields */}
-
-
-
+
+
+
Header Field Name
+
+
The request header to hash for upstream selection
+
+
{/* Cookie-based policy fields */}
-
-
-
-
-
-
+
+
+
+
Cookie Name
+
+
Name of the cookie for sticky sessions
+
+
+
Cookie Secret (Optional)
+
+
Secret key for HMAC cookie signing
+
+
+
{/* Retry Settings */}
-
-
- Retry Settings
-
-
-
-
-
-
-
+
+
Retry Settings
+
+
+
Try Duration
+
+
How long to try upstreams
+
+
+
Try Interval
+
+
Wait between attempts
+
+
+
Max Retries
+
+
Maximum retry attempts
+
+
+
{/* Active Health Checks */}
-
+
-
- setActiveHealthEnabled(checked)}
- size="small"
- />
- }
- label={
-
- Active Health Checks
-
- Periodically probe upstreams to check health
-
-
- }
- />
+
+
+
+
+
Active Health Checks
+
Periodically probe upstreams to check health
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
Health Check URI
+
+
Path to probe for health
+
+
+
Health Check Port
+
+
Override upstream port
+
+
+
+
+
Check Interval
+
+
How often to check
+
+
+
Check Timeout
+
+
Timeout for health probe
+
+
+
+
+
Expected Status Code
+
+
Expected HTTP status
+
+
+
Expected Body
+
+
Expected response body
+
+
+
+
+
+
{/* Passive Health Checks */}
-
+
-
- setPassiveHealthEnabled(checked)}
- size="small"
- />
- }
- label={
-
- Passive Health Checks
-
- Mark upstreams unhealthy based on response failures
-
-
- }
- />
+
+
+
+
+
Passive Health Checks
+
Mark upstreams unhealthy based on response failures
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
Fail Duration
+
+
How long to remember failures
+
+
+
Max Failures
+
+
Failures before marking unhealthy
+
+
+
+
+
Unhealthy Status Codes
+
+
Comma-separated status codes
+
+
+
Unhealthy Latency
+
+
Latency threshold for unhealthy
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/components/proxy-hosts/MtlsConfig.tsx b/src/components/proxy-hosts/MtlsConfig.tsx
index 69a189f8..3ba7e5a7 100644
--- a/src/components/proxy-hosts/MtlsConfig.tsx
+++ b/src/components/proxy-hosts/MtlsConfig.tsx
@@ -1,19 +1,13 @@
"use client";
-import {
- Alert,
- Box,
- Checkbox,
- Collapse,
- FormControlLabel,
- Stack,
- Switch,
- Typography,
-} from "@mui/material";
-import LockPersonIcon from "@mui/icons-material/LockPerson";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Switch } from "@/components/ui/switch";
+import { cn } from "@/lib/utils";
+import { LockKeyhole } from "lucide-react";
import { useState } from "react";
-import type { CaCertificate } from "@/src/lib/models/ca-certificates";
-import type { MtlsConfig } from "@/src/lib/models/proxy-hosts";
+import type { CaCertificate } from "@/lib/models/ca-certificates";
+import type { MtlsConfig } from "@/lib/models/proxy-hosts";
type Props = {
value?: MtlsConfig | null;
@@ -31,16 +25,7 @@ export function MtlsFields({ value, caCertificates }: Props) {
}
return (
-
- theme.palette.mode === "dark" ? "rgba(2,136,209,0.06)" : "rgba(2,136,209,0.04)",
- p: 2,
- }}
- >
+
{enabled && selectedIds.map(id => (
@@ -48,79 +33,60 @@ export function MtlsFields({ value, caCertificates }: Props) {
))}
{/* Header */}
-
-
-
-
-
-
-
- Mutual TLS (mTLS)
-
-
+
+
+
+
+
+
+
Mutual TLS (mTLS)
+
Require clients to present a certificate signed by a trusted CA
-
-
-
+
+
+
setEnabled(checked)}
- sx={{ flexShrink: 0 }}
+ onCheckedChange={setEnabled}
+ className="shrink-0"
/>
-
+
-
-
-
+
+
+
mTLS requires TLS to be configured on this host (certificate must be set).
-
+
+
-
- Trusted Client CA Certificates
-
+
+ Trusted Client CA Certificates
+
- {caCertificates.length === 0 ? (
-
- No CA certificates configured. Add them on the Certificates page.
-
- ) : (
-
- {caCertificates.map(ca => (
- toggleId(ca.id)}
- size="small"
- />
- }
- label={
- {ca.name}
- }
+ {caCertificates.length === 0 ? (
+
+ No CA certificates configured. Add them on the Certificates page.
+
+ ) : (
+
+ {caCertificates.map(ca => (
+
+ toggleId(ca.id)}
/>
- ))}
-
- )}
-
-
-
+
+ {ca.name}
+
+
+ ))}
+
+ )}
+
+
);
}
diff --git a/src/components/proxy-hosts/RedirectsFields.tsx b/src/components/proxy-hosts/RedirectsFields.tsx
index 586caa40..2ca97a3c 100644
--- a/src/components/proxy-hosts/RedirectsFields.tsx
+++ b/src/components/proxy-hosts/RedirectsFields.tsx
@@ -1,12 +1,10 @@
"use client";
import { useState } from "react";
-import {
- Box, Button, IconButton, MenuItem, Select,
- Table, TableBody, TableCell, TableHead, TableRow, TextField, Typography,
-} from "@mui/material";
-import DeleteIcon from "@mui/icons-material/Delete";
-import AddIcon from "@mui/icons-material/Add";
-import type { RedirectRule } from "@/src/lib/models/proxy-hosts";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Trash2, Plus } from "lucide-react";
+import type { RedirectRule } from "@/lib/models/proxy-hosts";
type Props = { initialData?: RedirectRule[] };
@@ -23,66 +21,65 @@ export function RedirectsFields({ initialData = [] }: Props) {
setRules((r) => r.map((rule, idx) => (idx === i ? { ...rule, [key]: value } : rule)));
return (
-
-
- Redirects
-
+
+
Redirects
{rules.length > 0 && (
-
-
-
- From Path
- To URL / Path
- Status
-
-
-
-
+
+
+ From Path
+ To URL / Path
+ Status
+
+
+
{rules.map((rule, i) => (
-
-
- updateRule(i, "from", e.target.value)}
- fullWidth
- />
-
-
- updateRule(i, "to", e.target.value)}
- fullWidth
- />
-
-
- updateRule(i, "status", Number(e.target.value))}
- >
+
+
updateRule(i, "from", e.target.value)}
+ className="h-8 text-sm"
+ />
+
updateRule(i, "to", e.target.value)}
+ className="h-8 text-sm"
+ />
+
updateRule(i, "status", Number(v))}
+ >
+
+
+
+
{[301, 302, 307, 308].map((s) => (
- {s}
+ {s}
))}
-
-
-
- removeRule(i)}>
-
-
-
-
+
+
+
removeRule(i)}
+ >
+
+
+
))}
-
-
+
+
)}
- } onClick={addRule}>
+
+
Add Redirect
-
+
);
}
diff --git a/src/components/proxy-hosts/RewriteFields.tsx b/src/components/proxy-hosts/RewriteFields.tsx
index 0c8ee84c..9f2f2a47 100644
--- a/src/components/proxy-hosts/RewriteFields.tsx
+++ b/src/components/proxy-hosts/RewriteFields.tsx
@@ -1,17 +1,20 @@
-import { TextField } from "@mui/material";
-import type { RewriteConfig } from "@/src/lib/models/proxy-hosts";
+import { Input } from "@/components/ui/input";
+import type { RewriteConfig } from "@/lib/models/proxy-hosts";
type Props = { initialData?: RewriteConfig | null };
export function RewriteFields({ initialData }: Props) {
return (
-
+
+
Path Prefix Rewrite
+
+
+ Prepend this prefix to every request before proxying (e.g. /recipes → /recipes/original/path)
+
+
);
}
diff --git a/src/components/proxy-hosts/SettingsToggles.tsx b/src/components/proxy-hosts/SettingsToggles.tsx
index d5f0d826..126df54d 100644
--- a/src/components/proxy-hosts/SettingsToggles.tsx
+++ b/src/components/proxy-hosts/SettingsToggles.tsx
@@ -1,6 +1,5 @@
-
-import { Box, Stack, Switch, Typography } from "@mui/material";
-import type { SwitchProps } from "@mui/material";
+import { Switch } from "@/components/ui/switch";
+import { cn } from "@/lib/utils";
import { useState } from "react";
type ToggleSetting = {
@@ -8,7 +7,6 @@ type ToggleSetting = {
label: string;
description: string;
defaultChecked: boolean;
- color?: SwitchProps["color"];
};
type SettingsTogglesProps = {
@@ -28,12 +26,8 @@ export function SettingsToggles({
enabled: enabled
});
- const handleChange = (name: keyof typeof values) => (event: React.ChangeEvent) => {
- setValues(prev => ({ ...prev, [name]: event.target.checked }));
- };
-
- const handleEnabledChange = (_: React.ChangeEvent, checked: boolean) => {
- setValues(prev => ({ ...prev, enabled: checked }));
+ const handleChange = (name: keyof typeof values) => (checked: boolean) => {
+ setValues(prev => ({ ...prev, [name]: checked }));
};
const settings: ToggleSetting[] = [
@@ -42,98 +36,67 @@ export function SettingsToggles({
label: "HSTS Subdomains",
description: "Include subdomains in the Strict-Transport-Security header",
defaultChecked: values.hsts_subdomains,
- color: "primary"
},
{
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: "primary"
}
];
return (
-
+
{/* Main Enable Switch */}
-
-
-
+
+
+
{values.enabled ? "Proxy Host Enabled" : "Proxy Host Paused"}
-
-
+
+
{values.enabled
? "This host is active and routing traffic"
: "This host is disabled and will not respond to requests"}
-
-
+
+
-
+
{/* Advanced Options */}
-
-
-
- Advanced Options
-
-
- }>
+
+
+
{settings.map((setting) => (
-
+
-
-
-
- {setting.label}
-
-
- {setting.description}
-
-
+
+
+
{setting.label}
+
{setting.description}
+
-
-
+
+
))}
-
-
-
+
+
+
);
}
diff --git a/src/components/proxy-hosts/UpstreamDnsResolutionFields.tsx b/src/components/proxy-hosts/UpstreamDnsResolutionFields.tsx
index ab3e6e4f..9123e672 100644
--- a/src/components/proxy-hosts/UpstreamDnsResolutionFields.tsx
+++ b/src/components/proxy-hosts/UpstreamDnsResolutionFields.tsx
@@ -1,7 +1,10 @@
-import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
-import { Alert, Box, Collapse, IconButton, MenuItem, Stack, TextField, Typography } from "@mui/material";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Button } from "@/components/ui/button";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { cn } from "@/lib/utils";
+import { ChevronDown } from "lucide-react";
import { useState } from "react";
-import type { ProxyHost } from "@/src/lib/models/proxy-hosts";
+import type { ProxyHost } from "@/lib/models/proxy-hosts";
type ResolutionMode = "inherit" | "enabled" | "disabled";
type FamilyMode = "inherit" | "ipv6" | "ipv4" | "both";
@@ -27,80 +30,84 @@ export function UpstreamDnsResolutionFields({
const mode = toResolutionMode(upstreamDnsResolution?.enabled);
const family = toFamilyMode(upstreamDnsResolution?.family);
const [expanded, setExpanded] = useState(mode !== "inherit" || family !== "inherit");
- const summary = mode === "inherit" && family === "inherit"
+ const [currentMode, setCurrentMode] = useState(mode);
+ const [currentFamily, setCurrentFamily] = useState(family);
+
+ const summary = currentMode === "inherit" && currentFamily === "inherit"
? "Using global upstream DNS pinning defaults"
- : `Override: ${mode === "inherit" ? "inherit mode" : mode}, ${family === "inherit" ? "inherit family" : family}`;
+ : `Override: ${currentMode === "inherit" ? "inherit mode" : currentMode}, ${currentFamily === "inherit" ? "inherit family" : currentFamily}`;
return (
-
+
-
-
-
-
- Upstream DNS Pinning
-
-
- {summary}
-
-
-
+
+
+
Upstream DNS Pinning
+
{summary}
+
+
setExpanded(prev => !prev)}
- sx={{
- transform: expanded ? "rotate(180deg)" : "rotate(0deg)",
- transition: "transform 0.2s ease"
- }}
+ className="h-8 w-8"
>
-
-
-
+
+
+
-
-
-
- Inherit Global
- Enabled
- Disabled
-
-
- Inherit Global
- Both (Prefer IPv6)
- IPv6 only
- IPv4 only
-
-
- When enabled, hostname upstreams are resolved during config apply and written to Caddy as concrete IP dials. If this handler has
- multiple different HTTPS upstream hostnames, HTTPS pinning is skipped for those HTTPS upstreams to avoid SNI mismatch.
+
+
+
+
Resolution Mode
+
+
setCurrentMode(v as ResolutionMode)}>
+
+
+
+
+ Inherit Global
+ Enabled
+ Disabled
+
+
+
+ Inherit uses the global setting. Enabled/Disabled overrides per host.
+
+
+
+
Address Family Preference
+
+
setCurrentFamily(v as FamilyMode)}>
+
+
+
+
+ Inherit Global
+ Both (Prefer IPv6)
+ IPv6 only
+ IPv4 only
+
+
+
Both resolves AAAA + A with IPv6 preferred ordering.
+
+
+
+ When enabled, hostname upstreams are resolved during config apply and written to Caddy as concrete IP dials. If this handler has
+ multiple different HTTPS upstream hostnames, HTTPS pinning is skipped for those HTTPS upstreams to avoid SNI mismatch.
+
-
-
-
-
+
+
+
+
);
}
diff --git a/src/components/proxy-hosts/UpstreamInput.tsx b/src/components/proxy-hosts/UpstreamInput.tsx
index 49428316..42a70a16 100644
--- a/src/components/proxy-hosts/UpstreamInput.tsx
+++ b/src/components/proxy-hosts/UpstreamInput.tsx
@@ -1,10 +1,9 @@
-import { Box, Button, IconButton, Stack, TextField, Tooltip, Typography, Autocomplete } from "@mui/material";
-import AddIcon from "@mui/icons-material/Add";
-import RemoveCircleIcon from "@mui/icons-material/RemoveCircle";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { MinusCircle, Plus } from "lucide-react";
import { useState } from "react";
-const PROTOCOL_OPTIONS = ["http://", "https://"];
-
type UpstreamEntry = {
protocol: string;
address: string;
@@ -33,7 +32,7 @@ export function UpstreamInput({
const [entries, setEntries] = useState(initialEntries);
- const handleProtocolChange = (index: number, newProtocol: string | null) => {
+ const handleProtocolChange = (index: number, newProtocol: string) => {
const updated = [...entries];
updated[index].protocol = newProtocol || "http://";
setEntries(updated);
@@ -69,69 +68,56 @@ export function UpstreamInput({
.join("\n");
return (
-
+
-
- Upstreams
-
-
+ Upstreams
+
{entries.map((entry, index) => (
-
- handleProtocolChange(index, newValue)}
- onInputChange={(_, newInputValue) => {
- if (newInputValue) {
- handleProtocolChange(index, newInputValue);
- }
- }}
- disableClearable
- sx={{ width: 140 }}
- renderInput={(params) => (
-
- )}
- />
-
+ handleProtocolChange(index, val)}>
+
+
+
+
+ http://
+ https://
+
+
+ handleAddressChange(index, e.target.value)}
placeholder="10.0.0.5:8080"
- size="small"
- fullWidth
+ className="flex-1"
required={index === 0}
/>
-
-
- handleRemove(index)}
- disabled={entries.length === 1}
- color="error"
- sx={{ mt: 0.5 }}
- >
-
-
-
-
-
+
+ handleRemove(index)}
+ disabled={entries.length === 1}
+ className="text-destructive hover:text-destructive mt-0.5"
+ >
+
+
+
+
))}
}
+ type="button"
+ variant="ghost"
+ size="sm"
onClick={handleAdd}
- size="small"
- sx={{ alignSelf: "flex-start" }}
+ className="self-start"
>
+
Add Upstream
-
-
+
+
Backend servers to proxy requests to
-
-
+
+
);
}
diff --git a/src/components/proxy-hosts/WafFields.tsx b/src/components/proxy-hosts/WafFields.tsx
index f7a63f4f..15154c67 100644
--- a/src/components/proxy-hosts/WafFields.tsx
+++ b/src/components/proxy-hosts/WafFields.tsx
@@ -1,22 +1,13 @@
"use client";
-import {
- Box,
- Button,
- Checkbox,
- Collapse,
- Divider,
- FormControlLabel,
- Stack,
- Switch,
- TextField,
- Typography,
-} from "@mui/material";
-import GppBadIcon from "@mui/icons-material/GppBad";
-import ContentCopyIcon from "@mui/icons-material/ContentCopy";
-import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
+import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Switch } from "@/components/ui/switch";
+import { Textarea } from "@/components/ui/textarea";
+import { cn } from "@/lib/utils";
+import { ChevronDown, ClipboardCopy, ShieldOff } from "lucide-react";
import { useState } from "react";
-import { type WafHostConfig } from "@/src/lib/models/proxy-hosts";
+import { type WafHostConfig } from "@/lib/models/proxy-hosts";
import { WafRuleExclusions } from "./WafRuleExclusions";
type WafMode = "merge" | "override";
@@ -45,16 +36,7 @@ export function WafFields({ value, showModeSelector = true }: Props) {
const [showTemplates, setShowTemplates] = useState(false);
return (
-
- theme.palette.mode === "dark" ? "rgba(211,47,47,0.06)" : "rgba(211,47,47,0.04)",
- p: 2,
- }}
- >
+
@@ -63,211 +45,158 @@ export function WafFields({ value, showModeSelector = true }: Props) {
{/* Header */}
-
-
-
-
-
-
-
- Web Application Firewall
-
-
+
+
+
+
+
+
+
Web Application Firewall
+
Inspect and block malicious requests via Coraza / OWASP CRS
-
-
-
+
+
+
setEnabled(checked)}
- sx={{ flexShrink: 0 }}
+ onCheckedChange={setEnabled}
+ className="shrink-0"
/>
-
+
{/* Expanded content */}
-
-
- {/* Override mode selector */}
- {showModeSelector && (
- <>
-
- {(["merge", "override"] as WafMode[]).map((v) => (
- setWafMode(v)}
- sx={{
- flex: 1,
- py: 0.75,
- px: 1.5,
- borderRadius: 1.5,
- border: "1.5px solid",
- borderColor: wafMode === v ? "error.main" : "divider",
- bgcolor:
- wafMode === v
- ? (theme) =>
- theme.palette.mode === "dark"
- ? "rgba(211,47,47,0.12)"
- : "rgba(211,47,47,0.08)"
- : "transparent",
- cursor: "pointer",
- textAlign: "center",
- transition: "all 0.15s ease",
- userSelect: "none",
- "&:hover": {
- borderColor: wafMode === v ? "error.main" : "text.disabled",
- },
- }}
- >
-
- {v === "merge" ? "Merge with global" : "Override global"}
-
-
- ))}
-
-
- >
- )}
- {!showModeSelector && }
-
- {/* Engine mode */}
-
- Engine Mode
-
-
- {(["inherit", "Off", "On"] as EngineMode[]).map((v) => (
- setEngineMode(v)}
- sx={{
- flex: 1,
- py: 0.75,
- px: 1,
- borderRadius: 1.5,
- border: "1.5px solid",
- borderColor: engineMode === v ? "error.main" : "divider",
- bgcolor:
- engineMode === v
- ? (theme) =>
- theme.palette.mode === "dark"
- ? "rgba(211,47,47,0.12)"
- : "rgba(211,47,47,0.08)"
- : "transparent",
- cursor: "pointer",
- textAlign: "center",
- transition: "all 0.15s ease",
- userSelect: "none",
- "&:hover": {
- borderColor: engineMode === v ? "error.main" : "text.disabled",
- },
- }}
- >
-
+ {/* Override mode selector */}
+ {showModeSelector && (
+ <>
+
+ {(["merge", "override"] as WafMode[]).map((v) => (
+
setWafMode(v)}
+ className={cn(
+ "flex-1 py-2 px-3 rounded-xl border-[1.5px] cursor-pointer text-center transition-all duration-150 select-none",
+ wafMode === v
+ ? "border-destructive bg-destructive/10"
+ : "border-border hover:border-muted-foreground"
+ )}
>
- {v === "inherit" ? "Global default" : v}
-
-
- ))}
-
+
+ {v === "merge" ? "Merge with global" : "Override global"}
+
+
+ ))}
+
+
+ >
+ )}
+ {!showModeSelector &&
}
-
-
- {/* OWASP CRS */}
- setLoadCrs(checked)}
- size="small"
- />
- }
- label={
-
-
- Load OWASP Core Rule Set
-
-
- Covers SQLi, XSS, LFI, RCE and hundreds of other attack patterns
-
-
- }
- />
-
- {/* Excluded rule IDs */}
-
-
-
-
- {/* Custom directives */}
-
- setCustomDirectives(e.target.value)}
- placeholder={`SecRule REQUEST_URI "@contains /secret" "id:9001,deny,status:403,log,msg:'Blocked path'"`}
- inputProps={{ style: { fontFamily: "monospace", fontSize: "0.8rem" } }}
- helperText="ModSecurity SecLang syntax. Appended after OWASP CRS if enabled."
- fullWidth
- />
-
-
- {/* Quick Templates */}
-
- }
- onClick={() => setShowTemplates((v) => !v)}
- sx={{ color: "text.secondary", textTransform: "none", px: 0 }}
+ {/* Engine mode */}
+
+ Engine Mode
+
+
+ {(["inherit", "Off", "On"] as EngineMode[]).map((v) => (
+
setEngineMode(v)}
+ className={cn(
+ "flex-1 py-2 px-2 rounded-xl border-[1.5px] cursor-pointer text-center transition-all duration-150 select-none",
+ engineMode === v
+ ? "border-destructive bg-destructive/10"
+ : "border-border hover:border-muted-foreground"
+ )}
>
- Quick Templates
-
-
-
- {QUICK_TEMPLATES.map((t) => (
- }
- onClick={() => setCustomDirectives((prev) => prev ? `${prev}\n${t.snippet}` : t.snippet)}
- sx={{ justifyContent: "flex-start", textTransform: "none", fontFamily: "monospace", fontSize: "0.72rem" }}
- >
- {t.label}
-
- ))}
-
-
-
-
-
-
+
+ {v === "inherit" ? "Global default" : v}
+
+
+ ))}
+
+
+
+
+ {/* OWASP CRS */}
+
+
setLoadCrs(!!checked)}
+ />
+
+ Load OWASP Core Rule Set
+
+ Covers SQLi, XSS, LFI, RCE and hundreds of other attack patterns
+
+
+
+
+ {/* Excluded rule IDs */}
+
+
+
+
+ {/* Custom directives */}
+
+
setCustomDirectives(e.target.value)}
+ className="font-mono text-xs min-h-[80px]"
+ rows={3}
+ />
+
+ Custom SecLang Directives — ModSecurity SecLang syntax. Appended after OWASP CRS if enabled.
+
+
+
+ {/* Quick Templates */}
+
+
setShowTemplates((v) => !v)}
+ className="text-muted-foreground px-0 text-sm"
+ >
+ Quick Templates
+
+
+
+
+ {QUICK_TEMPLATES.map((t) => (
+ setCustomDirectives((prev) => prev ? `${prev}\n${t.snippet}` : t.snippet)}
+ className="justify-start font-mono text-[0.72rem]"
+ >
+
+ {t.label}
+
+ ))}
+
+
+
+
+
);
}
diff --git a/src/components/proxy-hosts/WafRuleExclusions.tsx b/src/components/proxy-hosts/WafRuleExclusions.tsx
index 7716814f..9acb21f9 100644
--- a/src/components/proxy-hosts/WafRuleExclusions.tsx
+++ b/src/components/proxy-hosts/WafRuleExclusions.tsx
@@ -1,7 +1,9 @@
"use client";
-import { Box, Chip, IconButton, Stack, TextField, Typography } from "@mui/material";
-import AddIcon from "@mui/icons-material/Add";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Plus, X } from "lucide-react";
import { useState } from "react";
type Props = {
@@ -25,39 +27,45 @@ export function WafRuleExclusions({ value }: Props) {
}
return (
-
+
-
+
Excluded Rule IDs
-
-
+
+
Rules listed here are disabled via SecRuleRemoveById
-
-
- {ids.map((id) => (
- removeId(id)}
- sx={{ fontFamily: "monospace", fontSize: "0.75rem" }}
- />
- ))}
-
-
-
+ {ids.length > 0 && (
+
+ {ids.map((id) => (
+
+ {id}
+ removeId(id)}
+ className="rounded-full hover:bg-destructive/20 p-0.5"
+ >
+
+
+
+ ))}
+
+ )}
+
+
setInputVal(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); addId(); } }}
- inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
- sx={{ flex: 1 }}
+ inputMode="numeric"
+ pattern="[0-9]*"
+ className="flex-1 h-8 text-sm"
/>
-
-
-
-
-
+
+
+
+
+
);
}