Files
ALwrity/backend/routers/gsc_auth.py
2026-05-23 13:09:41 +05:30

391 lines
15 KiB
Python

"""Google Search Console Authentication Router for ALwrity."""
from fastapi import APIRouter, HTTPException, Depends, Query
from fastapi.responses import HTMLResponse, JSONResponse
from typing import Dict, List, Any, Optional
from pydantic import BaseModel
from loguru import logger
import os
from services.gsc_service import GSCService
from services.gsc_brainstorm_service import GSCBrainstormService
from middleware.auth_middleware import get_current_user
# Initialize router
router = APIRouter(prefix="/gsc", tags=["Google Search Console"])
# Initialize GSC service
gsc_service = GSCService()
brainstorm_service = GSCBrainstormService(gsc_service)
# Pydantic models
class GSCAnalyticsRequest(BaseModel):
site_url: str
start_date: Optional[str] = None
end_date: Optional[str] = None
class GSCBrainstormRequest(BaseModel):
keywords: str
site_url: Optional[str] = None
class GSCStatusResponse(BaseModel):
connected: bool
sites: Optional[List[Dict[str, Any]]] = None
last_sync: Optional[str] = None
@router.get("/auth/url")
async def get_gsc_auth_url(user: dict = Depends(get_current_user)):
"""Get Google Search Console OAuth authorization URL."""
try:
user_id = user.get('id')
if not user_id:
raise HTTPException(status_code=400, detail="User ID not found")
logger.info(f"Generating GSC OAuth URL for user: {user_id}")
auth_url = gsc_service.get_oauth_url(user_id)
logger.info(f"GSC OAuth URL generated successfully for user: {user_id}")
logger.info(f"OAuth URL: {auth_url[:100]}...")
return {"auth_url": auth_url}
except FileNotFoundError as e:
logger.error(f"GSC credentials not found: {e}")
raise HTTPException(
status_code=503,
detail="Google Search Console integration is not configured. Please add gsc_credentials.json to the backend directory or set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables."
)
except Exception as e:
logger.error(f"Error generating GSC OAuth URL: {e}")
logger.error(f"Error details: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error generating OAuth URL: {str(e)}")
@router.get("/callback")
async def handle_gsc_callback(
code: str = Query(..., description="Authorization code from Google"),
state: str = Query(..., description="State parameter for security")
):
"""Handle Google Search Console OAuth callback.
For a smoother UX when opened in a popup, this endpoint returns a tiny HTML
page that posts a completion message back to the opener window and closes
itself. The JSON payload is still included in the page for debugging.
"""
try:
logger.info(f"Handling GSC OAuth callback with code: {code[:10]}...")
success = gsc_service.handle_oauth_callback(code, state)
# If state verification failed, check if user is already connected
# (handles duplicate callbacks where state was consumed by a prior request)
if not success:
user_id_from_state = state.split(':')[0] if ':' in state else None
if user_id_from_state:
existing_creds = gsc_service.load_user_credentials(user_id_from_state)
if existing_creds:
logger.info(f"GSC OAuth state already consumed, but user {user_id_from_state} has valid credentials — treating as success")
success = True
if success:
logger.info("GSC OAuth callback handled successfully")
# Create GSC insights task immediately after successful connection
try:
from services.database import get_session_for_user
from services.platform_insights_monitoring_service import create_platform_insights_task
# Get user_id from state (stored during OAuth flow)
# Format is "user_id:random_string"
user_id = state.split(':')[0] if ':' in state else None
if user_id:
db = get_session_for_user(user_id)
if db:
try:
task_result = create_platform_insights_task(
user_id=user_id,
platform='gsc',
site_url=None,
db=db
)
if task_result.get('success'):
logger.info(f"Created GSC insights task for user {user_id}")
else:
logger.warning(f"Failed to create GSC insights task: {task_result.get('error')}")
finally:
db.close()
else:
logger.warning(f"Could not create DB session for user {user_id}")
else:
logger.warning(f"Could not extract user_id from state: {state}")
except Exception as e:
# Non-critical: log but don't fail OAuth callback
logger.warning(f"Failed to create GSC insights task after OAuth: {e}", exc_info=True)
html = """
<!doctype html>
<html>
<head><meta charset=\"utf-8\"><title>GSC Connected</title></head>
<body style=\"font-family: sans-serif; padding: 24px;\">
<p>Connection Successful. You can close this window.</p>
<script>
try {{ window.opener && window.opener.postMessage({{ type: 'GSC_AUTH_SUCCESS' }}, '*'); }} catch (e) {{}}
try {{ window.close(); }} catch (e) {{}}
</script>
</body>
</html>
"""
return HTMLResponse(
content=html,
headers={"Cross-Origin-Opener-Policy": "unsafe-none"},
)
else:
logger.error("Failed to handle GSC OAuth callback")
html = """
<!doctype html>
<html>
<head><meta charset=\"utf-8\"><title>GSC Connection Failed</title></head>
<body style=\"font-family: sans-serif; padding: 24px;\">
<p>Connection Failed. Please close this window and try again.</p>
<script>
try {{ window.opener && window.opener.postMessage({{ type: 'GSC_AUTH_ERROR' }}, '*'); }} catch (e) {{}}
</script>
</body>
</html>
"""
return HTMLResponse(
status_code=400,
content=html,
headers={"Cross-Origin-Opener-Policy": "unsafe-none"},
)
except Exception as e:
logger.error(f"Error handling GSC OAuth callback: {e}")
html = f"""
<!doctype html>
<html>
<head><meta charset=\"utf-8\"><title>GSC Connection Error</title></head>
<body style=\"font-family: sans-serif; padding: 24px;\">
<p>Connection Error. Please close this window and try again.</p>
<pre style=\"white-space: pre-wrap;\">{str(e)}</pre>
<script>
try {{ window.opener && window.opener.postMessage({{ type: 'GSC_AUTH_ERROR' }}, '*'); }} catch (e) {{}}
</script>
</body>
</html>
"""
return HTMLResponse(
status_code=500,
content=html,
headers={"Cross-Origin-Opener-Policy": "unsafe-none"},
)
@router.get("/sites")
async def get_gsc_sites(user: dict = Depends(get_current_user)):
"""Get user's Google Search Console sites."""
try:
user_id = user.get('id')
if not user_id:
raise HTTPException(status_code=400, detail="User ID not found")
logger.info(f"Getting GSC sites for user: {user_id}")
sites = gsc_service.get_site_list(user_id)
logger.info(f"Retrieved {len(sites)} GSC sites for user: {user_id}")
return {"sites": sites}
except Exception as e:
logger.error(f"Error getting GSC sites: {e}")
raise HTTPException(status_code=500, detail=f"Error getting sites: {str(e)}")
@router.post("/analytics")
async def get_gsc_analytics(
request: GSCAnalyticsRequest,
user: dict = Depends(get_current_user)
):
"""Get Google Search Console analytics data."""
try:
user_id = user.get('id')
if not user_id:
raise HTTPException(status_code=400, detail="User ID not found")
logger.info(f"Getting GSC analytics for user: {user_id}, site: {request.site_url}")
analytics = gsc_service.get_search_analytics(
user_id=user_id,
site_url=request.site_url,
start_date=request.start_date,
end_date=request.end_date
)
logger.info(f"Retrieved GSC analytics for user: {user_id}")
return analytics
except Exception as e:
logger.error(f"Error getting GSC analytics: {e}")
raise HTTPException(status_code=500, detail=f"Error getting analytics: {str(e)}")
@router.post("/brainstorm")
async def brainstorm_topics(
request: GSCBrainstormRequest,
user: dict = Depends(get_current_user),
):
"""Brainstorm blog topic suggestions based on the user's GSC data.
The user must have GSC connected. If no site_url is provided,
the first verified site is used automatically.
"""
try:
user_id = user.get('id')
if not user_id:
raise HTTPException(status_code=400, detail="User ID not found")
tokens = request.keywords.strip().split()
if len(tokens) < 3:
raise HTTPException(
status_code=400,
detail="Please provide at least 3 words for brainstorming topic suggestions.",
)
logger.info(f"GSC brainstorm for user: {user_id}, keywords: {request.keywords!r}")
result = brainstorm_service.brainstorm_topics(
user_id=user_id,
keywords=request.keywords,
site_url=request.site_url,
)
if "error" in result and not result.get("content_opportunities"):
status = 400 if "No GSC sites" in result["error"] else 500
raise HTTPException(status_code=status, detail=result["error"])
logger.info(f"GSC brainstorm completed for user: {user_id}")
return result
except HTTPException:
raise
except Exception as e:
logger.error(f"Error in GSC brainstorm: {e}")
raise HTTPException(status_code=500, detail=f"Error brainstorming topics: {str(e)}")
@router.get("/sitemaps/{site_url:path}")
async def get_gsc_sitemaps(
site_url: str,
user: dict = Depends(get_current_user)
):
"""Get sitemaps for a specific site."""
try:
user_id = user.get('id')
if not user_id:
raise HTTPException(status_code=400, detail="User ID not found")
logger.info(f"Getting GSC sitemaps for user: {user_id}, site: {site_url}")
sitemaps = gsc_service.get_sitemaps(user_id, site_url)
logger.info(f"Retrieved {len(sitemaps)} sitemaps for user: {user_id}")
return {"sitemaps": sitemaps}
except Exception as e:
logger.error(f"Error getting GSC sitemaps: {e}")
raise HTTPException(status_code=500, detail=f"Error getting sitemaps: {str(e)}")
@router.get("/status")
async def get_gsc_status(user: dict = Depends(get_current_user)):
"""Get GSC connection status for user."""
try:
user_id = user.get('id')
if not user_id:
raise HTTPException(status_code=400, detail="User ID not found")
logger.info(f"Checking GSC status for user: {user_id}")
# Check if user has credentials
credentials = gsc_service.load_user_credentials(user_id)
connected = credentials is not None
sites = []
if connected:
try:
sites = gsc_service.get_site_list(user_id)
except Exception as e:
logger.warning(f"Could not get sites for user {user_id}: {e}")
# Clear incomplete credentials and mark as disconnected
gsc_service.clear_incomplete_credentials(user_id)
connected = False
status_response = GSCStatusResponse(
connected=connected,
sites=sites if connected else None,
last_sync=None # Could be enhanced to track last sync time
)
logger.info(f"GSC status checked for user: {user_id}, connected: {connected}")
return status_response
except Exception as e:
logger.error(f"Error checking GSC status: {e}")
raise HTTPException(status_code=500, detail=f"Error checking status: {str(e)}")
@router.delete("/disconnect")
async def disconnect_gsc(user: dict = Depends(get_current_user)):
"""Disconnect user's Google Search Console account."""
try:
user_id = user.get('id')
if not user_id:
raise HTTPException(status_code=400, detail="User ID not found")
logger.info(f"Disconnecting GSC for user: {user_id}")
success = gsc_service.revoke_user_access(user_id)
if success:
logger.info(f"GSC disconnected successfully for user: {user_id}")
return {"success": True, "message": "GSC disconnected successfully"}
else:
logger.error(f"Failed to disconnect GSC for user: {user_id}")
raise HTTPException(status_code=500, detail="Failed to disconnect GSC")
except Exception as e:
logger.error(f"Error disconnecting GSC: {e}")
raise HTTPException(status_code=500, detail=f"Error disconnecting GSC: {str(e)}")
@router.post("/clear-incomplete")
async def clear_incomplete_credentials(user: dict = Depends(get_current_user)):
"""Clear incomplete GSC credentials that are missing required fields."""
try:
user_id = user.get('id')
if not user_id:
raise HTTPException(status_code=400, detail="User ID not found")
logger.info(f"Clearing incomplete GSC credentials for user: {user_id}")
success = gsc_service.clear_incomplete_credentials(user_id)
if success:
logger.info(f"Incomplete GSC credentials cleared for user: {user_id}")
return {"success": True, "message": "Incomplete credentials cleared"}
else:
logger.error(f"Failed to clear incomplete credentials for user: {user_id}")
raise HTTPException(status_code=500, detail="Failed to clear incomplete credentials")
except Exception as e:
logger.error(f"Error clearing incomplete credentials: {e}")
raise HTTPException(status_code=500, detail=f"Error clearing incomplete credentials: {str(e)}")
@router.get("/health")
async def gsc_health_check():
"""Health check for GSC service."""
try:
logger.info("GSC health check requested")
return {
"status": "healthy",
"service": "Google Search Console API",
"timestamp": "2024-01-15T10:30:00Z"
}
except Exception as e:
logger.error(f"GSC health check failed: {e}")
raise HTTPException(status_code=500, detail="GSC service unhealthy")