o2o-castad-backend/app/user/api/routers/v1/auth.py

167 lines
4.9 KiB
Python

"""
인증 API 라우터
카카오 로그인, 토큰 갱신, 로그아웃, 내 정보 조회 엔드포인트를 제공합니다.
"""
from typing import Optional
from fastapi import APIRouter, Depends, Header, Request, status
from fastapi.responses import Response
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_session
from app.user.dependencies import get_current_user
from app.user.models import User
from app.user.schemas.user_schema import (
AccessTokenResponse,
KakaoCallbackRequest,
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로 사용자를 리다이렉트하면
카카오 로그인 페이지가 표시됩니다.
"""
auth_url = kakao_client.get_authorization_url()
return KakaoLoginResponse(auth_url=auth_url)
@router.post(
"/kakao/callback",
response_model=LoginResponse,
summary="카카오 로그인 콜백",
description="카카오 인가 코드를 받아 로그인/가입을 처리하고 JWT 토큰을 발급합니다.",
)
async def kakao_callback(
request: Request,
body: KakaoCallbackRequest,
session: AsyncSession = Depends(get_session),
user_agent: Optional[str] = Header(None, alias="User-Agent"),
) -> LoginResponse:
"""
카카오 로그인 콜백
카카오 로그인 성공 후 발급받은 인가 코드로
JWT 토큰을 발급합니다.
신규 사용자인 경우 자동으로 회원가입이 처리됩니다.
"""
# 클라이언트 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()
return await auth_service.kakao_login(
code=body.code,
session=session,
user_agent=user_agent,
ip_address=ip_address,
)
@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)