"""admin RBAC 角色管理(仅 super_admin):列角色 / 权限目录 / 增删改角色。均写审计。 角色 = 一组「可见页面」(见 app/admin/permissions.py)。角色登录后台后左侧只展示其 pages 对应导航项。 只有 super_admin(内建全权角色)能进本组端点(dependencies=require_role() 无参 = 仅 super_admin)。 """ from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy import func, select from app.admin.audit import write_audit from app.admin.deps import AdminDb, CurrentAdmin, get_client_ip, require_role from app.admin.permissions import ( PERMISSION_CATALOG, SUPER_ADMIN_ROLE, effective_pages, sanitize_pages, ) from app.admin.repositories import admin_role as role_repo from app.admin.schemas.role import ( PermissionGroup, RoleCreateRequest, RoleOut, RoleUpdateRequest, ) from app.models.admin import AdminUser from app.models.admin_role import AdminRole router = APIRouter( prefix="/admin/api/roles", tags=["admin-roles"], dependencies=[Depends(require_role())], # 无参 = 仅 super_admin ) def _usage_counts(db: AdminDb) -> dict[str, int]: rows = db.execute( select(AdminUser.role, func.count(AdminUser.id)).group_by(AdminUser.role) ).all() return {r: c for r, c in rows} def _to_out(role: AdminRole, usage: dict[str, int]) -> RoleOut: return RoleOut( id=role.id, name=role.name, label=role.label or role.name, pages=effective_pages(role.name, role.pages), is_builtin=role.is_builtin, in_use=usage.get(role.name, 0), ) @router.get("", response_model=list[RoleOut], summary="角色列表(含可见页 + 使用数)") def list_roles(db: AdminDb) -> list[RoleOut]: usage = _usage_counts(db) return [_to_out(r, usage) for r in role_repo.list_roles(db)] @router.get("/catalog", response_model=list[PermissionGroup], summary="页面权限目录(分组)") def get_catalog() -> list[PermissionGroup]: return [PermissionGroup(**g) for g in PERMISSION_CATALOG] @router.post("", response_model=RoleOut, summary="新增角色(带审计)") def create_role( body: RoleCreateRequest, request: Request, admin: CurrentAdmin, db: AdminDb ) -> RoleOut: name = body.name.strip() if not name: raise HTTPException(status_code=400, detail="角色名称不能为空") if name == SUPER_ADMIN_ROLE: raise HTTPException(status_code=400, detail="super_admin 为内建角色,不能新建") if role_repo.get_role(db, name) is not None: raise HTTPException(status_code=409, detail="角色名称已存在") # 自定义角色:name(key)= label = 输入名称 role = role_repo.create_role(db, name=name, label=name, pages=body.pages) write_audit( db, admin, action="role.create", target_type="role", target_id=str(role.id), detail={"name": name, "pages": role.pages}, ip=get_client_ip(request), commit=True, ) return _to_out(role, _usage_counts(db)) @router.patch("/{role_id}", response_model=RoleOut, summary="改角色(展示名/可见页,带审计)") def update_role( role_id: int, body: RoleUpdateRequest, request: Request, admin: CurrentAdmin, db: AdminDb ) -> RoleOut: role = db.get(AdminRole, role_id) if role is None: raise HTTPException(status_code=404, detail="角色不存在") if role.is_builtin: raise HTTPException(status_code=400, detail="内建角色「管理员」不可编辑") new_label = body.label.strip() if body.label is not None else None changes: dict = {} if new_label and new_label != role.label: changes["label"] = {"before": role.label, "after": new_label} if body.pages is not None: changes["pages"] = {"before": role.pages, "after": sanitize_pages(body.pages)} if not changes: raise HTTPException(status_code=400, detail="无任何变更字段") # 只改展示名 + 可见页;key(name)不可变,故无需级联 admin_user.role role_repo.update_role(db, role, label=new_label, pages=body.pages) write_audit( db, admin, action="role.update", target_type="role", target_id=str(role_id), detail=changes, ip=get_client_ip(request), commit=True, ) return _to_out(role, _usage_counts(db)) @router.delete("/{role_id}", summary="删角色(带审计;内建/在用不可删)") def delete_role(role_id: int, request: Request, admin: CurrentAdmin, db: AdminDb) -> dict: role = db.get(AdminRole, role_id) if role is None: raise HTTPException(status_code=404, detail="角色不存在") if role.is_builtin: raise HTTPException(status_code=400, detail="内建角色不可删除") used = _usage_counts(db).get(role.name, 0) if used > 0: raise HTTPException(status_code=400, detail=f"该角色仍有 {used} 名管理员在用,请先改派再删") name = role.name role_repo.delete_role(db, role) write_audit( db, admin, action="role.delete", target_type="role", target_id=str(role_id), detail={"name": name}, ip=get_client_ip(request), commit=True, ) return {"deleted": True}