""" User management API routes. """ from typing import Optional, List, Dict from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy import select, func, or_, delete from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import get_db, get_current_user from app.core.response import success_response, paginated_response from app.core.security import get_password_hash from app.models.user import User, Role, UserRole from app.schemas.user import UserCreate, UserUpdate, ResetPasswordRequest from app.services.auth_service import auth_service router = APIRouter() def _status_to_is_active(status_value: Optional[str]) -> Optional[bool]: if not status_value: return None if status_value == "active": return True if status_value in {"disabled", "locked"}: return False return None def _role_to_dict(role: Role) -> 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, } def _user_to_dict(user: User, roles: List[Role]) -> Dict: return { "id": user.id, "username": user.username, "real_name": user.full_name or user.username, "email": user.email, "phone": user.phone, "avatar_url": user.avatar_url, "status": "active" if user.is_active else "disabled", "is_admin": user.is_superuser, "last_login_at": user.last_login_at, "created_at": user.created_at, "roles": [_role_to_dict(role) for role in roles], } async def _ensure_roles_exist(db: AsyncSession, role_ids: List[int]) -> None: if not role_ids: return result = await db.execute( select(Role.id).where(Role.id.in_(role_ids)).where(Role.deleted_at.is_(None)) ) existing_ids = {row[0] for row in result.all()} missing = set(role_ids) - existing_ids if missing: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid role IDs: {sorted(missing)}", ) async def _set_user_roles( db: AsyncSession, user_id: int, role_ids: List[int], operator_id: Optional[int] = None, ) -> None: await db.execute(delete(UserRole).where(UserRole.user_id == user_id)) if role_ids: for role_id in role_ids: db.add(UserRole(user_id=user_id, role_id=role_id, created_by=operator_id)) @router.get("/") async def list_users( page: int = Query(1, ge=1, description="Page number"), page_size: int = Query(20, ge=1, le=100, description="Page size"), keyword: Optional[str] = Query(None, description="Search keyword"), status_value: Optional[str] = Query(None, alias="status", description="Status"), db: AsyncSession = Depends(get_db), current_user=Depends(get_current_user), ): is_active = _status_to_is_active(status_value) query = select(User) count_query = select(func.count(User.id)) conditions = [] if keyword: like_value = f"%{keyword}%" conditions.append( or_( User.username.ilike(like_value), User.full_name.ilike(like_value), User.phone.ilike(like_value), User.email.ilike(like_value), ) ) if is_active is not None: conditions.append(User.is_active == is_active) if conditions: query = query.where(*conditions) count_query = count_query.where(*conditions) query = query.order_by(User.id.desc()) total_result = await db.execute(count_query) total = total_result.scalar() or 0 offset = (page - 1) * page_size result = await db.execute(query.offset(offset).limit(page_size)) users = list(result.scalars().all()) role_map: Dict[int, List[Role]] = {user.id: [] for user in users} if users: user_ids = [user.id for user in users] role_result = await db.execute( select(UserRole.user_id, Role) .join(Role, Role.id == UserRole.role_id) .where(UserRole.user_id.in_(user_ids)) .where(Role.deleted_at.is_(None)) ) for user_id, role in role_result.all(): role_map.setdefault(user_id, []).append(role) items = [_user_to_dict(user, role_map.get(user.id, [])) for user in users] return paginated_response(items, total, page, page_size) @router.post("/", status_code=status.HTTP_201_CREATED) async def create_user( payload: UserCreate, db: AsyncSession = Depends(get_db), current_user=Depends(get_current_user), ): existing = await db.execute(select(User).where(User.username == payload.username)) if existing.scalar_one_or_none(): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Username already exists") email_value = payload.email if not email_value: email_value = f"{payload.username}@local.invalid" email_check = await db.execute(select(User).where(User.email == email_value)) if email_check.scalar_one_or_none(): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email already exists") await _ensure_roles_exist(db, payload.role_ids) user = User( username=payload.username, email=email_value, hashed_password=get_password_hash(payload.password), full_name=payload.real_name, phone=payload.phone, is_active=True, is_superuser=False, ) db.add(user) await db.flush() await _set_user_roles(db, user.id, payload.role_ids, current_user.id) await db.commit() await db.refresh(user) roles_result = await db.execute( select(Role) .join(UserRole, UserRole.role_id == Role.id) .where(UserRole.user_id == user.id) .where(Role.deleted_at.is_(None)) ) roles = list(roles_result.scalars().all()) return success_response(data=_user_to_dict(user, roles)) @router.put("/{user_id}") async def update_user( user_id: int, payload: UserUpdate, db: AsyncSession = Depends(get_db), current_user=Depends(get_current_user), ): result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") update_data = payload.model_dump(exclude_unset=True) if "real_name" in update_data: user.full_name = update_data.pop("real_name") if "status" in update_data: status_value = update_data.pop("status") is_active = _status_to_is_active(status_value) if is_active is not None: user.is_active = is_active if "email" in update_data: email_value = update_data.pop("email") if email_value: email_check = await db.execute( select(User).where(User.email == email_value).where(User.id != user_id) ) if email_check.scalar_one_or_none(): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email already exists") user.email = email_value if "phone" in update_data: user.phone = update_data.pop("phone") role_ids = update_data.pop("role_ids", None) if role_ids is not None: await _ensure_roles_exist(db, role_ids) await _set_user_roles(db, user.id, role_ids, current_user.id) db.add(user) await db.commit() await db.refresh(user) roles_result = await db.execute( select(Role) .join(UserRole, UserRole.role_id == Role.id) .where(UserRole.user_id == user.id) .where(Role.deleted_at.is_(None)) ) roles = list(roles_result.scalars().all()) return success_response(data=_user_to_dict(user, roles)) @router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_user( user_id: int, db: AsyncSession = Depends(get_db), current_user=Depends(get_current_user), ): result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") await db.execute(delete(UserRole).where(UserRole.user_id == user_id)) await db.delete(user) await db.commit() return success_response(message="Deleted") @router.post("/{user_id}/reset-password") async def reset_password( user_id: int, payload: ResetPasswordRequest, db: AsyncSession = Depends(get_db), current_user=Depends(get_current_user), ): success = await auth_service.reset_password(db=db, user_id=user_id, new_password=payload.new_password) if not success: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") return success_response(message="Password reset")