254 lines
8.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.

/**
* API Key 管理页面
*/
'use client';
import { useState, useEffect } from 'react';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { parseFetchError, handleError } from '@/lib/error-handler';
import { useToast } from '@/hooks/useToast';
import Toast from '@/components/Toast';
interface ApiKey {
id: string;
name: string;
key: string;
isActive: boolean;
createdBy: string;
lastUsedAt: string | null;
createdAt: string;
}
export default function ApiKeysPage() {
const { data: session, status } = useSession();
const router = useRouter();
const { toast, showToast } = useToast();
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
const [loading, setLoading] = useState(true);
const [showCreateModal, setShowCreateModal] = useState(false);
const [newKeyName, setNewKeyName] = useState('');
const [createdKey, setCreatedKey] = useState<string | null>(null);
// 权限检查
useEffect(() => {
if (status === 'unauthenticated') {
router.push('/login');
} else if (status === 'authenticated' && session?.user) {
const userRole = (session.user as any).role;
if (userRole !== 'ADMIN' && userRole !== 'SYSTEM_ADMIN') {
router.push('/');
}
}
}, [status, session, router]);
// 加载 API Keys
useEffect(() => {
if (status === 'authenticated') {
fetchApiKeys();
}
}, [status]);
const fetchApiKeys = async () => {
try {
const response = await fetch('/api/admin/api-keys');
if (response.ok) {
const data = await response.json();
setApiKeys(data.data);
}
} catch (error) {
console.error('加载失败:', error);
} finally {
setLoading(false);
}
};
const handleCreate = async () => {
if (!newKeyName.trim()) {
showToast('请输入密钥名称', { variant: 'error' });
return;
}
try {
const response = await fetch('/api/admin/api-keys', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newKeyName }),
});
if (response.ok) {
const data = await response.json();
setCreatedKey(data.data.key);
setNewKeyName('');
fetchApiKeys();
showToast('API Key 创建成功', { variant: 'success' });
} else {
const error = await response.json();
showToast(error.error || '创建失败', { variant: 'error' });
}
} catch (error) {
console.error('创建失败:', error);
showToast('创建失败', { variant: 'error' });
}
};
const handleToggleActive = async (id: string, isActive: boolean) => {
try {
const response = await fetch(`/api/admin/api-keys/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ isActive: !isActive }),
});
if (response.ok) {
fetchApiKeys();
}
} catch (error) {
console.error('更新失败:', error);
}
};
const handleDelete = async (id: string) => {
if (!confirm('确定要删除此 API Key 吗?')) return;
try {
const response = await fetch(`/api/admin/api-keys/${id}`, {
method: 'DELETE',
});
if (response.ok) {
fetchApiKeys();
showToast('API Key 删除成功', { variant: 'success' });
}
} catch (error) {
console.error('删除失败:', error);
const { message } = handleError(error);
showToast(message, { variant: 'error' });
}
};
if (loading) {
return <div className="p-8">加载中...</div>;
}
return (
<div className="p-8">
<Toast toast={toast} />
<div className="max-w-6xl mx-auto">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">API Key 管理</h1>
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
创建 API Key
</button>
</div>
{/* API Keys 列表 */}
<div className="bg-white rounded-lg shadow overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">名称</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">密钥</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">状态</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">最后使用</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">创建时间</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">操作</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{apiKeys.map((key) => (
<tr key={key.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">{key.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-mono">{key.key}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-1 text-xs rounded ${key.isActive ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}>
{key.isActive ? '启用' : '禁用'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{key.lastUsedAt ? new Date(key.lastUsedAt).toLocaleString('zh-CN') : '从未使用'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{new Date(key.createdAt).toLocaleString('zh-CN')}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm space-x-2">
<button
onClick={() => handleToggleActive(key.id, key.isActive)}
className="text-blue-600 hover:text-blue-800"
>
{key.isActive ? '禁用' : '启用'}
</button>
<button
onClick={() => handleDelete(key.id)}
className="text-red-600 hover:text-red-800"
>
删除
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* 创建模态框 */}
{showCreateModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-md w-full">
<h2 className="text-xl font-bold mb-4">创建 API Key</h2>
{createdKey ? (
<div>
<p className="text-sm text-gray-600 mb-2">API Key 已创建,请妥善保存:</p>
<div className="bg-gray-100 p-3 rounded font-mono text-sm break-all mb-4">
{createdKey}
</div>
<p className="text-sm text-red-600 mb-4">注意:此密钥只显示一次,关闭后将无法再次查看</p>
<button
onClick={() => {
setCreatedKey(null);
setShowCreateModal(false);
}}
className="w-full px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
关闭
</button>
</div>
) : (
<div>
<label className="block text-sm font-medium mb-2">密钥名称</label>
<input
type="text"
value={newKeyName}
onChange={(e) => setNewKeyName(e.target.value)}
placeholder="例如:任务系统"
className="w-full px-3 py-2 border rounded mb-4"
/>
<div className="flex space-x-3">
<button
onClick={() => setShowCreateModal(false)}
className="flex-1 px-4 py-2 border rounded hover:bg-gray-50"
>
取消
</button>
<button
onClick={handleCreate}
className="flex-1 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
创建
</button>
</div>
</div>
)}
</div>
</div>
)}
</div>
</div>
);
}