"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Badge, Button, Card, CardSkeleton, Input, Modal, Toggle } from "@/shared/components"; import { useNotificationStore } from "@/store/notificationStore"; function getStatusVariant(status) { if (status === "active") return "success"; if (status === "error") return "error"; return "default"; } function formatDateTime(value) { if (!value) return "Never"; const date = new Date(value); if (Number.isNaN(date.getTime())) return "Never"; return date.toLocaleString(); } function normalizeFormData(data = {}) { return { name: data.name || "", proxyUrl: data.proxyUrl || "", noProxy: data.noProxy || "", isActive: data.isActive !== false, strictProxy: data.strictProxy === true, }; } export default function ProxyPoolsPage() { const [proxyPools, setProxyPools] = useState([]); const [loading, setLoading] = useState(true); const [showFormModal, setShowFormModal] = useState(false); const [showBatchImportModal, setShowBatchImportModal] = useState(false); const [showVercelModal, setShowVercelModal] = useState(false); const [editingProxyPool, setEditingProxyPool] = useState(null); const [formData, setFormData] = useState(normalizeFormData()); const [batchImportText, setBatchImportText] = useState(""); const [vercelForm, setVercelForm] = useState({ vercelToken: "", projectName: "vercel-relay" }); const [saving, setSaving] = useState(false); const [importing, setImporting] = useState(false); const [deploying, setDeploying] = useState(false); const [testingId, setTestingId] = useState(null); const notify = useNotificationStore(); const fetchProxyPools = useCallback(async () => { try { const res = await fetch("/api/proxy-pools?includeUsage=true", { cache: "no-store" }); const data = await res.json(); if (res.ok) { setProxyPools(data.proxyPools || []); } } catch (error) { console.log("Error fetching proxy pools:", error); } finally { setLoading(false); } }, []); useEffect(() => { fetchProxyPools(); }, [fetchProxyPools]); const resetForm = () => { setEditingProxyPool(null); setFormData(normalizeFormData()); }; const openCreateModal = () => { resetForm(); setShowFormModal(true); }; const openEditModal = (proxyPool) => { setEditingProxyPool(proxyPool); setFormData(normalizeFormData(proxyPool)); setShowFormModal(true); }; const closeFormModal = () => { setShowFormModal(false); resetForm(); }; const handleSave = async () => { const payload = { name: formData.name.trim(), proxyUrl: formData.proxyUrl.trim(), noProxy: formData.noProxy.trim(), isActive: formData.isActive === true, strictProxy: formData.strictProxy === true, }; if (!payload.name || !payload.proxyUrl) return; setSaving(true); try { const isEdit = !!editingProxyPool; const res = await fetch(isEdit ? `/api/proxy-pools/${editingProxyPool.id}` : "/api/proxy-pools", { method: isEdit ? "PUT" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (res.ok) { await fetchProxyPools(); closeFormModal(); notify.success(editingProxyPool ? "Proxy pool updated" : "Proxy pool created"); } else { const data = await res.json(); notify.error(data.error || "Failed to save proxy pool"); } } catch (error) { console.log("Error saving proxy pool:", error); } finally { setSaving(false); } }; const handleDelete = async (proxyPool) => { const deleting = confirm(`Delete proxy pool \"${proxyPool.name}\"?`); if (!deleting) return; try { const res = await fetch(`/api/proxy-pools/${proxyPool.id}`, { method: "DELETE" }); if (res.ok) { setProxyPools((prev) => prev.filter((item) => item.id !== proxyPool.id)); notify.success("Proxy pool deleted"); return; } const data = await res.json(); if (res.status === 409) { notify.warning(`Cannot delete: ${data.boundConnectionCount || 0} connection(s) are still using this pool.`); } else { notify.error(data.error || "Failed to delete proxy pool"); } } catch (error) { console.log("Error deleting proxy pool:", error); notify.error("Failed to delete proxy pool"); } }; const handleTest = async (proxyPoolId) => { setTestingId(proxyPoolId); try { const res = await fetch(`/api/proxy-pools/${proxyPoolId}/test`, { method: "POST" }); const data = await res.json(); if (!res.ok) { notify.error(data.error || "Failed to test proxy"); return; } await fetchProxyPools(); notify.success(data.ok ? "Proxy test passed" : "Proxy test failed"); } catch (error) { console.log("Error testing proxy pool:", error); notify.error("Failed to test proxy"); } finally { setTestingId(null); } }; const openBatchImportModal = () => { setBatchImportText(""); setShowBatchImportModal(true); }; const closeBatchImportModal = () => { if (importing) return; setShowBatchImportModal(false); }; const openVercelModal = () => { setVercelForm({ vercelToken: "", projectName: "vercel-relay" }); setShowVercelModal(true); }; const closeVercelModal = () => { if (deploying) return; setShowVercelModal(false); }; const handleVercelDeploy = async () => { if (!vercelForm.vercelToken.trim()) return; setDeploying(true); try { const res = await fetch("/api/proxy-pools/vercel-deploy", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(vercelForm), }); const data = await res.json(); if (res.ok) { await fetchProxyPools(); closeVercelModal(); notify.success(`Deployed: ${data.deployUrl}`); } else { notify.error(data.error || "Deploy failed"); } } catch (error) { console.log("Error deploying Vercel relay:", error); notify.error("Deploy failed"); } finally { setDeploying(false); } }; const parseProxyLine = (line) => { const trimmed = line.trim(); if (!trimmed) return null; if (trimmed.includes("://")) { const parsed = new URL(trimmed); const hostLabel = parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname; return { proxyUrl: parsed.toString(), name: `Imported ${hostLabel}`, }; } const parts = trimmed.split(":"); if (parts.length === 4) { const [host, port, username, password] = parts; if (!host || !port || !username || !password) { throw new Error("Invalid host:port:user:pass format"); } const proxyUrl = `http://${encodeURIComponent(username)}:${encodeURIComponent(password)}@${host}:${port}`; const parsed = new URL(proxyUrl); return { proxyUrl: parsed.toString(), name: `Imported ${host}:${port}`, }; } throw new Error("Unsupported format"); }; const handleBatchImport = async () => { const lines = batchImportText .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean); if (lines.length === 0) { notify.warning("Please paste at least one proxy line."); return; } const parsedEntries = []; const invalidLines = []; lines.forEach((line, index) => { try { const parsed = parseProxyLine(line); if (parsed) { parsedEntries.push({ ...parsed, lineNumber: index + 1, }); } } catch (error) { invalidLines.push(`Line ${index + 1}: ${error.message}`); } }); if (invalidLines.length > 0) { notify.error(`Invalid proxy format:\n${invalidLines.join("\n")}`); return; } setImporting(true); try { const existingKeys = new Set( proxyPools.map((pool) => `${(pool.proxyUrl || "").trim()}|||${(pool.noProxy || "").trim()}`) ); let created = 0; let skipped = 0; let failed = 0; for (const entry of parsedEntries) { const dedupeKey = `${entry.proxyUrl}|||`; if (existingKeys.has(dedupeKey)) { skipped += 1; continue; } const res = await fetch("/api/proxy-pools", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: entry.name, proxyUrl: entry.proxyUrl, noProxy: "", isActive: true, }), }); if (res.ok) { created += 1; existingKeys.add(dedupeKey); } else { failed += 1; } } await fetchProxyPools(); setShowBatchImportModal(false); notify.success(`Batch import completed: Created ${created}, Skipped ${skipped}, Failed ${failed}`); } catch (error) { console.log("Error batch importing proxies:", error); notify.error("Batch import failed"); } finally { setImporting(false); } }; const activeCount = useMemo( () => proxyPools.filter((pool) => pool.isActive === true).length, [proxyPools] ); if (loading) { return (
Manage reusable per-connection proxies and bind them to provider connections.
No proxy pool entries yet
Create a proxy pool entry, then assign it to connections.
{pool.name}
{pool.proxyUrl}
{pool.noProxy ? (No proxy: {pool.noProxy}
) : null}Last tested: {formatDateTime(pool.lastTestedAt)} {pool.lastError ? ` · ${pool.lastError}` : ""}
What is Vercel Relay?
Deploys an edge relay function to Vercel. All AI provider requests will be forwarded through Vercel's edge network, masking your real IP from providers.
Active
Inactive pools are ignored by runtime resolution.
Strict Proxy
Fail request if proxy is unreachable instead of falling back to direct.