248 lines
8.5 KiB
TypeScript
248 lines
8.5 KiB
TypeScript
/**
|
||
* 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';
|
||
|
||
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 [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()) {
|
||
alert('请输入密钥名称');
|
||
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();
|
||
} else {
|
||
const error = await response.json();
|
||
alert(error.error || '创建失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('创建失败:', error);
|
||
alert('创建失败');
|
||
}
|
||
};
|
||
|
||
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();
|
||
}
|
||
} catch (error) {
|
||
console.error('删除失败:', error);
|
||
const { message } = handleError(error);
|
||
alert(message);
|
||
}
|
||
};
|
||
|
||
if (loading) {
|
||
return <div className="p-8">加载中...</div>;
|
||
}
|
||
|
||
return (
|
||
<div className="p-8">
|
||
<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.substring(0, 16)}...</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>
|
||
);
|
||
}
|