better ui

This commit is contained in:
fuomag9
2026-03-04 00:31:02 +01:00
parent 0dad675c6d
commit 1a56e5e842

View File

@@ -1,146 +1,225 @@
"use client";
import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Checkbox,
FormControl,
Collapse,
Divider,
FormControlLabel,
FormLabel,
Radio,
RadioGroup,
Stack,
Switch,
TextField,
ToggleButton,
ToggleButtonGroup,
Typography,
} from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import SecurityIcon from "@mui/icons-material/Security";
import GppBadIcon from "@mui/icons-material/GppBad";
import { useState } from "react";
import { type WafHostConfig } from "@/src/lib/models/proxy-hosts";
type WafMode = "merge" | "override";
type EngineMode = "Off" | "DetectionOnly" | "On";
type Props = {
value?: WafHostConfig | null;
showModeSelector?: boolean;
};
export function WafFields({ value }: Props) {
export function WafFields({ value, showModeSelector = true }: Props) {
const [enabled, setEnabled] = useState(value?.enabled ?? false);
const [engineMode, setEngineMode] = useState<"Off" | "DetectionOnly" | "On">(
value?.mode ?? "DetectionOnly"
);
const [wafMode, setWafMode] = useState<WafMode>(value?.waf_mode ?? "merge");
const [engineMode, setEngineMode] = useState<EngineMode>(value?.mode ?? "DetectionOnly");
const [loadCrs, setLoadCrs] = useState(value?.load_owasp_crs ?? true);
const [customDirectives, setCustomDirectives] = useState(value?.custom_directives ?? "");
const [wafMode, setWafMode] = useState<"merge" | "override">(value?.waf_mode ?? "merge");
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Stack direction="row" spacing={1} alignItems="center">
<SecurityIcon fontSize="small" sx={{ color: "text.secondary" }} />
<Typography variant="subtitle2">Web Application Firewall (WAF)</Typography>
{enabled && (
<Typography variant="caption" color={engineMode === "On" ? "error" : "warning.main"} sx={{ ml: 1 }}>
{engineMode === "On" ? "Blocking" : engineMode === "DetectionOnly" ? "Detection Only" : "Off"}
</Typography>
)}
</Stack>
</AccordionSummary>
<AccordionDetails>
{/* Hidden marker so the server action knows WAF config was submitted */}
<input type="hidden" name="waf_present" value="1" />
<input type="hidden" name="waf_mode" value={wafMode} />
<input type="hidden" name="waf_engine_mode" value={engineMode} />
<input type="hidden" name="waf_load_owasp_crs" value={loadCrs ? "on" : ""} />
<input type="hidden" name="waf_custom_directives" value={customDirectives} />
<Box
sx={{
borderRadius: 2,
border: "1px solid",
borderColor: "error.main",
bgcolor: (theme) =>
theme.palette.mode === "dark" ? "rgba(211,47,47,0.06)" : "rgba(211,47,47,0.04)",
p: 2,
}}
>
<input type="hidden" name="waf_present" value="1" />
<input type="hidden" name="waf_mode" value={wafMode} />
<input type="hidden" name="waf_engine_mode" value={engineMode} />
<input type="hidden" name="waf_load_owasp_crs" value={loadCrs ? "on" : ""} />
<input type="hidden" name="waf_custom_directives" value={customDirectives} />
<Stack spacing={2}>
{/* Enable toggle */}
{/* Header */}
<Stack direction="row" alignItems="flex-start" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="flex-start" spacing={1.5} flex={1} minWidth={0}>
<Box
sx={{
mt: 0.25,
width: 32,
height: 32,
borderRadius: 1.5,
bgcolor: "error.main",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}
>
<GppBadIcon sx={{ fontSize: 18, color: "#fff" }} />
</Box>
<Box minWidth={0}>
<Typography variant="subtitle1" fontWeight={700} lineHeight={1.3}>
Web Application Firewall
</Typography>
<Typography variant="body2" color="text.secondary" mt={0.25}>
Inspect and block malicious requests via Coraza / OWASP CRS
</Typography>
</Box>
</Stack>
<Switch
checked={enabled}
onChange={(_, checked) => setEnabled(checked)}
sx={{ flexShrink: 0 }}
/>
</Stack>
{/* Expanded content */}
<Collapse in={enabled} timeout="auto" unmountOnExit>
<Box mt={2}>
{/* Override mode selector */}
{showModeSelector && (
<>
<Stack direction="row" spacing={1}>
{(["merge", "override"] as WafMode[]).map((v) => (
<Box
key={v}
onClick={() => 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",
},
}}
>
<Typography
variant="body2"
fontWeight={wafMode === v ? 600 : 400}
color={wafMode === v ? "error.main" : "text.secondary"}
sx={{ transition: "all 0.15s ease" }}
>
{v === "merge" ? "Merge with global" : "Override global"}
</Typography>
</Box>
))}
</Stack>
<Divider sx={{ mt: 2, mb: 2 }} />
</>
)}
{!showModeSelector && <Divider sx={{ mb: 2 }} />}
{/* Engine mode */}
<Typography
variant="caption"
color="text.secondary"
fontWeight={600}
sx={{ textTransform: "uppercase", letterSpacing: 0.5 }}
>
Engine Mode
</Typography>
<Stack direction="row" spacing={1} mt={0.75}>
{(["Off", "DetectionOnly", "On"] as EngineMode[]).map((v) => (
<Box
key={v}
onClick={() => 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",
},
}}
>
<Typography
variant="body2"
fontWeight={engineMode === v ? 600 : 400}
color={engineMode === v ? "error.main" : "text.secondary"}
sx={{ transition: "all 0.15s ease", fontSize: "0.8rem" }}
>
{v === "DetectionOnly" ? "Detect only" : v}
</Typography>
</Box>
))}
</Stack>
<Divider sx={{ mt: 2, mb: 1.5 }} />
{/* OWASP CRS */}
<FormControlLabel
control={
<Switch
checked={enabled}
onChange={(_, checked) => setEnabled(checked)}
<Checkbox
checked={loadCrs}
onChange={(_, checked) => setLoadCrs(checked)}
size="small"
/>
}
label="Enable WAF for this host"
label={
<Box>
<Typography variant="body2" fontWeight={500}>
Load OWASP Core Rule Set
</Typography>
<Typography variant="caption" color="text.secondary">
Covers SQLi, XSS, LFI, RCE and hundreds of other attack patterns
</Typography>
</Box>
}
/>
{enabled && (
<>
{/* Override mode */}
<Box>
<FormLabel sx={{ fontSize: "0.75rem", fontWeight: 600, textTransform: "uppercase", letterSpacing: 0.5 }}>
Override Mode
</FormLabel>
<ToggleButtonGroup
value={wafMode}
exclusive
onChange={(_, v) => v && setWafMode(v)}
size="small"
sx={{ mt: 0.5 }}
>
<ToggleButton value="merge">Merge with global</ToggleButton>
<ToggleButton value="override">Override global</ToggleButton>
</ToggleButtonGroup>
</Box>
{/* Engine mode */}
<FormControl>
<FormLabel sx={{ fontSize: "0.75rem", fontWeight: 600, textTransform: "uppercase", letterSpacing: 0.5 }}>
Engine Mode
</FormLabel>
<RadioGroup
row
value={engineMode}
onChange={(e) => setEngineMode(e.target.value as "Off" | "DetectionOnly" | "On")}
>
<FormControlLabel value="Off" control={<Radio size="small" />} label="Off" />
<FormControlLabel value="DetectionOnly" control={<Radio size="small" />} label="Detection Only" />
<FormControlLabel value="On" control={<Radio size="small" />} label="On (Blocking)" />
</RadioGroup>
</FormControl>
{/* OWASP CRS */}
<FormControlLabel
control={
<Checkbox
checked={loadCrs}
onChange={(_, checked) => setLoadCrs(checked)}
size="small"
/>
}
label={
<span>
Load OWASP Core Rule Set{" "}
<Typography component="span" variant="caption" color="text.secondary">
(covers SQLi, XSS, LFI, RCE)
</Typography>
</span>
}
/>
{/* Custom directives */}
<TextField
label="Custom SecLang Directives"
multiline
minRows={3}
maxRows={10}
value={customDirectives}
onChange={(e) => 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. Applied after OWASP CRS if enabled."
fullWidth
/>
</>
)}
</Stack>
</AccordionDetails>
</Accordion>
{/* Custom directives */}
<Box mt={1.5}>
<TextField
label="Custom SecLang Directives"
multiline
minRows={3}
maxRows={10}
value={customDirectives}
onChange={(e) => 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
/>
</Box>
</Box>
</Collapse>
</Box>
);
}