diff --git a/src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx b/src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx index b673d92f22..14b3610a67 100644 --- a/src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx @@ -38,6 +38,23 @@ type TestResult = { error?: string; }; +type ParsedProxyEntry = { + name: string; + host: string; + port: number; + username: string; + password: string; + type: string; + region: string; + status: string; + notes: string; +}; + +type ParseError = { + line: number; + reason: string; +}; + const EMPTY_FORM = { id: "", name: "", @@ -51,6 +68,85 @@ const EMPTY_FORM = { status: "active", }; +const BULK_IMPORT_TEMPLATE = `# Proxy Bulk Import +# Format: NAME|HOST|PORT|USERNAME|PASSWORD|TYPE|REGION|STATUS|NOTES +# Required: NAME, HOST, PORT +# Optional: USERNAME, PASSWORD, TYPE (http|https|socks5, default: socks5), REGION, STATUS (active|inactive, default: active), NOTES +# Lines starting with # are ignored. Existing proxies (same host+port) will be updated. +# +# SOCKS5 examples: +# proxy-us|138.99.147.218|50101|myuser|mypass|socks5|US-East|active|US production proxy +# proxy-eu|200.234.177.62|50101|myuser|mypass|socks5|EU-West +# +# HTTP/HTTPS examples: +# http-proxy|10.0.0.50|8080|||http||active|Internal HTTP proxy +# https-proxy|proxy.example.com|443|admin|secret123|https|US|active +`; + +const VALID_TYPES = new Set(["http", "https", "socks5"]); +const VALID_STATUSES = new Set(["active", "inactive"]); + +function parseBulkImportText(text: string): { + entries: ParsedProxyEntry[]; + errors: ParseError[]; + skipped: number; +} { + const lines = text.split("\n"); + const entries: ParsedProxyEntry[] = []; + const errors: ParseError[] = []; + let skipped = 0; + + for (let i = 0; i < lines.length; i++) { + const raw = lines[i].trim(); + if (!raw || raw.startsWith("#")) { + skipped++; + continue; + } + + const parts = raw.split("|").map((p) => p.trim()); + const [name, host, portStr, username, password, type, region, status, notes] = parts; + const lineNum = i + 1; + + if (!name) { + errors.push({ line: lineNum, reason: "Missing NAME" }); + continue; + } + if (!host) { + errors.push({ line: lineNum, reason: "Missing HOST" }); + continue; + } + const port = Number(portStr); + if (!portStr || isNaN(port) || port < 1 || port > 65535) { + errors.push({ line: lineNum, reason: "Invalid PORT (must be 1-65535)" }); + continue; + } + const normalizedType = (type || "socks5").toLowerCase(); + if (!VALID_TYPES.has(normalizedType)) { + errors.push({ line: lineNum, reason: `Invalid TYPE '${type}' (use http, https, or socks5)` }); + continue; + } + const normalizedStatus = (status || "active").toLowerCase(); + if (!VALID_STATUSES.has(normalizedStatus)) { + errors.push({ line: lineNum, reason: `Invalid STATUS '${status}' (use active or inactive)` }); + continue; + } + + entries.push({ + name, + host, + port, + username: username || "", + password: password || "", + type: normalizedType, + region: region || "", + status: normalizedStatus, + notes: notes || "", + }); + } + + return { entries, errors, skipped }; +} + export default function ProxyRegistryManager() { const t = useTranslations("proxyRegistry"); const [items, setItems] = useState([]); @@ -72,6 +168,20 @@ export default function ProxyRegistryManager() { const [bulkScopeIds, setBulkScopeIds] = useState(""); const [bulkProxyId, setBulkProxyId] = useState(""); + // Bulk Import state + const [bulkImportOpen, setBulkImportOpen] = useState(false); + const [bulkImportText, setBulkImportText] = useState(BULK_IMPORT_TEMPLATE); + const [bulkImportParsed, setBulkImportParsed] = useState([]); + const [bulkImportErrors, setBulkImportErrors] = useState([]); + const [bulkImportSkipped, setBulkImportSkipped] = useState(0); + const [bulkImportParsedOnce, setBulkImportParsedOnce] = useState(false); + const [bulkImporting, setBulkImporting] = useState(false); + const [bulkImportResult, setBulkImportResult] = useState<{ + created: number; + updated: number; + failed: number; + } | null>(null); + const editingId = useMemo(() => form.id || "", [form.id]); const loadHealth = useCallback(async () => { @@ -382,6 +492,77 @@ export default function ProxyRegistryManager() { } }; + const handleBulkImportParse = () => { + const { entries, errors, skipped } = parseBulkImportText(bulkImportText); + setBulkImportParsed(entries); + setBulkImportErrors(errors); + setBulkImportSkipped(skipped); + setBulkImportParsedOnce(true); + setBulkImportResult(null); + }; + + const handleBulkImportExecute = async () => { + if (bulkImportParsed.length === 0) return; + if (bulkImportParsed.length > 100) { + setError(t("bulkImportMaxExceeded")); + return; + } + + setBulkImporting(true); + setError(null); + setBulkImportResult(null); + + try { + const payload = { + items: bulkImportParsed.map((entry) => ({ + name: entry.name, + type: entry.type, + host: entry.host, + port: entry.port, + username: entry.username || undefined, + password: entry.password || undefined, + region: entry.region || null, + notes: entry.notes || null, + status: entry.status as "active" | "inactive", + })), + }; + + const res = await fetch("/api/settings/proxies/bulk-import", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + const data = await res.json().catch(() => ({})); + + if (!res.ok) { + setError(data?.error?.message || "Failed to import proxies"); + return; + } + + setBulkImportResult({ + created: data.created || 0, + updated: data.updated || 0, + failed: data.failed || 0, + }); + + await load(); + } catch (e: any) { + setError(e?.message || "Failed to import proxies"); + } finally { + setBulkImporting(false); + } + }; + + const openBulkImport = () => { + setBulkImportText(BULK_IMPORT_TEMPLATE); + setBulkImportParsed([]); + setBulkImportErrors([]); + setBulkImportSkipped(0); + setBulkImportParsedOnce(false); + setBulkImportResult(null); + setBulkImportOpen(true); + }; + return ( <> @@ -401,6 +582,15 @@ export default function ProxyRegistryManager() { > {t("importLegacy")} +