# app/api/v1/admin/users.py """ User management endpoints for admin. """ import logging import math from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query from sqlalchemy.orm import Session, joinedload from app.api.deps import get_current_admin_api from app.core.database import get_db from app.services.admin_service import admin_service from middleware.auth import AuthManager from app.services.stats_service import stats_service from models.database.user import User from models.schema.auth import ( UserCreate, UserDetailResponse, UserListResponse, UserResponse, UserUpdate, ) router = APIRouter(prefix="/users") logger = logging.getLogger(__name__) @router.get("", response_model=UserListResponse) def get_all_users( page: int = Query(1, ge=1), per_page: int = Query(10, ge=1, le=100), search: str = Query("", description="Search by username or email"), role: str = Query("", description="Filter by role"), is_active: str = Query("", description="Filter by active status"), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): """Get paginated list of all users (Admin only).""" query = db.query(User) # Apply filters if search: search_term = f"%{search.lower()}%" query = query.filter( (User.username.ilike(search_term)) | (User.email.ilike(search_term)) | (User.first_name.ilike(search_term)) | (User.last_name.ilike(search_term)) ) if role: query = query.filter(User.role == role) if is_active: query = query.filter(User.is_active == (is_active.lower() == "true")) # Get total count total = query.count() pages = math.ceil(total / per_page) if total > 0 else 1 # Apply pagination skip = (page - 1) * per_page users = query.order_by(User.created_at.desc()).offset(skip).limit(per_page).all() return UserListResponse( items=[UserResponse.model_validate(user) for user in users], total=total, page=page, per_page=per_page, pages=pages, ) @router.post("", response_model=UserDetailResponse) def create_user( user_data: UserCreate = Body(...), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): """Create a new user (Admin only).""" # Check if email exists if db.query(User).filter(User.email == user_data.email).first(): raise HTTPException(status_code=400, detail="Email already registered") # Check if username exists if db.query(User).filter(User.username == user_data.username).first(): raise HTTPException(status_code=400, detail="Username already taken") # Create user auth_manager = AuthManager() user = User( email=user_data.email, username=user_data.username, hashed_password=auth_manager.hash_password(user_data.password), first_name=user_data.first_name, last_name=user_data.last_name, role=user_data.role, is_active=True, ) db.add(user) db.commit() db.refresh(user) logger.info(f"Admin {current_admin.username} created user {user.username}") return UserDetailResponse( id=user.id, email=user.email, username=user.username, role=user.role, is_active=user.is_active, last_login=user.last_login, created_at=user.created_at, updated_at=user.updated_at, first_name=user.first_name, last_name=user.last_name, full_name=user.full_name, is_email_verified=user.is_email_verified, owned_companies_count=len(user.owned_companies), vendor_memberships_count=len(user.vendor_memberships), ) @router.get("/stats") def get_user_statistics( db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): """Get user statistics for admin dashboard (Admin only).""" return stats_service.get_user_statistics(db) @router.get("/search") def search_users( q: str = Query(..., min_length=2, description="Search query (username or email)"), limit: int = Query(10, ge=1, le=50), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): """ Search users by username or email (Admin only). Used for autocomplete in ownership transfer. """ search_term = f"%{q.lower()}%" users = ( db.query(User) .filter((User.username.ilike(search_term)) | (User.email.ilike(search_term))) .limit(limit) .all() ) return { "users": [ { "id": user.id, "username": user.username, "email": user.email, "is_active": user.is_active, } for user in users ] } @router.get("/{user_id}", response_model=UserDetailResponse) def get_user_details( user_id: int = Path(..., description="User ID"), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): """Get detailed user information (Admin only).""" user = ( db.query(User) .options(joinedload(User.owned_companies), joinedload(User.vendor_memberships)) .filter(User.id == user_id) .first() ) if not user: raise HTTPException(status_code=404, detail="User not found") return UserDetailResponse( id=user.id, email=user.email, username=user.username, role=user.role, is_active=user.is_active, last_login=user.last_login, created_at=user.created_at, updated_at=user.updated_at, first_name=user.first_name, last_name=user.last_name, full_name=user.full_name, is_email_verified=user.is_email_verified, owned_companies_count=len(user.owned_companies), vendor_memberships_count=len(user.vendor_memberships), ) @router.put("/{user_id}", response_model=UserDetailResponse) def update_user( user_id: int = Path(..., description="User ID"), user_update: UserUpdate = Body(...), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): """Update user information (Admin only).""" user = db.query(User).filter(User.id == user_id).first() if not user: raise HTTPException(status_code=404, detail="User not found") # Prevent changing own admin status if user.id == current_admin.id and user_update.role and user_update.role != "admin": raise HTTPException( status_code=400, detail="Cannot change your own admin role" ) # Check email uniqueness if changing if user_update.email and user_update.email != user.email: if db.query(User).filter(User.email == user_update.email).first(): raise HTTPException(status_code=400, detail="Email already registered") # Check username uniqueness if changing if user_update.username and user_update.username != user.username: if db.query(User).filter(User.username == user_update.username).first(): raise HTTPException(status_code=400, detail="Username already taken") # Update fields update_data = user_update.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(user, field, value) db.commit() db.refresh(user) logger.info(f"Admin {current_admin.username} updated user {user.username}") return UserDetailResponse( id=user.id, email=user.email, username=user.username, role=user.role, is_active=user.is_active, last_login=user.last_login, created_at=user.created_at, updated_at=user.updated_at, first_name=user.first_name, last_name=user.last_name, full_name=user.full_name, is_email_verified=user.is_email_verified, owned_companies_count=len(user.owned_companies), vendor_memberships_count=len(user.vendor_memberships), ) @router.put("/{user_id}/status") def toggle_user_status( user_id: int = Path(..., description="User ID"), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): """Toggle user active status (Admin only).""" user = db.query(User).filter(User.id == user_id).first() if not user: raise HTTPException(status_code=404, detail="User not found") # Prevent deactivating yourself if user.id == current_admin.id: raise HTTPException(status_code=400, detail="Cannot deactivate yourself") user.is_active = not user.is_active db.commit() action = "activated" if user.is_active else "deactivated" logger.info(f"Admin {current_admin.username} {action} user {user.username}") return {"message": f"User {action} successfully", "is_active": user.is_active} @router.delete("/{user_id}") def delete_user( user_id: int = Path(..., description="User ID"), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): """Delete a user (Admin only).""" user = ( db.query(User) .options(joinedload(User.owned_companies)) .filter(User.id == user_id) .first() ) if not user: raise HTTPException(status_code=404, detail="User not found") # Prevent deleting yourself if user.id == current_admin.id: raise HTTPException(status_code=400, detail="Cannot delete yourself") # Prevent deleting users who own companies if user.owned_companies: raise HTTPException( status_code=400, detail=f"Cannot delete user who owns {len(user.owned_companies)} company(ies). Transfer ownership first.", ) username = user.username db.delete(user) db.commit() logger.info(f"Admin {current_admin.username} deleted user {username}") return {"message": "User deleted successfully"}