2026-05-25 14:31:58 +08:00

290 lines
9.1 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 { useEffect, useState } from 'react';
import Link from 'next/link';
import { useParams, useRouter } from 'next/navigation';
interface ApiKeyItem {
id: string;
name: string;
isActive: boolean;
lastUsedAt: string | null;
createdAt: string;
}
interface AdminInfo {
id: string;
username: string;
isSuperAdmin: boolean;
}
export default function ProjectApiKeysPage() {
const router = useRouter();
const params = useParams();
const projectCode = params.projectCode as string;
const [admin, setAdmin] = useState<AdminInfo | null>(null);
const [apiKeys, setApiKeys] = useState<ApiKeyItem[]>([]);
const [loading, setLoading] = useState(true);
const [showModal, setShowModal] = useState(false);
const loadData = async () => {
try {
const adminRes = await fetch('/api/admin/me');
if (adminRes.status === 401) {
router.push('/admin/login');
return;
}
const adminData = await adminRes.json();
if (!adminData.isSuperAdmin) {
router.push(`/admin/${projectCode}`);
return;
}
setAdmin(adminData);
const apiKeyRes = await fetch(`/api/admin/${projectCode}/api-keys`);
if (apiKeyRes.status === 403 || apiKeyRes.status === 404) {
router.push('/admin/select-project');
return;
}
const data = await apiKeyRes.json();
setApiKeys(data.apiKeys || []);
} catch (error) {
console.error('获取项目 API Key 失败:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
void loadData();
}, [projectCode]);
const handleDisable = async (id: string) => {
if (!confirm('确定要禁用这个 API Key 吗?')) {
return;
}
try {
const response = await fetch(`/api/admin/${projectCode}/api-keys?id=${id}`, {
method: 'DELETE',
});
if (response.ok) {
await loadData();
}
} catch (error) {
console.error('禁用项目 API Key 失败:', error);
}
};
return (
<div className="min-h-screen bg-gray-100">
<nav className="bg-white shadow">
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4">
<div className="flex items-center gap-4">
<Link href={`/admin/${projectCode}`} className="text-gray-600 hover:text-gray-900">
← 返回
</Link>
<h1 className="text-xl font-bold">项目 API Key 管理</h1>
</div>
{admin?.isSuperAdmin ? (
<button
onClick={() => setShowModal(true)}
className="rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
创建 API Key
</button>
) : null}
</div>
</nav>
<main className="mx-auto max-w-5xl px-4 py-8">
<div className="rounded-lg bg-white shadow">
{loading ? (
<div className="p-8 text-center text-gray-500">加载中...</div>
) : (
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium uppercase text-gray-500">
名称
</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase text-gray-500">
状态
</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase text-gray-500">
最近使用
</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase text-gray-500">
创建时间
</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase text-gray-500">
操作
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{apiKeys.map((item) => (
<tr key={item.id}>
<td className="px-6 py-4 font-medium">{item.name}</td>
<td className="px-6 py-4">
<span
className={`rounded px-2 py-1 text-xs font-medium ${
item.isActive
? 'bg-green-100 text-green-700'
: 'bg-gray-100 text-gray-500'
}`}
>
{item.isActive ? '启用中' : '已禁用'}
</span>
</td>
<td className="px-6 py-4 text-gray-600">
{item.lastUsedAt ? new Date(item.lastUsedAt).toLocaleString() : '未使用'}
</td>
<td className="px-6 py-4 text-gray-600">
{new Date(item.createdAt).toLocaleString()}
</td>
<td className="px-6 py-4">
{item.isActive ? (
<button
onClick={() => void handleDisable(item.id)}
className="text-sm text-red-600 hover:underline"
>
禁用
</button>
) : (
<span className="text-sm text-gray-400">不可恢复</span>
)}
</td>
</tr>
))}
{!apiKeys.length ? (
<tr>
<td colSpan={5} className="px-6 py-8 text-center text-gray-500">
暂无 API Key
</td>
</tr>
) : null}
</tbody>
</table>
)}
</div>
</main>
{showModal ? (
<ApiKeyModal
projectCode={projectCode}
onClose={() => setShowModal(false)}
onSuccess={() => {
setShowModal(false);
void loadData();
}}
/>
) : null}
</div>
);
}
function ApiKeyModal({
projectCode,
onClose,
onSuccess,
}: {
projectCode: string;
onClose: () => void;
onSuccess: () => void;
}) {
const [name, setName] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [generatedKey, setGeneratedKey] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const response = await fetch(`/api/admin/${projectCode}/api-keys`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
});
const data = await response.json();
if (data.success) {
setGeneratedKey(data.key);
} else {
setError(data.error || '创建失败');
}
} catch {
setError('网络错误');
} finally {
setLoading(false);
}
};
if (generatedKey) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="w-full max-w-lg rounded-lg bg-white p-6 shadow-xl">
<h2 className="mb-4 text-lg font-medium">API Key 已创建</h2>
<div className="mb-4 rounded-lg bg-gray-100 p-4">
<p className="mb-2 text-sm text-gray-500">请立即保存,只会显示一次:</p>
<p className="break-all font-mono text-sm font-bold">{generatedKey}</p>
</div>
<button
onClick={onSuccess}
className="w-full rounded-lg bg-blue-600 py-2 text-white hover:bg-blue-700"
>
完成
</button>
</div>
</div>
);
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="w-full max-w-md rounded-lg bg-white shadow-xl">
<div className="border-b p-6">
<h2 className="text-lg font-medium">创建项目 API Key</h2>
</div>
<form onSubmit={handleSubmit} className="space-y-4 p-6">
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">名称 *</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full rounded-lg border px-3 py-2"
placeholder="例如:官网发证"
required
/>
</div>
{error ? <div className="rounded-lg bg-red-50 p-3 text-sm text-red-600">{error}</div> : null}
<div className="flex gap-3 pt-4">
<button
type="button"
onClick={onClose}
className="flex-1 rounded-lg border py-2 hover:bg-gray-50"
>
取消
</button>
<button
type="submit"
disabled={loading}
className="flex-1 rounded-lg bg-blue-600 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
>
{loading ? '创建中...' : '创建'}
</button>
</div>
</form>
</div>
</div>
);
}