feat: v0.12.5 admin 保护 + 管理员角色切换 + 团队详情加宽
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m37s

①admin 账号不可被禁用(包括自己,防误操作)
②admin 密码不可被其他超管重置(admin 自己可以)
③超管可在团队详情点击角色切换成员/管理员
④团队详情弹窗宽度 1080→1280px

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
seaislee1209 2026-03-23 20:16:57 +08:00
parent 969283690f
commit a026c04310
5 changed files with 59 additions and 3 deletions

View File

@ -22,6 +22,7 @@ urlpatterns = [
path('admin/teams/<int:team_id>/topup', views.admin_team_topup_view, name='admin_team_topup'), path('admin/teams/<int:team_id>/topup', views.admin_team_topup_view, name='admin_team_topup'),
path('admin/teams/<int:team_id>/set-pool', views.admin_team_set_pool_view, name='admin_team_set_pool'), path('admin/teams/<int:team_id>/set-pool', views.admin_team_set_pool_view, name='admin_team_set_pool'),
path('admin/teams/<int:team_id>/admin', views.admin_team_create_admin_view, name='admin_team_create_admin'), path('admin/teams/<int:team_id>/admin', views.admin_team_create_admin_view, name='admin_team_create_admin'),
path('admin/teams/<int:team_id>/members/<int:member_id>/role', views.admin_team_member_role_view, name='admin_team_member_role'),
# ── Super Admin: User management ── # ── Super Admin: User management ──
path('admin/users', views.admin_users_list_view, name='admin_users_list'), path('admin/users', views.admin_users_list_view, name='admin_users_list'),

View File

@ -1019,6 +1019,36 @@ def admin_team_detail_view(request, team_id):
}) })
@api_view(['PATCH'])
@permission_classes([IsSuperAdmin])
def admin_team_member_role_view(request, team_id, member_id):
"""PATCH /api/v1/admin/teams/<id>/members/<id>/role — Toggle team admin role."""
try:
team = Team.objects.get(id=team_id)
except Team.DoesNotExist:
return Response({'error': '团队不存在'}, status=status.HTTP_404_NOT_FOUND)
try:
member = team.members.get(id=member_id)
except User.DoesNotExist:
return Response({'error': '成员不存在'}, status=status.HTTP_404_NOT_FOUND)
is_admin = request.data.get('is_team_admin')
if is_admin is None:
return Response({'error': '请提供 is_team_admin 参数'}, status=status.HTTP_400_BAD_REQUEST)
before = {'is_team_admin': member.is_team_admin}
member.is_team_admin = bool(is_admin)
member.save(update_fields=['is_team_admin'])
log_admin_action(request, 'team_update', 'user', target_id=member.id, target_name=member.username,
before=before, after={'is_team_admin': member.is_team_admin})
return Response({
'user_id': member.id,
'username': member.username,
'is_team_admin': member.is_team_admin,
})
@api_view(['POST']) @api_view(['POST'])
@permission_classes([IsSuperAdmin]) @permission_classes([IsSuperAdmin])
def admin_team_topup_view(request, team_id): def admin_team_topup_view(request, team_id):
@ -1419,6 +1449,10 @@ def admin_user_status_view(request, user_id):
except User.DoesNotExist: except User.DoesNotExist:
return Response({'error': '用户不存在'}, status=status.HTTP_404_NOT_FOUND) return Response({'error': '用户不存在'}, status=status.HTTP_404_NOT_FOUND)
# 保护 admin 账号不被任何人禁用(包括自己,防误操作)
if user.username == 'admin':
return Response({'error': '不能禁用超级管理员账号'}, status=status.HTTP_403_FORBIDDEN)
serializer = UserStatusSerializer(data=request.data) serializer = UserStatusSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -1451,6 +1485,10 @@ def admin_reset_password_view(request, user_id):
except User.DoesNotExist: except User.DoesNotExist:
return Response({'error': '用户不存在'}, status=status.HTTP_404_NOT_FOUND) return Response({'error': '用户不存在'}, status=status.HTTP_404_NOT_FOUND)
# 保护 admin 账号密码不被其他超管重置
if user.username == 'admin' and request.user.username != 'admin':
return Response({'error': '不能重置超级管理员的密码'}, status=status.HTTP_403_FORBIDDEN)
new_password = request.data.get('new_password', '') new_password = request.data.get('new_password', '')
if len(new_password) < 8: if len(new_password) < 8:
return Response({'error': '密码至少8位'}, status=status.HTTP_400_BAD_REQUEST) return Response({'error': '密码至少8位'}, status=status.HTTP_400_BAD_REQUEST)

View File

@ -190,6 +190,9 @@ export const adminApi = {
createTeamAdmin: (teamId: number, data: { username: string; email: string; password: string }) => createTeamAdmin: (teamId: number, data: { username: string; email: string; password: string }) =>
api.post(`/admin/teams/${teamId}/admin`, data), api.post(`/admin/teams/${teamId}/admin`, data),
setMemberRole: (teamId: number, memberId: number, isTeamAdmin: boolean) =>
api.patch(`/admin/teams/${teamId}/members/${memberId}/role`, { is_team_admin: isTeamAdmin }),
// User management // User management
createUser: (data: { createUser: (data: {
username: string; username: string;

View File

@ -90,7 +90,7 @@
-webkit-backdrop-filter: blur(24px) saturate(180%); -webkit-backdrop-filter: blur(24px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px; border-radius: 16px;
width: 1080px; width: 1280px;
max-width: 96vw; max-width: 96vw;
min-height: 70vh; min-height: 70vh;
max-height: 90vh; max-height: 90vh;

View File

@ -823,8 +823,22 @@ export function TeamsPage() {
<td>{m.email}</td> <td>{m.email}</td>
<td> <td>
{m.is_team_admin ? ( {m.is_team_admin ? (
<span className={styles.adminBadge}></span> <span className={styles.adminBadge} style={{ cursor: 'pointer' }} title="点击取消管理员" onClick={async () => {
) : '成员'} try {
await adminApi.setMemberRole(detailTeam!.id, m.id, false);
showToast('已取消管理员');
const { data: refreshed } = await adminApi.getTeamDetail(detailTeam!.id); setDetailTeam(refreshed);
} catch { showToast('操作失败'); }
}}></span>
) : (
<span style={{ cursor: 'pointer', color: 'var(--color-text-secondary)' }} title="点击设为管理员" onClick={async () => {
try {
await adminApi.setMemberRole(detailTeam!.id, m.id, true);
showToast('已设为管理员');
const { data: refreshed } = await adminApi.getTeamDetail(detailTeam!.id); setDetailTeam(refreshed);
} catch { showToast('操作失败'); }
}}></span>
)}
</td> </td>
<td> <td>
<span className={`${styles.statusBadge} ${m.is_active ? styles.active : styles.disabled}`}> <span className={`${styles.statusBadge} ${m.is_active ? styles.active : styles.disabled}`}>