feat(admin): Phase 3 平台团队+用户管理 — 列/启停/详情/强制改密 + 团队停用拦登录

后端:adminpanel 增 teams(列/详情/启停)+ users(列/启停/强制改密),AdminTeam/User 序列化器;
login 校验团队 status,停用团队成员拒登;停用用户清 token、禁停平台超管;全程 IsPlatformAdmin + 审计。
前端:adminApi teams/users 系列;Admin 团队页(筛选+搜索+详情弹窗+启停)、用户页(启停+改密弹窗,超管行禁操作)。
测试:adminpanel 19 + accounts 22 = 41 单测过;无头 e2e _admin-p3.mjs 7 断言过 + 0 console error
(UI 停用团队→成员登录 400→启用→200;UI 改密→新密码登录 200);tsc+build 绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
seaislee1209
2026-06-19 21:39:15 +08:00
co-authored by Claude Opus 4.8
parent 45a4ceae51
commit 6560633959
12 changed files with 818 additions and 4 deletions
+86
View File
@@ -85,3 +85,89 @@ class AdminInvitationApiTests(TestCase):
format="json",
)
self.assertEqual(reg.status_code, 400)
class AdminTeamUserApiTests(TestCase):
"""Phase 3:平台团队 + 用户管理(列/详情/启停/强制改密)+ 权限 + 团队停用拦登录。"""
def setUp(self):
from apps.billing.models import CreditAccount
self.admin = User.objects.create_user(username="padmin3", password="x", is_platform_admin=True)
self.owner = User.objects.create_user(username="t-owner", password="ownerpass1")
self.member = User.objects.create_user(username="t-member", password="memberpass1")
self.team = Team.objects.create(name="Acme", owner=self.owner)
TeamMember.objects.create(team=self.team, user=self.owner, role=TeamMember.Role.OWNER)
TeamMember.objects.create(team=self.team, user=self.member, role=TeamMember.Role.MEMBER)
CreditAccount.objects.create(team=self.team, balance="123.4500")
self.ac = APIClient()
self.ac.force_authenticate(self.admin)
self.normal = APIClient()
self.normal.force_authenticate(self.owner)
def _login(self, username, password):
return APIClient().post("/api/auth/login/", {"username": username, "password": password}, format="json")
def test_teams_list_permission_and_fields(self):
self.assertEqual(self.normal.get("/api/admin/teams/").status_code, 403)
r = self.ac.get("/api/admin/teams/")
self.assertEqual(r.status_code, 200)
row = next(t for t in r.data["results"] if t["name"] == "Acme")
self.assertEqual(row["member_count"], 2)
self.assertEqual(row["owner_username"], "t-owner")
self.assertEqual(row["balance"], "123.4500")
def test_team_detail_has_members(self):
r = self.ac.get(f"/api/admin/teams/{self.team.id}/")
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data["members"]), 2)
def test_team_toggle_blocks_then_restores_member_login(self):
self.assertEqual(self._login("t-member", "memberpass1").status_code, 200)
tr = self.ac.post(f"/api/admin/teams/{self.team.id}/toggle/")
self.assertEqual(tr.status_code, 200)
self.assertEqual(tr.data["status"], Team.Status.DISABLED)
self.assertEqual(self._login("t-member", "memberpass1").status_code, 400) # 停用后被拒
self.ac.post(f"/api/admin/teams/{self.team.id}/toggle/") # 恢复
self.assertEqual(self._login("t-member", "memberpass1").status_code, 200)
self.assertTrue(AdminAuditLog.objects.filter(action="team.toggle_status").exists())
def test_team_toggle_requires_admin(self):
self.assertEqual(self.normal.post(f"/api/admin/teams/{self.team.id}/toggle/").status_code, 403)
def test_users_list_with_teams(self):
self.assertEqual(self.normal.get("/api/admin/users/").status_code, 403)
r = self.ac.get("/api/admin/users/")
self.assertEqual(r.status_code, 200)
member_row = next(u for u in r.data["results"] if u["username"] == "t-member")
self.assertEqual(member_row["teams"][0]["team_name"], "Acme")
def test_user_toggle_blocks_login(self):
r = self.ac.post(f"/api/admin/users/{self.member.id}/toggle/")
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data["status"], User.Status.DISABLED)
self.assertEqual(self._login("t-member", "memberpass1").status_code, 400)
self.assertTrue(AdminAuditLog.objects.filter(action="user.toggle_status").exists())
def test_cannot_disable_platform_admin(self):
self.assertEqual(self.ac.post(f"/api/admin/users/{self.admin.id}/toggle/").status_code, 400)
def test_reset_password(self):
r = self.ac.post(f"/api/admin/users/{self.member.id}/reset-password/", {"password": "newpass789"}, format="json")
self.assertEqual(r.status_code, 204)
self.assertEqual(self._login("t-member", "newpass789").status_code, 200)
self.assertEqual(self._login("t-member", "memberpass1").status_code, 400) # 旧密码失效
self.assertTrue(AdminAuditLog.objects.filter(action="user.reset_password").exists())
def test_reset_password_too_short_rejected(self):
self.assertEqual(
self.ac.post(f"/api/admin/users/{self.member.id}/reset-password/", {"password": "short"}, format="json").status_code,
400,
)
def test_user_actions_require_admin(self):
self.assertEqual(self.normal.post(f"/api/admin/users/{self.member.id}/toggle/").status_code, 403)
self.assertEqual(
self.normal.post(f"/api/admin/users/{self.member.id}/reset-password/", {"password": "newpass789"}, format="json").status_code,
403,
)