240 lines
7.9 KiB
Python
240 lines
7.9 KiB
Python
"""
|
|
인증 API 라우터
|
|
|
|
카카오 로그인, 토큰 갱신, 로그아웃, 내 정보 조회 엔드포인트를 제공합니다.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, Header, Request, status
|
|
from fastapi.responses import RedirectResponse, Response
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from config import prj_settings
|
|
from app.database.session import get_session
|
|
|
|
logger = logging.getLogger(__name__)
|
|
from app.user.dependencies import get_current_user
|
|
from app.user.models import User
|
|
from app.user.schemas.user_schema import (
|
|
AccessTokenResponse,
|
|
KakaoCodeRequest,
|
|
KakaoLoginResponse,
|
|
LoginResponse,
|
|
RefreshTokenRequest,
|
|
UserResponse,
|
|
)
|
|
from app.user.services import auth_service, kakao_client
|
|
|
|
router = APIRouter(prefix="/auth", tags=["Auth"])
|
|
|
|
|
|
@router.get(
|
|
"/kakao/login",
|
|
response_model=KakaoLoginResponse,
|
|
summary="카카오 로그인 URL 요청",
|
|
description="카카오 OAuth 인증 페이지 URL을 반환합니다.",
|
|
)
|
|
async def kakao_login() -> KakaoLoginResponse:
|
|
"""
|
|
카카오 로그인 URL 반환
|
|
|
|
프론트엔드에서 이 URL로 사용자를 리다이렉트하면
|
|
카카오 로그인 페이지가 표시됩니다.
|
|
"""
|
|
logger.info("[ROUTER] 카카오 로그인 URL 요청")
|
|
auth_url = kakao_client.get_authorization_url()
|
|
logger.debug(f"[ROUTER] 카카오 인증 URL 생성 완료 - auth_url: {auth_url}")
|
|
return KakaoLoginResponse(auth_url=auth_url)
|
|
|
|
|
|
@router.get(
|
|
"/kakao/callback",
|
|
summary="카카오 로그인 콜백",
|
|
description="카카오 인가 코드를 받아 로그인/가입을 처리하고 프론트엔드로 리다이렉트합니다.",
|
|
)
|
|
async def kakao_callback(
|
|
request: Request,
|
|
code: str,
|
|
session: AsyncSession = Depends(get_session),
|
|
user_agent: Optional[str] = Header(None, alias="User-Agent"),
|
|
) -> RedirectResponse:
|
|
"""
|
|
카카오 로그인 콜백
|
|
|
|
카카오 로그인 성공 후 발급받은 인가 코드로
|
|
JWT 토큰을 발급하고 프론트엔드로 리다이렉트합니다.
|
|
|
|
신규 사용자인 경우 자동으로 회원가입이 처리됩니다.
|
|
"""
|
|
logger.info(f"[ROUTER] 카카오 콜백 수신 - code: {code[:20]}...")
|
|
|
|
# 클라이언트 IP 추출
|
|
ip_address = request.client.host if request.client else None
|
|
|
|
# X-Forwarded-For 헤더 확인 (프록시/로드밸런서 뒤에 있는 경우)
|
|
forwarded_for = request.headers.get("X-Forwarded-For")
|
|
if forwarded_for:
|
|
ip_address = forwarded_for.split(",")[0].strip()
|
|
|
|
logger.debug(f"[ROUTER] 클라이언트 정보 - ip: {ip_address}, user_agent: {user_agent}")
|
|
|
|
result = await auth_service.kakao_login(
|
|
code=code,
|
|
session=session,
|
|
user_agent=user_agent,
|
|
ip_address=ip_address,
|
|
)
|
|
|
|
# 프론트엔드로 토큰과 함께 리다이렉트
|
|
redirect_url = (
|
|
f"https://{prj_settings.PROJECT_DOMAIN}"
|
|
f"?access_token={result.access_token}"
|
|
f"&refresh_token={result.refresh_token}"
|
|
)
|
|
logger.info(f"[ROUTER] 카카오 콜백 완료, 프론트엔드로 리다이렉트 - redirect_url: {redirect_url[:50]}...")
|
|
return RedirectResponse(url=redirect_url, status_code=302)
|
|
|
|
|
|
@router.post(
|
|
"/kakao/verify",
|
|
response_model=LoginResponse,
|
|
summary="카카오 인가 코드 검증 및 토큰 발급",
|
|
description="""
|
|
프론트엔드에서 카카오 로그인 후 받은 인가 코드를 검증하고 JWT 토큰을 발급합니다.
|
|
|
|
## 사용 시나리오
|
|
1. 프론트엔드가 카카오 로그인 완료 후 인가 코드(code)를 받음
|
|
2. 프론트엔드가 이 엔드포인트에 code를 POST로 전달
|
|
3. 서버가 카카오 서버에 code 검증 및 사용자 정보 조회
|
|
4. JWT 토큰 발급 및 사용자 정보 반환
|
|
|
|
## 응답
|
|
- 신규 사용자인 경우 `user.is_new_user`가 `true`로 반환됩니다.
|
|
- `redirect_url`은 로그인 후 이동할 프론트엔드 URL입니다.
|
|
""",
|
|
)
|
|
async def kakao_verify(
|
|
request: Request,
|
|
body: KakaoCodeRequest,
|
|
session: AsyncSession = Depends(get_session),
|
|
user_agent: Optional[str] = Header(None, alias="User-Agent"),
|
|
) -> LoginResponse:
|
|
"""
|
|
카카오 인가 코드 검증 및 토큰 발급
|
|
|
|
프론트엔드가 카카오 콜백에서 받은 인가 코드를 전달하면
|
|
카카오 서버에서 검증 후 JWT 토큰을 발급합니다.
|
|
|
|
신규 사용자인 경우 자동으로 회원가입이 처리됩니다.
|
|
"""
|
|
logger.info(f"[ROUTER] 카카오 인가 코드 검증 요청 - code: {body.code[:20]}...")
|
|
|
|
# 클라이언트 IP 추출
|
|
ip_address = request.client.host if request.client else None
|
|
|
|
# X-Forwarded-For 헤더 확인 (프록시/로드밸런서 뒤에 있는 경우)
|
|
forwarded_for = request.headers.get("X-Forwarded-For")
|
|
if forwarded_for:
|
|
ip_address = forwarded_for.split(",")[0].strip()
|
|
|
|
logger.debug(f"[ROUTER] 클라이언트 정보 - ip: {ip_address}, user_agent: {user_agent}")
|
|
|
|
result = await auth_service.kakao_login(
|
|
code=body.code,
|
|
session=session,
|
|
user_agent=user_agent,
|
|
ip_address=ip_address,
|
|
)
|
|
|
|
logger.info(f"[ROUTER] 카카오 인가 코드 검증 완료 - user_id: {result.user.id}, is_new_user: {result.user.is_new_user}")
|
|
return result
|
|
|
|
|
|
@router.post(
|
|
"/refresh",
|
|
response_model=AccessTokenResponse,
|
|
summary="토큰 갱신",
|
|
description="리프레시 토큰으로 새 액세스 토큰을 발급합니다.",
|
|
)
|
|
async def refresh_token(
|
|
body: RefreshTokenRequest,
|
|
session: AsyncSession = Depends(get_session),
|
|
) -> AccessTokenResponse:
|
|
"""
|
|
액세스 토큰 갱신
|
|
|
|
유효한 리프레시 토큰을 제출하면 새 액세스 토큰을 발급합니다.
|
|
리프레시 토큰은 변경되지 않습니다.
|
|
"""
|
|
return await auth_service.refresh_tokens(
|
|
refresh_token=body.refresh_token,
|
|
session=session,
|
|
)
|
|
|
|
|
|
@router.post(
|
|
"/logout",
|
|
status_code=status.HTTP_204_NO_CONTENT,
|
|
summary="로그아웃",
|
|
description="현재 세션의 리프레시 토큰을 폐기합니다.",
|
|
)
|
|
async def logout(
|
|
body: RefreshTokenRequest,
|
|
current_user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
) -> Response:
|
|
"""
|
|
로그아웃
|
|
|
|
현재 사용 중인 리프레시 토큰을 폐기합니다.
|
|
해당 토큰으로는 더 이상 액세스 토큰을 갱신할 수 없습니다.
|
|
"""
|
|
await auth_service.logout(
|
|
user_id=current_user.id,
|
|
refresh_token=body.refresh_token,
|
|
session=session,
|
|
)
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
@router.post(
|
|
"/logout/all",
|
|
status_code=status.HTTP_204_NO_CONTENT,
|
|
summary="모든 기기에서 로그아웃",
|
|
description="사용자의 모든 리프레시 토큰을 폐기합니다.",
|
|
)
|
|
async def logout_all(
|
|
current_user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
) -> Response:
|
|
"""
|
|
모든 기기에서 로그아웃
|
|
|
|
사용자의 모든 리프레시 토큰을 폐기합니다.
|
|
모든 기기에서 재로그인이 필요합니다.
|
|
"""
|
|
await auth_service.logout_all(
|
|
user_id=current_user.id,
|
|
session=session,
|
|
)
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
@router.get(
|
|
"/me",
|
|
response_model=UserResponse,
|
|
summary="내 정보 조회",
|
|
description="현재 로그인한 사용자의 정보를 반환합니다.",
|
|
)
|
|
async def get_me(
|
|
current_user: User = Depends(get_current_user),
|
|
) -> UserResponse:
|
|
"""
|
|
내 정보 조회
|
|
|
|
현재 로그인한 사용자의 상세 정보를 반환합니다.
|
|
"""
|
|
return UserResponse.model_validate(current_user)
|