feat(auth): add DB-backed login, token management, and idempotent auth migration
This commit is contained in:
+70
-20
@@ -1,26 +1,40 @@
|
||||
"""User management API routes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NoReturn
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||
|
||||
from app.core.auth import require_admin
|
||||
from app.core.auth import AuthContext, require_admin
|
||||
from app.core.config import Settings, get_settings
|
||||
from app.domain.models import User
|
||||
from app.repositories.factory import get_portfolio_repository_from_settings
|
||||
from app.schemas.user import CreateUserRequest, UpdateUserRequest, UserResponse
|
||||
from app.services.user_service import UserService, get_user_service
|
||||
from app.services.user_service import UserService
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["users"])
|
||||
|
||||
|
||||
def _get_service(settings: Settings) -> tuple[UserService, object]:
|
||||
repository = get_portfolio_repository_from_settings(settings)
|
||||
return UserService(repository), repository
|
||||
|
||||
|
||||
def _close_repository(repository: object) -> None:
|
||||
if callable(getattr(type(repository), "close", None)):
|
||||
repository.close()
|
||||
|
||||
|
||||
def _to_user_response(user: User) -> UserResponse:
|
||||
return UserResponse(
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
permission=user.permission,
|
||||
is_active=user.is_active,
|
||||
created_at=user.created_at,
|
||||
updated_at=user.updated_at,
|
||||
last_login_at=user.last_login_at,
|
||||
)
|
||||
|
||||
|
||||
@@ -33,43 +47,79 @@ def _raise_user_error(exc: ValueError) -> NoReturn:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=detail) from exc
|
||||
|
||||
|
||||
@router.get("", response_model=list[UserResponse], dependencies=[Depends(require_admin)])
|
||||
def list_users(service: UserService = Depends(get_user_service)) -> list[UserResponse]:
|
||||
return [_to_user_response(user) for user in service.list()]
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserResponse, dependencies=[Depends(require_admin)])
|
||||
def get_user(user_id: str, service: UserService = Depends(get_user_service)) -> UserResponse:
|
||||
@router.get("", response_model=list[UserResponse])
|
||||
def list_users(
|
||||
_auth: AuthContext = Depends(require_admin),
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> list[UserResponse]:
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
return _to_user_response(service.get(user_id))
|
||||
return [_to_user_response(user) for user in service.list()]
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserResponse)
|
||||
def get_user(
|
||||
user_id: str,
|
||||
_auth: AuthContext = Depends(require_admin),
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> UserResponse:
|
||||
try:
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
return _to_user_response(service.get(user_id))
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
except ValueError as exc:
|
||||
_raise_user_error(exc)
|
||||
|
||||
|
||||
@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_admin)])
|
||||
def create_user(payload: CreateUserRequest, service: UserService = Depends(get_user_service)) -> UserResponse:
|
||||
@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_user(
|
||||
payload: CreateUserRequest,
|
||||
_auth: AuthContext = Depends(require_admin),
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> UserResponse:
|
||||
try:
|
||||
return _to_user_response(service.create(payload))
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
return _to_user_response(service.create(payload))
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
except ValueError as exc:
|
||||
_raise_user_error(exc)
|
||||
|
||||
|
||||
@router.patch("/{user_id}", response_model=UserResponse, dependencies=[Depends(require_admin)])
|
||||
@router.patch("/{user_id}", response_model=UserResponse)
|
||||
def update_user(
|
||||
user_id: str,
|
||||
payload: UpdateUserRequest,
|
||||
service: UserService = Depends(get_user_service),
|
||||
_auth: AuthContext = Depends(require_admin),
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> UserResponse:
|
||||
try:
|
||||
return _to_user_response(service.update(user_id, payload))
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
return _to_user_response(service.update(user_id, payload))
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
except ValueError as exc:
|
||||
_raise_user_error(exc)
|
||||
|
||||
|
||||
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_admin)])
|
||||
def delete_user(user_id: str, service: UserService = Depends(get_user_service)) -> Response:
|
||||
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_user(
|
||||
user_id: str,
|
||||
_auth: AuthContext = Depends(require_admin),
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> Response:
|
||||
try:
|
||||
service.delete(user_id)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
service.delete(user_id)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
except ValueError as exc:
|
||||
_raise_user_error(exc)
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
Reference in New Issue
Block a user