gongxiangp2/components/SuperAdminsTab.tsx

496 lines
19 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 { parseFetchError } from '@/lib/error-handler'
interface SuperAdmin {
id: string
email: string
username: string
realName: string | null
department: string
role: string
createdAt: string
}
interface ApiKey {
id: string
name: string
key: string
isActive: boolean
createdBy: string
lastUsedAt: string | null
createdAt: string
}
interface SuperAdminsTabProps {
showToast: (message: string, type?: 'success' | 'error') => void
}
export default function SuperAdminsTab({ showToast }: SuperAdminsTabProps) {
const [superAdmins, setSuperAdmins] = useState<SuperAdmin[]>([])
const [loading, setLoading] = useState(true)
const [count, setCount] = useState(0)
const [canAdd, setCanAdd] = useState(false)
const [addingUserId, setAddingUserId] = useState('')
const [adding, setAdding] = useState(false)
const [removingUserId, setRemovingUserId] = useState<string | null>(null)
// API Keys 相关状态
const [apiKeys, setApiKeys] = useState<ApiKey[]>([])
const [apiKeysLoading, setApiKeysLoading] = useState(true)
const [showCreateModal, setShowCreateModal] = useState(false)
const [newKeyName, setNewKeyName] = useState('')
const [createdKey, setCreatedKey] = useState<string | null>(null)
// 加载超级管理员列表
const loadSuperAdmins = async () => {
try {
const response = await fetch('/api/admin/super-admins')
if (response.ok) {
const data = await response.json()
setSuperAdmins(data.superAdmins)
setCount(data.count)
setCanAdd(data.canAdd)
} else {
showToast('加载超级管理员列表失败', 'error')
}
} catch (error) {
console.error('Load super admins error:', error)
showToast('加载超级管理员列表失败', 'error')
} finally {
setLoading(false)
}
}
useEffect(() => {
loadSuperAdmins()
fetchApiKeys()
}, [])
// 添加超级管理员
const handleAddSuperAdmin = async () => {
if (!addingUserId.trim()) {
showToast('请输入用户ID', 'error')
return
}
setAdding(true)
try {
const response = await fetch('/api/admin/super-admins', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: addingUserId.trim() }),
})
if (response.ok) {
showToast('超级管理员添加成功', 'success')
setAddingUserId('')
await loadSuperAdmins()
} else {
const data = await response.json()
showToast(data.error || '添加失败', 'error')
}
} catch (error) {
console.error('Add super admin error:', error)
showToast('添加失败', 'error')
} finally {
setAdding(false)
}
}
// 移除超级管理员
const handleRemoveSuperAdmin = async (userId: string) => {
if (!confirm('确定要移除该超级管理员权限吗?')) {
return
}
setRemovingUserId(userId)
try {
const response = await fetch(`/api/admin/super-admins?userId=${userId}`, {
method: 'DELETE',
})
if (response.ok) {
showToast('超级管理员权限已移除', 'success')
await loadSuperAdmins()
} else {
const data = await response.json()
showToast(data.error || '移除失败', 'error')
}
} catch (error) {
console.error('Remove super admin error:', error)
showToast('移除失败', 'error')
} finally {
setRemovingUserId(null)
}
}
// 加载 API Keys
const fetchApiKeys = async () => {
try {
setApiKeysLoading(true)
const response = await fetch('/api/admin/api-keys')
if (response.ok) {
const data = await response.json()
setApiKeys(data.data || [])
} else {
const errorData = await parseFetchError(response)
showToast(errorData.message, 'error')
}
} catch (error) {
console.error('加载 API Keys 失败:', error)
const errorMessage = error instanceof Error ? error.message : '加载失败'
showToast(errorMessage, 'error')
} finally {
setApiKeysLoading(false)
}
}
// 创建 API Key
const handleCreateApiKey = async () => {
if (!newKeyName.trim()) {
showToast('请输入密钥名称', '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('')
showToast('API Key 创建成功', 'success')
fetchApiKeys()
} else {
const errorData = await parseFetchError(response)
showToast(errorData.message, 'error')
}
} catch (error) {
console.error('创建失败:', error)
const errorMessage = error instanceof Error ? error.message : '创建失败'
showToast(errorMessage, 'error')
}
}
// 切换 API Key 状态
const handleToggleApiKeyActive = 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) {
showToast(isActive ? 'API Key 已禁用' : 'API Key 已启用', 'success')
fetchApiKeys()
} else {
const errorData = await parseFetchError(response)
showToast(errorData.message, 'error')
}
} catch (error) {
console.error('更新失败:', error)
const errorMessage = error instanceof Error ? error.message : '更新失败'
showToast(errorMessage, 'error')
}
}
// 删除 API Key
const handleDeleteApiKey = async (id: string) => {
if (!confirm('确定要删除此 API Key 吗?删除后无法恢复。')) return
try {
const response = await fetch(`/api/admin/api-keys/${id}`, {
method: 'DELETE',
})
if (response.ok) {
showToast('API Key 已删除', 'success')
fetchApiKeys()
} else {
const errorData = await parseFetchError(response)
showToast(errorData.message, 'error')
}
} catch (error) {
console.error('删除失败:', error)
const errorMessage = error instanceof Error ? error.message : '删除失败'
showToast(errorMessage, 'error')
}
}
if (loading) {
return (
<div className="card-apple">
<div className="p-8 text-center text-gray-500 dark:text-gray-400">
加载中...
</div>
</div>
)
}
return (
<div className="card-apple">
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
超级管理员管理
</h2>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
当前:{count} / 4 个超级管理员
{!canAdd && <span className="ml-2 text-orange-600 dark:text-orange-400">(已达上限)</span>}
</p>
</div>
<div className="p-6 space-y-6">
{/* 添加超级管理员 */}
{canAdd && (
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
<h3 className="text-sm font-medium text-gray-900 dark:text-white mb-3">
添加超级管理员
</h3>
<div className="flex gap-2">
<input
type="text"
value={addingUserId}
onChange={(e) => setAddingUserId(e.target.value)}
placeholder="输入用户ID"
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
onClick={handleAddSuperAdmin}
disabled={adding || !addingUserId.trim()}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{adding ? '添加中...' : '添加'}
</button>
</div>
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
提示:只能添加管理员角色的用户,且最多4个超级管理员
</p>
</div>
)}
{/* 超级管理员列表 */}
<div>
<h3 className="text-sm font-medium text-gray-900 dark:text-white mb-3">
超级管理员列表
</h3>
{superAdmins.length === 0 ? (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
暂无超级管理员
</div>
) : (
<div className="space-y-2">
{superAdmins.map((admin) => (
<div
key={admin.id}
className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700"
>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium text-gray-900 dark:text-white">
{admin.realName || admin.username}
</span>
<span className="px-2 py-0.5 text-xs bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded">
超级管理员
</span>
</div>
<div className="mt-1 text-sm text-gray-500 dark:text-gray-400">
<span>{admin.email}</span>
{admin.department && (
<span className="ml-2">· {admin.department}</span>
)}
</div>
<div className="mt-1 text-xs text-gray-400 dark:text-gray-500">
创建时间:{new Date(admin.createdAt).toLocaleString('zh-CN')}
</div>
</div>
<button
onClick={() => handleRemoveSuperAdmin(admin.id)}
disabled={removingUserId === admin.id}
className="ml-4 px-3 py-1.5 text-sm text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 bg-red-50 dark:bg-red-900/20 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors disabled:opacity-50"
>
{removingUserId === admin.id ? '移除中...' : '移除'}
</button>
</div>
))}
</div>
)}
</div>
{/* API Key 管理卡片 */}
<div className="mt-8">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">
API Key 管理
</h3>
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm"
>
创建 API Key
</button>
</div>
{apiKeysLoading ? (
<div className="p-8 text-center text-gray-500 dark:text-gray-400">
加载中...
</div>
) : apiKeys.length === 0 ? (
<div className="p-8 text-center text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
<p>暂无 API Key</p>
<p className="mt-2 text-sm text-gray-400 dark:text-gray-500">
点击上方按钮创建第一个 API Key
</p>
</div>
) : (
<div className="overflow-x-auto bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-100 dark:bg-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
名称
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
密钥
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
状态
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
最后使用
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
创建时间
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
操作
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
{apiKeys.map((key) => (
<tr key={key.id} className="hover:bg-gray-50 dark:hover:bg-gray-800">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
{key.name}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-500 dark:text-gray-400">
{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 dark:bg-green-900 dark:text-green-200'
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'
}`}
>
{key.isActive ? '启用' : '禁用'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{key.lastUsedAt
? new Date(key.lastUsedAt).toLocaleString('zh-CN')
: '从未使用'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{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={() => handleToggleApiKeyActive(key.id, key.isActive)}
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
>
{key.isActive ? '禁用' : '启用'}
</button>
<button
onClick={() => handleDeleteApiKey(key.id)}
className="text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300"
>
删除
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
{/* 创建 API Key 模态框 */}
{showCreateModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
<h2 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
创建 API Key
</h2>
{createdKey ? (
<div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
API Key 已创建,请妥善保存:
</p>
<div className="bg-gray-100 dark:bg-gray-700 p-3 rounded font-mono text-sm break-all mb-4 text-gray-900 dark:text-white">
{createdKey}
</div>
<p className="text-sm text-red-600 dark:text-red-400 mb-4">
注意:此密钥只显示一次,关闭后将无法再次查看
</p>
<button
onClick={() => {
setCreatedKey(null)
setShowCreateModal(false)
}}
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
关闭
</button>
</div>
) : (
<div>
<label className="block text-sm font-medium mb-2 text-gray-900 dark:text-white">
密钥名称
</label>
<input
type="text"
value={newKeyName}
onChange={(e) => setNewKeyName(e.target.value)}
placeholder="例如:任务系统"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mb-4 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleCreateApiKey()
}
}}
/>
<div className="flex space-x-3">
<button
onClick={() => setShowCreateModal(false)}
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 transition-colors"
>
取消
</button>
<button
onClick={handleCreateApiKey}
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
创建
</button>
</div>
</div>
)}
</div>
</div>
)}
</div>
)
}