282 lines
9.1 KiB
Python
282 lines
9.1 KiB
Python
"""
|
|
소셜 OAuth API 라우터
|
|
|
|
소셜 미디어 계정 연동 관련 엔드포인트를 제공합니다.
|
|
"""
|
|
|
|
import logging
|
|
from urllib.parse import urlencode
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
from fastapi.responses import RedirectResponse
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from config import social_oauth_settings
|
|
from app.database.session import get_session
|
|
from app.social.constants import SocialPlatform
|
|
from app.social.schemas import (
|
|
MessageResponse,
|
|
SocialAccountListResponse,
|
|
SocialAccountResponse,
|
|
SocialConnectResponse,
|
|
)
|
|
from app.social.services import social_account_service
|
|
from app.user.dependencies import get_current_user
|
|
from app.user.models import User
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/oauth", tags=["Social 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(
|
|
"/{platform}/connect",
|
|
response_model=SocialConnectResponse,
|
|
summary="소셜 계정 연동 시작",
|
|
description="""
|
|
소셜 미디어 계정 연동을 시작합니다.
|
|
|
|
## 지원 플랫폼
|
|
- **youtube**: YouTube (Google OAuth)
|
|
- instagram, facebook, tiktok: 추후 지원 예정
|
|
|
|
## 플로우
|
|
1. 이 엔드포인트를 호출하여 `auth_url`과 `state`를 받음
|
|
2. 프론트엔드에서 `auth_url`로 사용자를 리다이렉트
|
|
3. 사용자가 플랫폼에서 권한 승인
|
|
4. 플랫폼이 `/callback` 엔드포인트로 리다이렉트
|
|
5. 연동 완료 후 프론트엔드로 리다이렉트
|
|
""",
|
|
)
|
|
async def start_connect(
|
|
platform: SocialPlatform,
|
|
current_user: User = Depends(get_current_user),
|
|
) -> SocialConnectResponse:
|
|
"""
|
|
소셜 계정 연동 시작
|
|
|
|
OAuth 인증 URL을 생성하고 state 토큰을 반환합니다.
|
|
프론트엔드에서 반환된 auth_url로 사용자를 리다이렉트하면 됩니다.
|
|
"""
|
|
logger.info(
|
|
f"[OAUTH_API] 소셜 연동 시작 - "
|
|
f"user_uuid: {current_user.user_uuid}, platform: {platform.value}"
|
|
)
|
|
|
|
return await social_account_service.start_connect(
|
|
user_uuid=current_user.user_uuid,
|
|
platform=platform,
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/{platform}/callback",
|
|
summary="OAuth 콜백",
|
|
description="""
|
|
소셜 플랫폼의 OAuth 콜백을 처리합니다.
|
|
|
|
이 엔드포인트는 소셜 플랫폼에서 직접 호출되며,
|
|
사용자를 프론트엔드로 리다이렉트합니다.
|
|
""",
|
|
)
|
|
async def oauth_callback(
|
|
platform: SocialPlatform,
|
|
code: str | None = Query(None, description="OAuth 인가 코드"),
|
|
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:
|
|
"""
|
|
OAuth 콜백 처리
|
|
|
|
소셜 플랫폼에서 리다이렉트된 후 호출됩니다.
|
|
인가 코드로 토큰을 교환하고 계정을 연동합니다.
|
|
"""
|
|
# 사용자가 취소하거나 에러가 발생한 경우
|
|
if error:
|
|
logger.info(
|
|
f"[OAUTH_API] OAuth 취소/에러 - "
|
|
f"platform: {platform.value}, error: {error}, description: {error_description}"
|
|
)
|
|
|
|
# 에러 메시지 생성
|
|
if error == "access_denied":
|
|
error_message = "사용자가 연동을 취소했습니다."
|
|
else:
|
|
error_message = error_description or error
|
|
|
|
redirect_url = _build_redirect_url(
|
|
is_success=False,
|
|
params={
|
|
"platform": platform.value,
|
|
"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"[OAUTH_API] OAuth 콜백 파라미터 누락 - "
|
|
f"platform: {platform.value}, code: {bool(code)}, state: {bool(state)}"
|
|
)
|
|
redirect_url = _build_redirect_url(
|
|
is_success=False,
|
|
params={
|
|
"platform": platform.value,
|
|
"error": "잘못된 요청입니다. 다시 시도해주세요.",
|
|
},
|
|
)
|
|
return RedirectResponse(url=redirect_url, status_code=302)
|
|
|
|
logger.info(
|
|
f"[OAUTH_API] OAuth 콜백 수신 - "
|
|
f"platform: {platform.value}, code: {code[:20]}..."
|
|
)
|
|
|
|
try:
|
|
account = await social_account_service.handle_callback(
|
|
code=code,
|
|
state=state,
|
|
session=session,
|
|
)
|
|
|
|
# 성공 시 프론트엔드로 리다이렉트 (계정 정보 포함)
|
|
redirect_url = _build_redirect_url(
|
|
is_success=True,
|
|
params={
|
|
"platform": platform.value,
|
|
"account_id": account.id,
|
|
"channel_name": account.display_name or account.platform_username or "",
|
|
"profile_image": account.profile_image_url or "",
|
|
},
|
|
)
|
|
logger.info(f"[OAUTH_API] 연동 성공, 리다이렉트 - url: {redirect_url}")
|
|
return RedirectResponse(url=redirect_url, status_code=302)
|
|
|
|
except Exception as e:
|
|
logger.error(f"[OAUTH_API] OAuth 콜백 처리 실패 - error: {e}")
|
|
# 실패 시 에러 페이지로 리다이렉트
|
|
redirect_url = _build_redirect_url(
|
|
is_success=False,
|
|
params={
|
|
"platform": platform.value,
|
|
"error": str(e),
|
|
},
|
|
)
|
|
return RedirectResponse(url=redirect_url, status_code=302)
|
|
|
|
|
|
@router.get(
|
|
"/accounts",
|
|
response_model=SocialAccountListResponse,
|
|
summary="연동된 소셜 계정 목록 조회",
|
|
description="현재 사용자가 연동한 모든 소셜 계정 목록을 반환합니다.",
|
|
)
|
|
async def get_connected_accounts(
|
|
current_user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
) -> SocialAccountListResponse:
|
|
"""
|
|
연동된 소셜 계정 목록 조회
|
|
|
|
현재 로그인한 사용자가 연동한 모든 소셜 계정을 조회합니다.
|
|
"""
|
|
logger.info(f"[OAUTH_API] 연동 계정 목록 조회 - user_uuid: {current_user.user_uuid}")
|
|
|
|
accounts = await social_account_service.get_connected_accounts(
|
|
user_uuid=current_user.user_uuid,
|
|
session=session,
|
|
)
|
|
|
|
return SocialAccountListResponse(
|
|
accounts=accounts,
|
|
total=len(accounts),
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/accounts/{platform}",
|
|
response_model=SocialAccountResponse,
|
|
summary="특정 플랫폼 연동 계정 조회",
|
|
description="특정 플랫폼에 연동된 계정 정보를 반환합니다.",
|
|
)
|
|
async def get_account_by_platform(
|
|
platform: SocialPlatform,
|
|
current_user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
) -> SocialAccountResponse:
|
|
"""
|
|
특정 플랫폼 연동 계정 조회
|
|
"""
|
|
logger.info(
|
|
f"[OAUTH_API] 특정 플랫폼 계정 조회 - "
|
|
f"user_uuid: {current_user.user_uuid}, platform: {platform.value}"
|
|
)
|
|
|
|
account = await social_account_service.get_account_by_platform(
|
|
user_uuid=current_user.user_uuid,
|
|
platform=platform,
|
|
session=session,
|
|
)
|
|
|
|
if account is None:
|
|
from app.social.exceptions import SocialAccountNotFoundError
|
|
|
|
raise SocialAccountNotFoundError(platform=platform.value)
|
|
|
|
return social_account_service._to_response(account)
|
|
|
|
|
|
@router.delete(
|
|
"/{platform}/disconnect",
|
|
response_model=MessageResponse,
|
|
summary="소셜 계정 연동 해제",
|
|
description="""
|
|
소셜 미디어 계정 연동을 해제합니다.
|
|
|
|
연동 해제 시:
|
|
- 플랫폼에서 토큰이 폐기됩니다
|
|
- 해당 플랫폼으로의 업로드가 불가능해집니다
|
|
- 기존 업로드 기록은 유지됩니다
|
|
""",
|
|
)
|
|
async def disconnect(
|
|
platform: SocialPlatform,
|
|
current_user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
) -> MessageResponse:
|
|
"""
|
|
소셜 계정 연동 해제
|
|
|
|
플랫폼 토큰을 폐기하고 연동을 해제합니다.
|
|
"""
|
|
logger.info(
|
|
f"[OAUTH_API] 소셜 연동 해제 - "
|
|
f"user_uuid: {current_user.user_uuid}, platform: {platform.value}"
|
|
)
|
|
|
|
await social_account_service.disconnect(
|
|
user_uuid=current_user.user_uuid,
|
|
platform=platform,
|
|
session=session,
|
|
)
|
|
|
|
return MessageResponse(
|
|
success=True,
|
|
message=f"{platform.value} 계정 연동이 해제되었습니다.",
|
|
)
|