from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db from app.crud import audit as audit_crud from app.crud import role as role_crud from app.deps import require_permission from app.models.user import User from app.schemas.rbac import PermissionRead, RoleCreate, RoleRead, RoleUpdate router = APIRouter() @router.get("", response_model=list[RoleRead]) async def list_roles( db: AsyncSession = Depends(get_db), _: User = Depends(require_permission("roles:read")), ): return await role_crud.list_roles(db) @router.post("", response_model=RoleRead, status_code=status.HTTP_201_CREATED) async def create_role( payload: RoleCreate, db: AsyncSession = Depends(get_db), actor: User = Depends(require_permission("roles:write")), ): if await role_crud.get_by_name(db, payload.name): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Role already exists" ) role = await role_crud.create( db, name=payload.name, description=payload.description, permission_ids=payload.permission_ids, ) await audit_crud.log( db, actor=actor, action="create", resource_type="Role", resource_id=role.id, description=audit_crud.describe_create("Role", role.id, role.name), ) return role @router.get("/permissions", response_model=list[PermissionRead]) async def list_permissions( db: AsyncSession = Depends(get_db), _: User = Depends(require_permission("roles:read")), ): return await role_crud.list_permissions(db) @router.get("/{role_id}", response_model=RoleRead) async def get_role( role_id: int, db: AsyncSession = Depends(get_db), _: User = Depends(require_permission("roles:read")), ): role = await role_crud.get(db, role_id) if role is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Role not found" ) return role @router.patch("/{role_id}", response_model=RoleRead) async def update_role( role_id: int, payload: RoleUpdate, db: AsyncSession = Depends(get_db), actor: User = Depends(require_permission("roles:write")), ): role = await role_crud.get(db, role_id) if role is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Role not found" ) changes = payload.model_dump(exclude_unset=True) if "name" in changes: existing = await role_crud.get_by_name(db, changes["name"]) if existing is not None and existing.id != role.id: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Role already exists" ) role = await role_crud.update(db, role, changes) await audit_crud.log( db, actor=actor, action="update", resource_type="Role", resource_id=role.id, description=audit_crud.describe_update("Role", role.id, changes), ) return role @router.delete("/{role_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_role( role_id: int, db: AsyncSession = Depends(get_db), actor: User = Depends(require_permission("roles:write")), ): role = await role_crud.get(db, role_id) if role is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Role not found" ) name = role.name await role_crud.delete(db, role) await audit_crud.log( db, actor=actor, action="delete", resource_type="Role", resource_id=role_id, description=audit_crud.describe_delete("Role", role_id, name), )