from __future__ import annotations import json from fastapi import APIRouter, Body, Depends, Request from typing import Any from app.api.v1.endpoints.platform import ok, fail from app.db.platform_store import get_platform_store from app.core.auth import is_admin, require_admin, require_tenant_admin, get_current_user, user_tenant_ids router = APIRouter(prefix="/tenants", tags=["tenant"]) def _actor(request: Request) -> str | None: auth = request.headers.get("Authorization", "") token = auth.replace("Bearer ", "").strip() return token or None @router.get("") def list_tenants(current_user: dict = Depends(require_admin)) -> dict[str, Any]: return ok(get_platform_store().tenants()) @router.get("/invitations") def my_invitations(current_user: dict = Depends(get_current_user)) -> dict[str, Any]: return ok(get_platform_store().tenant_invitations(str(current_user.get("id") or ""))) @router.get("/mine") def my_tenants(current_user: dict = Depends(get_current_user)) -> dict[str, Any]: return ok(get_platform_store().user_tenants(str(current_user.get("id") or ""), include_all=is_admin(current_user))) @router.post("") def create_tenant(payload: dict[str, Any] = Body(...), request: Request = None, current_user: dict = Depends(require_admin)) -> dict[str, Any]: store = get_platform_store() payload = {**payload, "owner_user_id": payload.get("owner_user_id") or current_user.get("id")} try: tenant = store.create_tenant(payload) except KeyError as e: raise fail(400, f"missing field: {e}") except ValueError as exc: raise fail(400, str(exc)) store.record_audit( action="tenant.create", actor_id=current_user.get("id"), target_type="tenant", target_id=tenant["id"], tenant_id=tenant["id"], detail=f"name={tenant.get('name')}", ) return ok(tenant) @router.get("/{tenant_id}") def get_tenant(tenant_id: str, current_user: dict = Depends(require_admin)) -> dict[str, Any]: try: return ok(get_platform_store().tenant(tenant_id)) except KeyError: raise fail(404, "tenant not found") @router.put("/{tenant_id}") def update_tenant(tenant_id: str, payload: dict[str, Any] = Body(...), request: Request = None, current_user: dict = Depends(require_admin)) -> dict[str, Any]: store = get_platform_store() try: tenant = store.update_tenant(tenant_id, payload) except KeyError: raise fail(404, "tenant not found") store.record_audit( action="tenant.update", actor_id=current_user.get("id"), target_type="tenant", target_id=tenant_id, tenant_id=tenant_id, detail=f"fields={','.join(payload.keys())}", ) return ok(tenant) @router.put("/{tenant_id}/quota") def set_quota(tenant_id: str, payload: dict[str, Any] = Body(...), request: Request = None, current_user: dict = Depends(require_admin)) -> dict[str, Any]: store = get_platform_store() try: tenant = store.set_tenant_quota(tenant_id, payload) except KeyError: raise fail(404, "tenant not found") store.record_audit( action="tenant.quota.set", actor_id=current_user.get("id"), target_type="tenant", target_id=tenant_id, tenant_id=tenant_id, ) return ok(tenant) @router.put("/{tenant_id}/retention-policy") def set_retention(tenant_id: str, payload: dict[str, Any] = Body(...), request: Request = None, current_user: dict = Depends(require_admin)) -> dict[str, Any]: store = get_platform_store() try: tenant = store.set_tenant_retention(tenant_id, payload.get("retention_policy_id")) except KeyError: raise fail(404, "tenant not found") store.record_audit( action="tenant.retention.set", actor_id=current_user.get("id"), target_type="tenant", target_id=tenant_id, tenant_id=tenant_id, ) return ok(tenant) @router.delete("/{tenant_id}") def delete_tenant(tenant_id: str, request: Request = None, current_user: dict = Depends(require_admin)) -> dict[str, Any]: store = get_platform_store() try: tenant = store.delete_tenant(tenant_id, str(current_user.get("id") or "system")) except KeyError: raise fail(404, "tenant not found") except ValueError as exc: raise fail(400, str(exc)) store.record_audit( action="tenant.delete", actor_id=current_user.get("id"), target_type="tenant", target_id=tenant_id, tenant_id=tenant_id, detail=f"name={tenant.get('name')}", ) return ok(tenant) @router.post("/{tenant_id}/restore") def restore_tenant(tenant_id: str, request: Request = None, current_user: dict = Depends(require_admin)) -> dict[str, Any]: store = get_platform_store() try: tenant = store.restore_tenant(tenant_id, str(current_user.get("id") or "system")) except KeyError: raise fail(404, "tenant not found") except ValueError as exc: raise fail(400, str(exc)) store.record_audit( action="tenant.restore", actor_id=current_user.get("id"), target_type="tenant", target_id=tenant_id, tenant_id=tenant_id, detail=f"name={tenant.get('name')}", ) return ok(tenant) @router.get("/{tenant_id}/quota/usage") def quota_usage(tenant_id: str, current_user: dict = Depends(require_tenant_admin)) -> dict[str, Any]: return ok(get_platform_store().tenant_quota_usage(tenant_id)) @router.post("/{tenant_id}/quota/request") def request_quota_change(tenant_id: str, payload: dict[str, Any] = Body(...), current_user: dict = Depends(get_current_user)) -> dict[str, Any]: if not is_admin(current_user) and tenant_id not in user_tenant_ids(current_user): raise fail(403, "tenant access denied") try: get_platform_store().assert_active_tenant(tenant_id) except ValueError as exc: raise fail(400, str(exc)) quota = payload.get("quota") if not isinstance(quota, dict): raise fail(400, "quota must be an object") instance = get_platform_store().create_approval_instance({ "resource_type": "tenant", "resource_id": tenant_id, "applicant_id": current_user.get("id"), "action": "tenant.quota.update", "tenant_id": tenant_id, "reason": json.dumps({"quota": quota}, ensure_ascii=False), }) get_platform_store().record_audit( action="tenant.quota.request", actor_id=current_user.get("id"), target_type="tenant", target_id=tenant_id, tenant_id=tenant_id, ) return ok({"approval_required": True, "approval_id": instance["id"], "approval": instance}) @router.get("/{tenant_id}/members") def list_members(tenant_id: str, current_user: dict = Depends(require_tenant_admin)) -> dict[str, Any]: try: return ok(get_platform_store().tenant_members(tenant_id)) except KeyError: raise fail(404, "tenant not found") @router.post("/{tenant_id}/members") def add_member( tenant_id: str, payload: dict[str, Any] = Body(...), request: Request = None, current_user: dict = Depends(require_tenant_admin), ) -> dict[str, Any]: if not payload.get("user_id"): raise fail(400, "user_id 必填") if not is_admin(current_user) and payload.get("role") == "owner": raise fail(403, "only platform administrator can grant owner role") try: member = get_platform_store().add_tenant_member( tenant_id, str(payload["user_id"]), str(payload.get("role") or "member"), current_user.get("id"), ) except KeyError: raise fail(404, "tenant or user not found") except ValueError as exc: raise fail(400, str(exc)) get_platform_store().record_audit( action="tenant.member.add", actor_id=current_user.get("id"), target_type="tenant_member", target_id=f"{tenant_id}:{payload['user_id']}", tenant_id=tenant_id, ) return ok(member) @router.post("/{tenant_id}/members/invite") def invite_member( tenant_id: str, payload: dict[str, Any] = Body(...), current_user: dict = Depends(require_tenant_admin), ) -> dict[str, Any]: user_id = str(payload.get("user_id") or "") if not user_id: raise fail(400, "user_id 必填") if payload.get("role") == "owner": raise fail(403, "tenant invitations cannot grant owner role") try: member = get_platform_store().invite_tenant_member( tenant_id, user_id, str(payload.get("role") or "member"), current_user.get("id"), payload.get("expires_at"), ) except KeyError: raise fail(404, "tenant or active user not found") except ValueError as exc: raise fail(400, str(exc)) get_platform_store().record_audit( action="tenant.member.invite", actor_id=current_user.get("id"), target_type="tenant_member", target_id=f"{tenant_id}:{user_id}", tenant_id=tenant_id, detail=f"role={member.get('role')};expires_at={member.get('expires_at')}", ) return ok(member) @router.put("/{tenant_id}/members/{user_id}") def update_member( tenant_id: str, user_id: str, payload: dict[str, Any] = Body(...), current_user: dict = Depends(require_tenant_admin), ) -> dict[str, Any]: try: if not is_admin(current_user) and payload.get("role") == "owner": raise fail(403, "only platform administrator can grant owner role") member = get_platform_store().update_tenant_member(tenant_id, user_id, payload) get_platform_store().record_audit( action="tenant.member.update", actor_id=current_user.get("id"), target_type="tenant_member", target_id=f"{tenant_id}:{user_id}", tenant_id=tenant_id, detail=f"fields={','.join(payload.keys())}", ) return ok(member) except KeyError: raise fail(404, "tenant member not found") except ValueError as exc: raise fail(400, str(exc)) @router.delete("/{tenant_id}/members/{user_id}") def remove_member(tenant_id: str, user_id: str, current_user: dict = Depends(require_tenant_admin)) -> dict[str, Any]: try: get_platform_store().remove_tenant_member(tenant_id, user_id) except KeyError: raise fail(404, "tenant member not found") except ValueError as exc: raise fail(400, str(exc)) get_platform_store().record_audit( action="tenant.member.remove", actor_id=current_user.get("id"), target_type="tenant_member", target_id=f"{tenant_id}:{user_id}", tenant_id=tenant_id, ) return ok({"tenant_id": tenant_id, "user_id": user_id, "removed": True}) @router.post("/{tenant_id}/members/{user_id}/accept") def accept_invitation( tenant_id: str, user_id: str, current_user: dict = Depends(get_current_user), ) -> dict[str, Any]: if not is_admin(current_user) and str(current_user.get("id") or "") != user_id: raise fail(403, "only the invited user can accept this invitation") try: member = get_platform_store().accept_tenant_invitation(tenant_id, user_id) except KeyError: raise fail(404, "tenant invitation not found") except ValueError as exc: raise fail(409, str(exc)) get_platform_store().record_audit( action="tenant.member.accept", actor_id=current_user.get("id"), target_type="tenant_member", target_id=f"{tenant_id}:{user_id}", tenant_id=tenant_id, ) return ok(member)