"use client"; import { useState, useEffect, useCallback, useRef, type ReactNode } from "react"; import { useTranslations } from "next-intl"; import Card from "./Card"; import Button from "./Button"; import DistributeProxiesButton from "./DistributeProxiesButton"; import NoAuthProviderToggle from "./NoAuthProviderToggle"; interface NoAuthAccountCardProps { providerId: string; providerName: string; generateAccountId: () => string; generateApiKey?: () => Promise; dataKey?: string; description?: string; addLabel?: string; enabled?: boolean; savingEnabled?: boolean; onEnabledChange?: (enabled: boolean) => void; providerProxyControl?: ReactNode; showManualKeyInput?: boolean; onManualApiKeyAdd?: (apiKey: string) => Promise; } interface Connection { id: string; provider: string; apiKey?: string; providerSpecificData?: Record; isActive?: boolean; } interface InlineProxy { type: string; host: string; port: number; username?: string; password?: string; } // #5217 (Gap 1): an account proxy is now stored as EITHER a Proxy Pool reference // (`proxyId`, resolved server-side so a pool edit propagates to every account) OR // a one-off inline `proxy` (the "custom" escape hatch / legacy entries). interface AccountProxyConfig { fingerprint: string; proxy?: InlineProxy | null; proxyId?: string | null; } interface SavedProxy { id: string; name?: string; type?: string; host?: string; port?: number | string; status?: string; } const PROXY_TYPES = [ { value: "http", label: "HTTP" }, { value: "https", label: "HTTPS" }, { value: "socks5", label: "SOCKS5" }, ]; function getAccountProxies(conn: Connection | undefined): AccountProxyConfig[] { return (conn?.providerSpecificData?.accountProxies as AccountProxyConfig[]) || []; } function getEntryForFingerprint(proxies: AccountProxyConfig[], fp: string) { return proxies.find((p) => p.fingerprint === fp) ?? null; } /** * Resolve the proxy to DISPLAY for an account: a by-id reference is looked up in * the Proxy Pool list, an inline proxy is shown directly. Returns null (direct) * when there is no entry or the referenced pool proxy no longer exists. */ function getDisplayProxy( entry: AccountProxyConfig | null, savedProxies: SavedProxy[] ): InlineProxy | null { if (!entry) return null; if (entry.proxyId) { const found = savedProxies.find((p) => p.id === entry.proxyId); if (!found || !found.host) return null; return { type: found.type || "socks5", host: found.host, port: Number(found.port) || 0 }; } return entry.proxy ?? null; } export default function NoAuthAccountCard({ providerId, providerName, generateAccountId, generateApiKey, dataKey = "fingerprints", description, addLabel, enabled = true, savingEnabled = false, onEnabledChange, providerProxyControl, onManualApiKeyAdd, }: NoAuthAccountCardProps) { const t = useTranslations("noAuthProvider"); const resolvedDescription = description || t("accountDescription"); const resolvedAddLabel = addLabel || t("addAccount"); const [connections, setConnections] = useState([]); const [loading, setLoading] = useState(true); const [adding, setAdding] = useState(false); const [proxyAccountId, setProxyAccountId] = useState(null); const [proxyMode, setProxyMode] = useState<"saved" | "custom">("saved"); const [savedProxies, setSavedProxies] = useState([]); const [selectedProxyId, setSelectedProxyId] = useState(""); const [proxyType, setProxyType] = useState("socks5"); const [proxyHost, setProxyHost] = useState(""); const [proxyPort, setProxyPort] = useState("1080"); const [proxyUsername, setProxyUsername] = useState(""); const [proxyPassword, setProxyPassword] = useState(""); const [savingProxy, setSavingProxy] = useState(false); const [manualApiKey, setManualApiKey] = useState(""); const [addingManualKey, setAddingManualKey] = useState(false); const [showManualKeyInput, setShowManualKeyInput] = useState(false); const popoverRef = useRef(null); const fetchConnections = useCallback(async () => { try { const res = await fetch("/api/providers"); if (res.ok) { const data = await res.json(); const filtered = (data.connections || []).filter( (c: Connection) => c.provider === providerId ); setConnections(filtered); } } catch (err) { console.error("Failed to fetch connections:", err); } finally { setLoading(false); } }, [providerId]); const fetchSavedProxies = useCallback(async () => { try { const res = await fetch("/api/settings/proxies"); if (res.ok) { const data = await res.json(); setSavedProxies(Array.isArray(data?.items) ? data.items : []); } } catch (err) { console.error("Failed to fetch saved proxies:", err); } }, []); useEffect(() => { const loadTimer = window.setTimeout(() => { void fetchConnections(); void fetchSavedProxies(); }, 0); return () => window.clearTimeout(loadTimer); }, [fetchConnections, fetchSavedProxies]); useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) { setProxyAccountId(null); } }; if (proxyAccountId) { document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); } }, [proxyAccountId]); const allAccountIds = connections.flatMap((c) => c.providerSpecificData?.[dataKey] || []); const conn = connections[0]; const accountProxies = getAccountProxies(conn); const handleAddAccount = async () => { setAdding(true); try { const accountId = generateAccountId(); const apiKey = generateApiKey ? await generateApiKey() : undefined; if (connections.length === 0) { const res = await fetch("/api/providers", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ provider: providerId, name: t("accountName", { provider: providerName, number: 1 }), ...(apiKey ? { apiKey } : {}), providerSpecificData: { [dataKey]: [accountId] }, }), }); if (!res.ok) { const errData = await res.json().catch(() => ({})); throw new Error(errData?.error || t("createConnectionFailed")); } } else { const updated = [...allAccountIds, accountId]; const res = await fetch(`/api/providers/${conn.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerSpecificData: { [dataKey]: updated }, }), }); if (!res.ok) throw new Error(t("updateConnectionFailed")); } await fetchConnections(); } catch (err) { console.error("Failed to add account:", err); } finally { setAdding(false); } }; const handleAddManualApiKey = async () => { if (!manualApiKey.trim()) return; setAddingManualKey(true); try { if (onManualApiKeyAdd) { await onManualApiKeyAdd(manualApiKey.trim()); } setManualApiKey(""); setShowManualKeyInput(false); await fetchConnections(); } catch (err) { console.error("Failed to add manual API key:", err); } finally { setAddingManualKey(false); } }; const handleRemoveAccount = async (accountId: string) => { if (!conn) return; const updated = allAccountIds.filter((id) => id !== accountId); const updatedProxies = accountProxies.filter((p) => p.fingerprint !== accountId); try { const res = await fetch(`/api/providers/${conn.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerSpecificData: { [dataKey]: updated, accountProxies: updatedProxies, }, }), }); if (res.ok) await fetchConnections(); } catch (err) { console.error("Failed to remove account:", err); } }; const openProxyConfig = (accountId: string) => { const existing = getEntryForFingerprint(accountProxies, accountId); // Reset custom-form fields, then prefill from whichever shape was stored. setProxyType("socks5"); setProxyHost(""); setProxyPort("1080"); setProxyUsername(""); setProxyPassword(""); setSelectedProxyId(""); if (existing?.proxyId) { setProxyMode("saved"); setSelectedProxyId(existing.proxyId); } else if (existing?.proxy?.host) { setProxyMode("custom"); setProxyType(existing.proxy.type); setProxyHost(existing.proxy.host); setProxyPort(String(existing.proxy.port)); setProxyUsername(existing.proxy.username || ""); setProxyPassword(existing.proxy.password || ""); } else { // New: default to the Proxy Pool dropdown when pool entries exist. setProxyMode(savedProxies.length > 0 ? "saved" : "custom"); } setProxyAccountId(accountId); }; const handleSaveProxy = async () => { if (!conn || !proxyAccountId) return; setSavingProxy(true); try { const others = accountProxies.filter((p) => p.fingerprint !== proxyAccountId); let newEntry: AccountProxyConfig | null = null; if (proxyMode === "saved") { // Store a REFERENCE (by id); server resolves it to a live proxy record. newEntry = selectedProxyId ? { fingerprint: proxyAccountId, proxyId: selectedProxyId } : null; } else { const trimmedHost = proxyHost.trim(); newEntry = trimmedHost ? { fingerprint: proxyAccountId, proxy: { type: proxyType, host: trimmedHost, port: Number(proxyPort) || 1080, ...(proxyUsername.trim() ? { username: proxyUsername.trim() } : {}), ...(proxyPassword.trim() ? { password: proxyPassword.trim() } : {}), }, } : null; } const updatedProxies = newEntry ? [...others, newEntry] : others; const res = await fetch(`/api/providers/${conn.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerSpecificData: { accountProxies: updatedProxies }, }), }); if (res.ok) { await fetchConnections(); setProxyAccountId(null); } } catch (err) { console.error("Failed to save proxy:", err); } finally { setSavingProxy(false); } }; const handleDistributeProxies = async () => { if (!conn || allAccountIds.length === 0) return; const proxiesRes = await fetch("/api/settings/proxies"); if (!proxiesRes.ok) throw new Error(t("fetchProxiesFailed")); const proxiesData = await proxiesRes.json(); const savedProxies = (proxiesData?.items || []).filter((p: any) => p.status === "active"); if (savedProxies.length === 0) { throw new Error(t("noSavedProxiesError")); } // #5217 (Gap 1): distribute stores by-id references too, so editing a pool // proxy later propagates to every account it was distributed to. const updatedProxies: AccountProxyConfig[] = allAccountIds.map((fp, i) => ({ fingerprint: fp, proxyId: savedProxies[i % savedProxies.length].id, })); const res = await fetch(`/api/providers/${conn.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerSpecificData: { accountProxies: updatedProxies }, }), }); if (!res.ok) throw new Error(t("updateConnectionFailed")); await fetchConnections(); }; return (
lock_open

{t("title")}

{resolvedDescription}

{providerProxyControl}
{t("accounts", { count: loading ? "..." : allAccountIds.length })}
{!loading && allAccountIds.length > 0 && ( )} {showManualKeyInput && (
setManualApiKey(e.target.value)} placeholder="Paste API key..." className="rounded-md border border-black/10 bg-bg px-2 py-1 text-xs dark:border-white/10" disabled={addingManualKey || !enabled} />
)} {!showManualKeyInput && onManualApiKeyAdd && ( )}
{!loading && allAccountIds.length === 0 && (

{t("autoGeneratedAccount", { addLabel: resolvedAddLabel })}

)} {!loading && allAccountIds.length > 0 && (
{allAccountIds.map((id, i) => { const proxy = getDisplayProxy( getEntryForFingerprint(accountProxies, id), savedProxies ); return (
{i + 1} {id.slice(0, 10)}…
); })}
)} {proxyAccountId && (

{t("proxyForAccount", { number: allAccountIds.indexOf(proxyAccountId) + 1, })}

{/* #5217 (Gap 1): pick a pre-saved Proxy Pool entry by reference, or fall back to a one-off custom proxy. */}
{proxyMode === "saved" ? ( ) : ( <>
setProxyHost(e.target.value)} placeholder={t("host")} className="flex-1 rounded-md border border-black/10 bg-bg px-2.5 py-1.5 text-xs dark:border-white/10" /> setProxyPort(e.target.value)} placeholder={t("port")} className="w-16 rounded-md border border-black/10 bg-bg px-2.5 py-1.5 text-xs dark:border-white/10" />
setProxyUsername(e.target.value)} placeholder={t("usernameOptional")} className="w-full rounded-md border border-black/10 bg-bg px-2.5 py-1.5 text-xs dark:border-white/10" /> setProxyPassword(e.target.value)} placeholder={t("passwordOptional")} className="w-full rounded-md border border-black/10 bg-bg px-2.5 py-1.5 text-xs dark:border-white/10" /> )}
)}
); }