后端:新建 apps/adminpanel(跨团队后台容器,/api/admin/);GET/POST invitations(列跨团队码+筛选分页 / 平台发 create_team 开团队码,写审计)+ revoke(写审计),全程 IsPlatformAdmin。 前端:adminApi 分层;Admin 邀请码页(类型筛选段控 + 表格 + 发码弹窗 + 复制邀请链接 + 撤销)接入 AdminApp toast; Invitation 类型补 kind/team_name;admin-page.css 补段控/表格/弹窗(仅 token)。 测试:adminpanel 9 + accounts 22 = 31 单测过;无头 e2e _admin-p2.mjs 6 断言过 + 0 console error + 端到端凭 UI 发的码注册开团队 201;tsc+build 绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
71 lines
2.9 KiB
Python
71 lines
2.9 KiB
Python
"""平台超管后台 · 跨团队端点。所有视图统一挂 IsPlatformAdmin,非超管一律 403,写操作记审计。"""
|
|
|
|
from django.db.models import Q
|
|
from rest_framework import status
|
|
from rest_framework.decorators import api_view, permission_classes
|
|
from rest_framework.response import Response
|
|
|
|
from apps.accounts.audit import log_admin_action
|
|
from apps.accounts.models import Invitation
|
|
from apps.accounts.permissions import IsPlatformAdmin
|
|
from apps.accounts.serializers import InvitationSerializer
|
|
from apps.common.pagination import DefaultPagination
|
|
|
|
|
|
@api_view(["GET", "POST"])
|
|
@permission_classes([IsPlatformAdmin])
|
|
def admin_invitations(request):
|
|
"""GET 列所有邀请码(跨团队,可按 kind/status/search 过滤,分页);
|
|
POST 平台超管发「开团队码」(create_team,不绑团队,新用户凭码开新团队当 owner)。"""
|
|
if request.method == "GET":
|
|
qs = Invitation.objects.select_related("team", "used_by").order_by("-created_at")
|
|
kind = request.query_params.get("kind")
|
|
if kind in {Invitation.Kind.JOIN_TEAM, Invitation.Kind.CREATE_TEAM}:
|
|
qs = qs.filter(kind=kind)
|
|
st = request.query_params.get("status")
|
|
if st in dict(Invitation.Status.choices):
|
|
qs = qs.filter(status=st)
|
|
search = (request.query_params.get("search") or "").strip()
|
|
if search:
|
|
qs = qs.filter(Q(code__icontains=search) | Q(team__name__icontains=search))
|
|
paginator = DefaultPagination()
|
|
page = paginator.paginate_queryset(qs, request)
|
|
return paginator.get_paginated_response(InvitationSerializer(page, many=True).data)
|
|
|
|
email = str(request.data.get("email") or "").strip()
|
|
invite = Invitation.objects.create(
|
|
kind=Invitation.Kind.CREATE_TEAM,
|
|
team=None,
|
|
email=email,
|
|
created_by=request.user,
|
|
)
|
|
log_admin_action(
|
|
request,
|
|
"invite.issue_create_team",
|
|
target_type="invitation",
|
|
target_id=invite.id,
|
|
target_name=invite.code,
|
|
after={"kind": invite.kind, "email": email},
|
|
)
|
|
return Response(InvitationSerializer(invite).data, status=status.HTTP_201_CREATED)
|
|
|
|
|
|
@api_view(["POST"])
|
|
@permission_classes([IsPlatformAdmin])
|
|
def admin_revoke_invitation(request, invite_id):
|
|
"""撤销一个待用邀请码(任意团队)。已用/已撤销/已过期则原样返回(幂等)。"""
|
|
invite = Invitation.objects.select_related("team").filter(id=invite_id).first()
|
|
if invite is None:
|
|
return Response({"detail": "not found"}, status=status.HTTP_404_NOT_FOUND)
|
|
if invite.status == Invitation.Status.PENDING:
|
|
invite.status = Invitation.Status.REVOKED
|
|
invite.save(update_fields=["status", "updated_at"])
|
|
log_admin_action(
|
|
request,
|
|
"invite.revoke",
|
|
target_type="invitation",
|
|
target_id=invite.id,
|
|
target_name=invite.code,
|
|
)
|
|
return Response(InvitationSerializer(invite).data)
|