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

183 lines
7.4 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 { Card, Button, Table, Badge, Modal, Input } from '@/components/ui'
import { Plus, Trash2, Copy, Check, Key } from 'lucide-react'
interface ApiKey {
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<ApiKey[]>([])
const [loading, setLoading] = useState(true)
const [createOpen, setCreateOpen] = useState(false)
const [editTarget, setEditTarget] = useState<ApiKey | null>(null)
const [deleteTarget, setDeleteTarget] = useState<ApiKey | null>(null)
const [name, setName] = useState('')
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
const [newKey, setNewKey] = useState<string | null>(null)
const [copied, setCopied] = useState(false)
const fetchKeys = () => {
fetch('/api/api-keys')
.then(r => r.json())
.then(d => { if (d.keys) setKeys(d.keys) })
.catch(() => {})
.finally(() => setLoading(false))
}
useEffect(() => { fetchKeys() }, [])
const handleCreate = async () => {
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(), permissions: ['tickets:read'] }),
})
const data = await res.json()
if (!res.ok) { setError(data.error || '创建失败'); return }
setNewKey(data.key)
setCreateOpen(false)
setName('')
fetchKeys()
} catch {
setError('创建失败')
} finally {
setSaving(false)
}
}
const handleDelete = async () => {
if (!deleteTarget) return
const res = await fetch(`/api/api-keys/${deleteTarget.id}`, { method: 'DELETE' })
if (res.ok) {
setDeleteTarget(null)
fetchKeys()
}
}
const handleToggleActive = async (k: ApiKey) => {
await fetch(`/api/api-keys/${k.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: k.name, permissions: JSON.parse(k.permissions), is_active: !k.is_active }),
})
fetchKeys()
}
const copyKey = (key: string) => {
navigator.clipboard.writeText(key)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
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-slate-100">API Key </h1>
<p className="text-slate-500 dark:text-slate-400 mt-1"> API</p>
</div>
<Button size="sm" onClick={() => { setCreateOpen(true); setNewKey(null) }}>
<Plus size={16} className="mr-1" /> Key
</Button>
</div>
{newKey && (
<div className="p-4 bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-800 rounded-xl">
<p className="text-sm font-medium text-emerald-800 dark:text-emerald-300 mb-2">
API Key
</p>
<div className="flex items-center gap-2">
<code className="flex-1 p-3 bg-white dark:bg-slate-800 rounded-lg border border-emerald-200 dark:border-emerald-700 text-sm font-mono break-all text-slate-800 dark:text-slate-200">
{newKey}
</code>
<Button variant="ghost" size="sm" onClick={() => copyKey(newKey)}>
{copied ? <Check size={16} className="text-emerald-500" /> : <Copy size={16} />}
</Button>
</div>
</div>
)}
{loading ? (
<div className="text-center py-12 text-slate-500 dark:text-slate-400">...</div>
) : keys.length === 0 ? (
<Card className="p-12 text-center">
<Key size={40} className="mx-auto text-slate-300 dark:text-slate-600 mb-3" />
<p className="text-slate-500 dark:text-slate-400"> API Key</p>
<p className="text-sm text-slate-400 dark:text-slate-500 mt-1"> Key</p>
</Card>
) : (
<Table headers={['名称', '权限', '状态', '最后使用', '过期时间', '创建时间', '操作']}>
{keys.map(k => {
const perms: string[] = JSON.parse(k.permissions)
return (
<tr key={k.id} className="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors">
<td className="px-4 py-3 font-medium text-slate-900 dark:text-slate-100">{k.name}</td>
<td className="px-4 py-3">
<div className="flex flex-wrap gap-1">
{perms.map(p => <Badge key={p} variant="default">{p}</Badge>)}
</div>
</td>
<td className="px-4 py-3">
<button onClick={() => handleToggleActive(k)}>
<Badge variant={k.is_active ? 'success' : 'danger'}>{k.is_active ? '启用' : '禁用'}</Badge>
</button>
</td>
<td className="px-4 py-3 text-slate-500 dark:text-slate-400 text-sm">{k.last_used_at ? new Date(k.last_used_at).toLocaleString('zh-CN') : '从未使用'}</td>
<td className="px-4 py-3 text-slate-500 dark:text-slate-400 text-sm">{k.expires_at ? new Date(k.expires_at).toLocaleString('zh-CN') : '永不过期'}</td>
<td className="px-4 py-3 text-slate-500 dark:text-slate-400 text-sm">{new Date(k.created_at).toLocaleString('zh-CN')}</td>
<td className="px-4 py-3">
<Button variant="ghost" size="sm" onClick={() => setDeleteTarget(k)}>
<Trash2 size={14} className="text-red-500" />
</Button>
</td>
</tr>
)
})}
</Table>
)}
<Modal open={createOpen} onClose={() => { setCreateOpen(false); setName(''); setError('') }} title="创建 API Key">
<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 variant="default">tickets:read</Badge>
</p>
{error && <p className="text-sm text-red-500">{error}</p>}
<div className="flex gap-3">
<Button onClick={handleCreate} loading={saving}></Button>
<Button variant="secondary" onClick={() => { setCreateOpen(false); setError('') }}></Button>
</div>
</div>
</Modal>
<Modal open={!!deleteTarget} onClose={() => setDeleteTarget(null)} title="确认删除">
<p className="text-sm text-slate-600 dark:text-slate-400">
API Key{deleteTarget?.name}使 Key 访
</p>
<div className="flex justify-end gap-3 mt-4">
<Button variant="secondary" onClick={() => setDeleteTarget(null)}></Button>
<Button variant="danger" onClick={handleDelete}></Button>
</div>
</Modal>
</div>
)
}