210 lines
6.9 KiB
Python
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 계정 연동이 해제되었습니다."}
|