134 lines
5.9 KiB
TypeScript
134 lines
5.9 KiB
TypeScript
'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>
|
||
)
|
||
}
|