"use client"; import { useState, useEffect } from "react"; import PropTypes from "prop-types"; import Modal from "./Modal"; import Button from "./Button"; const ALL_PROXY_TYPES = [ { value: "http", label: "HTTP" }, { value: "https", label: "HTTPS" }, { value: "socks5", label: "SOCKS5" }, ]; const SOCKS5_UI_ENABLED = process.env.NEXT_PUBLIC_ENABLE_SOCKS5_PROXY === "true"; const PROXY_TYPES = SOCKS5_UI_ENABLED ? ALL_PROXY_TYPES : ALL_PROXY_TYPES.filter((type) => type.value !== "socks5"); const LEVEL_LABELS = { global: "Global", provider: "Provider", combo: "Combo", key: "Key", direct: "Direct (none)", }; /** * ProxyConfigModal — Reusable proxy configuration modal for all 4 levels * @param {Object} props * @param {boolean} props.isOpen * @param {Function} props.onClose * @param {"global"|"provider"|"combo"|"key"} props.level * @param {string} [props.levelId] — providerId, comboId, or connectionId * @param {string} [props.levelLabel] — display name for the level * @param {Function} [props.onSaved] — callback after save */ export default function ProxyConfigModal({ isOpen, onClose, level, levelId, levelLabel, onSaved, }: { isOpen: any; onClose: any; level: any; levelId?: any; levelLabel?: any; onSaved?: any; }) { const [proxyType, setProxyType] = useState(PROXY_TYPES[0]?.value || "http"); const [host, setHost] = useState(""); const [port, setPort] = useState(""); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [showAuth, setShowAuth] = useState(false); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState(null); const [loading, setLoading] = useState(true); const [inheritedFrom, setInheritedFrom] = useState(null); const [hasOwnProxy, setHasOwnProxy] = useState(false); const [formError, setFormError] = useState(null); const getDefaultPort = (type) => (type === "socks5" ? "1080" : "8080"); // Load existing proxy config when modal opens useEffect(() => { if (!isOpen) return; setTestResult(null); setFormError(null); setLoading(true); const loadProxy = async () => { try { // Load own proxy const params = new URLSearchParams({ level }); if (levelId) params.set("id", levelId); const res = await fetch(`/api/settings/proxy?${params}`); if (res.ok) { const data = await res.json(); const proxy = data.proxy; if (proxy && proxy.host) { const normalizedType = String(proxy.type || "http").toLowerCase(); const hasTypeOption = PROXY_TYPES.some((entry) => entry.value === normalizedType); setProxyType(hasTypeOption ? normalizedType : PROXY_TYPES[0]?.value || "http"); setHost(proxy.host || ""); setPort(proxy.port || ""); setUsername(proxy.username || ""); setPassword(proxy.password || ""); setShowAuth(!!(proxy.username || proxy.password)); setHasOwnProxy(true); if (normalizedType === "socks5" && !SOCKS5_UI_ENABLED) { setFormError( "SOCKS5 is configured but hidden because NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=false." ); } } else { resetFields(); setHasOwnProxy(false); } } // Check inherited proxy (for non-global levels) if (level !== "global" && levelId) { // Try to resolve the effective proxy to show inheritance info const fullConfig = await fetch("/api/settings/proxy"); if (fullConfig.ok) { const config = await fullConfig.json(); // Determine inheritance source if (level === "key") { // Check combo, provider, global if (config.global) setInheritedFrom({ level: "Global", proxy: config.global }); // Provider info requires more context, showing global as fallback } else if (level === "combo") { if (config.global) setInheritedFrom({ level: "Global", proxy: config.global }); } else if (level === "provider") { if (config.global) setInheritedFrom({ level: "Global", proxy: config.global }); } } } } catch (error) { console.error("Error loading proxy config:", error); } finally { setLoading(false); } }; loadProxy(); }, [isOpen, level, levelId]); const resetFields = () => { setProxyType(PROXY_TYPES[0]?.value || "http"); setHost(""); setPort(""); setUsername(""); setPassword(""); setShowAuth(false); setFormError(null); }; const handleSave = async () => { if (!host.trim()) return; setFormError(null); setSaving(true); try { const proxy = { type: proxyType, host: host.trim(), port: port.trim() || getDefaultPort(proxyType), username: username.trim(), password: password.trim(), }; const res = await fetch("/api/settings/proxy", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ level, id: levelId, proxy }), }); const payload = await res.json().catch(() => ({})); if (!res.ok) { setFormError(payload?.error?.message || "Failed to save proxy configuration"); return; } setHasOwnProxy(true); onSaved?.(); } catch (error) { console.error("Error saving proxy:", error); setFormError(error.message || "Failed to save proxy configuration"); } finally { setSaving(false); } }; const handleClear = async () => { setFormError(null); setSaving(true); try { const params = new URLSearchParams({ level }); if (levelId) params.set("id", levelId); const res = await fetch(`/api/settings/proxy?${params}`, { method: "DELETE" }); const payload = await res.json().catch(() => ({})); if (!res.ok) { setFormError(payload?.error?.message || "Failed to clear proxy configuration"); return; } resetFields(); setHasOwnProxy(false); setTestResult(null); onSaved?.(); } catch (error) { console.error("Error clearing proxy:", error); setFormError(error.message || "Failed to clear proxy configuration"); } finally { setSaving(false); } }; const handleTest = async () => { if (!host.trim()) return; setFormError(null); setTesting(true); setTestResult(null); try { const proxy = { type: proxyType, host: host.trim(), port: port.trim() || getDefaultPort(proxyType), username: username.trim(), password: password.trim(), }; const res = await fetch("/api/settings/proxy/test", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ proxy }), }); const data = await res.json().catch(() => ({})); if (!res.ok) { const message = data?.error?.message || "Connection failed"; setTestResult({ success: false, error: message }); setFormError(message); return; } setTestResult(data); } catch (error) { setTestResult({ success: false, error: error.message }); setFormError(error.message || "Connection failed"); } finally { setTesting(false); } }; const title = level === "global" ? "Global Proxy Configuration" : `${LEVEL_LABELS[level]} Proxy — ${levelLabel || levelId || ""}`; return ( {loading ? (
Loading proxy configuration...
) : (
{/* Inheritance indicator */} {level !== "global" && !hasOwnProxy && inheritedFrom && (
subdirectory_arrow_right Inheriting from {inheritedFrom.level}: {inheritedFrom.proxy?.type} ://{inheritedFrom.proxy?.host}:{inheritedFrom.proxy?.port}
)} {/* Proxy Type Selector */}
{PROXY_TYPES.map((t) => ( ))}
{/* Host + Port */}
setHost(e.target.value)} placeholder="1.2.3.4 or proxy.example.com" className="w-full px-3 py-2.5 rounded-lg bg-bg-subtle border border-border text-sm text-text-primary placeholder:text-text-muted/50 focus:outline-none focus:border-primary transition-colors" />
setPort(e.target.value)} placeholder={getDefaultPort(proxyType)} className="w-full px-3 py-2.5 rounded-lg bg-bg-subtle border border-border text-sm text-text-primary placeholder:text-text-muted/50 focus:outline-none focus:border-primary transition-colors" />
{/* Auth Toggle */}
{showAuth && (
setUsername(e.target.value)} placeholder="Username" className="w-full px-3 py-2.5 rounded-lg bg-bg-subtle border border-border text-sm text-text-primary placeholder:text-text-muted/50 focus:outline-none focus:border-primary transition-colors" />
setPassword(e.target.value)} placeholder="Password" className="w-full px-3 py-2.5 rounded-lg bg-bg-subtle border border-border text-sm text-text-primary placeholder:text-text-muted/50 focus:outline-none focus:border-primary transition-colors" />
)}
{/* Test Result */} {formError && (
{formError}
)} {testResult && (
{testResult.success ? "check_circle" : "error"}
{testResult.success ? (
Connected IP: {testResult.publicIp} {testResult.latencyMs && ` · ${testResult.latencyMs}ms`}
) : (
{testResult.error || "Connection failed"} {testResult.latencyMs && ( ({testResult.latencyMs}ms) )}
)}
)} {/* Actions */}
{hasOwnProxy && ( )}
)}
); } ProxyConfigModal.propTypes = { isOpen: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, level: PropTypes.oneOf(["global", "provider", "combo", "key"]).isRequired, levelId: PropTypes.string, levelLabel: PropTypes.string, onSaved: PropTypes.func, };