# Site API Implementation (WordPress Site Management) This document describes the implementation of the WordPress Site Management API endpoints for the MaplePress frontend, integrated with the MaplePress backend API. ## Overview The Site API endpoints manage WordPress sites and their API credentials for the MaplePress plugin. Sites are automatically scoped to tenants (determined from JWT token) and include API key management, usage tracking, and search index configuration. ## Backend API Endpoints ### Create WordPress Site **Endpoint**: `POST /api/v1/sites` **Authentication**: Required (JWT token) ### List WordPress Sites **Endpoint**: `GET /api/v1/sites` **Authentication**: Required (JWT token) ### Get WordPress Site **Endpoint**: `GET /api/v1/sites/{id}` **Authentication**: Required (JWT token) ### Delete WordPress Site **Endpoint**: `DELETE /api/v1/sites/{id}` **Authentication**: Required (JWT token) ### Rotate Site API Key **Endpoint**: `POST /api/v1/sites/{id}/rotate-api-key` **Authentication**: Required (JWT token) **Documentation**: `/cloud/maplepress-backend/docs/API.md` (lines 805-1085) ## Request/Response Structures ### Create Site Request ```json { "site_url": "https://example.com" } ``` **Note**: The backend automatically extracts the domain from the `site_url`. **Headers Required:** - `Content-Type: application/json` - `Authorization: JWT {access_token}` ### Create Site Response ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "domain": "example.com", "site_url": "https://example.com", "api_key": "live_sk_1234567890abcdef...", "verification_token": "verify_abc123...", "status": "pending", "search_index_name": "site_550e8400-e29b-41d4-a716-446655440000" } ``` **⚠️ IMPORTANT**: The `api_key` is shown **ONLY ONCE** on site creation. Store it immediately! ### List Sites Request ``` GET /api/v1/sites?page_size=20&page_state=encoded_token ``` **Query Parameters:** - `page_size` (optional): Number of results per page (default: 20, max: 100) - `page_state` (optional): Pagination token from previous response ### List Sites Response ```json { "sites": [ { "id": "550e8400-...", "domain": "example.com", "status": "active", "is_verified": true, "created_at": "2024-10-24T00:00:00Z" } ], "page_state": "encoded_pagination_token" } ``` ### Get Site Response ```json { "id": "550e8400-...", "tenant_id": "850e8400-...", "domain": "example.com", "site_url": "https://example.com", "api_key_prefix": "live_sk_", "api_key_last_four": "f123", "status": "active", "is_verified": true, "search_index_name": "site_550e8400-...", "total_pages_indexed": 150, "last_indexed_at": "2024-10-24T12:00:00Z", "plugin_version": "1.0.0", "storage_used_bytes": 52428800, "search_requests_count": 1250, "monthly_pages_indexed": 45, "last_reset_at": "2024-10-01T00:00:00Z", "created_at": "2024-10-01T00:00:00Z", "updated_at": "2024-10-24T12:00:00Z" } ``` ### Rotate API Key Response ```json { "new_api_key": "live_sk_9876543210fedcba...", "old_key_last_four": "f123", "rotated_at": "2024-10-24T12:00:00Z" } ``` **⚠️ CRITICAL**: - The `new_api_key` is shown **ONLY ONCE**. Store it immediately! - The old API key is **immediately invalidated** - no grace period! - Your WordPress site will stop working until you update the plugin with the new key ## Frontend Implementation ### SiteService (`src/services/API/SiteService.js`) Handles all WordPress Site Management operations. **Key Features:** - Create new WordPress sites with API key generation - List all sites with pagination support - Get detailed site information including usage statistics - Hard delete sites (irreversible) - Rotate API keys with grace period - Client-side validation (domain and URL) - Response transformation (snake_case to camelCase) - User-friendly error message mapping **Methods:** #### `createSite(siteData)` Create a new WordPress site and generate API credentials. ```javascript import SiteService from './services/API/SiteService'; const site = await SiteService.createSite({ siteUrl: "https://example.com" }); console.log(site); // Output: // { // id: "550e8400-...", // domain: "example.com", // Extracted from siteUrl by backend // siteUrl: "https://example.com", // apiKey: "live_sk_...", // SAVE THIS NOW! // verificationToken: "verify_...", // status: "pending", // searchIndexName: "site_550e8400-..." // } ``` **Parameters:** - `siteData.siteUrl` (string, required): Full WordPress site URL (e.g., https://example.com) **Note**: The backend automatically extracts the domain (e.g., "example.com") from the `siteUrl`. **Returns:** ```javascript { id: string, // Site ID (UUID) domain: string, // Site domain siteUrl: string, // Full site URL apiKey: string, // API key (SHOWN ONLY ONCE!) verificationToken: string, // Token for plugin verification status: string, // Site status ("pending" or "active") searchIndexName: string // Meilisearch index name } ``` **Throws:** - "Site data is required" - If siteData is missing - "Site URL is required" - If siteUrl is missing - "Invalid site URL format" - If URL format is invalid - "Could not extract domain from URL" - If domain cannot be extracted - "This domain is already registered. Each domain can only be registered once." - Domain conflict (409) - "Authentication required. Please log in to continue." - Missing/invalid token #### `listSites(options)` List all WordPress sites for the authenticated user's tenant with pagination. ```javascript // First page const result = await SiteService.listSites({ pageSize: 20 }); console.log(result.sites); // Array of sites console.log(result.pageState); // Token for next page // Next page if (result.pageState) { const nextPage = await SiteService.listSites({ pageSize: 20, pageState: result.pageState }); } ``` **Parameters:** - `options.pageSize` (number, optional): Number of results per page (default: 20, max: 100) - `options.pageState` (string, optional): Pagination token from previous response **Returns:** ```javascript { sites: Array<{ id: string, domain: string, status: string, isVerified: boolean, createdAt: Date }>, pageState: string|null // Pagination token for next page } ``` **Throws:** - "Page size must be between 1 and 100" - Invalid page size - "Authentication required. Please log in to continue." - Missing/invalid token #### `getSiteById(siteId)` Get detailed information about a specific WordPress site including usage statistics. ```javascript const site = await SiteService.getSiteById("550e8400-..."); console.log(site.domain); // "example.com" console.log(site.totalPagesIndexed); // 150 console.log(site.storageUsedBytes); // 52428800 // Format storage for display const storage = SiteService.formatStorage(site.storageUsedBytes); console.log(storage); // "50 MB" ``` **Parameters:** - `siteId` (string, required): Site ID (UUID format) **Returns:** ```javascript { id: string, tenantId: string, domain: string, siteUrl: string, apiKeyPrefix: string, apiKeyLastFour: string, status: string, isVerified: boolean, searchIndexName: string, totalPagesIndexed: number, lastIndexedAt: Date|null, pluginVersion: string, storageUsedBytes: number, searchRequestsCount: number, monthlyPagesIndexed: number, lastResetAt: Date|null, createdAt: Date, updatedAt: Date } ``` **Throws:** - "Site ID is required" - If ID is missing - "Invalid site ID format" - If ID is not a valid UUID - "Site not found or doesn't belong to your organization." - If site doesn't exist (404) - "Authentication required. Please log in to continue." - Missing/invalid token #### `deleteSite(siteId)` Delete a WordPress site and all associated data (irreversible). ```javascript const result = await SiteService.deleteSite("550e8400-..."); console.log(result.success); // true console.log(result.message); // "Site deleted successfully" ``` **⚠️ WARNING**: This is a **hard delete** operation. All site data, including: - API keys (immediately invalidated) - Search index data - Usage statistics - Configuration ...will be permanently deleted and **cannot be recovered**. **Parameters:** - `siteId` (string, required): Site ID (UUID format) **Returns:** ```javascript { success: boolean, message: string } ``` **Throws:** - "Site ID is required" - If ID is missing - "Invalid site ID format" - If ID is not a valid UUID - "Site not found or doesn't belong to your organization." - If site doesn't exist (404) - "Authentication required. Please log in to continue." - Missing/invalid token #### `rotateApiKey(siteId)` Rotate a site's API key (use when the key is compromised). ```javascript const result = await SiteService.rotateApiKey("550e8400-..."); console.log(result.newApiKey); // "live_sk_..." - SAVE THIS NOW! console.log(result.oldKeyLastFour); // "f123" console.log(result.rotatedAt); // Date (now) ``` **🚨 CRITICAL**: - The `newApiKey` is shown **ONLY ONCE**. Store it immediately! - The old API key is **immediately invalidated** - no grace period! - Your WordPress site will stop working until you update the plugin with the new key - Update your WordPress plugin settings RIGHT NOW to restore functionality **Parameters:** - `siteId` (string, required): Site ID (UUID format) **Returns:** ```javascript { newApiKey: string, // New API key (shown only once!) oldKeyLastFour: string, // Last 4 chars of old key rotatedAt: Date // Rotation timestamp } ``` **Throws:** - "Site ID is required" - If ID is missing - "Invalid site ID format" - If ID is not a valid UUID - "Site not found or doesn't belong to your organization." - If site doesn't exist (404) - "Authentication required. Please log in to continue." - Missing/invalid token #### `validateDomain(domain)` Validate domain format. ```javascript const result = SiteService.validateDomain("example.com"); console.log(result); // { valid: true, error: null } const invalid = SiteService.validateDomain("invalid..domain"); console.log(invalid); // { valid: false, error: "Invalid domain format" } ``` **Returns:** `{ valid: boolean, error: string|null }` **Validation Rules:** - Required (non-empty) - Valid domain format (e.g., example.com, subdomain.example.com) - No protocol (http/https should be in siteUrl) #### `validateSiteUrl(url)` Validate site URL format. ```javascript const result = SiteService.validateSiteUrl("https://example.com"); console.log(result); // { valid: true, error: null } const invalid = SiteService.validateSiteUrl("ftp://example.com"); console.log(invalid); // { valid: false, error: "Site URL must use http or https protocol" } ``` **Returns:** `{ valid: boolean, error: string|null }` **Validation Rules:** - Required (non-empty) - Valid URL format - Must use http:// or https:// protocol #### `formatStorage(bytes)` Format storage bytes to human-readable format. ```javascript const formatted = SiteService.formatStorage(52428800); console.log(formatted); // "50 MB" const kb = SiteService.formatStorage(2048); console.log(kb); // "2 KB" ``` **Returns:** `string` (e.g., "50 MB", "2 KB", "1.5 GB") ## Data Flow ### Create Site Flow ``` User provides site URL ↓ SiteService.createSite() ↓ Validate URL (client-side) ↓ ApiClient.post() with JWT token ↓ Token automatically refreshed if needed ↓ POST /api/v1/sites with tenant from JWT ↓ Backend extracts domain from site URL ↓ Backend validates domain format ↓ Backend generates API key and verification token ↓ Backend creates Meilisearch index ↓ Backend returns site data with API key and extracted domain ↓ SiteService transforms response ↓ Component displays API key (ONLY TIME IT'S SHOWN!) ``` ### List Sites Flow ``` Component needs site list ↓ SiteService.listSites() ↓ ApiClient.get() with JWT token and pagination params ↓ Token automatically refreshed if needed ↓ GET /api/v1/sites with tenant from JWT ↓ Backend retrieves sites for tenant ↓ Backend returns paginated results ↓ SiteService transforms response ↓ Component receives sites array and next page token ``` ### Rotate API Key Flow ``` User requests key rotation ↓ SiteService.rotateApiKey() ↓ ApiClient.post() with JWT token ↓ Token automatically refreshed if needed ↓ POST /api/v1/sites/{id}/rotate-api-key ↓ Backend generates new API key ↓ Backend IMMEDIATELY invalidates old key (DELETE from DB) ↓ Backend inserts new key into database ↓ Backend returns new key (old key NO LONGER WORKS!) ↓ SiteService transforms response ↓ Component displays new key (ONLY TIME IT'S SHOWN!) ↓ User MUST update WordPress plugin NOW to restore functionality ``` ## Error Handling ### Error Types | Error Condition | Response | Frontend Behavior | |----------------|----------|-------------------| | Missing authentication | 401 Unauthorized | "Authentication required." | | Invalid site data | 400 Bad Request | Specific validation error | | Domain already exists | 409 Conflict | "Domain already registered by another user." | | Site not found | 404 Not Found | "Site not found or doesn't belong to your organization." | | Server error | 500 Internal Server Error | Generic error message | ## Usage Examples ### Create a New Site ```javascript import React, { useState } from 'react'; import SiteService from '../../services/API/SiteService'; function CreateSiteForm() { const [siteUrl, setSiteUrl] = useState(''); const [apiKey, setApiKey] = useState(''); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); const handleSubmit = async (e) => { e.preventDefault(); setError(''); setLoading(true); // Validate URL before sending const urlValidation = SiteService.validateSiteUrl(siteUrl); if (!urlValidation.valid) { setError(urlValidation.error); setLoading(false); return; } try { const site = await SiteService.createSite({ siteUrl }); // CRITICAL: Display API key immediately - it's shown only once! setApiKey(site.apiKey); alert(`IMPORTANT: Save this API key now! ${site.apiKey}`); console.log("Site created:", site); console.log("Domain extracted by backend:", site.domain); // Reset form setSiteUrl(''); } catch (err) { setError(err.message); } finally { setLoading(false); } }; return (
); } export default CreateSiteForm; ``` ### List Sites with Pagination ```javascript import React, { useEffect, useState } from 'react'; import SiteService from '../../services/API/SiteService'; function SiteList() { const [sites, setSites] = useState([]); const [pageState, setPageState] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const loadSites = async (nextPageState = null) => { try { setLoading(true); const result = await SiteService.listSites({ pageSize: 20, pageState: nextPageState }); if (nextPageState) { // Append to existing sites setSites(prev => [...prev, ...result.sites]); } else { // First page setSites(result.sites); } setPageState(result.pageState); } catch (err) { setError(err.message); } finally { setLoading(false); } }; useEffect(() => { loadSites(); }, []); return ({error}
} {sites.map(site => (Status: {site.status}
Verified: {site.isVerified ? 'Yes' : 'No'}
Created: {site.createdAt.toLocaleDateString()}
Loading sites...
} {pageState && !loading && ( )}URL: {site.siteUrl}
Status: {site.status}
Verified: {site.isVerified ? 'Yes' : 'No'}
Prefix: {site.apiKeyPrefix}
Last 4 digits: {site.apiKeyLastFour}
Total Pages Indexed: {site.totalPagesIndexed}
Monthly Pages Indexed: {site.monthlyPagesIndexed}
Storage Used: {SiteService.formatStorage(site.storageUsedBytes)}
Search Requests: {site.searchRequestsCount}
{site.lastIndexedAt && (Last Indexed: {site.lastIndexedAt.toLocaleString()}
)}Version: {site.pluginVersion || 'Not connected'}
Index Name: {site.searchIndexName}
Use this if your API key has been compromised.
{error &&{error}
} {newKey && ({newKey}
This key will not be shown again!
Old key is IMMEDIATELY INVALIDATED - Update your WordPress plugin NOW!
{error}
}This action cannot be undone!