Files
ALwrity/backend/api/youtube/oauth_router.py

170 lines
6.3 KiB
Python

"""
YouTube OAuth Router
Handles YouTube Data API v3 OAuth2 authentication flow.
Uses shared build_oauth_callback_html for popup-compatible callback responses.
"""
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from loguru import logger
from middleware.auth_middleware import get_current_user, get_optional_user
from services.youtube.youtube_oauth_service import YouTubeOAuthService
from services.integrations.oauth_callback_utils import build_oauth_callback_html
router = APIRouter(prefix="/youtube/oauth", tags=["youtube-oauth"])
def get_oauth_service() -> YouTubeOAuthService:
try:
return YouTubeOAuthService()
except ValueError as e:
logger.error(f"YouTube OAuth service init failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/auth/url")
def get_youtube_auth_url(
user: dict = Depends(get_current_user),
service: YouTubeOAuthService = Depends(get_oauth_service),
):
"""Generate YouTube OAuth authorization URL. Frontend opens this in a popup."""
try:
user_id = user.get("id")
if not user_id:
raise HTTPException(status_code=401, detail="Authentication required")
auth_url = service.generate_authorization_url(user_id)
if not auth_url:
raise HTTPException(
status_code=500,
detail="Failed to generate authorization URL. Check server logs.",
)
logger.info(f"YouTube OAuth URL generated for user {user_id}")
return {"auth_url": auth_url}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error generating YouTube auth URL: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/callback")
def handle_youtube_callback(
code: str = Query(None),
state: str = Query(None),
error: str = Query(None),
request: Request = None,
service: YouTubeOAuthService = Depends(get_oauth_service),
):
"""
Handle OAuth callback from Google.
Returns HTML with postMessage to the opener popup window (GSC/WordPress pattern).
Supports JSON response via ?format=json for server-side flows.
"""
# User denied authorization
if error:
logger.warning(f"YouTube OAuth: user denied authorization: {error}")
html = build_oauth_callback_html(
payload={"type": "YOUTUBE_OAUTH_ERROR", "error": error},
title="Authorization Denied",
heading="Authorization Denied",
message=f"You denied the authorization request. {error}",
)
return _response_as_html(request, html)
# Validate parameters
if not code or not state:
logger.error("YouTube OAuth: missing code or state parameters")
html = build_oauth_callback_html(
payload={"type": "YOUTUBE_OAUTH_ERROR", "error": "Missing authorization code or state"},
title="Authorization Failed",
heading="Missing Parameters",
message="The authorization request was missing required parameters. Please try again.",
)
return _response_as_html(request, html)
# Exchange code for tokens
result = service.handle_oauth_callback(authorization_code=code, state=state)
if result.get("success"):
channel_name = result.get("channel_name", "your channel")
html = build_oauth_callback_html(
payload={
"type": "YOUTUBE_OAUTH_SUCCESS",
"channel_id": result.get("channel_id", ""),
"channel_name": channel_name,
},
title="YouTube Connected",
heading="YouTube Connected!",
message=f"Successfully connected to {channel_name}. You can now close this window.",
)
logger.info(f"YouTube OAuth callback succeeded for channel: {channel_name}")
return _response_as_html(request, html)
error_msg = result.get("error", "Unknown error during authorization")
logger.error(f"YouTube OAuth callback failed: {error_msg}")
html = build_oauth_callback_html(
payload={"type": "YOUTUBE_OAUTH_ERROR", "error": error_msg},
title="Connection Failed",
heading="Connection Failed",
message=f"Failed to connect YouTube: {error_msg}. Please try again.",
)
return _response_as_html(request, html)
@router.get("/status")
def get_youtube_status(
user: dict = Depends(get_current_user),
service: YouTubeOAuthService = Depends(get_oauth_service),
):
"""Check YouTube connection status for the authenticated user."""
try:
user_id = user.get("id")
status = service.get_connection_status(user_id)
return {"success": True, **status}
except Exception as e:
logger.error(f"Error checking YouTube OAuth status: {e}")
return {"success": False, "connected": False, "channels": [], "error": str(e)}
@router.delete("/disconnect/{token_id}")
def disconnect_youtube(
token_id: int,
user: dict = Depends(get_current_user),
service: YouTubeOAuthService = Depends(get_oauth_service),
):
"""Deactivate a YouTube OAuth token."""
try:
user_id = user.get("id")
result = service.revoke_token(user_id, token_id)
if result:
return {"success": True, "message": "YouTube disconnected"}
return {"success": False, "message": "Failed to disconnect"}
except Exception as e:
logger.error(f"Error disconnecting YouTube: {e}")
return {"success": False, "error": str(e)}
def _response_as_html(request: Request, html: str):
"""Return HTML response, or JSON if ?format=json is present."""
if request and request.query_params.get("format") == "json":
from fastapi.responses import JSONResponse
import json as json_lib
# Extract payload from HTML for JSON response
try:
payload_start = html.index('"type":')
payload_end = html.index("</script>", payload_start)
snippet = html[payload_start : payload_end - 3]
payload = json_lib.loads("{" + snippet + "}")
return JSONResponse(content=payload)
except Exception:
return JSONResponse(content={"success": False, "error": "OAuth processing completed"})
from fastapi.responses import HTMLResponse
return HTMLResponse(content=html, headers={"Cross-Origin-Opener-Policy": "unsafe-none"})