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:
ajaysi
2025-10-18 10:28:15 +05:30
parent 40fb6ac95b
commit 1f087aad4c
69 changed files with 11995 additions and 189 deletions

View File

@@ -0,0 +1,5 @@
"""
Routers Package
FastAPI routers for the ALwrity backend.
"""

View 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))

View 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)}")

View 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)}")

View 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)}")

View 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"
}

View 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))

View File

@@ -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