feat(admin): Phase 2 邀请码发放管理 — apps/adminpanel 跨团队端点 + Admin 邀请码页
后端:新建 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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
3f8547564c
commit
45a4ceae51
@@ -0,0 +1,70 @@
|
||||
"""平台超管后台 · 跨团队端点。所有视图统一挂 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)
|
||||
Reference in New Issue
Block a user