Source code for app.domain.users.controllers.users

"""User Account CRUD Endpoints.

Defines all user account CRUD operations, requiring superuser privileges.
"""

from typing import Annotated
from uuid import UUID

from advanced_alchemy.exceptions import (
    DuplicateKeyError,
    NotFoundError,
)
from fastapi import (
    APIRouter,
    Depends,
    Query,
    Response,
    status,
)
from sqlalchemy.orm import undefer

from app.db.models.user import User as UserModel
from app.domain.users import urls
from app.domain.users.auth import Authenticate
from app.domain.users.deps import (
    RoleServiceDep,
    UserServiceDep,
)
from app.domain.users.filters import UserFilters
from app.domain.users.schemas import (
    User,
    UserAuth,
    UserCreate,
    UserUpdate,
)
from app.domain.users.utils import check_critical_action_forbidden
from app.lib.exceptions import (
    ConflictException,
    UserNotFound,
)
from app.lib.invalidate_cache import invalidate_user_cache
from app.lib.json_response import MsgSpecJSONResponse

users_router = APIRouter(
    tags=["User Accounts"],
)


[docs] @users_router.post( path=urls.USER_CREATE, operation_id="CreateUser", name="users:create", summary="Create a new user.", description="A user who can login and use the system.", ) async def create_user( _: Annotated[UserAuth, Depends(Authenticate.superuser_required())], users_service: UserServiceDep, roles_service: RoleServiceDep, data: UserCreate, ) -> MsgSpecJSONResponse: """Create a new user in the system. Returns: ~app.domain.users.schemas.User: The newly created user data. Raises: ConflictException: If a user with the provided email already exists. """ role_obj = await roles_service.get_default_role( default_role_slug=users_service.default_role, ) try: db_obj = await users_service.create(data=data.model_dump(exclude_unset=True) | {"role_id": role_obj.id}) user = users_service.to_schema(db_obj, schema_type=User) return MsgSpecJSONResponse(content=user, status_code=status.HTTP_201_CREATED) except DuplicateKeyError as exc: msg = f"A user with the email '{data.email}' is already registered in the system" raise ConflictException(message=msg) from exc
[docs] @users_router.get( path=urls.USER_DETAIL, operation_id="GetUser", name="users:get", summary="Retrieve the details of a user.", ) async def get_user( _: Annotated[UserAuth, Depends(Authenticate.superuser_required())], users_service: UserServiceDep, user_id: UUID, ) -> MsgSpecJSONResponse: """Retrieve a user by ID. Returns: ~app.domain.users.schemas.User: The detailed user data. Raises: UserNotFound: If the user with the given ID is not found. """ try: db_obj = await users_service.get(user_id) user = users_service.to_schema(db_obj, schema_type=User) return MsgSpecJSONResponse(content=user) except NotFoundError as exc: raise UserNotFound from exc
[docs] @users_router.get( path=urls.USER_LIST, operation_id="ListUsers", name="users:list", summary="List of users.", ) async def get_list_users( _: Annotated[UserAuth, Depends(Authenticate.superuser_required())], users_service: UserServiceDep, params: Annotated[UserFilters, Query()], ) -> MsgSpecJSONResponse: """Retrieve a list of users. Returns: OffsetPagination[~app.domain.users.schemas.User]: Paginated list of users data. """ users = await users_service.get_users_paginated_dto(params) return MsgSpecJSONResponse(content=users)
[docs] @users_router.patch( path=urls.USER_UPDATE, operation_id="UpdateUser", name="users:update", summary="Update user.", ) async def update_user( super_user: Annotated[UserAuth, Depends(Authenticate.superuser_required())], users_service: UserServiceDep, data: UserUpdate, user_id: UUID, ) -> MsgSpecJSONResponse: """Update user details by ID. This action also invalidates the user's authentication cache in Redis. Returns: ~app.domain.users.schemas.User: The updated user data. Raises: UserNotFound: If the user is not found. ConflictException: If the new email provided is already in use by another user. """ try: user_obj = await users_service.get( user_id, load=[undefer(UserModel.password)], ) check_critical_action_forbidden( target_user=user_obj, calling_superuser_id=super_user.id, ) db_obj = await users_service.update(data=data, item_id=user_id) await invalidate_user_cache( user_id=db_obj.id, ) user = users_service.to_schema(db_obj, schema_type=User) return MsgSpecJSONResponse(content=user) except (NotFoundError, DuplicateKeyError) as exc: if isinstance(exc, NotFoundError): raise UserNotFound from exc msg = f"A user with the email '{data.email}' is already registered in the system" raise ConflictException(message=msg) from exc
[docs] @users_router.delete( path=urls.USER_DELETE, operation_id="DeleteUser", name="users:delete", summary="Delete user.", ) async def delete_user( super_user: Annotated[UserAuth, Depends(Authenticate.superuser_required())], users_service: UserServiceDep, user_id: UUID, ) -> Response: """Delete a user from the system. This action also invalidates the user's authentication cache in Redis. Returns: Response: HTTP 204 No Content on successful deletion. Raises: UserNotFound: If the user is not found. """ try: user_obj = await users_service.get(user_id) check_critical_action_forbidden( target_user=user_obj, calling_superuser_id=super_user.id, ) _ = await users_service.delete(item_id=user_id) await invalidate_user_cache( user_id=user_id, ) return Response(status_code=status.HTTP_204_NO_CONTENT) except NotFoundError as exc: raise UserNotFound from exc