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:
@@ -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)
|
# v0.2.66 (2026-02-06)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ export default function APIPageClient({ machineId }) {
|
|||||||
const [tunnelStatus, setTunnelStatus] = useState(null);
|
const [tunnelStatus, setTunnelStatus] = useState(null);
|
||||||
const [showDisableModal, setShowDisableModal] = useState(false);
|
const [showDisableModal, setShowDisableModal] = useState(false);
|
||||||
const [showEnableModal, setShowEnableModal] = useState(false);
|
const [showEnableModal, setShowEnableModal] = useState(false);
|
||||||
|
// API key visibility toggle state
|
||||||
|
const [visibleKeys, setVisibleKeys] = useState(new Set());
|
||||||
|
|
||||||
const { copied, copy } = useCopyToClipboard();
|
const { copied, copy } = useCopyToClipboard();
|
||||||
|
|
||||||
@@ -352,6 +354,12 @@ export default function APIPageClient({ machineId }) {
|
|||||||
const res = await fetch(`/api/keys/${id}`, { method: "DELETE" });
|
const res = await fetch(`/api/keys/${id}`, { method: "DELETE" });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setKeys(keys.filter((k) => k.id !== id));
|
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) {
|
} catch (error) {
|
||||||
console.log("Error deleting key:", 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");
|
const [baseUrl, setBaseUrl] = useState("/v1");
|
||||||
|
|
||||||
// Hydration fix: Only access window on client side
|
// Hydration fix: Only access window on client side
|
||||||
@@ -506,7 +528,18 @@ export default function APIPageClient({ machineId }) {
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium">{key.name}</p>
|
<p className="text-sm font-medium">{key.name}</p>
|
||||||
<div className="flex items-center gap-2 mt-1">
|
<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
|
<button
|
||||||
onClick={() => copy(key.key, key.id)}
|
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"
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user