diff --git a/backend/src/admin-users/admin-users.controller.ts b/backend/src/admin-users/admin-users.controller.ts new file mode 100644 index 0000000..d48245b --- /dev/null +++ b/backend/src/admin-users/admin-users.controller.ts @@ -0,0 +1,52 @@ +import { Controller, Get, Post, Put, Delete, Body, Param, HttpCode, HttpStatus, UseGuards } from '@nestjs/common'; +import { AdminUsersService } from './admin-users.service'; +import { AdminRole } from '../entities/admin-user.entity'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; + +@Controller('admin/admin-users') +@UseGuards(JwtAuthGuard) +export class AdminUsersController { + constructor(private adminUsersService: AdminUsersService) {} + + @Get() + @HttpCode(HttpStatus.OK) + async findAll() { + return this.adminUsersService.findAll(); + } + + @Get(':id') + @HttpCode(HttpStatus.OK) + async findOne(@Param('id') id: string) { + return this.adminUsersService.findOne(Number(id)); + } + + @Post() + @HttpCode(HttpStatus.CREATED) + async create(@Body() createAdminUserDto: { + username: string; + password: string; + role: AdminRole; + }) { + return this.adminUsersService.create(createAdminUserDto); + } + + @Put(':id') + @HttpCode(HttpStatus.OK) + async update( + @Param('id') id: string, + @Body() updateAdminUserDto: { + username?: string; + password?: string; + role?: AdminRole; + }, + ) { + return this.adminUsersService.update(Number(id), updateAdminUserDto); + } + + @Delete(':id') + @HttpCode(HttpStatus.OK) + async remove(@Param('id') id: string) { + await this.adminUsersService.remove(Number(id)); + return { message: '删除成功' }; + } +} diff --git a/backend/src/admin-users/admin-users.module.ts b/backend/src/admin-users/admin-users.module.ts new file mode 100644 index 0000000..528a800 --- /dev/null +++ b/backend/src/admin-users/admin-users.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AdminUsersController } from './admin-users.controller'; +import { AdminUsersService } from './admin-users.service'; +import { AdminUser } from '../entities/admin-user.entity'; +import { AuthModule } from '../auth/auth.module'; + +@Module({ + imports: [TypeOrmModule.forFeature([AdminUser]), AuthModule], + controllers: [AdminUsersController], + providers: [AdminUsersService], + exports: [AdminUsersService], +}) +export class AdminUsersModule {} diff --git a/backend/src/admin-users/admin-users.service.ts b/backend/src/admin-users/admin-users.service.ts new file mode 100644 index 0000000..0ab23cf --- /dev/null +++ b/backend/src/admin-users/admin-users.service.ts @@ -0,0 +1,75 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { AdminUser, AdminRole } from '../entities/admin-user.entity'; +import * as bcrypt from 'bcrypt'; + +@Injectable() +export class AdminUsersService { + constructor( + @InjectRepository(AdminUser) + private adminUserRepository: Repository, + ) {} + + async findAll(): Promise { + return this.adminUserRepository.find({ + select: ['id', 'username', 'role', 'createdAt', 'updatedAt'], + order: { createdAt: 'DESC' }, + }); + } + + async findOne(id: number): Promise { + return this.adminUserRepository.findOne({ + where: { id }, + select: ['id', 'username', 'role', 'createdAt', 'updatedAt'], + }); + } + + async findByUsername(username: string): Promise { + return this.adminUserRepository.findOne({ + where: { username }, + }); + } + + async create(createAdminUserDto: { + username: string; + password: string; + role: AdminRole; + }): Promise { + const hashedPassword = await bcrypt.hash(createAdminUserDto.password, 10); + const adminUser = this.adminUserRepository.create({ + username: createAdminUserDto.username, + passwordHash: hashedPassword, + role: createAdminUserDto.role, + }); + return this.adminUserRepository.save(adminUser); + } + + async update(id: number, updateAdminUserDto: { + username?: string; + password?: string; + role?: AdminRole; + }): Promise { + const adminUser = await this.findOne(id); + if (!adminUser) { + return null; + } + + if (updateAdminUserDto.username) { + adminUser.username = updateAdminUserDto.username; + } + if (updateAdminUserDto.password) { + adminUser.passwordHash = await bcrypt.hash(updateAdminUserDto.password, 10); + } + if (updateAdminUserDto.role) { + adminUser.role = updateAdminUserDto.role; + } + + return this.adminUserRepository.save(adminUser); + } + + async remove(id: number): Promise { + const result = await this.adminUserRepository.delete(id); + return (result.affected ?? 0) > 0; + } +} diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index f0b811d..5b21afa 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -5,6 +5,7 @@ import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AuthModule } from './auth/auth.module'; import { PlayerModule } from './player/player.module'; +import { AdminUsersModule } from './admin-users/admin-users.module'; @Module({ imports: [ @@ -26,6 +27,7 @@ import { PlayerModule } from './player/player.module'; }), AuthModule, PlayerModule, + AdminUsersModule, ], controllers: [AppController], providers: [AppService], diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts index c51e8d9..2437ccb 100644 --- a/backend/src/auth/auth.module.ts +++ b/backend/src/auth/auth.module.ts @@ -17,6 +17,6 @@ import { AdminUser } from '../entities/admin-user.entity'; ], controllers: [AuthController], providers: [AuthService], - exports: [AuthService], + exports: [AuthService, JwtModule], }) export class AuthModule {} diff --git a/backend/src/auth/guards/jwt-auth.guard.ts b/backend/src/auth/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..579f444 --- /dev/null +++ b/backend/src/auth/guards/jwt-auth.guard.ts @@ -0,0 +1,30 @@ +import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; + +@Injectable() +export class JwtAuthGuard implements CanActivate { + constructor(private jwtService: JwtService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const authHeader = request.headers.authorization; + + if (!authHeader) { + throw new UnauthorizedException('未提供认证令牌'); + } + + const [type, token] = authHeader.split(' '); + + if (type !== 'Bearer' || !token) { + throw new UnauthorizedException('无效的认证令牌格式'); + } + + try { + const payload = await this.jwtService.verifyAsync(token); + request.user = payload; + return true; + } catch (error) { + throw new UnauthorizedException('无效的认证令牌'); + } + } +} diff --git a/frontend/package.json b/frontend/package.json index 4a717bb..2c57476 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,7 +10,7 @@ "preview": "vite preview" }, "dependencies": { - "antd": "^6.1.2", + "antd": "6.1.3", "axios": "^1.13.2", "react": "^19.2.0", "react-dom": "^19.2.0", diff --git a/frontend/src/components/AdminLayout.tsx b/frontend/src/components/AdminLayout.tsx index 66013fc..457647a 100644 --- a/frontend/src/components/AdminLayout.tsx +++ b/frontend/src/components/AdminLayout.tsx @@ -4,7 +4,7 @@ import { MenuFoldOutlined, MenuUnfoldOutlined, DashboardOutlined, - UserOutlined, + SettingOutlined, LogoutOutlined, } from '@ant-design/icons'; import { Outlet, useNavigate, useLocation } from 'react-router-dom'; @@ -30,7 +30,7 @@ const AdminLayout = () => { logout(); message.success('登出成功'); navigate('/admin/login'); - } catch (error) { + } catch { message.error('登出失败'); } }; @@ -43,12 +43,12 @@ const AdminLayout = () => { }, { key: 'user', - icon: , - label: '用户管理', + icon: , + label: '系统管理', children: [ { key: '/admin/users', - label: '用户列表', + label: '用户管理', }, ], }, diff --git a/frontend/src/pages/admin/AdminUsers.tsx b/frontend/src/pages/admin/AdminUsers.tsx new file mode 100644 index 0000000..737f24d --- /dev/null +++ b/frontend/src/pages/admin/AdminUsers.tsx @@ -0,0 +1,249 @@ +import { useState, useEffect } from 'react'; +import { + Table, + Button, + Space, + Modal, + Form, + Input, + Select, + message, + Popconfirm, + Tag, +} from 'antd'; +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, +} from '@ant-design/icons'; +import { adminUsersService, AdminRole } from '../../services/adminUsersService'; +import type { AdminUser, CreateAdminUserDto, UpdateAdminUserDto } from '../../services/adminUsersService'; + +const AdminUsers = () => { + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(false); + const [modalVisible, setModalVisible] = useState(false); + const [editingUser, setEditingUser] = useState(null); + const [form] = Form.useForm(); + + const roleOptions = [ + { label: '超级管理员', value: AdminRole.SUPER_ADMIN }, + { label: '操作员', value: AdminRole.OPERATOR }, + { label: '查看者', value: AdminRole.VIEWER }, + ]; + + const roleLabels: Record = { + [AdminRole.SUPER_ADMIN]: '超级管理员', + [AdminRole.OPERATOR]: '操作员', + [AdminRole.VIEWER]: '查看者', + }; + + const roleColors: Record = { + [AdminRole.SUPER_ADMIN]: 'red', + [AdminRole.OPERATOR]: 'blue', + [AdminRole.VIEWER]: 'green', + }; + + useEffect(() => { + fetchUsers(); + }, []); + + const fetchUsers = async () => { + setLoading(true); + try { + const data = await adminUsersService.findAll(); + setUsers(data); + } catch (error) { + message.error('获取用户列表失败'); + } finally { + setLoading(false); + } + }; + + const handleAdd = () => { + setEditingUser(null); + form.resetFields(); + setModalVisible(true); + }; + + const handleEdit = (user: AdminUser) => { + setEditingUser(user); + form.setFieldsValue({ + username: user.username, + role: user.role, + }); + setModalVisible(true); + }; + + const handleDelete = async (id: number) => { + try { + await adminUsersService.remove(id); + message.success('删除成功'); + fetchUsers(); + } catch (error) { + message.error('删除失败'); + } + }; + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + + if (editingUser) { + const updateData: UpdateAdminUserDto = { + username: values.username, + role: values.role, + }; + if (values.password) { + updateData.password = values.password; + } + await adminUsersService.update(editingUser.id, updateData); + message.success('更新成功'); + } else { + const createData: CreateAdminUserDto = { + username: values.username, + password: values.password, + role: values.role, + }; + await adminUsersService.create(createData); + message.success('创建成功'); + } + + setModalVisible(false); + form.resetFields(); + fetchUsers(); + } catch (error) { + message.error(editingUser ? '更新失败' : '创建失败'); + } + }; + + const columns = [ + { + title: 'ID', + dataIndex: 'id', + key: 'id', + width: 80, + }, + { + title: '用户名', + dataIndex: 'username', + key: 'username', + }, + { + title: '角色', + dataIndex: 'role', + key: 'role', + render: (role: AdminRole) => ( + {roleLabels[role]} + ), + }, + { + title: '创建时间', + dataIndex: 'createdAt', + key: 'createdAt', + render: (date: string) => new Date(date).toLocaleString('zh-CN'), + }, + { + title: '更新时间', + dataIndex: 'updatedAt', + key: 'updatedAt', + render: (date: string) => new Date(date).toLocaleString('zh-CN'), + }, + { + title: '操作', + key: 'action', + render: (_: any, record: AdminUser) => ( + + + handleDelete(record.id)} + okText="确定" + cancelText="取消" + > + + + + ), + }, + ]; + + return ( +
+
+

用户管理

+ +
+ + `共 ${total} 条`, + }} + /> + + { + setModalVisible(false); + form.resetFields(); + }} + okText="确定" + cancelText="取消" + > +
+ + + + + + + + + +