feat: implement API key management with reconciliation and add update key endpoint

This commit is contained in:
CJACK.
2026-04-22 15:51:43 +00:00
parent 8ff923cd77
commit 8f09e3b381
19 changed files with 629 additions and 107 deletions

View File

@@ -30,7 +30,10 @@ export default function AccountManagerContainer({ config, onRefresh, onMessage,
const {
showAddKey,
setShowAddKey,
openAddKey,
openEditKey,
closeKeyModal,
editingKey,
showAddAccount,
setShowAddAccount,
newKey,
@@ -94,7 +97,8 @@ export default function AccountManagerContainer({ config, onRefresh, onMessage,
config={config}
keysExpanded={keysExpanded}
setKeysExpanded={setKeysExpanded}
setShowAddKey={setShowAddKey}
onAddKey={openAddKey}
onEditKey={openEditKey}
copiedKey={copiedKey}
setCopiedKey={setCopiedKey}
onDeleteKey={deleteKey}
@@ -133,10 +137,11 @@ export default function AccountManagerContainer({ config, onRefresh, onMessage,
<AddKeyModal
show={showAddKey}
t={t}
editingKey={editingKey}
newKey={newKey}
setNewKey={setNewKey}
loading={loading}
onClose={() => setShowAddKey(false)}
onClose={closeKeyModal}
onAdd={addKey}
/>

View File

@@ -1,40 +1,47 @@
import { X } from 'lucide-react'
export default function AddKeyModal({ show, t, newKey, setNewKey, loading, onClose, onAdd }) {
export default function AddKeyModal({ show, t, editingKey, newKey, setNewKey, loading, onClose, onAdd }) {
if (!show) {
return null
}
const isEditing = Boolean(editingKey?.key)
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in">
<div className="bg-card w-full max-w-md rounded-xl border border-border shadow-2xl overflow-hidden animate-in zoom-in-95">
<div className="p-4 border-b border-border flex justify-between items-center">
<h3 className="font-semibold">{t('accountManager.modalAddKeyTitle')}</h3>
<h3 className="font-semibold">{isEditing ? t('accountManager.modalEditKeyTitle') : t('accountManager.modalAddKeyTitle')}</h3>
<button onClick={onClose} className="text-muted-foreground hover:text-foreground">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium mb-1.5">{t('accountManager.newKeyLabel')}</label>
<label className="block text-sm font-medium mb-1.5">{isEditing ? t('accountManager.keyLabel') : t('accountManager.newKeyLabel')}</label>
<div className="flex gap-2">
<input
type="text"
className="input-field bg-[#09090b] flex-1"
placeholder={t('accountManager.newKeyPlaceholder')}
className={isEditing ? "input-field bg-muted/30 flex-1 cursor-not-allowed" : "input-field bg-[#09090b] flex-1"}
placeholder={isEditing ? t('accountManager.keyReadonlyPlaceholder') : t('accountManager.newKeyPlaceholder')}
value={newKey.key}
onChange={e => setNewKey({ ...newKey, key: e.target.value })}
autoFocus
autoFocus={!isEditing}
readOnly={isEditing}
/>
<button
type="button"
onClick={() => setNewKey({ ...newKey, key: 'sk-' + crypto.randomUUID().replace(/-/g, '') })}
className="px-3 py-2 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors text-sm font-medium border border-border whitespace-nowrap"
>
{t('accountManager.generate')}
</button>
{!isEditing && (
<button
type="button"
onClick={() => setNewKey({ ...newKey, key: 'sk-' + crypto.randomUUID().replace(/-/g, '') })}
className="px-3 py-2 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors text-sm font-medium border border-border whitespace-nowrap"
>
{t('accountManager.generate')}
</button>
)}
</div>
<p className="text-xs text-muted-foreground mt-1.5">{t('accountManager.generateHint')}</p>
<p className="text-xs text-muted-foreground mt-1.5">
{isEditing ? t('accountManager.keyReadonlyHint') : t('accountManager.generateHint')}
</p>
</div>
<div>
<label className="block text-sm font-medium mb-1.5">{t('accountManager.nameOptional')}</label>
@@ -44,6 +51,7 @@ export default function AddKeyModal({ show, t, newKey, setNewKey, loading, onClo
placeholder={t('accountManager.namePlaceholder')}
value={newKey.name}
onChange={e => setNewKey({ ...newKey, name: e.target.value })}
autoFocus={isEditing}
/>
</div>
<div>
@@ -59,7 +67,9 @@ export default function AddKeyModal({ show, t, newKey, setNewKey, loading, onClo
<div className="flex justify-end gap-2 pt-2">
<button onClick={onClose} className="px-4 py-2 rounded-lg border border-border hover:bg-secondary transition-colors text-sm font-medium">{t('actions.cancel')}</button>
<button onClick={onAdd} disabled={loading} className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium disabled:opacity-50">
{loading ? t('accountManager.addKeyLoading') : t('accountManager.addKeyAction')}
{loading
? (isEditing ? t('accountManager.editKeyLoading') : t('accountManager.addKeyLoading'))
: (isEditing ? t('accountManager.editKeyAction') : t('accountManager.addKeyAction'))}
</button>
</div>
</div>

View File

@@ -1,5 +1,5 @@
import { useState } from 'react'
import { Check, ChevronDown, Copy, Plus, Trash2 } from 'lucide-react'
import { Check, ChevronDown, Copy, Pencil, Plus, Trash2 } from 'lucide-react'
import clsx from 'clsx'
function fallbackCopyText(text) {
@@ -31,7 +31,8 @@ export default function ApiKeysPanel({
config,
keysExpanded,
setKeysExpanded,
setShowAddKey,
onAddKey,
onEditKey,
copiedKey,
setCopiedKey,
onDeleteKey,
@@ -81,7 +82,7 @@ export default function ApiKeysPanel({
</div>
</div>
<button
onClick={(e) => { e.stopPropagation(); setShowAddKey(true) }}
onClick={(e) => { e.stopPropagation(); onAddKey() }}
className="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors font-medium text-sm shadow-sm"
>
<Plus className="w-4 h-4" />
@@ -112,6 +113,13 @@ export default function ApiKeysPanel({
)}
</div>
<div className="flex items-center gap-1">
<button
onClick={() => onEditKey(item)}
className="p-2 text-muted-foreground hover:text-primary hover:bg-primary/10 rounded-md transition-colors"
title={t('accountManager.editKeyTitle')}
>
<Pencil className="w-4 h-4" />
</button>
<button
onClick={() => handleCopyKey(item.key)}
className="p-2 text-muted-foreground hover:text-primary hover:bg-primary/10 rounded-md transition-colors"

View File

@@ -2,6 +2,7 @@ import { useState } from 'react'
export function useAccountActions({ apiFetch, t, onMessage, onRefresh, config, fetchAccounts, resolveAccountIdentifier }) {
const [showAddKey, setShowAddKey] = useState(false)
const [editingKey, setEditingKey] = useState(null)
const [showAddAccount, setShowAddAccount] = useState(false)
const [newKey, setNewKey] = useState({ key: '', name: '', remark: '' })
const [copiedKey, setCopiedKey] = useState(null)
@@ -14,23 +15,58 @@ export function useAccountActions({ apiFetch, t, onMessage, onRefresh, config, f
const [deletingSessions, setDeletingSessions] = useState({})
const [updatingProxy, setUpdatingProxy] = useState({})
const openAddKey = () => {
setEditingKey(null)
setNewKey({ key: '', name: '', remark: '' })
setShowAddKey(true)
}
const openEditKey = (item) => {
if (!item?.key) return
setEditingKey(item)
setNewKey({
key: item.key || '',
name: item.name || '',
remark: item.remark || '',
})
setShowAddKey(true)
}
const closeKeyModal = () => {
setShowAddKey(false)
setEditingKey(null)
setNewKey({ key: '', name: '', remark: '' })
}
const addKey = async () => {
if (!newKey.key.trim()) return
const isEditing = Boolean(editingKey?.key)
if (!isEditing && !newKey.key.trim()) {
return
}
setLoading(true)
try {
const res = await apiFetch('/admin/keys', {
method: 'POST',
const endpoint = isEditing
? `/admin/keys/${encodeURIComponent(editingKey.key)}`
: '/admin/keys'
const method = isEditing ? 'PUT' : 'POST'
const payload = isEditing
? { name: newKey.name, remark: newKey.remark }
: { key: newKey.key.trim(), name: newKey.name, remark: newKey.remark }
if (!isEditing && !payload.key) {
return
}
const res = await apiFetch(endpoint, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: newKey.key.trim(), name: newKey.name, remark: newKey.remark }),
body: JSON.stringify(payload),
})
if (res.ok) {
onMessage('success', t('accountManager.addKeySuccess'))
setNewKey({ key: '', name: '', remark: '' })
setShowAddKey(false)
onMessage('success', isEditing ? t('accountManager.updateKeySuccess') : t('accountManager.addKeySuccess'))
closeKeyModal()
onRefresh()
} else {
const data = await res.json()
onMessage('error', data.detail || t('messages.failedToAdd'))
onMessage('error', data.detail || (isEditing ? t('messages.requestFailed') : t('messages.failedToAdd')))
}
} catch (e) {
onMessage('error', t('messages.networkError'))
@@ -244,7 +280,10 @@ export function useAccountActions({ apiFetch, t, onMessage, onRefresh, config, f
return {
showAddKey,
setShowAddKey,
openAddKey,
openEditKey,
closeKeyModal,
editingKey,
showAddAccount,
setShowAddAccount,
newKey,