""" Role management API routes. """ from typing import List, Dict from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select, func, delete from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import get_db, get_current_user from app.core.response import success_response from app.models.user import Role, Permission, RolePermission, UserRole from app.schemas.user import RoleCreate, RoleUpdate router = APIRouter() def _permission_to_dict(permission: Permission) -> Dict: return { "id": permission.id, "permission_name": permission.permission_name, "permission_code": permission.permission_code, "module": permission.module, "module_name": permission.module, "resource": permission.resource, "action": permission.action, "description": permission.description, "created_at": permission.created_at, } def _role_to_dict(role: Role, permissions: List[Permission], user_count: int) -> Dict: return { "id": role.id, "role_name": role.role_name, "role_code": role.role_code, "description": role.description, "status": role.status, "sort_order": role.sort_order, "created_at": role.created_at, "permissions": [_permission_to_dict(p) for p in permissions], "user_count": user_count, } async def _ensure_permissions_exist(db: AsyncSession, permission_ids: List[int]) -> None: if not permission_ids: return result = await db.execute(select(Permission.id).where(Permission.id.in_(permission_ids))) existing_ids = {row[0] for row in result.all()} missing = set(permission_ids) - existing_ids if missing: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid permission IDs: {sorted(missing)}", ) async def _set_role_permissions( db: AsyncSession, role_id: int, permission_ids: List[int], operator_id: int, ) -> None: await db.execute(delete(RolePermission).where(RolePermission.role_id == role_id)) if permission_ids: for permission_id in permission_ids: db.add( RolePermission( role_id=role_id, permission_id=permission_id, created_by=operator_id, ) ) @router.get("/") async def list_roles( db: AsyncSession = Depends(get_db), current_user=Depends(get_current_user), ): result = await db.execute( select(Role).where(Role.deleted_at.is_(None)).order_by(Role.sort_order, Role.id) ) roles = list(result.scalars().all()) role_ids = [role.id for role in roles] permission_map: Dict[int, List[Permission]] = {role.id: [] for role in roles} user_count_map: Dict[int, int] = {role.id: 0 for role in roles} if role_ids: perm_result = await db.execute( select(RolePermission.role_id, Permission) .join(Permission, Permission.id == RolePermission.permission_id) .where(RolePermission.role_id.in_(role_ids)) ) for role_id, permission in perm_result.all(): permission_map.setdefault(role_id, []).append(permission) count_result = await db.execute( select(UserRole.role_id, func.count(UserRole.user_id)) .where(UserRole.role_id.in_(role_ids)) .group_by(UserRole.role_id) ) for role_id, count in count_result.all(): user_count_map[role_id] = count items = [ _role_to_dict(role, permission_map.get(role.id, []), user_count_map.get(role.id, 0)) for role in roles ] return success_response(data=items) @router.post("/", status_code=status.HTTP_201_CREATED) async def create_role( payload: RoleCreate, db: AsyncSession = Depends(get_db), current_user=Depends(get_current_user), ): existing = await db.execute(select(Role).where(Role.role_code == payload.role_code)) if existing.scalar_one_or_none(): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Role code already exists") existing_name = await db.execute(select(Role).where(Role.role_name == payload.role_name)) if existing_name.scalar_one_or_none(): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Role name already exists") await _ensure_permissions_exist(db, payload.permission_ids) role = Role( role_code=payload.role_code, role_name=payload.role_name, description=payload.description, status="active", created_by=current_user.id, ) db.add(role) await db.flush() await _set_role_permissions(db, role.id, payload.permission_ids, current_user.id) await db.commit() await db.refresh(role) perm_result = await db.execute( select(Permission) .join(RolePermission, RolePermission.permission_id == Permission.id) .where(RolePermission.role_id == role.id) ) permissions = list(perm_result.scalars().all()) return success_response(data=_role_to_dict(role, permissions, 0)) @router.put("/{role_id}") async def update_role( role_id: int, payload: RoleUpdate, db: AsyncSession = Depends(get_db), current_user=Depends(get_current_user), ): result = await db.execute(select(Role).where(Role.id == role_id).where(Role.deleted_at.is_(None))) role = result.scalar_one_or_none() if not role: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Role not found") update_data = payload.model_dump(exclude_unset=True) if "role_name" in update_data: role_name = update_data.pop("role_name") existing = await db.execute( select(Role).where(Role.role_name == role_name).where(Role.id != role_id) ) if existing.scalar_one_or_none(): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Role name already exists") role.role_name = role_name if "description" in update_data: role.description = update_data.pop("description") permission_ids = update_data.pop("permission_ids", None) if permission_ids is not None: await _ensure_permissions_exist(db, permission_ids) await _set_role_permissions(db, role.id, permission_ids, current_user.id) role.updated_by = current_user.id db.add(role) await db.commit() await db.refresh(role) perm_result = await db.execute( select(Permission) .join(RolePermission, RolePermission.permission_id == Permission.id) .where(RolePermission.role_id == role.id) ) permissions = list(perm_result.scalars().all()) count_result = await db.execute( select(func.count(UserRole.user_id)).where(UserRole.role_id == role.id) ) user_count = count_result.scalar() or 0 return success_response(data=_role_to_dict(role, permissions, user_count)) @router.delete("/{role_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_role( role_id: int, db: AsyncSession = Depends(get_db), current_user=Depends(get_current_user), ): result = await db.execute(select(Role).where(Role.id == role_id).where(Role.deleted_at.is_(None))) role = result.scalar_one_or_none() if not role: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Role not found") role.deleted_at = func.now() role.deleted_by = current_user.id role.status = "disabled" await db.commit() return success_response(message="Deleted")