ai_api / src /shared /components /NoAuthAccountCard.tsx
Yogesh
initial deploy
cd8bd0a
Raw
History Blame Contribute Delete
16 kB
"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import Card from "./Card";
import Button from "./Button";
import DistributeProxiesButton from "./DistributeProxiesButton";
import NoAuthProviderToggle from "./NoAuthProviderToggle";
interface NoAuthAccountCardProps {
providerId: string;
providerName: string;
generateAccountId: () => string;
dataKey?: string;
description?: string;
addLabel?: string;
enabled?: boolean;
savingEnabled?: boolean;
onEnabledChange?: (enabled: boolean) => void;
}
interface Connection {
id: string;
provider: string;
apiKey?: string;
providerSpecificData?: Record<string, any>;
isActive?: boolean;
}
interface AccountProxyConfig {
fingerprint: string;
proxy: { type: string; host: string; port: number; username?: string; password?: string } | null;
}
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 getProxyForFingerprint(proxies: AccountProxyConfig[], fp: string) {
return proxies.find((p) => p.fingerprint === fp)?.proxy ?? null;
}
export default function NoAuthAccountCard({
providerId,
providerName,
generateAccountId,
dataKey = "fingerprints",
description = "Ready to use — no signup needed. Add accounts for rate-limit rotation.",
addLabel = "Add Account",
enabled = true,
savingEnabled = false,
onEnabledChange,
}: NoAuthAccountCardProps) {
const [connections, setConnections] = useState<Connection[]>([]);
const [loading, setLoading] = useState(true);
const [adding, setAdding] = useState(false);
const [proxyAccountId, setProxyAccountId] = useState<string | null>(null);
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 popoverRef = useRef<HTMLDivElement>(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]);
useEffect(() => {
void fetchConnections();
}, [fetchConnections]);
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();
if (connections.length === 0) {
const res = await fetch("/api/providers", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
provider: providerId,
name: `${providerName} Account 1`,
providerSpecificData: { [dataKey]: [accountId] },
}),
});
if (!res.ok) throw new Error("Failed to create connection");
} 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("Failed to update connection");
}
await fetchConnections();
} catch (err) {
console.error("Failed to add account:", err);
} finally {
setAdding(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 = getProxyForFingerprint(accountProxies, accountId);
if (existing) {
setProxyType(existing.type);
setProxyHost(existing.host);
setProxyPort(String(existing.port));
setProxyUsername(existing.username || "");
setProxyPassword(existing.password || "");
} else {
setProxyType("socks5");
setProxyHost("");
setProxyPort("1080");
setProxyUsername("");
setProxyPassword("");
}
setProxyAccountId(accountId);
};
const handleSaveProxy = async () => {
if (!conn || !proxyAccountId) return;
setSavingProxy(true);
try {
const trimmedHost = proxyHost.trim();
const newProxy: AccountProxyConfig["proxy"] = trimmedHost
? {
type: proxyType,
host: trimmedHost,
port: Number(proxyPort) || 1080,
...(proxyUsername.trim() ? { username: proxyUsername.trim() } : {}),
...(proxyPassword.trim() ? { password: proxyPassword.trim() } : {}),
}
: null;
const existing = accountProxies.filter((p) => p.fingerprint !== proxyAccountId);
const updatedProxies = newProxy
? [...existing, { fingerprint: proxyAccountId, proxy: newProxy }]
: existing;
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("Failed to fetch proxies");
const proxiesData = await proxiesRes.json();
const savedProxies = (proxiesData?.items || []).filter((p: any) => p.status === "active");
if (savedProxies.length === 0) {
throw new Error("No saved proxies found. Add proxies in Settings → Proxy first.");
}
const updatedProxies: AccountProxyConfig[] = allAccountIds.map((fp, i) => {
const proxy = savedProxies[i % savedProxies.length];
return {
fingerprint: fp,
proxy: {
type: proxy.type || "socks5",
host: proxy.host,
port: proxy.port,
...(proxy.username ? { username: proxy.username } : {}),
...(proxy.password ? { password: proxy.password } : {}),
},
};
});
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("Failed to update connection");
await fetchConnections();
};
return (
<Card>
<div className="mb-3 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="flex min-w-0 items-center gap-3">
<div className="inline-flex shrink-0 items-center justify-center w-10 h-10 rounded-full bg-green-500/10 text-green-500">
<span className="material-symbols-outlined text-[20px]">lock_open</span>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium">No authentication required</p>
<p className="text-xs text-text-muted">{description}</p>
</div>
</div>
<NoAuthProviderToggle
className="w-full justify-end sm:w-auto"
enabled={enabled}
saving={savingEnabled}
onEnabledChange={onEnabledChange}
/>
</div>
<div className="border-t border-border pt-3 mt-3">
<div className="mb-2 flex items-center justify-between">
<span className="text-sm font-medium">
Accounts ({loading ? "..." : allAccountIds.length})
</span>
<div className="flex items-center justify-end gap-2">
{!loading && allAccountIds.length > 0 && (
<DistributeProxiesButton
onDistribute={handleDistributeProxies}
disabled={adding || !enabled}
size="sm"
/>
)}
<Button size="sm" icon="add" onClick={handleAddAccount} disabled={adding || !enabled}>
{adding ? "Adding..." : addLabel}
</Button>
</div>
</div>
{!loading && allAccountIds.length === 0 && (
<p className="text-xs text-text-muted py-2">
Using auto-generated account. Click &quot;{addLabel}&quot; for rate-limit rotation.
</p>
)}
{!loading && allAccountIds.length > 0 && (
<div
data-testid="noauth-account-grid"
className="grid max-h-72 grid-cols-1 gap-1.5 overflow-y-auto pr-1 sm:grid-cols-2 lg:grid-cols-3"
>
{allAccountIds.map((id, i) => {
const proxy = getProxyForFingerprint(accountProxies, id);
return (
<div
key={id}
data-account-id={id}
className="group flex items-center gap-2 rounded-lg border border-border bg-bg/40 px-2.5 py-2 transition-colors hover:bg-black/[0.03] dark:hover:bg-white/[0.03]"
>
<span className="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-bg text-[10px] font-medium text-text-muted">
{i + 1}
</span>
<span className="min-w-0 flex-1 truncate font-mono text-xs text-text-muted">
{id.slice(0, 10)}…
</span>
<button
type="button"
onClick={() => openProxyConfig(id)}
className={`inline-flex h-6 w-6 shrink-0 items-center justify-center rounded transition-colors hover:bg-black/5 dark:hover:bg-white/5 ${proxy ? "text-blue-400" : "text-text-muted"}`}
title={
proxy
? `Proxy: ${proxy.type}://${proxy.host}:${proxy.port}`
: "Configure proxy"
}
aria-label={proxy ? `Proxy configured: ${proxy.host}` : "Configure proxy"}
>
<span
className="material-symbols-outlined text-[16px]"
style={proxy ? { fontVariationSettings: "'FILL' 1" } : undefined}
>
shield
</span>
</button>
<button
type="button"
onClick={() => handleRemoveAccount(id)}
className="shrink-0 rounded p-1 text-text-muted opacity-0 transition-colors hover:bg-red-500/10 hover:text-red-500 group-hover:opacity-100"
aria-label="Remove account"
>
<span className="material-symbols-outlined text-[16px]">delete</span>
</button>
</div>
);
})}
</div>
)}
{proxyAccountId && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
<div
ref={popoverRef}
className="w-80 max-w-full rounded-lg border border-black/10 bg-surface p-4 shadow-lg dark:border-white/10"
>
<p className="mb-3 text-sm font-medium">
Proxy for Account {allAccountIds.indexOf(proxyAccountId) + 1}
</p>
<div className="space-y-3">
<div className="flex gap-2">
<select
value={proxyType}
onChange={(e) => setProxyType(e.target.value)}
className="flex-shrink-0 rounded-md border border-black/10 bg-bg px-2.5 py-1.5 text-xs dark:border-white/10"
>
{PROXY_TYPES.map((t) => (
<option key={t.value} value={t.value}>
{t.label}
</option>
))}
</select>
<input
type="text"
value={proxyHost}
onChange={(e) => setProxyHost(e.target.value)}
placeholder="Host"
className="flex-1 rounded-md border border-black/10 bg-bg px-2.5 py-1.5 text-xs dark:border-white/10"
/>
<input
type="text"
value={proxyPort}
onChange={(e) => setProxyPort(e.target.value)}
placeholder="Port"
className="w-16 rounded-md border border-black/10 bg-bg px-2.5 py-1.5 text-xs dark:border-white/10"
/>
</div>
<input
type="text"
value={proxyUsername}
onChange={(e) => setProxyUsername(e.target.value)}
placeholder="Username (optional)"
className="w-full rounded-md border border-black/10 bg-bg px-2.5 py-1.5 text-xs dark:border-white/10"
/>
<input
type="password"
value={proxyPassword}
onChange={(e) => setProxyPassword(e.target.value)}
placeholder="Password (optional)"
className="w-full rounded-md border border-black/10 bg-bg px-2.5 py-1.5 text-xs dark:border-white/10"
/>
<div className="flex justify-end gap-2 pt-1">
<button
onClick={() => setProxyAccountId(null)}
className="rounded-md px-3 py-1.5 text-xs text-text-muted transition-colors hover:bg-black/5 hover:text-text-main dark:hover:bg-white/5"
>
Cancel
</button>
<button
onClick={handleSaveProxy}
disabled={savingProxy}
className="rounded-md bg-primary/10 px-3 py-1.5 text-xs text-primary transition-colors hover:bg-primary/20 disabled:opacity-50"
>
{savingProxy ? "Saving..." : "Save"}
</button>
</div>
</div>
</div>
</div>
)}
</div>
</Card>
);
}