enhance API key management UI and improve watcher configuration.

This commit is contained in:
decolua
2026-06-20 11:19:03 +07:00
parent 090886ced9
commit 25e8723ad1
4 changed files with 150 additions and 120 deletions

View File

@@ -1093,6 +1093,119 @@ export default function APIPageClient({ machineId }) {
)}
</Card>
{/* API Keys */}
<Card id="require-api-key">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold flex items-center gap-2">
<span className="material-symbols-outlined text-primary">vpn_key</span>
API Keys
</h2>
<Button icon="add" onClick={() => setShowAddModal(true)}>
Create Key
</Button>
</div>
<div className="flex items-center justify-between pb-4 mb-4 border-b border-border">
<div>
<p className="font-medium">Require API key</p>
<p className="text-sm text-text-muted">
Requests without a valid key will be rejected
</p>
</div>
<Toggle
checked={requireApiKey}
onChange={() => handleRequireApiKey(!requireApiKey)}
/>
</div>
{isRemoteHost && !requireApiKey && (
<div className="mb-4 -mt-2">
<SecurityWarning message="Endpoint is exposed without an API key." />
</div>
)}
{keys.length === 0 ? (
<div className="text-center py-12">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary mb-4">
<span className="material-symbols-outlined text-[32px]">vpn_key</span>
</div>
<p className="text-text-main font-medium mb-1">No API keys yet</p>
<p className="text-sm text-text-muted mb-4">Create your first API key to get started</p>
<Button icon="add" onClick={() => setShowAddModal(true)}>
Create Key
</Button>
</div>
) : (
<div className="flex flex-col">
{keys.map((key) => (
<div
key={key.id}
className={`group flex items-center justify-between py-3 border-b border-black/[0.03] dark:border-white/[0.03] last:border-b-0 ${key.isActive === false ? "opacity-60" : ""}`}
>
<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">
{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-100 sm:opacity-0 sm: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-100 sm:opacity-0 sm:group-hover:opacity-100 transition-all"
>
<span className="material-symbols-outlined text-[14px]">
{copied === key.id ? "check" : "content_copy"}
</span>
</button>
</div>
<p className="text-xs text-text-muted mt-1">
Created {new Date(key.createdAt).toLocaleDateString()}
</p>
{key.isActive === false && (
<p className="text-xs text-orange-500 mt-1">Paused</p>
)}
</div>
<div className="flex items-center gap-2">
<Toggle
size="sm"
checked={key.isActive ?? true}
onChange={(checked) => {
if (key.isActive && !checked) {
setConfirmState({
title: "Pause API Key",
message: `Pause API key "${key.name}"?\n\nThis key will stop working immediately but can be resumed later.`,
onConfirm: async () => {
setConfirmState(null);
handleToggleKey(key.id, checked);
}
});
} else {
handleToggleKey(key.id, checked);
}
}}
title={key.isActive ? "Pause key" : "Resume key"}
/>
<button
onClick={() => handleDeleteKey(key.id)}
className="p-2 hover:bg-red-500/10 rounded text-red-500 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-all"
>
<span className="material-symbols-outlined text-[18px]">delete</span>
</button>
</div>
</div>
))}
</div>
)}
</Card>
{/* Token Saver (RTK + Caveman) */}
<Card id="rtk">
<div className="flex items-center justify-between mb-2">
@@ -1260,119 +1373,6 @@ export default function APIPageClient({ machineId }) {
</div>
</Card>
{/* API Keys */}
<Card id="require-api-key">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold flex items-center gap-2">
<span className="material-symbols-outlined text-primary">vpn_key</span>
API Keys
</h2>
<Button icon="add" onClick={() => setShowAddModal(true)}>
Create Key
</Button>
</div>
<div className="flex items-center justify-between pb-4 mb-4 border-b border-border">
<div>
<p className="font-medium">Require API key</p>
<p className="text-sm text-text-muted">
Requests without a valid key will be rejected
</p>
</div>
<Toggle
checked={requireApiKey}
onChange={() => handleRequireApiKey(!requireApiKey)}
/>
</div>
{isRemoteHost && !requireApiKey && (
<div className="mb-4 -mt-2">
<SecurityWarning message="Endpoint is exposed without an API key." />
</div>
)}
{keys.length === 0 ? (
<div className="text-center py-12">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary mb-4">
<span className="material-symbols-outlined text-[32px]">vpn_key</span>
</div>
<p className="text-text-main font-medium mb-1">No API keys yet</p>
<p className="text-sm text-text-muted mb-4">Create your first API key to get started</p>
<Button icon="add" onClick={() => setShowAddModal(true)}>
Create Key
</Button>
</div>
) : (
<div className="flex flex-col">
{keys.map((key) => (
<div
key={key.id}
className={`group flex items-center justify-between py-3 border-b border-black/[0.03] dark:border-white/[0.03] last:border-b-0 ${key.isActive === false ? "opacity-60" : ""}`}
>
<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">
{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-100 sm:opacity-0 sm: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-100 sm:opacity-0 sm:group-hover:opacity-100 transition-all"
>
<span className="material-symbols-outlined text-[14px]">
{copied === key.id ? "check" : "content_copy"}
</span>
</button>
</div>
<p className="text-xs text-text-muted mt-1">
Created {new Date(key.createdAt).toLocaleDateString()}
</p>
{key.isActive === false && (
<p className="text-xs text-orange-500 mt-1">Paused</p>
)}
</div>
<div className="flex items-center gap-2">
<Toggle
size="sm"
checked={key.isActive ?? true}
onChange={(checked) => {
if (key.isActive && !checked) {
setConfirmState({
title: "Pause API Key",
message: `Pause API key "${key.name}"?\n\nThis key will stop working immediately but can be resumed later.`,
onConfirm: async () => {
setConfirmState(null);
handleToggleKey(key.id, checked);
}
});
} else {
handleToggleKey(key.id, checked);
}
}}
title={key.isActive ? "Pause key" : "Resume key"}
/>
<button
onClick={() => handleDeleteKey(key.id)}
className="p-2 hover:bg-red-500/10 rounded text-red-500 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-all"
>
<span className="material-symbols-outlined text-[18px]">delete</span>
</button>
</div>
</div>
))}
</div>
)}
</Card>
{/* Add Key Modal */}
<Modal
isOpen={showAddModal}