385 lines
10 KiB
Python
385 lines
10 KiB
Python
"""
|
|
WordPress Integration API Router.
|
|
|
|
Endpoints:
|
|
- POST /wordpress/sync_users: Synchronize users from WordPress
|
|
- POST /wordpress/verify_session: Verify WordPress session/token
|
|
- GET /wordpress/website/{website_id}/users: Get all users for a website
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Header, status
|
|
from sqlalchemy import func, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.database import get_db
|
|
from app.models.user import User
|
|
from app.models.website import Website
|
|
from app.schemas.wordpress import (
|
|
SyncUsersResponse,
|
|
SyncStatsResponse,
|
|
UserListResponse,
|
|
VerifySessionRequest,
|
|
VerifySessionResponse,
|
|
WordPressUserResponse,
|
|
)
|
|
from app.services.wordpress_auth import (
|
|
get_wordpress_user,
|
|
sync_wordpress_users,
|
|
verify_website_exists,
|
|
verify_wordpress_token,
|
|
get_or_create_user,
|
|
WordPressAPIError,
|
|
WordPressRateLimitError,
|
|
WordPressTokenInvalidError,
|
|
WebsiteNotFoundError,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/wordpress", tags=["wordpress"])
|
|
|
|
|
|
def get_website_id_from_header(
|
|
x_website_id: Optional[str] = Header(None, alias="X-Website-ID"),
|
|
) -> int:
|
|
"""
|
|
Extract and validate website_id from request header.
|
|
|
|
Args:
|
|
x_website_id: Website ID from header
|
|
|
|
Returns:
|
|
Validated website ID as integer
|
|
|
|
Raises:
|
|
HTTPException: If header is missing or invalid
|
|
"""
|
|
if x_website_id is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="X-Website-ID header is required",
|
|
)
|
|
try:
|
|
return int(x_website_id)
|
|
except ValueError:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="X-Website-ID must be a valid integer",
|
|
)
|
|
|
|
|
|
async def get_valid_website(
|
|
website_id: int,
|
|
db: AsyncSession,
|
|
) -> Website:
|
|
"""
|
|
Validate website_id exists and return Website model.
|
|
|
|
Args:
|
|
website_id: Website identifier
|
|
db: Database session
|
|
|
|
Returns:
|
|
Website model instance
|
|
|
|
Raises:
|
|
HTTPException: If website not found
|
|
"""
|
|
try:
|
|
return await verify_website_exists(website_id, db)
|
|
except WebsiteNotFoundError:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Website {website_id} not found",
|
|
)
|
|
|
|
|
|
@router.post(
|
|
"/sync_users",
|
|
response_model=SyncUsersResponse,
|
|
summary="Synchronize users from WordPress",
|
|
description="Fetch all users from WordPress API and sync to local database. Requires admin WordPress token.",
|
|
)
|
|
async def sync_users_endpoint(
|
|
db: AsyncSession = Depends(get_db),
|
|
website_id: int = Depends(get_website_id_from_header),
|
|
authorization: Optional[str] = Header(None, alias="Authorization"),
|
|
) -> SyncUsersResponse:
|
|
"""
|
|
Synchronize users from WordPress to local database.
|
|
|
|
Process:
|
|
1. Validate website_id exists
|
|
2. Extract admin token from Authorization header
|
|
3. Fetch all users from WordPress API
|
|
4. Upsert: Update existing users, insert new users
|
|
5. Return sync statistics
|
|
|
|
Args:
|
|
db: Database session
|
|
website_id: Website ID from header
|
|
authorization: Authorization header with Bearer token
|
|
|
|
Returns:
|
|
SyncUsersResponse with sync statistics
|
|
|
|
Raises:
|
|
HTTPException: If website not found, token invalid, or API error
|
|
"""
|
|
# Validate website exists
|
|
await get_valid_website(website_id, db)
|
|
|
|
# Extract token from Authorization header
|
|
if authorization is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Authorization header is required",
|
|
)
|
|
|
|
# Parse Bearer token
|
|
parts = authorization.split()
|
|
if len(parts) != 2 or parts[0].lower() != "bearer":
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid Authorization header format. Use: Bearer {token}",
|
|
)
|
|
|
|
admin_token = parts[1]
|
|
|
|
try:
|
|
sync_stats = await sync_wordpress_users(
|
|
website_id=website_id,
|
|
admin_token=admin_token,
|
|
db=db,
|
|
)
|
|
|
|
return SyncUsersResponse(
|
|
synced=SyncStatsResponse(
|
|
inserted=sync_stats.inserted,
|
|
updated=sync_stats.updated,
|
|
total=sync_stats.total,
|
|
errors=sync_stats.errors,
|
|
),
|
|
website_id=website_id,
|
|
message=f"Sync completed: {sync_stats.inserted} inserted, {sync_stats.updated} updated",
|
|
)
|
|
|
|
except WordPressTokenInvalidError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail=str(e),
|
|
)
|
|
except WordPressRateLimitError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
|
detail=str(e),
|
|
)
|
|
except WordPressAPIError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail=str(e),
|
|
)
|
|
except WebsiteNotFoundError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=str(e),
|
|
)
|
|
|
|
|
|
@router.post(
|
|
"/verify_session",
|
|
response_model=VerifySessionResponse,
|
|
summary="Verify WordPress session",
|
|
description="Verify WordPress JWT token and user identity.",
|
|
)
|
|
async def verify_session_endpoint(
|
|
request: VerifySessionRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> VerifySessionResponse:
|
|
"""
|
|
Verify WordPress session/token.
|
|
|
|
Process:
|
|
1. Validate website_id exists
|
|
2. Call WordPress API to verify token
|
|
3. Verify wp_user_id matches token owner
|
|
4. Get or create local user
|
|
5. Return validation result
|
|
|
|
Args:
|
|
request: VerifySessionRequest with wp_user_id, token, website_id
|
|
db: Database session
|
|
|
|
Returns:
|
|
VerifySessionResponse with validation result
|
|
|
|
Raises:
|
|
HTTPException: If website not found or API error
|
|
"""
|
|
# Validate website exists
|
|
await get_valid_website(request.website_id, db)
|
|
|
|
try:
|
|
# Verify token with WordPress
|
|
wp_user_info = await verify_wordpress_token(
|
|
token=request.token,
|
|
website_id=request.website_id,
|
|
wp_user_id=request.wp_user_id,
|
|
db=db,
|
|
)
|
|
|
|
if wp_user_info is None:
|
|
return VerifySessionResponse(
|
|
valid=False,
|
|
error="User ID mismatch or invalid credentials",
|
|
)
|
|
|
|
# Get or create local user
|
|
user = await get_or_create_user(
|
|
wp_user_id=request.wp_user_id,
|
|
website_id=request.website_id,
|
|
db=db,
|
|
)
|
|
|
|
return VerifySessionResponse(
|
|
valid=True,
|
|
user=WordPressUserResponse.model_validate(user),
|
|
wp_user_info={
|
|
"username": wp_user_info.username,
|
|
"email": wp_user_info.email,
|
|
"display_name": wp_user_info.display_name,
|
|
"roles": wp_user_info.roles,
|
|
},
|
|
)
|
|
|
|
except WordPressTokenInvalidError as e:
|
|
return VerifySessionResponse(
|
|
valid=False,
|
|
error=f"Invalid credentials: {str(e)}",
|
|
)
|
|
except WordPressRateLimitError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
|
detail=str(e),
|
|
)
|
|
except WordPressAPIError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail=str(e),
|
|
)
|
|
except WebsiteNotFoundError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=str(e),
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/website/{website_id}/users",
|
|
response_model=UserListResponse,
|
|
summary="Get users for website",
|
|
description="Retrieve all users for a specific website from local database with pagination.",
|
|
)
|
|
async def get_website_users(
|
|
website_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
page: int = 1,
|
|
page_size: int = 50,
|
|
) -> UserListResponse:
|
|
"""
|
|
Get all users for a website.
|
|
|
|
Args:
|
|
website_id: Website identifier
|
|
db: Database session
|
|
page: Page number (default: 1)
|
|
page_size: Number of users per page (default: 50, max: 100)
|
|
|
|
Returns:
|
|
UserListResponse with paginated user list
|
|
|
|
Raises:
|
|
HTTPException: If website not found
|
|
"""
|
|
# Validate website exists
|
|
await get_valid_website(website_id, db)
|
|
|
|
# Clamp page_size
|
|
page_size = min(max(1, page_size), 100)
|
|
page = max(1, page)
|
|
|
|
# Get total count
|
|
count_result = await db.execute(
|
|
select(func.count()).select_from(User).where(User.website_id == website_id)
|
|
)
|
|
total = count_result.scalar() or 0
|
|
|
|
# Calculate pagination
|
|
offset = (page - 1) * page_size
|
|
total_pages = (total + page_size - 1) // page_size if total > 0 else 1
|
|
|
|
# Get users
|
|
result = await db.execute(
|
|
select(User)
|
|
.where(User.website_id == website_id)
|
|
.order_by(User.id)
|
|
.offset(offset)
|
|
.limit(page_size)
|
|
)
|
|
users = result.scalars().all()
|
|
|
|
return UserListResponse(
|
|
users=[WordPressUserResponse.model_validate(user) for user in users],
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size,
|
|
total_pages=total_pages,
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/website/{website_id}/user/{wp_user_id}",
|
|
response_model=WordPressUserResponse,
|
|
summary="Get specific user",
|
|
description="Retrieve a specific user by WordPress user ID.",
|
|
)
|
|
async def get_user_endpoint(
|
|
website_id: int,
|
|
wp_user_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> WordPressUserResponse:
|
|
"""
|
|
Get a specific user by WordPress user ID.
|
|
|
|
Args:
|
|
website_id: Website identifier
|
|
wp_user_id: WordPress user ID
|
|
db: Database session
|
|
|
|
Returns:
|
|
WordPressUserResponse with user data
|
|
|
|
Raises:
|
|
HTTPException: If website or user not found
|
|
"""
|
|
# Validate website exists
|
|
await get_valid_website(website_id, db)
|
|
|
|
# Get user
|
|
user = await get_wordpress_user(
|
|
wp_user_id=wp_user_id,
|
|
website_id=website_id,
|
|
db=db,
|
|
)
|
|
|
|
if user is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"User {wp_user_id} not found for website {website_id}",
|
|
)
|
|
|
|
return WordPressUserResponse.model_validate(user)
|