mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 21:22:28 +03:00
629 lines
24 KiB
TypeScript
629 lines
24 KiB
TypeScript
"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<string>;
|
|
dataKey?: string;
|
|
description?: string;
|
|
addLabel?: string;
|
|
enabled?: boolean;
|
|
savingEnabled?: boolean;
|
|
onEnabledChange?: (enabled: boolean) => void;
|
|
providerProxyControl?: ReactNode;
|
|
showManualKeyInput?: boolean;
|
|
onManualApiKeyAdd?: (apiKey: string) => Promise<void>;
|
|
}
|
|
|
|
interface Connection {
|
|
id: string;
|
|
provider: string;
|
|
apiKey?: string;
|
|
providerSpecificData?: Record<string, any>;
|
|
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<Connection[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [adding, setAdding] = useState(false);
|
|
const [proxyAccountId, setProxyAccountId] = useState<string | null>(null);
|
|
const [proxyMode, setProxyMode] = useState<"saved" | "custom">("saved");
|
|
const [savedProxies, setSavedProxies] = useState<SavedProxy[]>([]);
|
|
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<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]);
|
|
|
|
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 (
|
|
<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">{t("title")}</p>
|
|
<p className="text-xs text-text-muted">{resolvedDescription}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex w-full flex-wrap items-center justify-end gap-2 sm:w-auto sm:flex-nowrap">
|
|
{providerProxyControl}
|
|
<NoAuthProviderToggle
|
|
className="w-full justify-end sm:w-auto"
|
|
enabled={enabled}
|
|
saving={savingEnabled}
|
|
onEnabledChange={onEnabledChange}
|
|
/>
|
|
</div>
|
|
</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">
|
|
{t("accounts", { count: 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 ? t("adding") : resolvedAddLabel}
|
|
</Button>
|
|
{showManualKeyInput && (
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="password"
|
|
value={manualApiKey}
|
|
onChange={(e) => 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}
|
|
/>
|
|
<Button
|
|
size="sm"
|
|
icon="add"
|
|
onClick={handleAddManualApiKey}
|
|
disabled={addingManualKey || !manualApiKey.trim() || !enabled}
|
|
>
|
|
{addingManualKey ? t("adding") : t("add")}
|
|
</Button>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setShowManualKeyInput(false);
|
|
setManualApiKey("");
|
|
}}
|
|
className="rounded p-1 text-text-muted hover:bg-black/5 dark:hover:bg-white/5"
|
|
>
|
|
<span className="material-symbols-outlined text-[16px]">close</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
{!showManualKeyInput && onManualApiKeyAdd && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowManualKeyInput(true)}
|
|
className="rounded-md px-2 py-1 text-xs text-text-muted transition-colors hover:bg-black/5 hover:text-text-main dark:hover:bg-white/5"
|
|
>
|
|
{t("manualApiKey")}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{!loading && allAccountIds.length === 0 && (
|
|
<p className="text-xs text-text-muted py-2">
|
|
{t("autoGeneratedAccount", { addLabel: resolvedAddLabel })}
|
|
</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 = getDisplayProxy(
|
|
getEntryForFingerprint(accountProxies, id),
|
|
savedProxies
|
|
);
|
|
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}`
|
|
: t("configureProxy")
|
|
}
|
|
aria-label={
|
|
proxy ? t("proxyConfigured", { host: proxy.host }) : t("configureProxy")
|
|
}
|
|
>
|
|
<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={t("removeAccount")}
|
|
>
|
|
<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">
|
|
{t("proxyForAccount", {
|
|
number: allAccountIds.indexOf(proxyAccountId) + 1,
|
|
})}
|
|
</p>
|
|
<div className="space-y-3">
|
|
{/* #5217 (Gap 1): pick a pre-saved Proxy Pool entry by reference,
|
|
or fall back to a one-off custom proxy. */}
|
|
<div className="flex gap-1 rounded-lg border border-border bg-bg-subtle p-1">
|
|
<button
|
|
type="button"
|
|
onClick={() => setProxyMode("saved")}
|
|
className={`flex-1 rounded-md px-3 py-1.5 text-xs font-medium transition-colors ${
|
|
proxyMode === "saved"
|
|
? "bg-primary text-white"
|
|
: "text-text-muted hover:text-text-main"
|
|
}`}
|
|
>
|
|
{t("saved")}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setProxyMode("custom")}
|
|
className={`flex-1 rounded-md px-3 py-1.5 text-xs font-medium transition-colors ${
|
|
proxyMode === "custom"
|
|
? "bg-primary text-white"
|
|
: "text-text-muted hover:text-text-main"
|
|
}`}
|
|
>
|
|
{t("custom")}
|
|
</button>
|
|
</div>
|
|
|
|
{proxyMode === "saved" ? (
|
|
<select
|
|
value={selectedProxyId}
|
|
onChange={(e) => setSelectedProxyId(e.target.value)}
|
|
className="w-full rounded-md border border-black/10 bg-bg px-2.5 py-1.5 text-xs dark:border-white/10"
|
|
>
|
|
<option value="">
|
|
{savedProxies.length === 0 ? t("noSavedProxies") : t("directConnection")}
|
|
</option>
|
|
{savedProxies.map((p) => (
|
|
<option key={p.id} value={p.id}>
|
|
{(p.name || p.host) ?? p.id} ({p.type || "socks5"}://{p.host}:{p.port})
|
|
</option>
|
|
))}
|
|
</select>
|
|
) : (
|
|
<>
|
|
<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={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"
|
|
/>
|
|
<input
|
|
type="text"
|
|
value={proxyPort}
|
|
onChange={(e) => 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"
|
|
/>
|
|
</div>
|
|
<input
|
|
type="text"
|
|
value={proxyUsername}
|
|
onChange={(e) => 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"
|
|
/>
|
|
<input
|
|
type="password"
|
|
value={proxyPassword}
|
|
onChange={(e) => 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"
|
|
/>
|
|
</>
|
|
)}
|
|
<div className="flex justify-end gap-2 pt-1">
|
|
<button
|
|
type="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"
|
|
>
|
|
{t("cancel")}
|
|
</button>
|
|
<button
|
|
type="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 ? t("saving") : t("save")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|