feat: image generation overhaul (model-aware text, dim clamping, \.30 pricing), event-driven dashboard cache invalidation, SEO insights (AI visibility, GSC, keyword gap), YouTube OAuth/publish, blog writer & content planning improvements, scheduler monitoring updates
This commit is contained in:
169
backend/api/youtube/oauth_router.py
Normal file
169
backend/api/youtube/oauth_router.py
Normal file
@@ -0,0 +1,169 @@
|
||||
"""
|
||||
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"})
|
||||
Reference in New Issue
Block a user