Added onboarding progress tracking & landing page
This commit is contained in:
465
backend/api/wix_routes.py
Normal file
465
backend/api/wix_routes.py
Normal file
@@ -0,0 +1,465 @@
|
||||
"""
|
||||
Wix Integration API Routes
|
||||
|
||||
Handles Wix authentication, connection status, and blog publishing.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, Request
|
||||
from typing import Dict, Any, Optional
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
|
||||
from services.wix_service import WixService
|
||||
from middleware.auth_middleware import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/wix", tags=["Wix Integration"])
|
||||
|
||||
# Initialize Wix service
|
||||
wix_service = WixService()
|
||||
|
||||
|
||||
class WixAuthRequest(BaseModel):
|
||||
"""Request model for Wix authentication"""
|
||||
code: str
|
||||
state: Optional[str] = None
|
||||
|
||||
|
||||
class WixPublishRequest(BaseModel):
|
||||
"""Request model for publishing to Wix"""
|
||||
title: str
|
||||
content: str
|
||||
cover_image_url: Optional[str] = None
|
||||
category_ids: Optional[list] = None
|
||||
tag_ids: Optional[list] = None
|
||||
publish: bool = True
|
||||
# Optional access token for test-real publish flow
|
||||
access_token: Optional[str] = None
|
||||
class WixCreateCategoryRequest(BaseModel):
|
||||
access_token: str
|
||||
label: str
|
||||
description: Optional[str] = None
|
||||
language: Optional[str] = None
|
||||
|
||||
|
||||
class WixCreateTagRequest(BaseModel):
|
||||
access_token: str
|
||||
label: str
|
||||
language: Optional[str] = None
|
||||
|
||||
|
||||
class WixConnectionStatus(BaseModel):
|
||||
"""Response model for Wix connection status"""
|
||||
connected: bool
|
||||
has_permissions: bool
|
||||
site_info: Optional[Dict[str, Any]] = None
|
||||
permissions: Optional[Dict[str, Any]] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
@router.get("/auth/url")
|
||||
async def get_authorization_url(state: Optional[str] = None) -> Dict[str, str]:
|
||||
"""
|
||||
Get Wix OAuth authorization URL
|
||||
|
||||
Args:
|
||||
state: Optional state parameter for security
|
||||
|
||||
Returns:
|
||||
Authorization URL
|
||||
"""
|
||||
try:
|
||||
url = wix_service.get_authorization_url(state)
|
||||
return {"authorization_url": url}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate authorization URL: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/auth/callback")
|
||||
async def handle_oauth_callback(request: WixAuthRequest, current_user: dict = Depends(get_current_user)) -> Dict[str, Any]:
|
||||
"""
|
||||
Handle OAuth callback and exchange code for tokens
|
||||
|
||||
Args:
|
||||
request: OAuth callback request with code
|
||||
current_user: Current authenticated user
|
||||
|
||||
Returns:
|
||||
Token information and connection status
|
||||
"""
|
||||
try:
|
||||
# Exchange code for tokens
|
||||
tokens = wix_service.exchange_code_for_tokens(request.code)
|
||||
|
||||
# Get site information
|
||||
site_info = wix_service.get_site_info(tokens['access_token'])
|
||||
|
||||
# Check permissions
|
||||
permissions = wix_service.check_blog_permissions(tokens['access_token'])
|
||||
|
||||
# TODO: Store tokens securely in database associated with current_user
|
||||
# For now, we'll return them (in production, store in encrypted database)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"tokens": {
|
||||
"access_token": tokens['access_token'],
|
||||
"refresh_token": tokens.get('refresh_token'),
|
||||
"expires_in": tokens.get('expires_in'),
|
||||
"token_type": tokens.get('token_type', 'Bearer')
|
||||
},
|
||||
"site_info": site_info,
|
||||
"permissions": permissions,
|
||||
"message": "Successfully connected to Wix"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to handle OAuth callback: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/connection/status")
|
||||
async def get_connection_status(current_user: dict = Depends(get_current_user)) -> WixConnectionStatus:
|
||||
"""
|
||||
Check Wix connection status and permissions
|
||||
|
||||
Args:
|
||||
current_user: Current authenticated user
|
||||
|
||||
Returns:
|
||||
Connection status and permissions
|
||||
"""
|
||||
try:
|
||||
# TODO: Retrieve stored tokens from database for current_user
|
||||
# For now, we'll return a mock response
|
||||
# In production, you'd check if tokens exist and are valid
|
||||
|
||||
return WixConnectionStatus(
|
||||
connected=False,
|
||||
has_permissions=False,
|
||||
error="No Wix connection found. Please connect your Wix account first."
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to check connection status: {e}")
|
||||
return WixConnectionStatus(
|
||||
connected=False,
|
||||
has_permissions=False,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/publish")
|
||||
async def publish_to_wix(request: WixPublishRequest, current_user: dict = Depends(get_current_user)) -> Dict[str, Any]:
|
||||
"""
|
||||
Publish blog post to Wix
|
||||
|
||||
Args:
|
||||
request: Blog post data
|
||||
current_user: Current authenticated user
|
||||
|
||||
Returns:
|
||||
Published blog post information
|
||||
"""
|
||||
try:
|
||||
# TODO: Retrieve stored access token from database for current_user
|
||||
# For now, we'll return an error asking user to connect first
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Wix account not connected. Please connect your Wix account first.",
|
||||
"message": "Use the /api/wix/auth/url endpoint to get the authorization URL"
|
||||
}
|
||||
|
||||
# Example of what the actual implementation would look like:
|
||||
# access_token = get_stored_access_token(current_user['id'])
|
||||
#
|
||||
# if not access_token:
|
||||
# raise HTTPException(status_code=401, detail="Wix account not connected")
|
||||
#
|
||||
# # Check if token is still valid, refresh if needed
|
||||
# try:
|
||||
# site_info = wix_service.get_site_info(access_token)
|
||||
# except:
|
||||
# # Token expired, try to refresh
|
||||
# refresh_token = get_stored_refresh_token(current_user['id'])
|
||||
# if refresh_token:
|
||||
# new_tokens = wix_service.refresh_access_token(refresh_token)
|
||||
# access_token = new_tokens['access_token']
|
||||
# # Store new tokens
|
||||
# else:
|
||||
# raise HTTPException(status_code=401, detail="Wix session expired. Please reconnect.")
|
||||
#
|
||||
# # Get current member ID (required for third-party apps)
|
||||
# member_info = wix_service.get_current_member(access_token)
|
||||
# member_id = member_info.get('member', {}).get('id')
|
||||
#
|
||||
# if not member_id:
|
||||
# raise HTTPException(status_code=400, detail="Could not retrieve member ID")
|
||||
#
|
||||
# # Create blog post
|
||||
# result = wix_service.create_blog_post(
|
||||
# access_token=access_token,
|
||||
# title=request.title,
|
||||
# content=request.content,
|
||||
# cover_image_url=request.cover_image_url,
|
||||
# category_ids=request.category_ids,
|
||||
# tag_ids=request.tag_ids,
|
||||
# publish=request.publish,
|
||||
# member_id=member_id # Required for third-party apps
|
||||
# )
|
||||
#
|
||||
# return {
|
||||
# "success": True,
|
||||
# "post_id": result.get('draftPost', {}).get('id'),
|
||||
# "url": result.get('draftPost', {}).get('url'),
|
||||
# "message": "Blog post published successfully to Wix"
|
||||
# }
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to publish to Wix: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/categories")
|
||||
async def get_blog_categories(current_user: dict = Depends(get_current_user)) -> Dict[str, Any]:
|
||||
"""
|
||||
Get available blog categories from Wix
|
||||
|
||||
Args:
|
||||
current_user: Current authenticated user
|
||||
|
||||
Returns:
|
||||
List of blog categories
|
||||
"""
|
||||
try:
|
||||
# TODO: Retrieve stored access token from database for current_user
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Wix account not connected. Please connect your Wix account first."
|
||||
}
|
||||
|
||||
# Example implementation:
|
||||
# access_token = get_stored_access_token(current_user['id'])
|
||||
# if not access_token:
|
||||
# raise HTTPException(status_code=401, detail="Wix account not connected")
|
||||
#
|
||||
# categories = wix_service.get_blog_categories(access_token)
|
||||
# return {"categories": categories}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get blog categories: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/tags")
|
||||
async def get_blog_tags(current_user: dict = Depends(get_current_user)) -> Dict[str, Any]:
|
||||
"""
|
||||
Get available blog tags from Wix
|
||||
|
||||
Args:
|
||||
current_user: Current authenticated user
|
||||
|
||||
Returns:
|
||||
List of blog tags
|
||||
"""
|
||||
try:
|
||||
# TODO: Retrieve stored access token from database for current_user
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Wix account not connected. Please connect your Wix account first."
|
||||
}
|
||||
|
||||
# Example implementation:
|
||||
# access_token = get_stored_access_token(current_user['id'])
|
||||
# if not access_token:
|
||||
# raise HTTPException(status_code=401, detail="Wix account not connected")
|
||||
#
|
||||
# tags = wix_service.get_blog_tags(access_token)
|
||||
# return {"tags": tags}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get blog tags: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/disconnect")
|
||||
async def disconnect_wix(current_user: dict = Depends(get_current_user)) -> Dict[str, Any]:
|
||||
"""
|
||||
Disconnect Wix account
|
||||
|
||||
Args:
|
||||
current_user: Current authenticated user
|
||||
|
||||
Returns:
|
||||
Disconnection status
|
||||
"""
|
||||
try:
|
||||
# TODO: Remove stored tokens from database for current_user
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Wix account disconnected successfully"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to disconnect Wix: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TEST ENDPOINTS - No authentication required for testing
|
||||
# =============================================================================
|
||||
|
||||
@router.get("/test/connection/status")
|
||||
async def get_test_connection_status() -> WixConnectionStatus:
|
||||
"""
|
||||
TEST ENDPOINT: Check Wix connection status without authentication
|
||||
|
||||
Returns:
|
||||
Connection status and permissions
|
||||
"""
|
||||
try:
|
||||
logger.info("TEST: Checking Wix connection status (no auth required)")
|
||||
|
||||
return WixConnectionStatus(
|
||||
connected=False,
|
||||
has_permissions=False,
|
||||
error="No stored tokens found. Please connect your Wix account first."
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"TEST: Failed to check connection status: {e}")
|
||||
return WixConnectionStatus(
|
||||
connected=False,
|
||||
has_permissions=False,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/test/auth/url")
|
||||
async def get_test_authorization_url(state: Optional[str] = None) -> Dict[str, str]:
|
||||
"""
|
||||
TEST ENDPOINT: Get Wix OAuth authorization URL without authentication
|
||||
|
||||
Args:
|
||||
state: Optional state parameter for security
|
||||
|
||||
Returns:
|
||||
Authorization URL for user to visit
|
||||
"""
|
||||
try:
|
||||
logger.info("TEST: Generating Wix authorization URL (no auth required)")
|
||||
|
||||
# Check if Wix service is properly configured
|
||||
if not wix_service.client_id:
|
||||
logger.warning("TEST: Wix Client ID not configured, returning mock URL")
|
||||
return {
|
||||
"url": "https://www.wix.com/oauth/access?client_id=YOUR_CLIENT_ID&redirect_uri=http://localhost:3000/wix/callback&response_type=code&scope=BLOG.CREATE-DRAFT,BLOG.PUBLISH,MEDIA.MANAGE&code_challenge=test&code_challenge_method=S256",
|
||||
"state": state or "test_state",
|
||||
"message": "WIX_CLIENT_ID not configured. Please set it in your .env file to get a real authorization URL."
|
||||
}
|
||||
|
||||
auth_url = wix_service.get_authorization_url(state)
|
||||
return {"url": auth_url, "state": state or "test_state"}
|
||||
except Exception as e:
|
||||
logger.error(f"TEST: Failed to generate authorization URL: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/test/publish")
|
||||
async def test_publish_to_wix(request: WixPublishRequest) -> Dict[str, Any]:
|
||||
"""
|
||||
TEST ENDPOINT: Simulate publishing a blog post to Wix without authentication.
|
||||
|
||||
Returns a fake success response so the frontend can validate the flow.
|
||||
"""
|
||||
try:
|
||||
logger.info("TEST: Simulating publish to Wix (no auth required)")
|
||||
return {
|
||||
"success": True,
|
||||
"post_id": "test_post_id",
|
||||
"url": "https://example.com/blog/test-post",
|
||||
"message": "Simulated blog post published successfully (test mode)"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"TEST: Failed to simulate publish: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/test/publish/real")
|
||||
async def test_publish_real(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
TEST ENDPOINT: Perform a real publish to Wix using a provided access token.
|
||||
|
||||
Notes:
|
||||
- Expects request.access_token from the frontend's Wix SDK tokens
|
||||
- Derives member_id server-side (required by Wix for third-party apps)
|
||||
"""
|
||||
try:
|
||||
access_token = payload.get("access_token")
|
||||
if not access_token:
|
||||
raise HTTPException(status_code=400, detail="Missing access_token")
|
||||
|
||||
# Derive current member id from token (try local decode first, then API fallback)
|
||||
member_id = wix_service.extract_member_id_from_access_token(access_token)
|
||||
if not member_id:
|
||||
member_info = wix_service.get_current_member(access_token)
|
||||
member_id = (
|
||||
(member_info.get("member") or {}).get("id")
|
||||
or member_info.get("id")
|
||||
)
|
||||
if not member_id:
|
||||
raise HTTPException(status_code=400, detail="Unable to resolve member_id from token")
|
||||
|
||||
result = wix_service.create_blog_post(
|
||||
access_token=access_token,
|
||||
title=payload.get("title") or "Untitled",
|
||||
content=payload.get("content") or "",
|
||||
cover_image_url=payload.get("cover_image_url"),
|
||||
category_ids=payload.get("category_ids") or None,
|
||||
tag_ids=payload.get("tag_ids") or None,
|
||||
publish=bool(payload.get("publish", True)),
|
||||
member_id=member_id,
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"post_id": (result.get("draftPost") or result.get("post") or {}).get("id"),
|
||||
"url": (result.get("draftPost") or result.get("post") or {}).get("url"),
|
||||
"message": "Blog post published to Wix",
|
||||
"raw": result,
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"TEST: Real publish failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/test/category")
|
||||
async def test_create_category(request: WixCreateCategoryRequest) -> Dict[str, Any]:
|
||||
try:
|
||||
result = wix_service.create_category(
|
||||
access_token=request.access_token,
|
||||
label=request.label,
|
||||
description=request.description,
|
||||
language=request.language,
|
||||
)
|
||||
return {"success": True, "category": result.get("category", {}), "raw": result}
|
||||
except Exception as e:
|
||||
logger.error(f"TEST: Create category failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/test/tag")
|
||||
async def test_create_tag(request: WixCreateTagRequest) -> Dict[str, Any]:
|
||||
try:
|
||||
result = wix_service.create_tag(
|
||||
access_token=request.access_token,
|
||||
label=request.label,
|
||||
language=request.language,
|
||||
)
|
||||
return {"success": True, "tag": result.get("tag", {}), "raw": result}
|
||||
except Exception as e:
|
||||
logger.error(f"TEST: Create tag failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
Reference in New Issue
Block a user