from __future__ import annotations from fastapi import APIRouter, Body, Depends, Request from typing import Any from app.api.v1.endpoints.platform import ok, fail from app.core.auth import filter_accessible_resource_ids, get_current_user, has_resource_access, is_admin from app.db.platform_store import get_platform_store router = APIRouter(prefix="/projects", tags=["project"]) def _actor(request: Request) -> str | None: auth = request.headers.get("Authorization", "") token = auth.replace("Bearer ", "").strip() return token or None def _require_no_pending_approval(resource_type: str, resource_id: str) -> None: """第 4 周:写操作审批拦截——存在待审批实例时拒绝执行。""" store = get_platform_store() pending = [ i for i in store.approval_instances(status="pending") if i["resource_type"] == resource_type and i["resource_id"] == resource_id ] if pending: raise fail(409, "存在待审批的变更,请先完成审批") def _require_approval_or_admin( resource_type: str, resource_id: str, current_user: dict[str, Any], action_desc: str = "", ) -> dict[str, Any] | None: """高风险操作审批旁路:admin 直接放行,普通用户创建审批实例(code=202)。""" if is_admin(current_user): return None store = get_platform_store() instance = store.create_approval_instance({ "resource_type": resource_type, "resource_id": resource_id, "applicant_id": current_user.get("id"), "template_id": None, }) return { "code": 202, "message": f"操作已提交审批,等待管理员批准:{action_desc}", "data": {"approval_required": True, "approval_id": instance["id"]}, } @router.get("") def list_projects( tenant_id: str = "default", status: str | None = None, keyword: str | None = None, current_user: dict = Depends(get_current_user), ) -> dict[str, Any]: store = get_platform_store() projects = store.projects(tenant_id=tenant_id, status=status, keyword=keyword) # #1 ACL 过滤:admin 直接放行,普通用户只能看到自己被授权的项目 accessible_ids = set( filter_accessible_resource_ids("project", [p["id"] for p in projects], current_user) ) filtered = [p for p in projects if p["id"] in accessible_ids] return ok(filtered) @router.post("") def create_project(payload: dict[str, Any] = Body(...), request: Request = None) -> dict[str, Any]: store = get_platform_store() proj = store.create_project(payload) store.record_audit( action="project.create", actor_id=_actor(request) if request else None, target_type="project", target_id=proj["id"], tenant_id=proj.get("tenant_id"), detail=f"name={proj.get('name')}", ) return ok(proj) @router.get("/{project_id}") def get_project(project_id: str, current_user: dict = Depends(get_current_user)) -> dict[str, Any]: # #2 访问控制:普通用户无 read 权限则拒绝 if not has_resource_access("project", project_id, current_user, "read"): raise fail(403, "no permission to access this project") try: return ok(get_platform_store().project(project_id)) except KeyError: raise fail(404, "project not found") @router.put("/{project_id}") def update_project( project_id: str, payload: dict[str, Any] = Body(...), request: Request = None, current_user: dict = Depends(get_current_user), ) -> dict[str, Any]: if not has_resource_access("project", project_id, current_user, "write"): raise fail(403, "no permission to update this project") store = get_platform_store() try: proj = store.update_project(project_id, payload) except KeyError: raise fail(404, "project not found") store.record_audit( action="project.update", actor_id=_actor(request) if request else None, target_type="project", target_id=project_id, tenant_id=proj.get("tenant_id"), detail=f"fields={','.join(payload.keys())}", ) return ok(proj) @router.post("/{project_id}/archive") def archive_project( project_id: str, request: Request = None, current_user: dict = Depends(get_current_user), ) -> dict[str, Any]: _require_no_pending_approval("project", project_id) pending = _require_approval_or_admin("project", project_id, current_user, f"归档项目 {project_id}") if pending: return pending store = get_platform_store() try: proj = store.archive_project(project_id) except KeyError: raise fail(404, "project not found") store.record_audit( action="project.archive", actor_id=_actor(request) if request else None, target_type="project", target_id=project_id, tenant_id=proj.get("tenant_id"), ) return ok(proj) @router.delete("/{project_id}") def delete_project( project_id: str, request: Request = None, current_user: dict = Depends(get_current_user), ) -> dict[str, Any]: _require_no_pending_approval("project", project_id) pending = _require_approval_or_admin("project", project_id, current_user, f"删除项目 {project_id}") if pending: return pending store = get_platform_store() store.delete_project(project_id) store.record_audit( action="project.delete", actor_id=_actor(request) if request else None, target_type="project", target_id=project_id, ) return ok(None) @router.get("/{project_id}/members") def list_members(project_id: str, current_user: dict = Depends(get_current_user)) -> dict[str, Any]: if not has_resource_access("project", project_id, current_user, "read"): raise fail(403, "no permission to access this project") try: return ok(get_platform_store().project_members(project_id)) except KeyError: raise fail(404, "project not found") @router.post("/{project_id}/members") def add_member( project_id: str, payload: dict[str, Any] = Body(...), request: Request = None, current_user: dict = Depends(get_current_user), ) -> dict[str, Any]: if not has_resource_access("project", project_id, current_user, "write"): raise fail(403, "no permission to manage members of this project") store = get_platform_store() try: member = store.add_project_member(project_id, payload) except KeyError: raise fail(404, "project not found") store.record_audit( action="project.member.add", actor_id=_actor(request) if request else None, target_type="project.member", target_id=project_id, detail=f"user_id={payload.get('user_id')},role={payload.get('role')}", ) return ok(member) @router.put("/{project_id}/members/{user_id}") def update_member( project_id: str, user_id: str, payload: dict[str, Any] = Body(...), request: Request = None, current_user: dict = Depends(get_current_user), ) -> dict[str, Any]: if not has_resource_access("project", project_id, current_user, "write"): raise fail(403, "no permission to manage members of this project") store = get_platform_store() try: member = store.update_project_member_role(project_id, user_id, payload) except KeyError: raise fail(404, "project or member not found") store.record_audit( action="project.member.update", actor_id=_actor(request) if request else None, target_type="project.member", target_id=project_id, detail=f"user_id={user_id},role={payload.get('role')}", ) return ok(member) @router.delete("/{project_id}/members/{user_id}") def remove_member( project_id: str, user_id: str, request: Request = None, current_user: dict = Depends(get_current_user), ) -> dict[str, Any]: if not has_resource_access("project", project_id, current_user, "write"): raise fail(403, "no permission to manage members of this project") store = get_platform_store() store.remove_project_member(project_id, user_id) store.record_audit( action="project.member.remove", actor_id=_actor(request) if request else None, target_type="project.member", target_id=project_id, detail=f"user_id={user_id}", ) return ok(None)