assets-ai/src/app/(app)/settings/api-keys/page.tsx

134 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useState, useEffect } from 'react'
import Table, { Column } from '@/components/ui/Table'
import Button from '@/components/ui/Button'
import Input from '@/components/ui/Input'
import Modal from '@/components/ui/Modal'
import Badge from '@/components/ui/Badge'
import { Plus, Trash2, Copy, CheckCircle } from 'lucide-react'
interface ApiKeyItem {
id: number; name: string; permissions: string; last_used_at: string | null;
expires_at: string | null; is_active: number; created_at: string
}
export default function ApiKeysPage() {
const [keys, setKeys] = useState<ApiKeyItem[]>([])
const [loading, setLoading] = useState(true)
const [createOpen, setCreateOpen] = useState(false)
const [name, setName] = useState('')
const [saving, setSaving] = useState(false)
const [newKey, setNewKey] = useState<string | null>(null)
const [deleteTarget, setDeleteTarget] = useState<ApiKeyItem | null>(null)
const [copied, setCopied] = useState(false)
const [error, setError] = useState('')
async function fetchKeys() {
const res = await fetch('/api/api-keys')
if (res.ok) { const data = await res.json(); setKeys(data.keys) }
}
useEffect(() => { fetchKeys().finally(() => setLoading(false)) }, [])
async function handleCreate() {
if (!name.trim()) return
setSaving(true); setError('')
try {
const res = await fetch('/api/api-keys', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name.trim() }),
})
const data = await res.json()
if (!res.ok) { setError(data.error || '创建失败'); return }
setNewKey(data.key)
setCreateOpen(false)
setName('')
fetchKeys()
} catch { setError('创建失败') }
finally { setSaving(false) }
}
async function toggleActive(k: ApiKeyItem) {
await fetch(`/api/api-keys/${k.id}`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_active: k.is_active ? 0 : 1 }),
})
fetchKeys()
}
async function handleDelete() {
if (!deleteTarget) return
const res = await fetch(`/api/api-keys/${deleteTarget.id}`, { method: 'DELETE' })
if (res.ok) { setDeleteTarget(null); fetchKeys() }
}
function copyKey() {
if (newKey) {
navigator.clipboard.writeText(newKey)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
}
const columns: Column<ApiKeyItem>[] = [
{ key: 'name', title: '名称', render: (r) => <span className="text-slate-900 dark:text-white font-medium">{r.name}</span> },
{ key: 'permissions', title: '权限', render: (r) => {
const perms: string[] = JSON.parse(r.permissions)
return <div className="flex flex-wrap gap-1">{perms.map(p => <Badge key={p} color="gray">{p}</Badge>)}</div>
}},
{ key: 'is_active', title: '状态', render: (r) => <button onClick={() => toggleActive(r)}><Badge color={r.is_active ? 'green' : 'red'}>{r.is_active ? '启用' : '禁用'}</Badge></button> },
{ key: 'last_used_at', title: '最后使用', render: (r) => r.last_used_at || '从未使用' },
{ key: 'expires_at', title: '过期时间', render: (r) => r.expires_at || '永不过期' },
{ key: 'created_at', title: '创建时间' },
{ key: 'actions', title: '操作', render: (r) => (
<Button variant="ghost" size="sm" onClick={() => setDeleteTarget(r)}><Trash2 size={14} className="text-red-500" /></Button>
)},
]
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">API Key </h1>
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1"> API</p>
</div>
<Button size="sm" onClick={() => setCreateOpen(true)}><Plus size={16} /> Key</Button>
</div>
{newKey && (
<div className="p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
<p className="text-sm font-medium text-green-800 dark:text-green-300 mb-2">API Key </p>
<div className="flex items-center gap-2">
<code className="flex-1 p-2 bg-white dark:bg-slate-800 rounded border border-green-200 dark:border-green-700 text-sm font-mono break-all">{newKey}</code>
<Button variant="ghost" size="sm" onClick={copyKey}>{copied ? <CheckCircle size={16} className="text-green-500" /> : <Copy size={16} />}</Button>
</div>
</div>
)}
{loading ? <div className="py-20 text-center text-slate-500">...</div> : <Table columns={columns} data={keys} rowKey={r => r.id} />}
<Modal open={createOpen} onClose={() => setCreateOpen(false)} title="创建 API Key" footer={
<>
<Button variant="ghost" onClick={() => setCreateOpen(false)}></Button>
<Button onClick={handleCreate} loading={saving}></Button>
</>
}>
<div className="space-y-4">
<Input label="名称" value={name} onChange={e => setName(e.target.value)} placeholder="例如:监控系统" />
<p className="text-xs text-slate-500 dark:text-slate-400"><Badge color="gray">assets:read</Badge></p>
{error && <p className="text-sm text-red-500">{error}</p>}
</div>
</Modal>
<Modal open={!!deleteTarget} onClose={() => setDeleteTarget(null)} title="确认删除" footer={
<>
<Button variant="ghost" onClick={() => setDeleteTarget(null)}></Button>
<Button variant="danger" onClick={handleDelete}></Button>
</>
}>
<p className="text-sm text-slate-600 dark:text-slate-400"> API Key{deleteTarget?.name}使 Key 访</p>
</Modal>
</div>
)
}