Files
leocrm/app/api/v1/users.py
T

156 lines
4.8 KiB
Python

"""User API endpoints: me, list, create, update, soft-delete."""
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import get_db
from app.core.deps import get_current_admin_user, get_current_user
from app.models.user import User, UserRole
from app.schemas.user import (
UserCreateRequest,
UserListResponse,
UserOut,
UserUpdate,
)
from app.services import user_service
router = APIRouter(prefix="/users", tags=["users"])
@router.get(
"/me",
response_model=UserOut,
summary="Get the currently authenticated user",
)
async def get_me(
current_user: User = Depends(get_current_user),
) -> UserOut:
"""Returns the user from the JWT. Used by the frontend for auth checks and profile display."""
return UserOut.model_validate(current_user)
@router.patch(
"/me",
response_model=UserOut,
summary="Update own profile (name, avatar, notification prefs)",
)
async def update_me(
payload: UserUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> UserOut:
"""A user can update their own profile, but not their role."""
updated = await user_service.update_user_profile(
db, current_user, payload, is_admin=False
)
return UserOut.model_validate(updated)
@router.get(
"/",
response_model=UserListResponse,
summary="List all users in the current org (admin only)",
)
async def list_users(
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=100),
current_user: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_db),
) -> UserListResponse:
"""Paginated list of active users. Restricted to admin role."""
skip = (page - 1) * page_size
users = await user_service.list_users(
db, org_id=current_user.org_id, skip=skip, limit=page_size
)
total = await user_service.count_users_in_org(db, current_user.org_id)
return UserListResponse(
items=[UserOut.model_validate(u) for u in users],
total=total,
page=page,
page_size=page_size,
)
@router.post(
"/",
response_model=UserOut,
status_code=status.HTTP_201_CREATED,
summary="Create a new user in the current org (admin only)",
)
async def create_user(
payload: UserCreateRequest,
current_user: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_db),
) -> UserOut:
"""Admin creates a new user in the same org."""
try:
new_user = await user_service.create_user_as_admin(
db, payload, org_id=current_user.org_id
)
except user_service.EmailAlreadyTaken as e:
raise HTTPException(status_code=409, detail=str(e)) from e
return UserOut.model_validate(new_user)
@router.patch(
"/{user_id}",
response_model=UserOut,
summary="Update a user (admin or self for non-role fields)",
)
async def update_user(
user_id: int,
payload: UserUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> UserOut:
"""Admin can update any user (including role); non-admin can update only their own profile fields."""
is_admin = (
current_user.role == UserRole.admin
if hasattr(current_user.role, "__eq__") and not isinstance(current_user.role, str)
else str(current_user.role) == UserRole.admin.value
)
if not is_admin and current_user.id != user_id:
raise HTTPException(
status_code=403,
detail="You can only update your own profile",
)
target = await user_service.get_user_by_id(db, user_id)
if target is None:
raise HTTPException(status_code=404, detail="User not found")
if target.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="User not found")
updated = await user_service.update_user_profile(
db, target, payload, is_admin=is_admin
)
return UserOut.model_validate(updated)
@router.delete(
"/{user_id}",
status_code=status.HTTP_204_NO_CONTENT,
response_class=Response,
summary="Soft-delete a user (admin only)",
)
async def delete_user(
user_id: int,
current_user: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_db),
) -> Response:
"""Sets deleted_at timestamp. The record stays in the DB for audit purposes."""
if current_user.id == user_id:
raise HTTPException(
status_code=400, detail="You cannot delete your own account"
)
target = await user_service.get_user_by_id(db, user_id)
if target is None or target.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="User not found")
await user_service.soft_delete_user(db, target)
return Response(status_code=status.HTTP_204_NO_CONTENT)