From cbc19a6d9e998691c5cc2a8339ecf0062f5efebb Mon Sep 17 00:00:00 2001 From: seaislee1209 Date: Fri, 20 Mar 2026 18:20:14 +0800 Subject: [PATCH] feat: add admin management, change password, and operation log - Change password: current user can change their own password - Admin management: superuser can create/toggle/reset-password for admins - Operation log: view all system operations with type filter - All operations are recorded to AlertRecord for audit trail Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/apps/accounts/serializers.py | 14 ++ backend/apps/accounts/urls.py | 5 + backend/apps/accounts/views.py | 139 ++++++++++- frontend/src/layouts/MainLayout.vue | 4 + frontend/src/router/index.js | 1 + frontend/src/views/admin/AdminView.vue | 307 +++++++++++++++++++++++++ 6 files changed, 469 insertions(+), 1 deletion(-) create mode 100644 frontend/src/views/admin/AdminView.vue diff --git a/backend/apps/accounts/serializers.py b/backend/apps/accounts/serializers.py index 457be64..4ca71f5 100644 --- a/backend/apps/accounts/serializers.py +++ b/backend/apps/accounts/serializers.py @@ -10,3 +10,17 @@ class UserInfoSerializer(serializers.Serializer): id = serializers.IntegerField() username = serializers.CharField() is_superuser = serializers.BooleanField() + is_active = serializers.BooleanField() + date_joined = serializers.DateTimeField() + last_login = serializers.DateTimeField() + + +class ChangePasswordSerializer(serializers.Serializer): + old_password = serializers.CharField(write_only=True) + new_password = serializers.CharField(write_only=True, min_length=6) + + +class AdminUserCreateSerializer(serializers.Serializer): + username = serializers.CharField(max_length=150) + password = serializers.CharField(write_only=True, min_length=6) + is_superuser = serializers.BooleanField(default=False) diff --git a/backend/apps/accounts/urls.py b/backend/apps/accounts/urls.py index 60f6eb0..23c181a 100644 --- a/backend/apps/accounts/urls.py +++ b/backend/apps/accounts/urls.py @@ -5,4 +5,9 @@ urlpatterns = [ path('login/', views.login_view), path('refresh/', views.refresh_view), path('me/', views.me_view), + path('change-password/', views.change_password_view), + path('admins/', views.admin_list_view), + path('admins/create/', views.admin_create_view), + path('admins//toggle/', views.admin_toggle_view), + path('admins//reset-password/', views.admin_reset_password_view), ] diff --git a/backend/apps/accounts/views.py b/backend/apps/accounts/views.py index 708afc2..e5f7d67 100644 --- a/backend/apps/accounts/views.py +++ b/backend/apps/accounts/views.py @@ -5,7 +5,11 @@ from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework_simplejwt.tokens import RefreshToken -from .serializers import LoginSerializer, UserInfoSerializer +from .models import AdminUser +from .serializers import ( + LoginSerializer, UserInfoSerializer, + ChangePasswordSerializer, AdminUserCreateSerializer, +) @api_view(['POST']) @@ -58,3 +62,136 @@ def refresh_view(request): @api_view(['GET']) def me_view(request): return Response(UserInfoSerializer(request.user).data) + + +@api_view(['POST']) +def change_password_view(request): + """修改当前用户密码""" + serializer = ChangePasswordSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + if not request.user.check_password(serializer.validated_data['old_password']): + return Response({'error': 'wrong_password', 'message': '原密码错误'}, + status=status.HTTP_400_BAD_REQUEST) + + request.user.set_password(serializer.validated_data['new_password']) + request.user.save() + + # Log operation + from apps.monitor.models import AlertRecord + AlertRecord.objects.create( + alert_type=AlertRecord.AlertType.MANUAL, + title=f"管理员 {request.user.username} 修改密码", + content=f"操作人: {request.user.username}", + ) + + return Response({'message': '密码修改成功,请重新登录'}) + + +# ==================== Admin User Management ==================== + +@api_view(['GET']) +def admin_list_view(request): + """列出所有管理员""" + if not request.user.is_superuser: + return Response({'error': 'forbidden', 'message': '仅超级管理员可操作'}, + status=status.HTTP_403_FORBIDDEN) + + users = AdminUser.objects.all().order_by('id') + return Response(UserInfoSerializer(users, many=True).data) + + +@api_view(['POST']) +def admin_create_view(request): + """创建管理员账号""" + if not request.user.is_superuser: + return Response({'error': 'forbidden', 'message': '仅超级管理员可操作'}, + status=status.HTTP_403_FORBIDDEN) + + serializer = AdminUserCreateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + d = serializer.validated_data + + if AdminUser.objects.filter(username=d['username']).exists(): + return Response({'error': 'user_exists', 'message': f'用户名 {d["username"]} 已存在'}, + status=status.HTTP_409_CONFLICT) + + user = AdminUser.objects.create_user( + username=d['username'], + password=d['password'], + is_superuser=d.get('is_superuser', False), + is_staff=True, + ) + + from apps.monitor.models import AlertRecord + AlertRecord.objects.create( + alert_type=AlertRecord.AlertType.MANUAL, + title=f"创建管理员 {d['username']}", + content=f"操作人: {request.user.username},超级管理员: {'是' if d.get('is_superuser') else '否'}", + ) + + return Response({ + 'message': f'管理员 {d["username"]} 创建成功', + 'user': UserInfoSerializer(user).data, + }, status=status.HTTP_201_CREATED) + + +@api_view(['POST']) +def admin_toggle_view(request, pk): + """启用/停用管理员""" + if not request.user.is_superuser: + return Response({'error': 'forbidden', 'message': '仅超级管理员可操作'}, + status=status.HTTP_403_FORBIDDEN) + + try: + user = AdminUser.objects.get(pk=pk) + except AdminUser.DoesNotExist: + return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND) + + if user.pk == request.user.pk: + return Response({'error': 'self_toggle', 'message': '不能停用自己'}, + status=status.HTTP_400_BAD_REQUEST) + + user.is_active = not user.is_active + user.save(update_fields=['is_active']) + + action = '启用' if user.is_active else '停用' + from apps.monitor.models import AlertRecord + AlertRecord.objects.create( + alert_type=AlertRecord.AlertType.MANUAL, + title=f"{action}管理员 {user.username}", + content=f"操作人: {request.user.username}", + ) + + return Response({'message': f'已{action}管理员 {user.username}', + 'user': UserInfoSerializer(user).data}) + + +@api_view(['POST']) +def admin_reset_password_view(request, pk): + """超管重置其他管理员密码""" + if not request.user.is_superuser: + return Response({'error': 'forbidden', 'message': '仅超级管理员可操作'}, + status=status.HTTP_403_FORBIDDEN) + + try: + user = AdminUser.objects.get(pk=pk) + except AdminUser.DoesNotExist: + return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND) + + new_password = request.data.get('new_password', '') + if len(new_password) < 6: + return Response({'error': 'weak_password', 'message': '密码至少6位'}, + status=status.HTTP_400_BAD_REQUEST) + + user.set_password(new_password) + user.save() + + from apps.monitor.models import AlertRecord + AlertRecord.objects.create( + alert_type=AlertRecord.AlertType.MANUAL, + title=f"重置管理员 {user.username} 密码", + content=f"操作人: {request.user.username}", + ) + + return Response({'message': f'已重置 {user.username} 的密码'}) diff --git a/frontend/src/layouts/MainLayout.vue b/frontend/src/layouts/MainLayout.vue index 60d534c..2d7d061 100644 --- a/frontend/src/layouts/MainLayout.vue +++ b/frontend/src/layouts/MainLayout.vue @@ -24,6 +24,10 @@ 系统设置 + + + 系统管理 + diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 7fa91ca..8896347 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -17,6 +17,7 @@ const routes = [ { path: 'billing', name: 'Billing', component: () => import('../views/billing/BillingView.vue') }, { path: 'alerts', name: 'Alerts', component: () => import('../views/alerts/AlertList.vue') }, { path: 'settings', name: 'Settings', component: () => import('../views/settings/SettingsView.vue') }, + { path: 'admin', name: 'Admin', component: () => import('../views/admin/AdminView.vue') }, ], }, ] diff --git a/frontend/src/views/admin/AdminView.vue b/frontend/src/views/admin/AdminView.vue new file mode 100644 index 0000000..c5cdfa0 --- /dev/null +++ b/frontend/src/views/admin/AdminView.vue @@ -0,0 +1,307 @@ + + +