"use client"; import { Accordion, 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, Home, 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 ───────────────────────────────────────────────────────────── 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 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} ); } // ─── CountryPicker ──────────────────────────────────────────────────────────── const CONTINENTS = [ { code: "AF", name: "Africa", emoji: "🌍" }, { code: "AN", name: "Antarctica", emoji: "🧊" }, { code: "AS", name: "Asia", emoji: "🌏" }, { code: "EU", name: "Europe", emoji: "🌍" }, { code: "NA", name: "N. America", emoji: "🌎" }, { code: "OC", name: "Oceania", emoji: "🌏" }, { code: "SA", name: "S. America", emoji: "🌎" }, ]; type CountryPickerProps = { name: string; initialValues?: string[]; accentColor?: "warning" | "success"; }; function CountryPicker({ name, initialValues = [], accentColor = "warning" }: CountryPickerProps) { const [selected, setSelected] = useState>( () => new Set(initialValues.map((c) => c.toUpperCase()).filter(Boolean)) ); const [search, setSearch] = useState(""); const filtered = useMemo(() => { const q = search.toLowerCase().trim(); if (!q) return COUNTRIES; return COUNTRIES.filter( (c) => c.name.toLowerCase().includes(q) || c.code.toLowerCase().startsWith(q) ); }, [search]); const toggle = useCallback((code: string) => { setSelected((prev) => { const next = new Set(prev); if (next.has(code)) next.delete(code); else next.add(code); return next; }); }, []); const selectFiltered = useCallback(() => { setSelected((prev) => { const next = new Set(prev); filtered.forEach((c) => next.add(c.code)); return next; }); }, [filtered]); const clearFiltered = useCallback(() => { setSelected((prev) => { const next = new Set(prev); filtered.forEach((c) => next.delete(c.code)); return next; }); }, [filtered]); 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)} className="h-8 text-sm pr-8" /> {search && ( )}
{/* Toolbar */}
{selected.size > 0 ? ( <>{selected.size} selected{search && `, ${selectedInFiltered} shown`} ) : ( None selected )}
·
{/* Grid */}
{filtered.length === 0 ? ( No countries match “{search}” ) : ( filtered.map((country) => { const isSelected = selected.has(country.code); return ( ); }) )}
{/* Selected summary chips (shown when search is active and selected items are hidden) */} {search && selected.size > 0 && (
All selected ({selected.size}):
{[...selected].map((code) => { const country = COUNTRIES.find((c) => c.code === code); return ( {flagEmoji(code)} {country?.name ?? code} ); })}
)}
); } // ─── ContinentPicker ────────────────────────────────────────────────────────── type ContinentPickerProps = { name: string; initialValues?: string[]; accentColor?: "warning" | "success"; }; function ContinentPicker({ name, initialValues = [], accentColor = "warning" }: ContinentPickerProps) { const [selected, setSelected] = useState>( () => new Set(initialValues.map((c) => c.toUpperCase()).filter(Boolean)) ); const toggle = (code: string) => { setSelected((prev) => { const next = new Set(prev); if (next.has(code)) next.delete(code); else next.add(code); return next; }); }; const isWarning = accentColor === "warning"; return (
{selected.size > 0 ? `${selected.size} selected` : None selected}
·
{CONTINENTS.map((c) => { const isSelected = selected.has(c.code); return ( ); })}
); } // ─── 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 (
0 && "pb-1" )}> {tags.map((tag) => ( {tag} ))} 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}

}
); } // ─── 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 (

Custom Response Headers

{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))} className="h-8 text-sm" /> setRows((prev) => prev.map((r, j) => j === i ? { ...r, value: e.target.value } : r))} className="h-8 text-sm" />
))}
)}
); } // ─── RulesPanel ─────────────────────────────────────────────────────────────── type RulesPanelProps = { prefix: "block" | "allow"; initial: GeoBlockSettings | null; resetKey?: number; }; function RulesPanel({ prefix, initial, resetKey = 0 }: RulesPanelProps) { const accentColor = prefix === "block" ? "warning" : "success"; const countries = prefix === "block" ? (initial?.block_countries ?? []) : (initial?.allow_countries ?? []); const continents = prefix === "block" ? (initial?.block_continents ?? []) : (initial?.allow_continents ?? []); const asns = prefix === "block" ? (initial?.block_asns ?? []) : (initial?.allow_asns ?? []); const cidrs = prefix === "block" ? (initial?.block_cidrs ?? []) : (initial?.allow_cidrs ?? []); const ips = prefix === "block" ? (initial?.block_ips ?? []) : (initial?.allow_ips ?? []); return (
{/* Countries */}

Countries

{/* Continents */}

Continents

{/* ASNs */} /^\d+$/.test(v)} /> {/* CIDRs + IPs */}
); } // ─── GeoBlockFields ─────────────────────────────────────────────────────────── type GeoBlockFieldsProps = { initialValues?: { geoblock: GeoBlockSettings | null; geoblock_mode: GeoBlockMode; }; showModeSelector?: boolean; }; const RFC1918_CIDRS = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]; const BLOCK_ALL_CIDR = "0.0.0.0/0"; export function GeoBlockFields({ initialValues, showModeSelector = true }: GeoBlockFieldsProps) { const rawInitial = initialValues?.geoblock ?? null; const [enabled, setEnabled] = useState(rawInitial?.enabled ?? false); const [mode, setMode] = useState(initialValues?.geoblock_mode ?? "merge"); const [resetKey, setResetKey] = useState(0); const [initial, setInitial] = useState(rawInitial); function applyLanOnlyPreset() { setEnabled(true); setInitial((prev) => ({ enabled: true, block_countries: prev?.block_countries ?? [], block_continents: prev?.block_continents ?? [], block_asns: prev?.block_asns ?? [], block_cidrs: [BLOCK_ALL_CIDR], block_ips: prev?.block_ips ?? [], allow_countries: prev?.allow_countries ?? [], allow_continents: prev?.allow_continents ?? [], allow_asns: prev?.allow_asns ?? [], allow_cidrs: RFC1918_CIDRS, allow_ips: prev?.allow_ips ?? [], trusted_proxies: prev?.trusted_proxies ?? [], fail_closed: prev?.fail_closed ?? false, response_status: prev?.response_status ?? 403, response_body: prev?.response_body ?? "Forbidden", response_headers: prev?.response_headers ?? {}, redirect_url: prev?.redirect_url ?? "", })); setResetKey((k) => k + 1); } return (
{/* Header */}

Geo Blocking

Block or allow traffic by country, continent, ASN, CIDR, or IP

{/* Mode selector */} {/* Detail fields */}
{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 &&
} {/* Presets */}
Presets:
{/* Block / Allow tabs */} Block Rules Allow Rules

Allow rules take precedence over block rules.

{/* Advanced: Trusted Proxies + Block Response */}
Trusted Proxies & Block Response

HTTP status when blocked

Body text returned to blocked clients

If set, sends a 302 redirect instead of status/body above

); }