Fix API compatibility and add user/role/permission and asset import/export

This commit is contained in:
2026-01-25 23:36:23 +08:00
commit 501d11e14e
371 changed files with 68853 additions and 0 deletions

221
backend/app/api/v1/roles.py Normal file
View File

@@ -0,0 +1,221 @@
"""
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")