"""Admin router for user management. Super Admin only.""" from typing import List, Optional import uuid from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel from sqlalchemy import func from sqlalchemy.ext.asyncio import AsyncSession from sqlmodel import select, update from models.sql.presentation import PresentationModel from models.sql.user import UserModel from services.database import get_async_session from utils.auth_dependencies import require_super_admin USERS_ROUTER = APIRouter(prefix="/users", tags=["Admin - Users"]) VALID_ROLES = {"super_admin", "client_admin", "user"} class TransferOwnershipRequest(BaseModel): new_owner_id: uuid.UUID client_id: Optional[uuid.UUID] = None @USERS_ROUTER.get("", response_model=List[dict]) async def list_users( _: UserModel = Depends(require_super_admin), is_active: Optional[bool] = Query(None), role: Optional[str] = Query(None), offset: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=200), session: AsyncSession = Depends(get_async_session), ): query = select(UserModel) if is_active is not None: query = query.where(UserModel.is_active == is_active) if role: query = query.where(UserModel.role == role) query = query.order_by(UserModel.created_at.desc()).offset(offset).limit(limit) result = await session.execute(query) users = result.scalars().all() return [ { "id": str(u.id), "email": u.email, "display_name": u.display_name, "role": u.role, "is_active": u.is_active, "last_login_at": u.last_login_at.isoformat() if u.last_login_at else None, "created_at": u.created_at.isoformat() if u.created_at else None, } for u in users ] @USERS_ROUTER.get("/{user_id}", response_model=dict) async def get_user( user_id: uuid.UUID, _: UserModel = Depends(require_super_admin), session: AsyncSession = Depends(get_async_session), ): user = await session.get(UserModel, user_id) if not user: raise HTTPException(status_code=404, detail="User not found") return { "id": str(user.id), "email": user.email, "display_name": user.display_name, "role": user.role, "is_active": user.is_active, "last_login_at": user.last_login_at.isoformat() if user.last_login_at else None, "created_at": user.created_at.isoformat() if user.created_at else None, } @USERS_ROUTER.put("/{user_id}/role") async def update_user_role( user_id: uuid.UUID, role: str = Query(...), admin: UserModel = Depends(require_super_admin), session: AsyncSession = Depends(get_async_session), ): if role not in VALID_ROLES: raise HTTPException( status_code=400, detail=f"Invalid role. Must be one of: {', '.join(VALID_ROLES)}", ) user = await session.get(UserModel, user_id) if not user: raise HTTPException(status_code=404, detail="User not found") if user.id == admin.id: raise HTTPException(status_code=400, detail="Cannot change your own role") user.role = role session.add(user) await session.commit() return {"message": "Role updated", "user_id": str(user.id), "role": role} @USERS_ROUTER.post("/{user_id}/transfer-ownership") async def transfer_ownership( user_id: uuid.UUID, body: TransferOwnershipRequest, _: UserModel = Depends(require_super_admin), session: AsyncSession = Depends(get_async_session), ): """Transfer all presentations from one user to another. Used for GDPR compliance before deactivating a user. """ if user_id == body.new_owner_id: raise HTTPException( status_code=400, detail="Cannot transfer ownership to the same user" ) # Validate source user exists source_user = await session.get(UserModel, user_id) if not source_user: raise HTTPException(status_code=404, detail="Source user not found") # Validate target user exists target_user = await session.get(UserModel, body.new_owner_id) if not target_user: raise HTTPException(status_code=404, detail="Target user not found") # Build the update statement for non-deleted presentations owned by the source user stmt = ( update(PresentationModel) .where(PresentationModel.owner_id == user_id) .where(PresentationModel.deleted_at.is_(None)) ) if body.client_id is not None: stmt = stmt.where(PresentationModel.client_id == body.client_id) stmt = stmt.values(owner_id=body.new_owner_id) result = await session.execute(stmt) await session.commit() transferred_count = result.rowcount return { "message": f"Transferred {transferred_count} presentations", "from_user_id": str(user_id), "to_user_id": str(body.new_owner_id), } @USERS_ROUTER.delete("/{user_id}") async def deactivate_user( user_id: uuid.UUID, admin: UserModel = Depends(require_super_admin), session: AsyncSession = Depends(get_async_session), ): user = await session.get(UserModel, user_id) if not user: raise HTTPException(status_code=404, detail="User not found") if user.id == admin.id: raise HTTPException(status_code=400, detail="Cannot deactivate yourself") # Check how many active presentations this user still owns count_query = ( select(func.count()) .select_from(PresentationModel) .where(PresentationModel.owner_id == user_id) .where(PresentationModel.deleted_at.is_(None)) ) count_result = await session.execute(count_query) presentation_count = count_result.scalar_one() user.is_active = False session.add(user) await session.commit() response = {"message": "User deactivated", "user_id": str(user.id)} if presentation_count > 0: response["warning"] = ( f"User still has {presentation_count} active presentations. " "Consider transferring ownership first." ) return response