Files
yellow-bank-soal/app/routers/wordpress.py
Dwindi Ramadhana cf193d7ea0 first commit
2026-03-21 23:32:59 +07:00

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)