o2o-castad-backend/app/sns/api/routers/v1/oauth.py

210 lines
6.9 KiB
Python

"""
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 <token>)
""",
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 <token>)
""",
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 계정 연동이 해제되었습니다."}