""" 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)