diff --git a/src/components/proxy-hosts/GeoBlockFields.tsx b/src/components/proxy-hosts/GeoBlockFields.tsx new file mode 100644 index 00000000..a2f9f099 --- /dev/null +++ b/src/components/proxy-hosts/GeoBlockFields.tsx @@ -0,0 +1,423 @@ +"use client"; + +import { + Box, + Chip, + Collapse, + Divider, + FormControlLabel, + IconButton, + Stack, + Switch, + TextField, + ToggleButton, + ToggleButtonGroup, + Typography +} from "@mui/material"; +import { useState, KeyboardEvent } from "react"; +import { GeoBlockSettings } from "@/src/lib/settings"; +import { GeoBlockMode } from "@/src/lib/models/proxy-hosts"; + +// ─── TagInput ──────────────────────────────────────────────────────────────── + +type TagInputProps = { + name: string; + label: string; + initialValues?: string[]; + placeholder?: string; + validate?: (value: string) => boolean; + uppercase?: boolean; +}; + +function TagInput({ name, label, initialValues = [], placeholder, validate, uppercase = false }: TagInputProps) { + const [tags, setTags] = useState(initialValues); + const [inputValue, setInputValue] = useState(""); + + function addTag(raw: string) { + const value = uppercase ? raw.trim().toUpperCase() : raw.trim(); + if (!value) return; + if (validate && !validate(value)) return; + if (tags.includes(value)) return; + setTags((prev) => [...prev, value]); + setInputValue(""); + } + + function handleKeyDown(e: KeyboardEvent) { + if (e.key === "Enter" || e.key === ",") { + e.preventDefault(); + addTag(inputValue); + } else if (e.key === "Backspace" && inputValue === "" && tags.length > 0) { + setTags((prev) => prev.slice(0, -1)); + } + } + + function handleBlur() { + if (inputValue.trim()) { + addTag(inputValue); + } + } + + function removeTag(index: number) { + setTags((prev) => prev.filter((_, i) => i !== index)); + } + + return ( + + + { + const input = (e.currentTarget as HTMLElement).querySelector("input[type='text']") as HTMLInputElement | null; + input?.focus(); + }} + > + + {label} + + {tags.map((tag, i) => ( + removeTag(i)} + /> + ))} + setInputValue(e.target.value)} + onKeyDown={handleKeyDown} + onBlur={handleBlur} + placeholder={tags.length === 0 ? placeholder : ""} + style={{ + border: "none", + outline: "none", + background: "transparent", + fontSize: "0.875rem", + flexGrow: 1, + minWidth: 80, + padding: "2px 4px" + }} + /> + + + ); +} + +// ─── ResponseHeadersEditor ──────────────────────────────────────────────────── + +type HeaderRow = { key: string; value: string }; + +function ResponseHeadersEditor({ initialHeaders }: { initialHeaders: Record }) { + const [rows, setRows] = useState(() => + Object.entries(initialHeaders).map(([key, value]) => ({ key, value })) + ); + + function addRow() { + setRows((prev) => [...prev, { key: "", value: "" }]); + } + + function removeRow(index: number) { + setRows((prev) => prev.filter((_, i) => i !== index)); + } + + function updateRow(index: number, field: "key" | "value", val: string) { + setRows((prev) => prev.map((row, i) => (i === index ? { ...row, [field]: val } : row))); + } + + return ( + + + + Response Headers + + + + + + + {rows.length === 0 && ( + + No custom response headers. Click + to add one. + + )} + + {rows.map((row, i) => ( + + + + updateRow(i, "key", e.target.value)} + size="small" + fullWidth + /> + updateRow(i, "value", e.target.value)} + size="small" + fullWidth + /> + removeRow(i)} title="Remove header" color="error"> + × + + + ))} + + + ); +} + +// ─── 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"); + + return ( + + {/* Always-present sentinel */} + + + + {/* Header row: title + toggle */} + + + + Geo Blocking + + + Block or allow traffic by country, continent, ASN, CIDR, or IP + + + setEnabled(checked)} + /> + + + {/* Mode selector (merge vs override) */} + {showModeSelector && ( + + + { + if (newMode) setMode(newMode); + }} + > + Merge with global + Override global + + + )} + + {/* Collapsible detail fields */} + + + + + {/* Block Rules */} + + + Block Rules + + + + + /^\d+$/.test(v)} + /> + + + + + + + + {/* Allow Rules */} + + + Allow Rules{" "} + + (override block rules) + + + + + + /^\d+$/.test(v)} + /> + + + + + + + + {/* Trusted Proxies */} + + + Trusted Proxies + + + + + + + {/* Block Response */} + + + Block Response + + + + + + + + + + + + + + + ); +}