Bing Analytics and Insights added, background jobs added, database setup updated, environment setup updated, frontend updated, backend updated.
Onboarding Manager and Router Manager refactored, analytics and background jobs added, database setup updated, environment setup updated, frontend updated, backend updated. Critical onboarding database migration implemented.
This commit is contained in:
5
backend/routers/__init__.py
Normal file
5
backend/routers/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
Routers Package
|
||||
|
||||
FastAPI routers for the ALwrity backend.
|
||||
"""
|
||||
353
backend/routers/background_jobs.py
Normal file
353
backend/routers/background_jobs.py
Normal file
@@ -0,0 +1,353 @@
|
||||
"""
|
||||
Background Jobs API Routes
|
||||
|
||||
Provides endpoints for managing background jobs like comprehensive Bing insights generation.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query, BackgroundTasks
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
|
||||
from services.background_jobs import background_job_service
|
||||
from middleware.auth_middleware import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/background-jobs", tags=["Background Jobs"])
|
||||
|
||||
|
||||
class JobRequest(BaseModel):
|
||||
"""Request model for creating a job"""
|
||||
job_type: str
|
||||
data: Dict[str, Any]
|
||||
|
||||
|
||||
class JobResponse(BaseModel):
|
||||
"""Response model for job operations"""
|
||||
success: bool
|
||||
job_id: Optional[str] = None
|
||||
message: str
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
@router.post("/create")
|
||||
async def create_background_job(
|
||||
request: JobRequest,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
) -> JobResponse:
|
||||
"""
|
||||
Create a new background job
|
||||
|
||||
Args:
|
||||
request: Job creation request
|
||||
current_user: Current authenticated user
|
||||
|
||||
Returns:
|
||||
Job creation result
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get('id')
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=400, detail="User ID not found")
|
||||
|
||||
# Validate job type
|
||||
valid_job_types = ['bing_comprehensive_insights', 'bing_data_collection', 'analytics_refresh']
|
||||
if request.job_type not in valid_job_types:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid job type. Valid types: {valid_job_types}")
|
||||
|
||||
# Create the job
|
||||
job_id = background_job_service.create_job(
|
||||
job_type=request.job_type,
|
||||
user_id=user_id,
|
||||
data=request.data
|
||||
)
|
||||
|
||||
logger.info(f"Created background job {job_id} for user {user_id}")
|
||||
|
||||
return JobResponse(
|
||||
success=True,
|
||||
job_id=job_id,
|
||||
message=f"Background job created successfully",
|
||||
data={'job_id': job_id}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating background job: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/status/{job_id}")
|
||||
async def get_job_status(
|
||||
job_id: str,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
) -> JobResponse:
|
||||
"""
|
||||
Get the status of a background job
|
||||
|
||||
Args:
|
||||
job_id: Job ID to check
|
||||
current_user: Current authenticated user
|
||||
|
||||
Returns:
|
||||
Job status information
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get('id')
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=400, detail="User ID not found")
|
||||
|
||||
job_status = background_job_service.get_job_status(job_id)
|
||||
|
||||
if not job_status:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
|
||||
# Verify the job belongs to the user
|
||||
if job_status['user_id'] != user_id:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
return JobResponse(
|
||||
success=True,
|
||||
message="Job status retrieved successfully",
|
||||
data=job_status
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting job status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/user-jobs")
|
||||
async def get_user_jobs(
|
||||
limit: int = Query(10, description="Maximum number of jobs to return"),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
) -> JobResponse:
|
||||
"""
|
||||
Get recent jobs for the current user
|
||||
|
||||
Args:
|
||||
limit: Maximum number of jobs to return
|
||||
current_user: Current authenticated user
|
||||
|
||||
Returns:
|
||||
List of user's jobs
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get('id')
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=400, detail="User ID not found")
|
||||
|
||||
jobs = background_job_service.get_user_jobs(user_id, limit)
|
||||
|
||||
return JobResponse(
|
||||
success=True,
|
||||
message=f"Retrieved {len(jobs)} jobs for user",
|
||||
data={'jobs': jobs}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user jobs: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/cancel/{job_id}")
|
||||
async def cancel_job(
|
||||
job_id: str,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
) -> JobResponse:
|
||||
"""
|
||||
Cancel a pending background job
|
||||
|
||||
Args:
|
||||
job_id: Job ID to cancel
|
||||
current_user: Current authenticated user
|
||||
|
||||
Returns:
|
||||
Cancellation result
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get('id')
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=400, detail="User ID not found")
|
||||
|
||||
# Check if job exists and belongs to user
|
||||
job_status = background_job_service.get_job_status(job_id)
|
||||
if not job_status:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
|
||||
if job_status['user_id'] != user_id:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
# Cancel the job
|
||||
success = background_job_service.cancel_job(job_id)
|
||||
|
||||
if success:
|
||||
return JobResponse(
|
||||
success=True,
|
||||
message="Job cancelled successfully",
|
||||
data={'job_id': job_id}
|
||||
)
|
||||
else:
|
||||
return JobResponse(
|
||||
success=False,
|
||||
message="Job cannot be cancelled (may be running or completed)",
|
||||
data={'job_id': job_id}
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error cancelling job: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/bing/comprehensive-insights")
|
||||
async def create_bing_comprehensive_insights_job(
|
||||
site_url: str = Query(..., description="Site URL to analyze"),
|
||||
days: int = Query(30, description="Number of days to analyze"),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
) -> JobResponse:
|
||||
"""
|
||||
Create a background job to generate comprehensive Bing insights
|
||||
|
||||
Args:
|
||||
site_url: Site URL to analyze
|
||||
days: Number of days to analyze
|
||||
current_user: Current authenticated user
|
||||
|
||||
Returns:
|
||||
Job creation result
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get('id')
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=400, detail="User ID not found")
|
||||
|
||||
# Create the job
|
||||
job_id = background_job_service.create_job(
|
||||
job_type='bing_comprehensive_insights',
|
||||
user_id=user_id,
|
||||
data={
|
||||
'site_url': site_url,
|
||||
'days': days
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(f"Created Bing comprehensive insights job {job_id} for user {user_id}")
|
||||
|
||||
return JobResponse(
|
||||
success=True,
|
||||
job_id=job_id,
|
||||
message="Bing comprehensive insights job created successfully. Check status for progress.",
|
||||
data={
|
||||
'job_id': job_id,
|
||||
'site_url': site_url,
|
||||
'days': days,
|
||||
'estimated_time': '2-5 minutes'
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating Bing comprehensive insights job: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/bing/data-collection")
|
||||
async def create_bing_data_collection_job(
|
||||
site_url: str = Query(..., description="Site URL to collect data for"),
|
||||
days_back: int = Query(30, description="Number of days back to collect"),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
) -> JobResponse:
|
||||
"""
|
||||
Create a background job to collect fresh Bing data from API
|
||||
|
||||
Args:
|
||||
site_url: Site URL to collect data for
|
||||
days_back: Number of days back to collect
|
||||
current_user: Current authenticated user
|
||||
|
||||
Returns:
|
||||
Job creation result
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get('id')
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=400, detail="User ID not found")
|
||||
|
||||
# Create the job
|
||||
job_id = background_job_service.create_job(
|
||||
job_type='bing_data_collection',
|
||||
user_id=user_id,
|
||||
data={
|
||||
'site_url': site_url,
|
||||
'days_back': days_back
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(f"Created Bing data collection job {job_id} for user {user_id}")
|
||||
|
||||
return JobResponse(
|
||||
success=True,
|
||||
job_id=job_id,
|
||||
message="Bing data collection job created successfully. This will collect fresh data from Bing API.",
|
||||
data={
|
||||
'job_id': job_id,
|
||||
'site_url': site_url,
|
||||
'days_back': days_back,
|
||||
'estimated_time': '3-7 minutes'
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating Bing data collection job: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/analytics/refresh")
|
||||
async def create_analytics_refresh_job(
|
||||
platforms: str = Query("bing,gsc", description="Comma-separated list of platforms to refresh"),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
) -> JobResponse:
|
||||
"""
|
||||
Create a background job to refresh analytics data for all platforms
|
||||
|
||||
Args:
|
||||
platforms: Comma-separated list of platforms to refresh
|
||||
current_user: Current authenticated user
|
||||
|
||||
Returns:
|
||||
Job creation result
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get('id')
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=400, detail="User ID not found")
|
||||
|
||||
platform_list = [p.strip() for p in platforms.split(',')]
|
||||
|
||||
# Create the job
|
||||
job_id = background_job_service.create_job(
|
||||
job_type='analytics_refresh',
|
||||
user_id=user_id,
|
||||
data={
|
||||
'platforms': platform_list
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(f"Created analytics refresh job {job_id} for user {user_id}")
|
||||
|
||||
return JobResponse(
|
||||
success=True,
|
||||
job_id=job_id,
|
||||
message="Analytics refresh job created successfully. This will refresh data for all connected platforms.",
|
||||
data={
|
||||
'job_id': job_id,
|
||||
'platforms': platform_list,
|
||||
'estimated_time': '1-3 minutes'
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating analytics refresh job: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
166
backend/routers/bing_analytics.py
Normal file
166
backend/routers/bing_analytics.py
Normal file
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
Bing Webmaster Analytics API Routes
|
||||
Provides endpoints for accessing Bing Webmaster Tools analytics data.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
from loguru import logger
|
||||
|
||||
from services.integrations.bing_oauth import BingOAuthService
|
||||
from middleware.auth_middleware import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/bing", tags=["Bing Analytics"])
|
||||
|
||||
# Initialize Bing OAuth service
|
||||
bing_service = BingOAuthService()
|
||||
|
||||
@router.get("/query-stats")
|
||||
async def get_query_stats(
|
||||
site_url: str = Query(..., description="The site URL to get query stats for"),
|
||||
start_date: Optional[str] = Query(None, description="Start date in YYYY-MM-DD format"),
|
||||
end_date: Optional[str] = Query(None, description="End date in YYYY-MM-DD format"),
|
||||
page: int = Query(0, description="Page number for pagination"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Get search query statistics for a Bing Webmaster site."""
|
||||
try:
|
||||
user_id = current_user.get("user_id")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User not authenticated")
|
||||
|
||||
logger.info(f"Getting Bing query stats for user {user_id}, site: {site_url}")
|
||||
|
||||
# Get query stats from Bing service
|
||||
result = bing_service.get_query_stats(
|
||||
user_id=user_id,
|
||||
site_url=site_url,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
page=page
|
||||
)
|
||||
|
||||
if "error" in result:
|
||||
logger.error(f"Bing query stats error: {result['error']}")
|
||||
raise HTTPException(status_code=400, detail=result["error"])
|
||||
|
||||
logger.info(f"Successfully retrieved Bing query stats for {site_url}")
|
||||
return {
|
||||
"success": True,
|
||||
"data": result,
|
||||
"site_url": site_url,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
"page": page
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting Bing query stats: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||
|
||||
@router.get("/user-sites")
|
||||
async def get_user_sites(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Get list of user's verified sites from Bing Webmaster."""
|
||||
try:
|
||||
user_id = current_user.get("user_id")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User not authenticated")
|
||||
|
||||
logger.info(f"Getting Bing user sites for user {user_id}")
|
||||
|
||||
# Get user sites from Bing service
|
||||
sites = bing_service.get_user_sites(user_id)
|
||||
|
||||
logger.info(f"Successfully retrieved {len(sites)} Bing sites for user {user_id}")
|
||||
return {
|
||||
"success": True,
|
||||
"sites": sites,
|
||||
"total_sites": len(sites)
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting Bing user sites: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||
|
||||
@router.get("/query-stats/summary")
|
||||
async def get_query_stats_summary(
|
||||
site_url: str = Query(..., description="The site URL to get query stats summary for"),
|
||||
start_date: Optional[str] = Query(None, description="Start date in YYYY-MM-DD format"),
|
||||
end_date: Optional[str] = Query(None, description="End date in YYYY-MM-DD format"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Get summarized query statistics for a Bing Webmaster site."""
|
||||
try:
|
||||
user_id = current_user.get("user_id")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User not authenticated")
|
||||
|
||||
logger.info(f"Getting Bing query stats summary for user {user_id}, site: {site_url}")
|
||||
|
||||
# Get query stats from Bing service
|
||||
result = bing_service.get_query_stats(
|
||||
user_id=user_id,
|
||||
site_url=site_url,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
page=0 # Just get first page for summary
|
||||
)
|
||||
|
||||
if "error" in result:
|
||||
logger.error(f"Bing query stats error: {result['error']}")
|
||||
raise HTTPException(status_code=400, detail=result["error"])
|
||||
|
||||
# Extract summary data
|
||||
query_data = result.get('d', {})
|
||||
queries = query_data.get('results', [])
|
||||
|
||||
# Calculate summary statistics
|
||||
total_clicks = sum(query.get('Clicks', 0) for query in queries)
|
||||
total_impressions = sum(query.get('Impressions', 0) for query in queries)
|
||||
total_queries = len(queries)
|
||||
avg_ctr = (total_clicks / total_impressions * 100) if total_impressions > 0 else 0
|
||||
avg_position = sum(query.get('AvgClickPosition', 0) for query in queries) / total_queries if total_queries > 0 else 0
|
||||
|
||||
# Get top queries
|
||||
top_queries = sorted(queries, key=lambda x: x.get('Clicks', 0), reverse=True)[:5]
|
||||
|
||||
summary = {
|
||||
"total_queries": total_queries,
|
||||
"total_clicks": total_clicks,
|
||||
"total_impressions": total_impressions,
|
||||
"average_ctr": round(avg_ctr, 2),
|
||||
"average_position": round(avg_position, 2),
|
||||
"top_queries": [
|
||||
{
|
||||
"query": q.get('Query', ''),
|
||||
"clicks": q.get('Clicks', 0),
|
||||
"impressions": q.get('Impressions', 0),
|
||||
"ctr": round(q.get('Clicks', 0) / q.get('Impressions', 1) * 100, 2),
|
||||
"position": q.get('AvgClickPosition', 0)
|
||||
}
|
||||
for q in top_queries
|
||||
]
|
||||
}
|
||||
|
||||
logger.info(f"Successfully created Bing query stats summary for {site_url}")
|
||||
return {
|
||||
"success": True,
|
||||
"summary": summary,
|
||||
"site_url": site_url,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
"raw_data": result
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting Bing query stats summary: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||
453
backend/routers/bing_analytics_storage.py
Normal file
453
backend/routers/bing_analytics_storage.py
Normal file
@@ -0,0 +1,453 @@
|
||||
"""
|
||||
Bing Analytics Storage API Routes
|
||||
|
||||
Provides endpoints for accessing stored Bing analytics data,
|
||||
historical trends, and performance analysis.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime, timedelta
|
||||
from loguru import logger
|
||||
import os
|
||||
import json
|
||||
from sqlalchemy import and_
|
||||
|
||||
from services.bing_analytics_storage_service import BingAnalyticsStorageService
|
||||
from middleware.auth_middleware import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/bing-analytics", tags=["Bing Analytics Storage"])
|
||||
|
||||
# Initialize storage service
|
||||
DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///./bing_analytics.db')
|
||||
storage_service = BingAnalyticsStorageService(DATABASE_URL)
|
||||
|
||||
|
||||
@router.post("/collect-data")
|
||||
async def collect_bing_data(
|
||||
background_tasks: BackgroundTasks,
|
||||
site_url: str = Query(..., description="Site URL to collect data for"),
|
||||
days_back: int = Query(30, description="Number of days back to collect data"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Collect and store Bing analytics data for a site.
|
||||
This endpoint triggers data collection from Bing API and stores it in the database.
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get("user_id")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User not authenticated")
|
||||
|
||||
logger.info(f"Starting Bing data collection for user {user_id}, site: {site_url}")
|
||||
|
||||
# Run data collection in background
|
||||
background_tasks.add_task(
|
||||
storage_service.collect_and_store_data,
|
||||
user_id=user_id,
|
||||
site_url=site_url,
|
||||
days_back=days_back
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Bing data collection started for {site_url}",
|
||||
"site_url": site_url,
|
||||
"days_back": days_back,
|
||||
"status": "collecting"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting Bing data collection: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/summary")
|
||||
async def get_analytics_summary(
|
||||
site_url: str = Query(..., description="Site URL to get analytics for"),
|
||||
days: int = Query(30, description="Number of days for analytics summary"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get comprehensive analytics summary for a site over a specified period.
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get("user_id")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User not authenticated")
|
||||
|
||||
logger.info(f"Getting analytics summary for user {user_id}, site: {site_url}, days: {days}")
|
||||
|
||||
summary = storage_service.get_analytics_summary(
|
||||
user_id=user_id,
|
||||
site_url=site_url,
|
||||
days=days
|
||||
)
|
||||
|
||||
if 'error' in summary:
|
||||
raise HTTPException(status_code=404, detail=summary['error'])
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": summary,
|
||||
"site_url": site_url,
|
||||
"period_days": days
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting analytics summary: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/daily-metrics")
|
||||
async def get_daily_metrics(
|
||||
site_url: str = Query(..., description="Site URL to get daily metrics for"),
|
||||
days: int = Query(30, description="Number of days to retrieve"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get daily metrics for a site over a specified period.
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get("user_id")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User not authenticated")
|
||||
|
||||
logger.info(f"Getting daily metrics for user {user_id}, site: {site_url}, days: {days}")
|
||||
|
||||
db = storage_service._get_db_session()
|
||||
|
||||
# Calculate date range
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=days)
|
||||
|
||||
# Get daily metrics
|
||||
daily_metrics = db.query(storage_service.BingDailyMetrics).filter(
|
||||
and_(
|
||||
storage_service.BingDailyMetrics.user_id == user_id,
|
||||
storage_service.BingDailyMetrics.site_url == site_url,
|
||||
storage_service.BingDailyMetrics.metric_date >= start_date,
|
||||
storage_service.BingDailyMetrics.metric_date <= end_date
|
||||
)
|
||||
).order_by(storage_service.BingDailyMetrics.metric_date).all()
|
||||
|
||||
db.close()
|
||||
|
||||
# Format response
|
||||
metrics_data = []
|
||||
for metric in daily_metrics:
|
||||
metrics_data.append({
|
||||
"date": metric.metric_date.isoformat(),
|
||||
"total_clicks": metric.total_clicks,
|
||||
"total_impressions": metric.total_impressions,
|
||||
"total_queries": metric.total_queries,
|
||||
"avg_ctr": metric.avg_ctr,
|
||||
"avg_position": metric.avg_position,
|
||||
"clicks_change": metric.clicks_change,
|
||||
"impressions_change": metric.impressions_change,
|
||||
"ctr_change": metric.ctr_change,
|
||||
"top_queries": json.loads(metric.top_queries) if metric.top_queries else [],
|
||||
"collected_at": metric.collected_at.isoformat()
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": metrics_data,
|
||||
"site_url": site_url,
|
||||
"period_days": days,
|
||||
"metrics_count": len(metrics_data)
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting daily metrics: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/top-queries")
|
||||
async def get_top_queries(
|
||||
site_url: str = Query(..., description="Site URL to get top queries for"),
|
||||
days: int = Query(30, description="Number of days to analyze"),
|
||||
limit: int = Query(50, description="Number of top queries to return"),
|
||||
sort_by: str = Query("clicks", description="Sort by: clicks, impressions, or ctr"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get top performing queries for a site over a specified period.
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get("user_id")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User not authenticated")
|
||||
|
||||
if sort_by not in ["clicks", "impressions", "ctr"]:
|
||||
raise HTTPException(status_code=400, detail="sort_by must be 'clicks', 'impressions', or 'ctr'")
|
||||
|
||||
logger.info(f"Getting top queries for user {user_id}, site: {site_url}, sort_by: {sort_by}")
|
||||
|
||||
db = storage_service._get_db_session()
|
||||
|
||||
# Calculate date range
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=days)
|
||||
|
||||
# Get raw query data
|
||||
query_stats = db.query(storage_service.BingQueryStats).filter(
|
||||
and_(
|
||||
storage_service.BingQueryStats.user_id == user_id,
|
||||
storage_service.BingQueryStats.site_url == site_url,
|
||||
storage_service.BingQueryStats.query_date >= start_date,
|
||||
storage_service.BingQueryStats.query_date <= end_date
|
||||
)
|
||||
).all()
|
||||
|
||||
db.close()
|
||||
|
||||
if not query_stats:
|
||||
return {
|
||||
"success": True,
|
||||
"data": [],
|
||||
"message": "No query data found for the specified period"
|
||||
}
|
||||
|
||||
# Aggregate queries
|
||||
query_aggregates = {}
|
||||
for stat in query_stats:
|
||||
query = stat.query
|
||||
if query not in query_aggregates:
|
||||
query_aggregates[query] = {
|
||||
"query": query,
|
||||
"total_clicks": 0,
|
||||
"total_impressions": 0,
|
||||
"avg_ctr": 0,
|
||||
"avg_position": 0,
|
||||
"days_appeared": 0,
|
||||
"category": stat.category,
|
||||
"is_brand": stat.is_brand_query
|
||||
}
|
||||
|
||||
query_aggregates[query]["total_clicks"] += stat.clicks
|
||||
query_aggregates[query]["total_impressions"] += stat.impressions
|
||||
query_aggregates[query]["days_appeared"] += 1
|
||||
|
||||
# Calculate weighted average position
|
||||
if stat.avg_click_position > 0:
|
||||
query_aggregates[query]["avg_position"] = (
|
||||
query_aggregates[query]["avg_position"] * (query_aggregates[query]["days_appeared"] - 1) +
|
||||
stat.avg_click_position
|
||||
) / query_aggregates[query]["days_appeared"]
|
||||
|
||||
# Calculate CTR for each query
|
||||
for query_data in query_aggregates.values():
|
||||
query_data["avg_ctr"] = (
|
||||
query_data["total_clicks"] / query_data["total_impressions"] * 100
|
||||
) if query_data["total_impressions"] > 0 else 0
|
||||
|
||||
# Sort and limit results
|
||||
sorted_queries = sorted(
|
||||
list(query_aggregates.values()),
|
||||
key=lambda x: x[f"total_{sort_by}"],
|
||||
reverse=True
|
||||
)[:limit]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": sorted_queries,
|
||||
"site_url": site_url,
|
||||
"period_days": days,
|
||||
"sort_by": sort_by,
|
||||
"total_queries": len(query_aggregates),
|
||||
"returned_queries": len(sorted_queries)
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting top queries: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/query-details")
|
||||
async def get_query_details(
|
||||
site_url: str = Query(..., description="Site URL"),
|
||||
query: str = Query(..., description="Specific query to analyze"),
|
||||
days: int = Query(30, description="Number of days to analyze"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get detailed performance data for a specific query.
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get("user_id")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User not authenticated")
|
||||
|
||||
logger.info(f"Getting query details for user {user_id}, query: {query}")
|
||||
|
||||
db = storage_service._get_db_session()
|
||||
|
||||
# Calculate date range
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=days)
|
||||
|
||||
# Get query stats
|
||||
query_stats = db.query(storage_service.BingQueryStats).filter(
|
||||
and_(
|
||||
storage_service.BingQueryStats.user_id == user_id,
|
||||
storage_service.BingQueryStats.site_url == site_url,
|
||||
storage_service.BingQueryStats.query == query,
|
||||
storage_service.BingQueryStats.query_date >= start_date,
|
||||
storage_service.BingQueryStats.query_date <= end_date
|
||||
)
|
||||
).order_by(storage_service.BingQueryStats.query_date).all()
|
||||
|
||||
db.close()
|
||||
|
||||
if not query_stats:
|
||||
return {
|
||||
"success": True,
|
||||
"data": None,
|
||||
"message": f"No data found for query: {query}"
|
||||
}
|
||||
|
||||
# Calculate summary statistics
|
||||
total_clicks = sum(stat.clicks for stat in query_stats)
|
||||
total_impressions = sum(stat.impressions for stat in query_stats)
|
||||
avg_ctr = (total_clicks / total_impressions * 100) if total_impressions > 0 else 0
|
||||
avg_position = sum(stat.avg_click_position for stat in query_stats if stat.avg_click_position > 0) / len([stat for stat in query_stats if stat.avg_click_position > 0]) if any(stat.avg_click_position > 0 for stat in query_stats) else 0
|
||||
|
||||
# Daily performance data
|
||||
daily_data = []
|
||||
for stat in query_stats:
|
||||
daily_data.append({
|
||||
"date": stat.query_date.isoformat(),
|
||||
"clicks": stat.clicks,
|
||||
"impressions": stat.impressions,
|
||||
"ctr": stat.ctr,
|
||||
"avg_click_position": stat.avg_click_position,
|
||||
"avg_impression_position": stat.avg_impression_position
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"query": query,
|
||||
"period_days": days,
|
||||
"total_clicks": total_clicks,
|
||||
"total_impressions": total_impressions,
|
||||
"avg_ctr": round(avg_ctr, 2),
|
||||
"avg_position": round(avg_position, 2),
|
||||
"days_appeared": len(query_stats),
|
||||
"category": query_stats[0].category,
|
||||
"is_brand_query": query_stats[0].is_brand_query,
|
||||
"daily_performance": daily_data
|
||||
}
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting query details: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/sites")
|
||||
async def get_user_sites(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get list of sites with stored Bing analytics data.
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get("user_id")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User not authenticated")
|
||||
|
||||
logger.info(f"Getting user sites for user {user_id}")
|
||||
|
||||
db = storage_service._get_db_session()
|
||||
|
||||
# Get unique sites for the user
|
||||
sites = db.query(storage_service.BingDailyMetrics.site_url).filter(
|
||||
storage_service.BingDailyMetrics.user_id == user_id
|
||||
).distinct().all()
|
||||
|
||||
db.close()
|
||||
|
||||
sites_data = []
|
||||
for site_tuple in sites:
|
||||
site_url = site_tuple[0]
|
||||
|
||||
# Get latest metrics for each site
|
||||
summary = storage_service.get_analytics_summary(user_id, site_url, 7)
|
||||
|
||||
sites_data.append({
|
||||
"site_url": site_url,
|
||||
"latest_summary": summary if 'error' not in summary else None,
|
||||
"has_data": 'error' not in summary
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": sites_data,
|
||||
"total_sites": len(sites_data)
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user sites: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/generate-daily-metrics")
|
||||
async def generate_daily_metrics(
|
||||
background_tasks: BackgroundTasks,
|
||||
site_url: str = Query(..., description="Site URL to generate metrics for"),
|
||||
target_date: Optional[str] = Query(None, description="Target date (YYYY-MM-DD), defaults to yesterday"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Generate daily metrics for a specific date from stored raw data.
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get("user_id")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User not authenticated")
|
||||
|
||||
# Parse target date
|
||||
if target_date:
|
||||
try:
|
||||
target_dt = datetime.strptime(target_date, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid date format. Use YYYY-MM-DD")
|
||||
else:
|
||||
target_dt = None
|
||||
|
||||
logger.info(f"Generating daily metrics for user {user_id}, site: {site_url}, date: {target_dt}")
|
||||
|
||||
# Run in background
|
||||
background_tasks.add_task(
|
||||
storage_service.generate_daily_metrics,
|
||||
user_id=user_id,
|
||||
site_url=site_url,
|
||||
target_date=target_dt
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Daily metrics generation started for {site_url}",
|
||||
"site_url": site_url,
|
||||
"target_date": target_dt.isoformat() if target_dt else "yesterday"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating daily metrics: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||
219
backend/routers/bing_insights.py
Normal file
219
backend/routers/bing_insights.py
Normal file
@@ -0,0 +1,219 @@
|
||||
"""
|
||||
Bing Insights API Routes
|
||||
|
||||
Provides endpoints for accessing Bing Webmaster insights and recommendations.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
from loguru import logger
|
||||
import os
|
||||
|
||||
from services.analytics.insights.bing_insights_service import BingInsightsService
|
||||
from middleware.auth_middleware import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/bing-insights", tags=["Bing Insights"])
|
||||
|
||||
# Initialize insights service
|
||||
DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///./bing_analytics.db')
|
||||
insights_service = BingInsightsService(DATABASE_URL)
|
||||
|
||||
|
||||
@router.get("/performance")
|
||||
async def get_performance_insights(
|
||||
site_url: str = Query(..., description="Site URL to analyze"),
|
||||
days: int = Query(30, description="Number of days to analyze"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get performance insights including trends and patterns for a Bing Webmaster site.
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get("id") or current_user.get("user_id")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User not authenticated")
|
||||
|
||||
logger.info(f"Getting performance insights for user {user_id}, site: {site_url}")
|
||||
|
||||
insights = insights_service.get_performance_insights(user_id, site_url, days)
|
||||
|
||||
if 'error' in insights:
|
||||
raise HTTPException(status_code=404, detail=insights['error'])
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": insights,
|
||||
"site_url": site_url,
|
||||
"analysis_period": f"{days} days",
|
||||
"generated_at": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting performance insights: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/seo")
|
||||
async def get_seo_insights(
|
||||
site_url: str = Query(..., description="Site URL to analyze"),
|
||||
days: int = Query(30, description="Number of days to analyze"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get SEO-specific insights and opportunities for a Bing Webmaster site.
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get("id") or current_user.get("user_id")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User not authenticated")
|
||||
|
||||
logger.info(f"Getting SEO insights for user {user_id}, site: {site_url}")
|
||||
|
||||
insights = insights_service.get_seo_insights(user_id, site_url, days)
|
||||
|
||||
if 'error' in insights:
|
||||
raise HTTPException(status_code=404, detail=insights['error'])
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": insights,
|
||||
"site_url": site_url,
|
||||
"analysis_period": f"{days} days",
|
||||
"generated_at": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting SEO insights: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/competitive")
|
||||
async def get_competitive_insights(
|
||||
site_url: str = Query(..., description="Site URL to analyze"),
|
||||
days: int = Query(30, description="Number of days to analyze"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get competitive analysis and market insights for a Bing Webmaster site.
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get("id") or current_user.get("user_id")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User not authenticated")
|
||||
|
||||
logger.info(f"Getting competitive insights for user {user_id}, site: {site_url}")
|
||||
|
||||
insights = insights_service.get_competitive_insights(user_id, site_url, days)
|
||||
|
||||
if 'error' in insights:
|
||||
raise HTTPException(status_code=404, detail=insights['error'])
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": insights,
|
||||
"site_url": site_url,
|
||||
"analysis_period": f"{days} days",
|
||||
"generated_at": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting competitive insights: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/recommendations")
|
||||
async def get_actionable_recommendations(
|
||||
site_url: str = Query(..., description="Site URL to analyze"),
|
||||
days: int = Query(30, description="Number of days to analyze"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get actionable recommendations for improving search performance.
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get("id") or current_user.get("user_id")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User not authenticated")
|
||||
|
||||
logger.info(f"Getting actionable recommendations for user {user_id}, site: {site_url}")
|
||||
|
||||
recommendations = insights_service.get_actionable_recommendations(user_id, site_url, days)
|
||||
|
||||
if 'error' in recommendations:
|
||||
raise HTTPException(status_code=404, detail=recommendations['error'])
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": recommendations,
|
||||
"site_url": site_url,
|
||||
"analysis_period": f"{days} days",
|
||||
"generated_at": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting actionable recommendations: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/comprehensive")
|
||||
async def get_comprehensive_insights(
|
||||
site_url: str = Query(..., description="Site URL to analyze"),
|
||||
days: int = Query(30, description="Number of days to analyze"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get comprehensive insights including performance, SEO, competitive, and recommendations.
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get("id") or current_user.get("user_id")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User not authenticated")
|
||||
|
||||
logger.info(f"Getting comprehensive insights for user {user_id}, site: {site_url}")
|
||||
|
||||
# Get all types of insights
|
||||
performance = insights_service.get_performance_insights(user_id, site_url, days)
|
||||
seo = insights_service.get_seo_insights(user_id, site_url, days)
|
||||
competitive = insights_service.get_competitive_insights(user_id, site_url, days)
|
||||
recommendations = insights_service.get_actionable_recommendations(user_id, site_url, days)
|
||||
|
||||
# Check for errors
|
||||
errors = []
|
||||
if 'error' in performance:
|
||||
errors.append(f"Performance insights: {performance['error']}")
|
||||
if 'error' in seo:
|
||||
errors.append(f"SEO insights: {seo['error']}")
|
||||
if 'error' in competitive:
|
||||
errors.append(f"Competitive insights: {competitive['error']}")
|
||||
if 'error' in recommendations:
|
||||
errors.append(f"Recommendations: {recommendations['error']}")
|
||||
|
||||
if errors:
|
||||
logger.warning(f"Some insights failed: {errors}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"performance": performance,
|
||||
"seo": seo,
|
||||
"competitive": competitive,
|
||||
"recommendations": recommendations
|
||||
},
|
||||
"site_url": site_url,
|
||||
"analysis_period": f"{days} days",
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"warnings": errors if errors else None
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting comprehensive insights: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||
281
backend/routers/bing_oauth.py
Normal file
281
backend/routers/bing_oauth.py
Normal file
@@ -0,0 +1,281 @@
|
||||
"""
|
||||
Bing Webmaster OAuth2 Routes
|
||||
Handles Bing Webmaster Tools OAuth2 authentication flow.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from fastapi.responses import RedirectResponse, HTMLResponse
|
||||
from typing import Dict, Any, Optional
|
||||
from pydantic import BaseModel
|
||||
from loguru import logger
|
||||
|
||||
from services.integrations.bing_oauth import BingOAuthService
|
||||
from middleware.auth_middleware import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/bing", tags=["Bing Webmaster OAuth"])
|
||||
|
||||
# Initialize OAuth service
|
||||
oauth_service = BingOAuthService()
|
||||
|
||||
# Pydantic Models
|
||||
class BingOAuthResponse(BaseModel):
|
||||
auth_url: str
|
||||
state: str
|
||||
|
||||
class BingCallbackResponse(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
access_token: Optional[str] = None
|
||||
expires_in: Optional[int] = None
|
||||
|
||||
class BingStatusResponse(BaseModel):
|
||||
connected: bool
|
||||
sites: list
|
||||
total_sites: int
|
||||
|
||||
@router.get("/auth/url", response_model=BingOAuthResponse)
|
||||
async def get_bing_auth_url(
|
||||
user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Get Bing Webmaster OAuth2 authorization URL."""
|
||||
try:
|
||||
user_id = user.get('id')
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="User ID not found.")
|
||||
|
||||
auth_data = oauth_service.generate_authorization_url(user_id)
|
||||
if not auth_data:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Bing Webmaster OAuth is not properly configured. Please check that BING_CLIENT_ID and BING_CLIENT_SECRET environment variables are set with valid Bing Webmaster application credentials."
|
||||
)
|
||||
|
||||
return BingOAuthResponse(**auth_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating Bing Webmaster OAuth URL: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to generate Bing Webmaster OAuth URL."
|
||||
)
|
||||
|
||||
@router.get("/callback")
|
||||
async def handle_bing_callback(
|
||||
code: str = Query(..., description="Authorization code from Bing"),
|
||||
state: str = Query(..., description="State parameter for security"),
|
||||
error: Optional[str] = Query(None, description="Error from Bing OAuth")
|
||||
):
|
||||
"""Handle Bing Webmaster OAuth2 callback."""
|
||||
try:
|
||||
if error:
|
||||
logger.error(f"Bing Webmaster OAuth error: {error}")
|
||||
html_content = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Bing Webmaster Connection Failed</title>
|
||||
<script>
|
||||
// Send error message to parent window
|
||||
window.onload = function() {{
|
||||
window.parent.postMessage({{
|
||||
type: 'BING_OAUTH_ERROR',
|
||||
success: false,
|
||||
error: '{error}'
|
||||
}}, '*');
|
||||
window.close();
|
||||
}};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Connection Failed</h1>
|
||||
<p>There was an error connecting to Bing Webmaster Tools.</p>
|
||||
<p>You can close this window and try again.</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return HTMLResponse(content=html_content, headers={
|
||||
"Cross-Origin-Opener-Policy": "unsafe-none",
|
||||
"Cross-Origin-Embedder-Policy": "unsafe-none"
|
||||
})
|
||||
|
||||
if not code or not state:
|
||||
logger.error("Missing code or state parameter in Bing Webmaster OAuth callback")
|
||||
html_content = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Bing Webmaster Connection Failed</title>
|
||||
<script>
|
||||
// Send error message to opener/parent window
|
||||
window.onload = function() {{
|
||||
(window.opener || window.parent).postMessage({{
|
||||
type: 'BING_OAUTH_ERROR',
|
||||
success: false,
|
||||
error: 'Missing parameters'
|
||||
}}, '*');
|
||||
window.close();
|
||||
}};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Connection Failed</h1>
|
||||
<p>Missing required parameters.</p>
|
||||
<p>You can close this window and try again.</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return HTMLResponse(content=html_content, headers={
|
||||
"Cross-Origin-Opener-Policy": "unsafe-none",
|
||||
"Cross-Origin-Embedder-Policy": "unsafe-none"
|
||||
})
|
||||
|
||||
# Exchange code for token
|
||||
result = oauth_service.handle_oauth_callback(code, state)
|
||||
|
||||
if not result or not result.get('success'):
|
||||
logger.error("Failed to exchange Bing Webmaster OAuth code for token")
|
||||
html_content = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Bing Webmaster Connection Failed</title>
|
||||
<script>
|
||||
// Send error message to opener/parent window
|
||||
window.onload = function() {{
|
||||
(window.opener || window.parent).postMessage({{
|
||||
type: 'BING_OAUTH_ERROR',
|
||||
success: false,
|
||||
error: 'Token exchange failed'
|
||||
}}, '*');
|
||||
window.close();
|
||||
}};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Connection Failed</h1>
|
||||
<p>Failed to exchange authorization code for access token.</p>
|
||||
<p>You can close this window and try again.</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return HTMLResponse(content=html_content)
|
||||
|
||||
# Return success page with postMessage script
|
||||
html_content = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Bing Webmaster Connection Successful</title>
|
||||
<script>
|
||||
// Send success message to opener/parent window
|
||||
window.onload = function() {{
|
||||
(window.opener || window.parent).postMessage({{
|
||||
type: 'BING_OAUTH_SUCCESS',
|
||||
success: true,
|
||||
accessToken: '{result.get('access_token', '')}',
|
||||
expiresIn: {result.get('expires_in', 0)}
|
||||
}}, '*');
|
||||
window.close();
|
||||
}};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Connection Successful!</h1>
|
||||
<p>Your Bing Webmaster Tools account has been connected successfully.</p>
|
||||
<p>You can close this window now.</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
return HTMLResponse(content=html_content, headers={
|
||||
"Cross-Origin-Opener-Policy": "unsafe-none",
|
||||
"Cross-Origin-Embedder-Policy": "unsafe-none"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling Bing Webmaster OAuth callback: {e}")
|
||||
html_content = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Bing Webmaster Connection Failed</title>
|
||||
<script>
|
||||
// Send error message to opener/parent window
|
||||
window.onload = function() {{
|
||||
(window.opener || window.parent).postMessage({{
|
||||
type: 'BING_OAUTH_ERROR',
|
||||
success: false,
|
||||
error: 'Callback error'
|
||||
}}, '*');
|
||||
window.close();
|
||||
}};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Connection Failed</h1>
|
||||
<p>An unexpected error occurred during connection.</p>
|
||||
<p>You can close this window and try again.</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return HTMLResponse(content=html_content, headers={
|
||||
"Cross-Origin-Opener-Policy": "unsafe-none",
|
||||
"Cross-Origin-Embedder-Policy": "unsafe-none"
|
||||
})
|
||||
|
||||
@router.get("/status", response_model=BingStatusResponse)
|
||||
async def get_bing_oauth_status(
|
||||
user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Get Bing Webmaster OAuth connection status."""
|
||||
try:
|
||||
user_id = user.get('id')
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="User ID not found.")
|
||||
|
||||
status_data = oauth_service.get_connection_status(user_id)
|
||||
return BingStatusResponse(**status_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting Bing Webmaster OAuth status: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get Bing Webmaster connection status."
|
||||
)
|
||||
|
||||
@router.delete("/disconnect/{token_id}")
|
||||
async def disconnect_bing_site(
|
||||
token_id: int,
|
||||
user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Disconnect a Bing Webmaster site."""
|
||||
try:
|
||||
user_id = user.get('id')
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="User ID not found.")
|
||||
|
||||
success = oauth_service.revoke_token(user_id, token_id)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Bing Webmaster token not found or could not be disconnected."
|
||||
)
|
||||
|
||||
return {"success": True, "message": f"Bing Webmaster site disconnected successfully."}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error disconnecting Bing Webmaster site: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to disconnect Bing Webmaster site."
|
||||
)
|
||||
|
||||
@router.get("/health")
|
||||
async def bing_oauth_health():
|
||||
"""Bing Webmaster OAuth health check."""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "bing_oauth",
|
||||
"timestamp": "2024-01-01T00:00:00Z",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
318
backend/routers/platform_analytics.py
Normal file
318
backend/routers/platform_analytics.py
Normal file
@@ -0,0 +1,318 @@
|
||||
"""
|
||||
Platform Analytics API Routes
|
||||
|
||||
Provides endpoints for retrieving analytics data from connected platforms.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query
|
||||
from typing import Dict, Any, List, Optional
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
|
||||
from services.analytics import PlatformAnalyticsService
|
||||
from middleware.auth_middleware import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/analytics", tags=["Platform Analytics"])
|
||||
|
||||
# Initialize analytics service
|
||||
analytics_service = PlatformAnalyticsService()
|
||||
|
||||
|
||||
class AnalyticsRequest(BaseModel):
|
||||
"""Request model for analytics data"""
|
||||
platforms: Optional[List[str]] = None
|
||||
date_range: Optional[Dict[str, str]] = None
|
||||
|
||||
|
||||
class AnalyticsResponse(BaseModel):
|
||||
"""Response model for analytics data"""
|
||||
success: bool
|
||||
data: Dict[str, Any]
|
||||
summary: Dict[str, Any]
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
@router.get("/platforms")
|
||||
async def get_platform_connection_status(current_user: dict = Depends(get_current_user)) -> Dict[str, Any]:
|
||||
"""
|
||||
Get connection status for all platforms
|
||||
|
||||
Args:
|
||||
current_user: Current authenticated user
|
||||
|
||||
Returns:
|
||||
Connection status for each platform
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get('id')
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=400, detail="User ID not found")
|
||||
|
||||
logger.info(f"Getting platform connection status for user: {user_id}")
|
||||
|
||||
status = await analytics_service.get_platform_connection_status(user_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"platforms": status,
|
||||
"total_connected": sum(1 for p in status.values() if p.get('connected', False))
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get platform connection status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/data")
|
||||
async def get_analytics_data(
|
||||
platforms: Optional[str] = Query(None, description="Comma-separated list of platforms (gsc,wix,wordpress)"),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
) -> AnalyticsResponse:
|
||||
"""
|
||||
Get analytics data from connected platforms
|
||||
|
||||
Args:
|
||||
platforms: Comma-separated list of platforms to get data from
|
||||
current_user: Current authenticated user
|
||||
|
||||
Returns:
|
||||
Analytics data from specified platforms
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get('id')
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=400, detail="User ID not found")
|
||||
|
||||
# Parse platforms parameter
|
||||
platform_list = None
|
||||
if platforms:
|
||||
platform_list = [p.strip() for p in platforms.split(',') if p.strip()]
|
||||
|
||||
logger.info(f"Getting analytics data for user: {user_id}, platforms: {platform_list}")
|
||||
|
||||
# Get analytics data
|
||||
analytics_data = await analytics_service.get_comprehensive_analytics(user_id, platform_list)
|
||||
|
||||
# Generate summary
|
||||
summary = analytics_service.get_analytics_summary(analytics_data)
|
||||
|
||||
# Convert AnalyticsData objects to dictionaries
|
||||
data_dict = {}
|
||||
for platform, data in analytics_data.items():
|
||||
data_dict[platform] = {
|
||||
'platform': data.platform,
|
||||
'metrics': data.metrics,
|
||||
'date_range': data.date_range,
|
||||
'last_updated': data.last_updated,
|
||||
'status': data.status,
|
||||
'error_message': data.error_message
|
||||
}
|
||||
|
||||
return AnalyticsResponse(
|
||||
success=True,
|
||||
data=data_dict,
|
||||
summary=summary,
|
||||
error=None
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get analytics data: {e}")
|
||||
return AnalyticsResponse(
|
||||
success=False,
|
||||
data={},
|
||||
summary={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/data")
|
||||
async def get_analytics_data_post(
|
||||
request: AnalyticsRequest,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
) -> AnalyticsResponse:
|
||||
"""
|
||||
Get analytics data from connected platforms (POST version)
|
||||
|
||||
Args:
|
||||
request: Analytics request with platforms and date range
|
||||
current_user: Current authenticated user
|
||||
|
||||
Returns:
|
||||
Analytics data from specified platforms
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get('id')
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=400, detail="User ID not found")
|
||||
|
||||
logger.info(f"Getting analytics data for user: {user_id}, platforms: {request.platforms}")
|
||||
|
||||
# Get analytics data
|
||||
analytics_data = await analytics_service.get_comprehensive_analytics(user_id, request.platforms)
|
||||
|
||||
# Generate summary
|
||||
summary = analytics_service.get_analytics_summary(analytics_data)
|
||||
|
||||
# Convert AnalyticsData objects to dictionaries
|
||||
data_dict = {}
|
||||
for platform, data in analytics_data.items():
|
||||
data_dict[platform] = {
|
||||
'platform': data.platform,
|
||||
'metrics': data.metrics,
|
||||
'date_range': data.date_range,
|
||||
'last_updated': data.last_updated,
|
||||
'status': data.status,
|
||||
'error_message': data.error_message
|
||||
}
|
||||
|
||||
return AnalyticsResponse(
|
||||
success=True,
|
||||
data=data_dict,
|
||||
summary=summary,
|
||||
error=None
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get analytics data: {e}")
|
||||
return AnalyticsResponse(
|
||||
success=False,
|
||||
data={},
|
||||
summary={},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/gsc")
|
||||
async def get_gsc_analytics(
|
||||
current_user: dict = Depends(get_current_user)
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get Google Search Console analytics data specifically
|
||||
|
||||
Args:
|
||||
current_user: Current authenticated user
|
||||
|
||||
Returns:
|
||||
GSC analytics data
|
||||
"""
|
||||
try:
|
||||
user_id = current_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}")
|
||||
|
||||
# Get GSC analytics
|
||||
gsc_data = await analytics_service._get_gsc_analytics(user_id)
|
||||
|
||||
return {
|
||||
"success": gsc_data.status == 'success',
|
||||
"platform": gsc_data.platform,
|
||||
"metrics": gsc_data.metrics,
|
||||
"date_range": gsc_data.date_range,
|
||||
"last_updated": gsc_data.last_updated,
|
||||
"status": gsc_data.status,
|
||||
"error": gsc_data.error_message
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get GSC analytics: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/summary")
|
||||
async def get_analytics_summary(current_user: dict = Depends(get_current_user)) -> Dict[str, Any]:
|
||||
"""
|
||||
Get a summary of analytics data across all connected platforms
|
||||
|
||||
Args:
|
||||
current_user: Current authenticated user
|
||||
|
||||
Returns:
|
||||
Analytics summary
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get('id')
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=400, detail="User ID not found")
|
||||
|
||||
logger.info(f"Getting analytics summary for user: {user_id}")
|
||||
|
||||
# Get analytics data from all platforms
|
||||
analytics_data = await analytics_service.get_comprehensive_analytics(user_id)
|
||||
|
||||
# Generate summary
|
||||
summary = analytics_service.get_analytics_summary(analytics_data)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"summary": summary,
|
||||
"platforms_connected": summary['connected_platforms'],
|
||||
"platforms_total": summary['total_platforms']
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get analytics summary: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/cache/test")
|
||||
async def test_cache_endpoint(current_user: dict = Depends(get_current_user)) -> Dict[str, Any]:
|
||||
"""
|
||||
Test endpoint to verify cache routes are working
|
||||
"""
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Cache endpoint is working",
|
||||
"user_id": current_user.get('id'),
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
|
||||
@router.post("/cache/clear")
|
||||
async def clear_analytics_cache(
|
||||
platform: Optional[str] = Query(None, description="Specific platform to clear cache for (optional)"),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Clear analytics cache for a user
|
||||
|
||||
Args:
|
||||
platform: Specific platform to clear cache for (optional, clears all if None)
|
||||
current_user: Current authenticated user
|
||||
|
||||
Returns:
|
||||
Cache clearing result
|
||||
"""
|
||||
try:
|
||||
from datetime import datetime
|
||||
user_id = current_user.get('id')
|
||||
logger.info(f"Cache clear request received for user {user_id}, platform: {platform}")
|
||||
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=400, detail="User ID not found")
|
||||
|
||||
if platform:
|
||||
# Clear cache for specific platform
|
||||
analytics_service.invalidate_platform_cache(user_id, platform)
|
||||
message = f"Cleared cache for {platform}"
|
||||
else:
|
||||
# Clear all cache for user
|
||||
analytics_service.invalidate_user_cache(user_id)
|
||||
message = "Cleared all analytics cache"
|
||||
|
||||
logger.info(f"Cache cleared for user {user_id}: {message}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"user_id": user_id,
|
||||
"platform": platform,
|
||||
"message": message,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error clearing cache: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Handles WordPress.com OAuth2 authentication flow.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.responses import RedirectResponse, HTMLResponse
|
||||
from typing import Dict, Any, Optional
|
||||
from pydantic import BaseModel
|
||||
from loguru import logger
|
||||
|
||||
Reference in New Issue
Block a user