"""
Wix Integration API Routes
Handles Wix authentication, connection status, and blog publishing.
"""
from fastapi import APIRouter, HTTPException, Depends, Request
from fastapi.responses import HTMLResponse
from typing import Dict, Any, Optional
from loguru import logger
from pydantic import BaseModel
from services.wix_service import WixService
from services.integrations.wix_oauth import WixOAuthService
from middleware.auth_middleware import get_current_user
import os
router = APIRouter(prefix="/api/wix", tags=["Wix Integration"])
# Initialize Wix service
wix_service = WixService()
# Initialize Wix OAuth service for token storage
wix_oauth_service = WixOAuthService(db_path=os.path.abspath("alwrity.db"))
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:
user_id = current_user.get('id')
if not user_id:
raise HTTPException(status_code=400, detail="User ID not found")
# Exchange code for tokens
tokens = wix_service.exchange_code_for_tokens(request.code)
# Get site information to extract site_id and member_id
site_info = wix_service.get_site_info(tokens['access_token'])
site_id = site_info.get('siteId') or site_info.get('site_id')
# Extract member_id from token if possible
member_id = None
try:
member_id = wix_service.extract_member_id_from_access_token(tokens['access_token'])
except Exception:
pass
# Check permissions
permissions = wix_service.check_blog_permissions(tokens['access_token'])
# Store tokens securely in database
stored = wix_oauth_service.store_tokens(
user_id=user_id,
access_token=tokens['access_token'],
refresh_token=tokens.get('refresh_token'),
expires_in=tokens.get('expires_in'),
token_type=tokens.get('token_type', 'Bearer'),
scope=tokens.get('scope'),
site_id=site_id,
member_id=member_id
)
if not stored:
logger.warning(f"Failed to store Wix tokens for user {user_id}, but OAuth succeeded")
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("/callback")
async def handle_oauth_callback_get(code: str, state: Optional[str] = None, request: Request = None, current_user: dict = Depends(get_current_user)):
"""HTML callback page for Wix OAuth that exchanges code and notifies opener via postMessage."""
try:
tokens = wix_service.exchange_code_for_tokens(code)
site_info = wix_service.get_site_info(tokens['access_token'])
permissions = wix_service.check_blog_permissions(tokens['access_token'])
# Store tokens in database if we have user_id
user_id = current_user.get('id') if current_user else None
if user_id:
site_id = site_info.get('siteId') or site_info.get('site_id')
member_id = None
try:
member_id = wix_service.extract_member_id_from_access_token(tokens['access_token'])
except Exception:
pass
stored = wix_oauth_service.store_tokens(
user_id=user_id,
access_token=tokens['access_token'],
refresh_token=tokens.get('refresh_token'),
expires_in=tokens.get('expires_in'),
token_type=tokens.get('token_type', 'Bearer'),
scope=tokens.get('scope'),
site_id=site_id,
member_id=member_id
)
if not stored:
logger.warning(f"Failed to store Wix tokens for user {user_id} in GET callback")
# Build success payload for postMessage
payload = {
"type": "WIX_OAUTH_SUCCESS",
"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
}
html = f"""
Wix Connected
"""
return HTMLResponse(content=html, headers={
"Cross-Origin-Opener-Policy": "unsafe-none",
"Cross-Origin-Embedder-Policy": "unsafe-none"
})
except Exception as e:
logger.error(f"Wix OAuth GET callback failed: {e}")
html = f"""
Wix Connection Failed
"""
return HTMLResponse(content=html, headers={
"Cross-Origin-Opener-Policy": "unsafe-none",
"Cross-Origin-Embedder-Policy": "unsafe-none"
})
@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:
# Check if user has Wix tokens stored in sessionStorage (frontend approach)
# This is a simplified check - in production you'd store tokens in database
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.get("/status")
async def get_wix_status(current_user: dict = Depends(get_current_user)) -> Dict[str, Any]:
"""
Get Wix connection status (similar to GSC/WordPress pattern)
Note: Wix tokens are stored in frontend sessionStorage, so we can't directly check them here.
The frontend will check sessionStorage and update the UI accordingly.
"""
try:
# Since Wix tokens are stored in frontend sessionStorage (not backend database),
# we return a default response. The frontend will check sessionStorage directly.
return {
"connected": False,
"sites": [],
"total_sites": 0,
"error": "Wix connection status managed by frontend sessionStorage"
}
except Exception as e:
logger.error(f"Failed to get Wix status: {e}")
return {
"connected": False,
"sites": [],
"total_sites": 0,
"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-POST,BLOG.READ-CATEGORY,"
"BLOG.CREATE-CATEGORY,BLOG.READ-TAG,BLOG.CREATE-TAG,"
"MEDIA.SITE_MEDIA_FILES_IMPORT"
"&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("/refresh-token")
async def refresh_wix_token(request: Dict[str, Any]) -> Dict[str, Any]:
"""
Refresh Wix access token using refresh token
Args:
request: Dict containing refresh_token
Returns:
New token information with access_token, refresh_token, expires_in
"""
try:
refresh_token = request.get("refresh_token")
if not refresh_token:
raise HTTPException(status_code=400, detail="Missing refresh_token")
# Refresh the token
new_tokens = wix_service.refresh_access_token(refresh_token)
return {
"success": True,
"access_token": new_tokens.get("access_token"),
"refresh_token": new_tokens.get("refresh_token"),
"expires_in": new_tokens.get("expires_in"),
"token_type": new_tokens.get("token_type", "Bearer")
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to refresh Wix token: {e}")
raise HTTPException(status_code=500, detail=f"Failed to refresh token: {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:
# Normalize access_token from payload (could be string, dict, or other format)
from services.integrations.wix.utils import normalize_token_string
raw_access_token = payload.get("access_token")
if not raw_access_token:
raise HTTPException(status_code=400, detail="Missing access_token")
# Normalize token to string (handles dict with accessToken.value, int, etc.)
access_token = normalize_token_string(raw_access_token)
if not access_token:
# Fallback: try to convert to string directly
access_token = str(raw_access_token).strip()
if not access_token or access_token == "None":
raise HTTPException(status_code=400, detail="Invalid access_token format")
# 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")
# Extract SEO metadata if provided
seo_metadata = payload.get("seo_metadata")
# Extract category/tag IDs or names
# Can be either:
# - IDs: List of UUID strings
# - Names: List of name strings (will be looked up/created)
category_ids = payload.get("category_ids") or payload.get("category_names")
tag_ids = payload.get("tag_ids") or payload.get("tag_names")
# If SEO metadata has categories/tags but they weren't explicitly provided, use them
if seo_metadata:
if not category_ids and seo_metadata.get("blog_categories"):
category_ids = seo_metadata.get("blog_categories")
if not tag_ids and seo_metadata.get("blog_tags"):
tag_ids = seo_metadata.get("blog_tags")
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=category_ids,
tag_ids=tag_ids,
publish=bool(payload.get("publish", True)),
member_id=member_id,
seo_metadata=seo_metadata,
)
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))