feat: v0.12.5 admin 保护 + 管理员角色切换 + 团队详情加宽
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m37s
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:
parent
969283690f
commit
a026c04310
@ -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'),
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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}`}>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user