"""User management endpoints (admin).""" from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_user, require_permission from app.core.security import get_password_hash from app.db.session import get_async_session from app.models import User from app.schemas.user import UserCreateRequest, UserUpdateRequest, UserResponse, UserListResponse router = APIRouter(prefix="/users", tags=["users"]) @router.get("", response_model=UserListResponse) async def list_users( page: int = Query(1, ge=1), size: int = Query(20, ge=1, le=100), current_user: User = Depends(require_permission("users:read")), session: AsyncSession = Depends(get_async_session), ): """List all users within the current account.""" account_id = current_user.account_id # Count total count_q = select(func.count(User.id)).where(User.account_id == account_id) total = (await session.execute(count_q)).scalar() or 0 # Fetch page q = ( select(User) .where(User.account_id == account_id) .order_by(User.created_at.desc()) .offset((page - 1) * size) .limit(size) ) result = await session.execute(q) users = result.scalars().all() return UserListResponse( items=[UserResponse.model_validate(u) for u in users], total=total, page=page, size=size, ) @router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED) async def create_user( body: UserCreateRequest, current_user: User = Depends(require_permission("users:write")), session: AsyncSession = Depends(get_async_session), ): """Create a new user within the current account.""" account_id = current_user.account_id # Check email uniqueness existing = await session.execute(select(User).where(User.email == body.email)) if existing.scalars().first(): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="A user with this email already exists", ) # If role_id is provided, verify it belongs to same account (optional) # (We'll skip role validation for now; can be added later) user = User( account_id=account_id, email=body.email, full_name=body.full_name, password_hash=get_password_hash(body.password), role_id=body.role_id, ) session.add(user) await session.commit() await session.refresh(user) return UserResponse.model_validate(user) @router.get("/{user_id}", response_model=UserResponse) async def get_user( user_id: str, current_user: User = Depends(require_permission("users:read")), session: AsyncSession = Depends(get_async_session), ): """Get a specific user by ID.""" account_id = current_user.account_id result = await session.execute( select(User).where(User.id == user_id, User.account_id == account_id) ) user = result.scalars().first() if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found", ) return UserResponse.model_validate(user) @router.put("/{user_id}", response_model=UserResponse) async def update_user( user_id: str, body: UserUpdateRequest, current_user: User = Depends(require_permission("users:write")), session: AsyncSession = Depends(get_async_session), ): """Update a user's role, active status, or name.""" account_id = current_user.account_id result = await session.execute( select(User).where(User.id == user_id, User.account_id == account_id) ) user = result.scalars().first() if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found", ) if body.full_name is not None: user.full_name = body.full_name if body.role_id is not None: user.role_id = body.role_id if body.is_active is not None: user.is_active = body.is_active await session.commit() await session.refresh(user) return UserResponse.model_validate(user) @router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_user( user_id: str, current_user: User = Depends(require_permission("users:delete")), session: AsyncSession = Depends(get_async_session), ): """Soft-delete (deactivate) a user. Does not actually delete.""" account_id = current_user.account_id result = await session.execute( select(User).where(User.id == user_id, User.account_id == account_id) ) user = result.scalars().first() if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found", ) # Prevent self-deactivation if user.id == current_user.id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="You cannot deactivate your own account", ) user.is_active = False await session.commit() return None