ALwrity + Wordpress + Wix + GSC integration

This commit is contained in:
ajaysi
2025-10-08 10:13:14 +05:30
parent 14dfb2e5c0
commit 3bab3450dc
147 changed files with 19815 additions and 17053 deletions

View File

@@ -1,6 +1,7 @@
"""Google Search Console Authentication Router for ALwrity."""
from fastapi import APIRouter, HTTPException, Depends, Query
from fastapi.responses import HTMLResponse, JSONResponse
from typing import Dict, List, Any, Optional
from pydantic import BaseModel
from loguru import logger
@@ -39,10 +40,12 @@ async def get_gsc_auth_url(user: dict = Depends(get_current_user)):
auth_url = gsc_service.get_oauth_url(user_id)
logger.info(f"GSC OAuth URL generated successfully for user: {user_id}")
logger.info(f"OAuth URL: {auth_url[:100]}...")
return {"auth_url": auth_url}
except Exception as e:
logger.error(f"Error generating GSC OAuth URL: {e}")
logger.error(f"Error details: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error generating OAuth URL: {str(e)}")
@router.get("/callback")
@@ -50,7 +53,12 @@ async def handle_gsc_callback(
code: str = Query(..., description="Authorization code from Google"),
state: str = Query(..., description="State parameter for security")
):
"""Handle Google Search Console OAuth callback."""
"""Handle Google Search Console OAuth callback.
For a smoother UX when opened in a popup, this endpoint returns a tiny HTML
page that posts a completion message back to the opener window and closes
itself. The JSON payload is still included in the page for debugging.
"""
try:
logger.info(f"Handling GSC OAuth callback with code: {code[:10]}...")
@@ -58,14 +66,52 @@ async def handle_gsc_callback(
if success:
logger.info("GSC OAuth callback handled successfully")
return {"success": True, "message": "GSC connected successfully"}
html = """
<!doctype html>
<html>
<head><meta charset=\"utf-8\"><title>GSC Connected</title></head>
<body style=\"font-family: sans-serif; padding: 24px;\">
<p>Connection Successful. You can close this window.</p>
<script>
try {{ window.opener && window.opener.postMessage({{ type: 'GSC_AUTH_SUCCESS' }}, '*'); }} catch (e) {{}}
try {{ window.close(); }} catch (e) {{}}
</script>
</body>
</html>
"""
return HTMLResponse(content=html)
else:
logger.error("Failed to handle GSC OAuth callback")
raise HTTPException(status_code=400, detail="Failed to connect GSC")
html = """
<!doctype html>
<html>
<head><meta charset=\"utf-8\"><title>GSC Connection Failed</title></head>
<body style=\"font-family: sans-serif; padding: 24px;\">
<p>Connection Failed. Please close this window and try again.</p>
<script>
try {{ window.opener && window.opener.postMessage({{ type: 'GSC_AUTH_ERROR' }}, '*'); }} catch (e) {{}}
</script>
</body>
</html>
"""
return HTMLResponse(status_code=400, content=html)
except Exception as e:
logger.error(f"Error handling GSC OAuth callback: {e}")
raise HTTPException(status_code=500, detail=f"Error handling OAuth callback: {str(e)}")
html = f"""
<!doctype html>
<html>
<head><meta charset=\"utf-8\"><title>GSC Connection Error</title></head>
<body style=\"font-family: sans-serif; padding: 24px;\">
<p>Connection Error. Please close this window and try again.</p>
<pre style=\"white-space: pre-wrap;\">{str(e)}</pre>
<script>
try {{ window.opener && window.opener.postMessage({{ type: 'GSC_AUTH_ERROR' }}, '*'); }} catch (e) {{}}
</script>
</body>
</html>
"""
return HTMLResponse(status_code=500, content=html)
@router.get("/sites")
async def get_gsc_sites(user: dict = Depends(get_current_user)):
@@ -155,6 +201,8 @@ async def get_gsc_status(user: dict = Depends(get_current_user)):
sites = gsc_service.get_site_list(user_id)
except Exception as e:
logger.warning(f"Could not get sites for user {user_id}: {e}")
# Clear incomplete credentials and mark as disconnected
gsc_service.clear_incomplete_credentials(user_id)
connected = False
status_response = GSCStatusResponse(
@@ -193,6 +241,29 @@ async def disconnect_gsc(user: dict = Depends(get_current_user)):
logger.error(f"Error disconnecting GSC: {e}")
raise HTTPException(status_code=500, detail=f"Error disconnecting GSC: {str(e)}")
@router.post("/clear-incomplete")
async def clear_incomplete_credentials(user: dict = Depends(get_current_user)):
"""Clear incomplete GSC credentials that are missing required fields."""
try:
user_id = user.get('id')
if not user_id:
raise HTTPException(status_code=400, detail="User ID not found")
logger.info(f"Clearing incomplete GSC credentials for user: {user_id}")
success = gsc_service.clear_incomplete_credentials(user_id)
if success:
logger.info(f"Incomplete GSC credentials cleared for user: {user_id}")
return {"success": True, "message": "Incomplete credentials cleared"}
else:
logger.error(f"Failed to clear incomplete credentials for user: {user_id}")
raise HTTPException(status_code=500, detail="Failed to clear incomplete credentials")
except Exception as e:
logger.error(f"Error clearing incomplete credentials: {e}")
raise HTTPException(status_code=500, detail=f"Error clearing incomplete credentials: {str(e)}")
@router.get("/health")
async def gsc_health_check():
"""Health check for GSC service."""

View File

@@ -0,0 +1,409 @@
"""
WordPress API Routes
REST API endpoints for WordPress integration management.
"""
from fastapi import APIRouter, HTTPException, Depends, status
from fastapi.responses import JSONResponse
from typing import List, Optional, Dict, Any
from pydantic import BaseModel, HttpUrl
from loguru import logger
from services.integrations.wordpress_service import WordPressService
from services.integrations.wordpress_publisher import WordPressPublisher
from middleware.auth_middleware import get_current_user
router = APIRouter(prefix="/wordpress", tags=["WordPress"])
# Pydantic Models
class WordPressSiteRequest(BaseModel):
site_url: str
site_name: str
username: str
app_password: str
class WordPressSiteResponse(BaseModel):
id: int
site_url: str
site_name: str
username: str
is_active: bool
created_at: str
updated_at: str
class WordPressPublishRequest(BaseModel):
site_id: int
title: str
content: str
excerpt: Optional[str] = ""
featured_image_path: Optional[str] = None
categories: Optional[List[str]] = None
tags: Optional[List[str]] = None
status: str = "draft"
meta_description: Optional[str] = ""
class WordPressPublishResponse(BaseModel):
success: bool
post_id: Optional[int] = None
post_url: Optional[str] = None
error: Optional[str] = None
class WordPressPostResponse(BaseModel):
id: int
wp_post_id: int
title: str
status: str
published_at: Optional[str]
created_at: str
site_name: str
site_url: str
class WordPressStatusResponse(BaseModel):
connected: bool
sites: Optional[List[WordPressSiteResponse]] = None
total_sites: int = 0
# Initialize services
wp_service = WordPressService()
wp_publisher = WordPressPublisher()
@router.get("/status", response_model=WordPressStatusResponse)
async def get_wordpress_status(user: dict = Depends(get_current_user)):
"""Get WordPress connection status for the current user."""
try:
user_id = user.get('id')
if not user_id:
raise HTTPException(status_code=400, detail="User ID not found")
logger.info(f"Checking WordPress status for user: {user_id}")
# Get user's WordPress sites
sites = wp_service.get_all_sites(user_id)
if sites:
# Convert to response format
site_responses = [
WordPressSiteResponse(
id=site['id'],
site_url=site['site_url'],
site_name=site['site_name'],
username=site['username'],
is_active=site['is_active'],
created_at=site['created_at'],
updated_at=site['updated_at']
)
for site in sites
]
logger.info(f"Found {len(sites)} WordPress sites for user {user_id}")
return WordPressStatusResponse(
connected=True,
sites=site_responses,
total_sites=len(sites)
)
else:
logger.info(f"No WordPress sites found for user {user_id}")
return WordPressStatusResponse(
connected=False,
sites=[],
total_sites=0
)
except Exception as e:
logger.error(f"Error getting WordPress status for user {user_id}: {e}")
raise HTTPException(status_code=500, detail=f"Error checking WordPress status: {str(e)}")
@router.post("/sites", response_model=WordPressSiteResponse)
async def add_wordpress_site(
site_request: WordPressSiteRequest,
user: dict = Depends(get_current_user)
):
"""Add a new WordPress site connection."""
try:
user_id = user.get('id')
if not user_id:
raise HTTPException(status_code=400, detail="User ID not found")
logger.info(f"Adding WordPress site for user {user_id}: {site_request.site_name}")
# Add the site
success = wp_service.add_site(
user_id=user_id,
site_url=site_request.site_url,
site_name=site_request.site_name,
username=site_request.username,
app_password=site_request.app_password
)
if not success:
raise HTTPException(
status_code=400,
detail="Failed to connect to WordPress site. Please check your credentials."
)
# Get the added site info
sites = wp_service.get_all_sites(user_id)
if sites:
latest_site = sites[0] # Most recent site
return WordPressSiteResponse(
id=latest_site['id'],
site_url=latest_site['site_url'],
site_name=latest_site['site_name'],
username=latest_site['username'],
is_active=latest_site['is_active'],
created_at=latest_site['created_at'],
updated_at=latest_site['updated_at']
)
else:
raise HTTPException(status_code=500, detail="Site added but could not retrieve details")
except HTTPException:
raise
except Exception as e:
logger.error(f"Error adding WordPress site: {e}")
raise HTTPException(status_code=500, detail=f"Error adding WordPress site: {str(e)}")
@router.get("/sites", response_model=List[WordPressSiteResponse])
async def get_wordpress_sites(user: dict = Depends(get_current_user)):
"""Get all WordPress sites for the current user."""
try:
user_id = user.get('id')
if not user_id:
raise HTTPException(status_code=400, detail="User ID not found")
logger.info(f"Getting WordPress sites for user: {user_id}")
sites = wp_service.get_all_sites(user_id)
site_responses = [
WordPressSiteResponse(
id=site['id'],
site_url=site['site_url'],
site_name=site['site_name'],
username=site['username'],
is_active=site['is_active'],
created_at=site['created_at'],
updated_at=site['updated_at']
)
for site in sites
]
logger.info(f"Retrieved {len(sites)} WordPress sites for user {user_id}")
return site_responses
except Exception as e:
logger.error(f"Error getting WordPress sites for user {user_id}: {e}")
raise HTTPException(status_code=500, detail=f"Error retrieving WordPress sites: {str(e)}")
@router.delete("/sites/{site_id}")
async def disconnect_wordpress_site(
site_id: int,
user: dict = Depends(get_current_user)
):
"""Disconnect a WordPress site."""
try:
user_id = user.get('id')
if not user_id:
raise HTTPException(status_code=400, detail="User ID not found")
logger.info(f"Disconnecting WordPress site {site_id} for user {user_id}")
success = wp_service.disconnect_site(user_id, site_id)
if not success:
raise HTTPException(
status_code=404,
detail="WordPress site not found or already disconnected"
)
logger.info(f"WordPress site {site_id} disconnected successfully")
return {"success": True, "message": "WordPress site disconnected successfully"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error disconnecting WordPress site {site_id}: {e}")
raise HTTPException(status_code=500, detail=f"Error disconnecting WordPress site: {str(e)}")
@router.post("/publish", response_model=WordPressPublishResponse)
async def publish_to_wordpress(
publish_request: WordPressPublishRequest,
user: dict = Depends(get_current_user)
):
"""Publish content to a WordPress site."""
try:
user_id = user.get('id')
if not user_id:
raise HTTPException(status_code=400, detail="User ID not found")
logger.info(f"Publishing to WordPress site {publish_request.site_id} for user {user_id}")
# Publish the content
result = wp_publisher.publish_blog_post(
user_id=user_id,
site_id=publish_request.site_id,
title=publish_request.title,
content=publish_request.content,
excerpt=publish_request.excerpt,
featured_image_path=publish_request.featured_image_path,
categories=publish_request.categories,
tags=publish_request.tags,
status=publish_request.status,
meta_description=publish_request.meta_description
)
if result['success']:
logger.info(f"Content published successfully to WordPress: {result['post_id']}")
return WordPressPublishResponse(
success=True,
post_id=result['post_id'],
post_url=result.get('post_url')
)
else:
logger.error(f"Failed to publish content: {result['error']}")
return WordPressPublishResponse(
success=False,
error=result['error']
)
except Exception as e:
logger.error(f"Error publishing to WordPress: {e}")
return WordPressPublishResponse(
success=False,
error=f"Error publishing content: {str(e)}"
)
@router.get("/posts", response_model=List[WordPressPostResponse])
async def get_wordpress_posts(
site_id: Optional[int] = None,
user: dict = Depends(get_current_user)
):
"""Get published posts from WordPress sites."""
try:
user_id = user.get('id')
if not user_id:
raise HTTPException(status_code=400, detail="User ID not found")
logger.info(f"Getting WordPress posts for user {user_id}, site_id: {site_id}")
posts = wp_service.get_posts_for_site(user_id, site_id) if site_id else wp_service.get_posts_for_all_sites(user_id)
post_responses = [
WordPressPostResponse(
id=post['id'],
wp_post_id=post['wp_post_id'],
title=post['title'],
status=post['status'],
published_at=post['published_at'],
created_at=post['created_at'],
site_name=post['site_name'],
site_url=post['site_url']
)
for post in posts
]
logger.info(f"Retrieved {len(posts)} WordPress posts for user {user_id}")
return post_responses
except Exception as e:
logger.error(f"Error getting WordPress posts for user {user_id}: {e}")
raise HTTPException(status_code=500, detail=f"Error retrieving WordPress posts: {str(e)}")
@router.put("/posts/{post_id}/status")
async def update_post_status(
post_id: int,
status: str,
user: dict = Depends(get_current_user)
):
"""Update the status of a WordPress post (draft/publish)."""
try:
user_id = user.get('id')
if not user_id:
raise HTTPException(status_code=400, detail="User ID not found")
if status not in ['draft', 'publish', 'private']:
raise HTTPException(
status_code=400,
detail="Invalid status. Must be 'draft', 'publish', or 'private'"
)
logger.info(f"Updating WordPress post {post_id} status to {status} for user {user_id}")
success = wp_publisher.update_post_status(user_id, post_id, status)
if not success:
raise HTTPException(
status_code=404,
detail="Post not found or update failed"
)
logger.info(f"WordPress post {post_id} status updated to {status}")
return {"success": True, "message": f"Post status updated to {status}"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating WordPress post {post_id} status: {e}")
raise HTTPException(status_code=500, detail=f"Error updating post status: {str(e)}")
@router.delete("/posts/{post_id}")
async def delete_wordpress_post(
post_id: int,
force: bool = False,
user: dict = Depends(get_current_user)
):
"""Delete a WordPress post."""
try:
user_id = user.get('id')
if not user_id:
raise HTTPException(status_code=400, detail="User ID not found")
logger.info(f"Deleting WordPress post {post_id} for user {user_id}, force: {force}")
success = wp_publisher.delete_post(user_id, post_id, force)
if not success:
raise HTTPException(
status_code=404,
detail="Post not found or deletion failed"
)
logger.info(f"WordPress post {post_id} deleted successfully")
return {"success": True, "message": "Post deleted successfully"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error deleting WordPress post {post_id}: {e}")
raise HTTPException(status_code=500, detail=f"Error deleting post: {str(e)}")
@router.get("/health")
async def wordpress_health_check():
"""WordPress integration health check."""
try:
return {
"status": "healthy",
"service": "wordpress",
"timestamp": "2024-01-01T00:00:00Z",
"version": "1.0.0"
}
except Exception as e:
logger.error(f"WordPress health check failed: {e}")
raise HTTPException(status_code=500, detail="WordPress service unhealthy")

View File

@@ -0,0 +1,282 @@
"""
WordPress OAuth2 Routes
Handles WordPress.com OAuth2 authentication flow.
"""
from fastapi import APIRouter, Depends, HTTPException, status, Query
from fastapi.responses import RedirectResponse
from typing import Dict, Any, Optional
from pydantic import BaseModel
from loguru import logger
from services.integrations.wordpress_oauth import WordPressOAuthService
from middleware.auth_middleware import get_current_user
router = APIRouter(prefix="/wp", tags=["WordPress OAuth"])
# Initialize OAuth service
oauth_service = WordPressOAuthService()
# Pydantic Models
class WordPressOAuthResponse(BaseModel):
auth_url: str
state: str
class WordPressCallbackResponse(BaseModel):
success: bool
message: str
blog_url: Optional[str] = None
blog_id: Optional[str] = None
class WordPressStatusResponse(BaseModel):
connected: bool
sites: list
total_sites: int
@router.get("/auth/url", response_model=WordPressOAuthResponse)
async def get_wordpress_auth_url(
user: Dict[str, Any] = Depends(get_current_user)
):
"""Get WordPress 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="WordPress OAuth is not properly configured. Please check that WORDPRESS_CLIENT_ID and WORDPRESS_CLIENT_SECRET environment variables are set with valid WordPress.com application credentials."
)
return WordPressOAuthResponse(**auth_data)
except Exception as e:
logger.error(f"Error generating WordPress OAuth URL: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to generate WordPress OAuth URL."
)
@router.get("/callback")
async def handle_wordpress_callback(
code: str = Query(..., description="Authorization code from WordPress"),
state: str = Query(..., description="State parameter for security"),
error: Optional[str] = Query(None, description="Error from WordPress OAuth")
):
"""Handle WordPress OAuth2 callback."""
try:
if error:
logger.error(f"WordPress OAuth error: {error}")
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<title>WordPress.com Connection Failed</title>
<script>
// Send error message to parent window
window.onload = function() {{
window.parent.postMessage({{
type: 'WPCOM_OAUTH_ERROR',
success: false,
error: '{error}'
}}, '*');
window.close();
}};
</script>
</head>
<body>
<h1>Connection Failed</h1>
<p>There was an error connecting to WordPress.com.</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 WordPress OAuth callback")
html_content = """
<!DOCTYPE html>
<html>
<head>
<title>WordPress.com Connection Failed</title>
<script>
// Send error message to opener/parent window
window.onload = function() {{
(window.opener || window.parent).postMessage({{
type: 'WPCOM_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 WordPress OAuth code for token")
html_content = """
<!DOCTYPE html>
<html>
<head>
<title>WordPress.com Connection Failed</title>
<script>
// Send error message to opener/parent window
window.onload = function() {{
(window.opener || window.parent).postMessage({{
type: 'WPCOM_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
blog_url = result.get('blog_url', '')
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<title>WordPress.com Connection Successful</title>
<script>
// Send success message to opener/parent window
window.onload = function() {{
(window.opener || window.parent).postMessage({{
type: 'WPCOM_OAUTH_SUCCESS',
success: true,
blogUrl: '{blog_url}',
blogId: '{result.get('blog_id', '')}'
}}, '*');
window.close();
}};
</script>
</head>
<body>
<h1>Connection Successful!</h1>
<p>Your WordPress.com site 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 WordPress OAuth callback: {e}")
html_content = """
<!DOCTYPE html>
<html>
<head>
<title>WordPress.com Connection Failed</title>
<script>
// Send error message to opener/parent window
window.onload = function() {{
(window.opener || window.parent).postMessage({{
type: 'WPCOM_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=WordPressStatusResponse)
async def get_wordpress_oauth_status(
user: Dict[str, Any] = Depends(get_current_user)
):
"""Get WordPress 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 WordPressStatusResponse(**status_data)
except Exception as e:
logger.error(f"Error getting WordPress OAuth status: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get WordPress connection status."
)
@router.delete("/disconnect/{token_id}")
async def disconnect_wordpress_site(
token_id: int,
user: Dict[str, Any] = Depends(get_current_user)
):
"""Disconnect a WordPress 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="WordPress token not found or could not be disconnected."
)
return {"success": True, "message": f"WordPress site disconnected successfully."}
except Exception as e:
logger.error(f"Error disconnecting WordPress site: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to disconnect WordPress site."
)
@router.get("/health")
async def wordpress_oauth_health():
"""WordPress OAuth health check."""
return {
"status": "healthy",
"service": "wordpress_oauth",
"timestamp": "2024-01-01T00:00:00Z",
"version": "1.0.0"
}