feat(auth): add DB-backed login, token management, and idempotent auth migration
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"""Authentication API routes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NoReturn
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||
|
||||
from app.core.auth import AuthContext, require_read
|
||||
from app.domain.models import AuthToken, User
|
||||
from app.schemas.auth import AuthTokenResponse, CreateClientTokenRequest, IssuedClientTokenResponse, LoginRequest, TokenResponse
|
||||
from app.schemas.user import UserResponse
|
||||
from app.services.auth_service import AuthService, IssuedClientToken, LoginResult, get_auth_service
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
def _to_auth_token_response(token: AuthToken) -> AuthTokenResponse:
|
||||
return AuthTokenResponse(
|
||||
id=token.id,
|
||||
user_id=token.user_id,
|
||||
name=token.name,
|
||||
token_prefix=token.token_prefix,
|
||||
permission=token.permission,
|
||||
token_type=token.token_type,
|
||||
expires_at=token.expires_at,
|
||||
last_used_at=token.last_used_at,
|
||||
revoked_at=token.revoked_at,
|
||||
created_at=token.created_at,
|
||||
)
|
||||
|
||||
|
||||
def _to_login_response(result: LoginResult) -> TokenResponse:
|
||||
return TokenResponse(
|
||||
access_token=result.token,
|
||||
expires_at=result.auth_token.expires_at,
|
||||
permission=result.auth_token.permission,
|
||||
user=_to_user_response(result.user),
|
||||
)
|
||||
|
||||
|
||||
def _to_issued_token_response(result: IssuedClientToken) -> IssuedClientTokenResponse:
|
||||
return IssuedClientTokenResponse(
|
||||
token=result.token,
|
||||
auth_token=_to_auth_token_response(result.auth_token),
|
||||
)
|
||||
|
||||
|
||||
def _raise_auth_error(exc: ValueError) -> NoReturn:
|
||||
detail = str(exc)
|
||||
if "잘못된 사용자명 또는 비밀번호" in detail:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=detail) from exc
|
||||
if "초과" in detail:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=detail) from exc
|
||||
if "찾을 수 없습니다" in detail:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=detail) from exc
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=detail) from exc
|
||||
|
||||
|
||||
@router.post("/login", response_model=TokenResponse)
|
||||
def login(payload: LoginRequest, service: AuthService = Depends(get_auth_service)) -> TokenResponse:
|
||||
try:
|
||||
return _to_login_response(service.login(payload))
|
||||
except ValueError as exc:
|
||||
_raise_auth_error(exc)
|
||||
|
||||
|
||||
@router.get("/tokens", response_model=list[AuthTokenResponse], dependencies=[Depends(require_read)])
|
||||
def list_tokens(
|
||||
auth_context: AuthContext = Depends(require_read),
|
||||
service: AuthService = Depends(get_auth_service),
|
||||
) -> list[AuthTokenResponse]:
|
||||
try:
|
||||
return [_to_auth_token_response(token) for token in service.list_tokens(auth_context)]
|
||||
except ValueError as exc:
|
||||
_raise_auth_error(exc)
|
||||
|
||||
|
||||
@router.post("/tokens", response_model=IssuedClientTokenResponse, status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_read)])
|
||||
def create_token(
|
||||
payload: CreateClientTokenRequest,
|
||||
auth_context: AuthContext = Depends(require_read),
|
||||
service: AuthService = Depends(get_auth_service),
|
||||
) -> IssuedClientTokenResponse:
|
||||
try:
|
||||
return _to_issued_token_response(service.issue_client_token(auth_context, payload))
|
||||
except ValueError as exc:
|
||||
_raise_auth_error(exc)
|
||||
|
||||
|
||||
@router.delete("/tokens/{token_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_read)])
|
||||
def revoke_token(
|
||||
token_id: str,
|
||||
auth_context: AuthContext = Depends(require_read),
|
||||
service: AuthService = Depends(get_auth_service),
|
||||
) -> Response:
|
||||
try:
|
||||
service.revoke_token(auth_context, token_id)
|
||||
except ValueError as exc:
|
||||
_raise_auth_error(exc)
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
Reference in New Issue
Block a user