Skip to content

Commit ac63569

Browse files
author
CodeJudge
committed
feat: 管理员用户管理
1 parent b9fe442 commit ac63569

2 files changed

Lines changed: 78 additions & 2 deletions

File tree

backend/src/routes/auth.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const router = require('express').Router();
22
const { register, login, getProfile, changePassword } = require('../controllers/authController');
33
const { authenticate } = require('../middleware/auth');
4-
const { queryAll } = require('../config/db');
4+
const { queryAll, run } = require('../config/db');
55

66
router.post('/register', register);
77
router.post('/login', login);
@@ -63,4 +63,16 @@ router.get('/leaderboard', async (req, res) => {
6363
}
6464
});
6565

66+
router.get('/admin/users', authenticate, (req, res) => {
67+
if (req.user.role !== 'admin') return res.status(403).json({ error: '需要管理员权限' });
68+
const users = queryAll('SELECT id, username, email, role, created_at FROM users ORDER BY id');
69+
res.json({ users });
70+
});
71+
72+
router.delete('/admin/users/:id', authenticate, (req, res) => {
73+
if (req.user.role !== 'admin') return res.status(403).json({ error: '需要管理员权限' });
74+
run('DELETE FROM users WHERE id = ? AND role != ?', [req.params.id, 'admin']);
75+
res.json({ message: '用户已删除' });
76+
});
77+
6678
module.exports = router;

frontend/src/pages/Admin.tsx

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useState, useEffect, useCallback } from 'react';
22
import { useNavigate } from 'react-router-dom';
3-
import { Plus, Edit, Trash2, Users, Code, BarChart3, AlertTriangle, Loader2, Activity, Percent, Clock, Download, Upload } from 'lucide-react';
3+
import { Plus, Edit, Trash2, Users, Shield, Code, BarChart3, AlertTriangle, Loader2, Activity, Percent, Clock, Download, Upload } from 'lucide-react';
44
import toast from 'react-hot-toast';
55
import { useAuth } from '../context/AuthContext';
66
import api from '../services/api';
@@ -16,6 +16,8 @@ export default function Admin() {
1616
const [loading, setLoading] = useState(true);
1717
const [error, setError] = useState<string | null>(null);
1818
const [deletingId, setDeletingId] = useState<number | null>(null);
19+
const [users, setUsers] = useState<any[]>([]);
20+
const [usersLoading, setUsersLoading] = useState(false);
1921

2022
const fetchData = useCallback(async () => {
2123
setLoading(true);
@@ -33,6 +35,10 @@ export default function Admin() {
3335
const adminRes = await api.problems.getAdminStats();
3436
setAdminStats(adminRes);
3537
} catch { /* silently ignore */ }
38+
try {
39+
const userRes = await fetch('/api/auth/admin/users', { headers: { Authorization: `Bearer ${localStorage.getItem('oj_token')}` } });
40+
if (userRes.ok) setUsers((await userRes.json()).users);
41+
} catch {}
3642
}
3743
} catch (err: any) {
3844
const msg = err.message || '加载数据失败';
@@ -93,6 +99,23 @@ export default function Admin() {
9399
}
94100
};
95101

102+
const handleDeleteUser = async (userId: number, username: string) => {
103+
if (!window.confirm(`确定要删除用户「${username}」吗?此操作不可撤销。`)) return;
104+
try {
105+
const res = await fetch(`/api/auth/admin/users/${userId}`, {
106+
method: 'DELETE',
107+
headers: { Authorization: `Bearer ${localStorage.getItem('oj_token')}` },
108+
});
109+
if (res.ok) {
110+
toast.success('用户已删除');
111+
setUsers(prev => prev.filter(u => u.id !== userId));
112+
} else {
113+
const data = await res.json();
114+
toast.error(data.error || '删除失败');
115+
}
116+
} catch { toast.error('删除失败'); }
117+
};
118+
96119
const TYPE_LABELS: Record<string, string> = {
97120
programming: '编程题',
98121
choice: '选择题',
@@ -213,6 +236,47 @@ export default function Admin() {
213236
</div>
214237
)}
215238

239+
{/* User Management */}
240+
<div className="card p-6 mb-6">
241+
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
242+
<Users className="w-5 h-5 text-blue-400" /> 用户管理
243+
</h2>
244+
<div className="overflow-x-auto">
245+
<table className="w-full text-sm">
246+
<thead>
247+
<tr className="border-b border-dark-700">
248+
<th className="text-left py-2 px-3 text-dark-400">ID</th>
249+
<th className="text-left py-2 px-3 text-dark-400">用户名</th>
250+
<th className="text-left py-2 px-3 text-dark-400 hidden md:table-cell">邮箱</th>
251+
<th className="text-center py-2 px-3 text-dark-400">角色</th>
252+
<th className="text-center py-2 px-3 text-dark-400 hidden md:table-cell">注册时间</th>
253+
<th className="text-center py-2 px-3 text-dark-400">操作</th>
254+
</tr>
255+
</thead>
256+
<tbody>
257+
{users.map(u => (
258+
<tr key={u.id} className="border-b border-dark-800">
259+
<td className="py-2 px-3 text-dark-400">{u.id}</td>
260+
<td className="py-2 px-3 text-white">{u.username}</td>
261+
<td className="py-2 px-3 text-dark-300 hidden md:table-cell">{u.email}</td>
262+
<td className="py-2 px-3 text-center">
263+
{u.role === 'admin' ? <Shield className="w-4 h-4 text-primary-400 mx-auto" /> : <span className="text-dark-400 text-xs">user</span>}
264+
</td>
265+
<td className="py-2 px-3 text-dark-400 text-center hidden md:table-cell">{u.created_at?.slice(0, 10)}</td>
266+
<td className="py-2 px-3 text-center">
267+
{u.role !== 'admin' && (
268+
<button onClick={() => handleDeleteUser(u.id, u.username)} className="btn-danger text-xs px-2 py-1">
269+
<Trash2 className="w-3.5 h-3.5" />
270+
</button>
271+
)}
272+
</td>
273+
</tr>
274+
))}
275+
</tbody>
276+
</table>
277+
</div>
278+
</div>
279+
216280
{/* Actions */}
217281
<div className="flex items-center justify-between mb-4">
218282
<h2 className="text-lg font-semibold text-white">题目管理</h2>

0 commit comments

Comments
 (0)