Coverage for src / mysingle / auth / router / auth.py: 0%
74 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-02 00:58 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-02 00:58 +0900
1"""Health check utilities and endpoints."""
3from typing import Annotated
5from beanie import PydanticObjectId
6from fastapi import (
7 APIRouter,
8 Cookie,
9 Depends,
10 Header,
11 HTTPException,
12 Request,
13 Response,
14 status,
15)
16from fastapi.security import OAuth2PasswordRequestForm
18from ...core.config import settings
19from ...core.logging import get_structured_logger
20from ..authenticate import authenticator
21from ..deps import get_current_user, get_current_user_optional, verified_only
22from ..exceptions import AuthenticationFailed
23from ..schemas.auth import LoginResponse, UserInfo, VerifyTokenResponse
24from ..user_manager import UserManager
26logger = get_structured_logger(__name__)
27access_token_expire_minutes = settings.ACCESS_TOKEN_EXPIRE_MINUTES
28user_manager = UserManager()
29authenticator = authenticator
32def create_auth_router() -> APIRouter:
33 router = APIRouter()
35 @router.post(
36 "/login",
37 response_model=LoginResponse,
38 status_code=status.HTTP_200_OK, # 202 -> 200 변경
39 )
40 async def login(
41 response: Response, # Response 객체를 직접 받도록 수정
42 form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
43 ) -> LoginResponse | None:
44 user = await user_manager.authenticate(
45 username=form_data.username, password=form_data.password
46 )
48 if not user:
49 raise AuthenticationFailed("Invalid credentials")
51 # authenticator.login을 호출하여 토큰 생성
52 token_data = authenticator.login(user=user, response=response)
54 if settings.TOKEN_TRANSPORT_TYPE in ["bearer", "hybrid"]:
55 # Bearer 또는 Hybrid 방식: 응답에 토큰 포함
56 return LoginResponse(
57 access_token=token_data.access_token if token_data else None,
58 refresh_token=token_data.refresh_token if token_data else None,
59 token_type="bearer",
60 user_info=UserInfo(**user.model_dump(by_alias=True)),
61 )
62 else:
63 # Cookie 방식: 토큰은 쿠키에만 설정, 응답에는 사용자 정보만
64 return None
66 @router.post("/logout", status_code=status.HTTP_204_NO_CONTENT)
67 async def logout(
68 request: Request,
69 response: Response,
70 ) -> None:
71 """
72 로그아웃 엔드포인트.
74 쿠키에서 토큰을 삭제하고 로그아웃 처리를 합니다.
75 """
76 # 현재 사용자 가져오기 (선택적)
77 user = get_current_user_optional(request)
79 # authenticator를 사용하여 쿠키 삭제
80 authenticator.logout(response)
82 # 사용자가 인증된 경우에만 후처리 로직 실행
83 if user:
84 try:
85 current_user = await user_manager.get(PydanticObjectId(user.id))
86 if current_user:
87 await user_manager.on_after_logout(current_user, request)
88 except Exception as e:
89 logger.warning(f"Failed to execute logout callback: {e}")
91 # HTTP 204는 응답 본문이 없어야 하므로 None 반환
92 return None
94 @router.post("/refresh", response_model=LoginResponse)
95 async def refresh_token(
96 response: Response, # Response 객체 추가
97 refresh_token_header: str | None = Header(None, alias="X-Refresh-Token"),
98 refresh_token_cookie: str | None = Cookie(None, alias="refresh_token"),
99 ) -> LoginResponse | None:
100 """JWT 토큰 갱신 엔드포인트"""
102 # 토큰 전송 방식에 따라 refresh token 소스 결정
103 if settings.TOKEN_TRANSPORT_TYPE == "bearer":
104 refresh_token = refresh_token_header
105 elif settings.TOKEN_TRANSPORT_TYPE == "cookie":
106 refresh_token = refresh_token_cookie
107 else: # hybrid
108 refresh_token = refresh_token_header or refresh_token_cookie
110 if not refresh_token:
111 raise AuthenticationFailed("Refresh token not provided")
113 try:
114 # 토큰 전송 방식에 맞게 새 토큰 생성
115 from typing import Literal
117 transport_type: Literal["cookie", "header", "bearer", "hybrid"]
118 if settings.TOKEN_TRANSPORT_TYPE == "bearer":
119 transport_type = "bearer"
120 elif settings.TOKEN_TRANSPORT_TYPE == "cookie":
121 transport_type = "cookie"
122 else: # hybrid
123 transport_type = "hybrid"
125 token_data = authenticator.refresh_token(
126 refresh_token=refresh_token,
127 response=response,
128 transport_type=transport_type,
129 )
130 except HTTPException:
131 raise AuthenticationFailed("Invalid refresh token")
133 # 사용자 정보 조회
134 try:
135 payload = authenticator.validate_token(refresh_token)
136 user_id = payload.get("sub")
137 user = await user_manager.get(PydanticObjectId(user_id))
138 if not user:
139 raise AuthenticationFailed("User not found")
140 except Exception:
141 raise AuthenticationFailed("Failed to retrieve user information")
143 # 토큰 전송 방식에 따른 응답 생성
144 if settings.TOKEN_TRANSPORT_TYPE in ["bearer", "hybrid"] and token_data:
145 return LoginResponse(
146 access_token=token_data.access_token,
147 refresh_token=token_data.refresh_token,
148 token_type=token_data.token_type,
149 user_info=UserInfo(**user.model_dump(by_alias=True)),
150 )
151 else:
152 # Cookie 방식
153 return None
155 @router.get("/token/verify")
156 @verified_only
157 async def verify_token(
158 request: Request,
159 ) -> VerifyTokenResponse:
160 """토큰 검증 및 사용자 정보 반환 (디버깅용)"""
161 current_user = get_current_user(request)
162 return VerifyTokenResponse(
163 valid=True,
164 user_id=str(current_user.id),
165 email=current_user.email,
166 is_verified=current_user.is_verified,
167 is_superuser=current_user.is_superuser,
168 is_active=current_user.is_active,
169 )
171 return router