"""User management router.""" from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel, EmailStr from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.dependencies import CurrentUser from app.models.user import User router = APIRouter() # ─── Pydantic schemas ──────────────────────────────────────────────────────── class UserResponse(BaseModel): id: int username: str email: str is_active: bool model_config = {"from_attributes": True} class UserSummary(BaseModel): id: int username: str model_config = {"from_attributes": True} class UserUpdate(BaseModel): email: EmailStr | None = None is_active: bool | None = None # ─── Endpoints ─────────────────────────────────────────────────────────────── @router.get("/me", response_model=UserResponse) async def get_current_user_profile( current_user: CurrentUser, ): """Get the currently authenticated user's profile.""" return current_user @router.get("/search", response_model=list[UserSummary]) async def search_users( current_user: CurrentUser, db: Annotated[AsyncSession, Depends(get_db)], q: str = "", limit: int = 20, ): """Search active users by username substring.""" cleaned_q = q.strip() result = await db.execute( select(User) .where( User.is_active == True, User.username.ilike(f"%{cleaned_q}%"), User.id != current_user.id, ) .limit(limit) ) return list(result.scalars().all()) @router.get("/by-username/{username}", response_model=UserSummary) async def get_user_by_username( username: str, current_user: CurrentUser, db: Annotated[AsyncSession, Depends(get_db)], ): """Get active user by exact username.""" result = await db.execute( select(User).where(User.username == username.strip(), User.is_active == True) ) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="User not found") return user @router.get("/{user_id}", response_model=UserResponse) async def get_user( user_id: int, current_user: CurrentUser, db: Annotated[AsyncSession, Depends(get_db)], ): """Get a user by ID. Only the owner can view their own profile.""" result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="User not found") # Only the owner may view their own profile. if user.id != current_user.id: raise HTTPException(status_code=403, detail="Not authorized to view this user") return user @router.patch("/{user_id}", response_model=UserResponse) async def update_user( user_id: int, user_update: UserUpdate, current_user: CurrentUser, db: Annotated[AsyncSession, Depends(get_db)], ): """Update a user. Only the owner can update their own account.""" result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="User not found") # Only the owner may update their own account. if user.id != current_user.id: raise HTTPException(status_code=403, detail="Not authorized to update this user") if user_update.email is not None: user.email = user_update.email if user_update.is_active is not None: user.is_active = user_update.is_active await db.commit() await db.refresh(user) return user @router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_user( user_id: int, current_user: CurrentUser, db: Annotated[AsyncSession, Depends(get_db)], ): """Delete a user. Only the owner can delete their own account.""" result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="User not found") # Only the owner may delete their own account. if user.id != current_user.id: raise HTTPException(status_code=403, detail="Not authorized to delete this user") await db.delete(user) await db.commit()