"use client"; import { Accordion, AccordionDetails, AccordionSummary, Autocomplete, Box, Chip, CircularProgress, Collapse, Divider, Grid, IconButton, 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 DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; import WarningAmberIcon from "@mui/icons-material/WarningAmber"; import PublicIcon from "@mui/icons-material/Public"; import { useState, useEffect, SyntheticEvent } from "react"; import { GeoBlockSettings } from "@/src/lib/settings"; import { GeoBlockMode } from "@/src/lib/models/proxy-hosts"; // ─── GeoIpStatus ───────────────────────────────────────────────────────────── type GeoIpStatusData = { country: boolean; asn: boolean } | null; function GeoIpStatus() { const [status, setStatus] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { fetch("/api/geoip-status") .then((r) => r.json()) .then((d) => setStatus(d)) .catch(() => setStatus(null)) .finally(() => setLoading(false)); }, []); if (loading) { return ; } const allLoaded = status?.country && status?.asn; const noneLoaded = !status?.country && !status?.asn; const color = allLoaded ? "success" : noneLoaded ? "error" : "warning"; const Icon = allLoaded ? CheckCircleOutlineIcon : noneLoaded ? ErrorOutlineIcon : 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." : !status?.country ? "GeoLite2-Country database missing — country/continent blocking disabled" : !status?.asn ? "GeoLite2-ASN database missing — ASN blocking disabled" : "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" } }} /> ); } // ─── TagInput ──────────────────────────────────────────────────────────────── type TagInputProps = { name: string; label: string; initialValues?: string[]; placeholder?: string; helperText?: string; validate?: (value: string) => boolean; uppercase?: boolean; }; function TagInput({ name, label, initialValues = [], placeholder, helperText, validate, uppercase = false }: TagInputProps) { const [tags, setTags] = useState(initialValues); const [inputValue, setInputValue] = useState(""); function processValue(raw: string): string { return uppercase ? raw.trim().toUpperCase() : raw.trim(); } function commitInput(raw: string) { const value = processValue(raw); if (!value) return; if (validate && !validate(value)) return; if (tags.includes(value)) { setInputValue(""); return; } setTags((prev) => [...prev, value]); setInputValue(""); } return ( { if (reason === "input") setInputValue(value); }} onChange={(_, newValue) => { // Called when user selects/removes a chip via Autocomplete internals const processed = newValue.map((v) => processValue(v as string)).filter((v) => { if (!v) return false; if (validate && !validate(v)) return false; return true; }); setTags([...new Set(processed)]); }} renderTags={(value, getTagProps) => value.map((option, index) => { const { key, ...tagProps } = getTagProps({ index }); return ; }) } renderInput={(params) => ( { if (e.key === "," || e.key === " " || e.key === "Enter") { e.preventDefault(); e.stopPropagation(); commitInput(inputValue); } }} /> )} onBlur={() => { if (inputValue.trim()) commitInput(inputValue); }} /> ); } // ─── ResponseHeadersEditor ──────────────────────────────────────────────────── type HeaderRow = { key: string; value: string }; function ResponseHeadersEditor({ initialHeaders }: { initialHeaders: Record }) { const [rows, setRows] = useState(() => Object.entries(initialHeaders).map(([key, value]) => ({ key, value })) ); return ( 0 ? 1 : 0}> Custom Response Headers setRows((prev) => [...prev, { key: "", value: "" }])}> {rows.length === 0 ? ( 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 /> setRows((prev) => prev.map((r, j) => j === i ? { ...r, value: e.target.value } : r))} size="small" fullWidth /> setRows((prev) => prev.filter((_, j) => j !== i))} sx={{ mt: 0.5 }}> ))} )} ); } // ─── GeoBlockFields ─────────────────────────────────────────────────────────── type GeoBlockFieldsProps = { initialValues?: { geoblock: GeoBlockSettings | null; geoblock_mode: GeoBlockMode; }; showModeSelector?: boolean; }; export function GeoBlockFields({ initialValues, showModeSelector = true }: GeoBlockFieldsProps) { 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 }} /> {/* 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 && } {/* Block / Allow tabs */} setRulesTab(v)} variant="fullWidth" sx={{ mb: 2, "& .MuiTab-root": { textTransform: "none", fontWeight: 500 } }} > {/* Block Rules */} {/* Allow Rules */} {/* Advanced: Trusted Proxies + Block Response */} } sx={{ minHeight: 44, "& .MuiAccordionSummary-content": { my: 0.5 } }}> Trusted Proxies & Block Response ); }