""" SNS OAuth API 라우터 Facebook OAuth 연동 관련 엔드포인트를 제공합니다. """ from urllib.parse import urlencode from fastapi import APIRouter, Depends, Query from fastapi.responses import RedirectResponse from sqlalchemy.ext.asyncio import AsyncSession from app.database.session import get_session from app.sns.schemas.facebook_schema import FacebookConnectResponse from app.sns.services.facebook import facebook_service from app.user.dependencies.auth import get_current_user from app.user.models import User from app.utils.logger import get_logger from config import social_oauth_settings logger = get_logger(__name__) router = APIRouter(prefix="/sns", tags=["SNS OAuth"]) def _build_redirect_url(is_success: bool, params: dict) -> str: """OAuth 완료 후 프론트엔드 리다이렉트 URL 생성""" base_url = social_oauth_settings.OAUTH_FRONTEND_URL.rstrip("/") path = ( social_oauth_settings.OAUTH_SUCCESS_PATH if is_success else social_oauth_settings.OAUTH_ERROR_PATH ) return f"{base_url}{path}?{urlencode(params)}" @router.get( "/facebook/connect", response_model=FacebookConnectResponse, summary="Facebook OAuth 연동 시작", description=""" ## 개요 Facebook OAuth 2.0 인증을 시작합니다. ## 플로우 1. 이 엔드포인트를 호출하여 `auth_url`과 `state`를 받음 2. 프론트엔드에서 `auth_url`로 사용자를 리다이렉트 3. 사용자가 Facebook에서 로그인 및 권한 승인 4. Facebook이 `/sns/facebook/callback` 엔드포인트로 리다이렉트 5. 연동 완료 후 프론트엔드로 리다이렉트 ## 인증 - Bearer 토큰 필요 (Authorization: Bearer ) """, responses={ 200: {"description": "인증 URL 반환 성공"}, 401: {"description": "인증 실패"}, }, ) async def facebook_connect( current_user: User = Depends(get_current_user), ) -> FacebookConnectResponse: """Facebook OAuth 연동을 시작합니다.""" logger.info(f"[SNS_OAUTH] Facebook 연동 시작 - user_uuid: {current_user.user_uuid}") # FacebookService를 통해 연동 시작 response = await facebook_service.start_connect(user_uuid=current_user.user_uuid) logger.info("[SNS_OAUTH] Facebook 연동 URL 생성 완료") return response @router.get( "/facebook/callback", summary="Facebook OAuth 콜백", description=""" ## 개요 Facebook OAuth 콜백을 처리합니다. 이 엔드포인트는 Facebook에서 직접 호출되며, 처리 완료 후 프론트엔드로 리다이렉트합니다. ## 파라미터 - **code**: Facebook에서 발급한 인가 코드 - **state**: CSRF 방지용 state 토큰 - **error**: OAuth 에러 코드 (사용자 취소 등) """, responses={ 302: {"description": "프론트엔드로 리다이렉트"}, }, ) async def facebook_callback( code: str | None = Query(None, description="Facebook 인가 코드"), state: str | None = Query(None, description="CSRF 방지용 state 토큰"), error: str | None = Query(None, description="OAuth 에러 코드"), error_description: str | None = Query(None, description="OAuth 에러 설명"), session: AsyncSession = Depends(get_session), ) -> RedirectResponse: """Facebook OAuth 콜백을 처리합니다.""" # 사용자가 취소하거나 에러가 발생한 경우 if error: logger.info( f"[SNS_OAUTH] Facebook 콜백 에러/취소 - " f"error: {error}, description: {error_description}" ) # 에러 메시지 분기 if error == "access_denied": error_message = "사용자가 Facebook 연동을 취소했습니다." else: error_message = error_description or error redirect_url = _build_redirect_url( is_success=False, params={ "platform": "facebook", "error": error_message, "cancelled": "true" if error == "access_denied" else "false", }, ) return RedirectResponse(url=redirect_url, status_code=302) # code 또는 state가 없는 경우 if not code or not state: logger.warning( f"[SNS_OAUTH] Facebook 콜백 파라미터 누락 - " f"code: {bool(code)}, state: {bool(state)}" ) redirect_url = _build_redirect_url( is_success=False, params={ "platform": "facebook", "error": "잘못된 요청입니다. 다시 시도해주세요.", }, ) return RedirectResponse(url=redirect_url, status_code=302) logger.info(f"[SNS_OAUTH] Facebook 콜백 수신 - code: {code[:20]}...") try: # FacebookService를 통해 콜백 처리 account_response = await facebook_service.handle_callback( code=code, state=state, session=session, ) # 성공 시 프론트엔드로 리다이렉트 redirect_url = _build_redirect_url( is_success=True, params={ "platform": "facebook", "account_id": account_response.account_id, "channel_name": account_response.platform_username, }, ) logger.info("[SNS_OAUTH] Facebook 연동 성공, 리다이렉트") return RedirectResponse(url=redirect_url, status_code=302) except Exception as e: logger.error(f"[SNS_OAUTH] Facebook 콜백 처리 실패 - error: {e}") # 실패 시 에러 페이지로 리다이렉트 redirect_url = _build_redirect_url( is_success=False, params={ "platform": "facebook", "error": str(e), }, ) return RedirectResponse(url=redirect_url, status_code=302) @router.delete( "/facebook/disconnect", summary="Facebook 계정 연동 해제", description=""" ## 개요 Facebook 계정 연동을 해제합니다. ## 연동 해제 시 - Facebook으로의 업로드가 불가능해집니다 - 기존 업로드 기록은 유지됩니다 - 재연동 시 다시 권한 승인이 필요합니다 ## 인증 - Bearer 토큰 필요 (Authorization: Bearer ) """, responses={ 200: {"description": "연동 해제 성공"}, 401: {"description": "인증 실패"}, 404: {"description": "연동된 Facebook 계정 없음"}, }, ) async def facebook_disconnect( current_user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session), ) -> dict: """Facebook 계정 연동을 해제합니다.""" logger.info(f"[SNS_OAUTH] Facebook 연동 해제 - user_uuid: {current_user.user_uuid}") # FacebookService를 통해 연동 해제 await facebook_service.disconnect( user_uuid=current_user.user_uuid, session=session, ) logger.info("[SNS_OAUTH] Facebook 연동 해제 완료") return {"success": True, "message": "Facebook 계정 연동이 해제되었습니다."}