feat: add API key visibility toggle in Endpoint dashboard (#214)

- Added eye icon button to show/hide individual API keys
- Keys hidden by default on page load for security
- Copy button always copies full key regardless of visibility state
- Implemented per-key visibility state with React useState
- Added maskKey helper to display first 8 characters + "..."
- Clean up visibility state when keys are deleted

Improves security and UX when managing API keys in the dashboard.
This commit is contained in:
Thiên Toán
2026-02-28 11:07:51 +07:00
committed by GitHub
parent 5a015e5b4d
commit d1ce98ca8a
2 changed files with 39 additions and 1 deletions

View File

@@ -1,3 +1,8 @@
# Unreleased
## Features
- Added API key visibility toggle (eye icon) to Endpoint dashboard page for improved UX and security.
# v0.2.66 (2026-02-06)
## Features

View File

@@ -50,6 +50,8 @@ export default function APIPageClient({ machineId }) {
const [tunnelStatus, setTunnelStatus] = useState(null);
const [showDisableModal, setShowDisableModal] = useState(false);
const [showEnableModal, setShowEnableModal] = useState(false);
// API key visibility toggle state
const [visibleKeys, setVisibleKeys] = useState(new Set());
const { copied, copy } = useCopyToClipboard();
@@ -352,6 +354,12 @@ export default function APIPageClient({ machineId }) {
const res = await fetch(`/api/keys/${id}`, { method: "DELETE" });
if (res.ok) {
setKeys(keys.filter((k) => k.id !== id));
// Clean up visibility state
setVisibleKeys(prev => {
const next = new Set(prev);
next.delete(id);
return next;
});
}
} catch (error) {
console.log("Error deleting key:", error);
@@ -373,6 +381,20 @@ export default function APIPageClient({ machineId }) {
}
};
const maskKey = (fullKey) => {
if (!fullKey) return "";
return fullKey.length > 8 ? fullKey.slice(0, 8) + "..." : fullKey;
};
const toggleKeyVisibility = (keyId) => {
setVisibleKeys(prev => {
const next = new Set(prev);
if (next.has(keyId)) next.delete(keyId);
else next.add(keyId);
return next;
});
};
const [baseUrl, setBaseUrl] = useState("/v1");
// Hydration fix: Only access window on client side
@@ -506,7 +528,18 @@ export default function APIPageClient({ machineId }) {
<div className="flex-1 min-w-0">
<p className="text-sm font-medium">{key.name}</p>
<div className="flex items-center gap-2 mt-1">
<code className="text-xs text-text-muted font-mono">{key.key}</code>
<code className="text-xs text-text-muted font-mono">
{visibleKeys.has(key.id) ? key.key : maskKey(key.key)}
</code>
<button
onClick={() => toggleKeyVisibility(key.id)}
className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary opacity-0 group-hover:opacity-100 transition-all"
title={visibleKeys.has(key.id) ? "Hide key" : "Show key"}
>
<span className="material-symbols-outlined text-[14px]">
{visibleKeys.has(key.id) ? "visibility_off" : "visibility"}
</span>
</button>
<button
onClick={() => copy(key.key, key.id)}
className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary opacity-0 group-hover:opacity-100 transition-all"