/** * ApiClient - Handle communication with content API */ export class ApiClient { constructor(options = {}) { // Smart server detection based on environment const isDevelopment = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; const defaultEndpoint = isDevelopment ? 'http://localhost:8080/api/content' // Development: separate API server : '/api/content'; // Production: same-origin API this.baseUrl = options.apiEndpoint || defaultEndpoint; this.siteId = options.siteId || this.handleMissingSiteId(); // Log API configuration in development (keep for debugging) if (isDevelopment && !options.apiEndpoint) { console.log(`🔌 API Client: Using development server at ${this.baseUrl}`); } } /** * Get collections API URL (helper to avoid repetition) */ getCollectionsUrl() { return this.baseUrl.replace('/api/content', '/api/collections'); } async getContent(contentId) { try { const response = await fetch(`${this.baseUrl}/${contentId}?site_id=${this.siteId}`); return response.ok ? await response.json() : null; } catch (error) { console.warn('Failed to fetch content:', contentId, error); return null; } } async createContent(content, htmlMarkup) { try { const payload = { html_markup: htmlMarkup, // Always send HTML markup - server extracts ID or generates new one html_content: content, file_path: this.getCurrentFilePath() // Always include file path for consistent ID generation }; const response = await fetch(`${this.baseUrl}?site_id=${this.siteId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.getAuthToken()}` }, body: JSON.stringify(payload) }); if (response.ok) { const result = await response.json(); return result; } else { console.warn(`⚠️ Create failed (${response.status}): server will generate ID`); return null; } } catch (error) { if (error.name === 'TypeError' && error.message.includes('fetch')) { console.warn(`🔌 API Server not reachable at ${this.baseUrl}`); console.warn('💡 Start full-stack development: just dev'); } else { console.error('Failed to create content:', error); } return false; } } async updateContent(contentId, content) { try { const payload = { html_content: content }; const response = await fetch(`${this.baseUrl}/${contentId}?site_id=${this.siteId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.getAuthToken()}` }, body: JSON.stringify(payload) }); if (response.ok) { const result = await response.json(); return result; } else { console.warn(`⚠️ Update failed (${response.status}): ${contentId}`); return false; } } catch (error) { if (error.name === 'TypeError' && error.message.includes('fetch')) { console.warn(`🔌 API Server not reachable at ${this.baseUrl}`); console.warn('💡 Start full-stack development: just dev'); } else { console.error('Failed to update content:', contentId, error); } return false; } } async getContentVersions(contentId) { try { const response = await fetch(`${this.baseUrl}/${contentId}/versions?site_id=${this.siteId}`); if (response.ok) { const result = await response.json(); return result.versions || []; } else { console.warn(`⚠️ Failed to fetch versions (${response.status}): ${contentId}`); return []; } } catch (error) { console.error('Failed to fetch version history:', contentId, error); return []; } } async rollbackContent(contentId, versionId) { try { const response = await fetch(`${this.baseUrl}/${contentId}/rollback?site_id=${this.siteId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.getAuthToken()}` }, body: JSON.stringify({ version_id: versionId }) }); if (response.ok) { console.log(`✅ Content rolled back: ${contentId} to version ${versionId}`); return await response.json(); } else { console.warn(`⚠️ Rollback failed (${response.status}): ${contentId}`); return false; } } catch (error) { console.error('Failed to rollback content:', contentId, error); return false; } } // ============================================================================= // Collection API Methods // ============================================================================= /** * Create a new collection item * @param {string} collectionId - Collection ID * @param {number} templateId - Template ID to use (defaults to 1) * @param {string} htmlContent - Optional initial HTML content * @returns {Promise} Created collection item */ async createCollectionItem(collectionId, templateId = 1, htmlContent = '') { try { const collectionsUrl = this.getCollectionsUrl(); const payload = { site_id: this.siteId, template_id: templateId, html_content: htmlContent }; const response = await fetch(`${collectionsUrl}/${collectionId}/items`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.getAuthToken()}` }, body: JSON.stringify(payload) }); if (response.ok) { const result = await response.json(); return result; } else { const errorText = await response.text(); console.error(`❌ Failed to create collection item (${response.status}): ${errorText}`); throw new Error(`Failed to create collection item: ${response.status} ${errorText}`); } } catch (error) { console.error('❌ Error creating collection item:', error); throw error; } } /** * Delete a collection item * @param {string} collectionId - Collection ID * @param {string} itemId - Item ID to delete * @returns {Promise} Success status */ async deleteCollectionItem(collectionId, itemId) { try { const collectionsUrl = this.getCollectionsUrl(); const response = await fetch(`${collectionsUrl}/${collectionId}/items/${itemId}?site_id=${this.siteId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${this.getAuthToken()}` } }); if (response.ok) { return true; } else { const errorText = await response.text(); console.error(`❌ Failed to delete collection item (${response.status}): ${errorText}`); return false; } } catch (error) { console.error('❌ Error deleting collection item:', error); return false; } } /** * Get all collection items * @param {string} collectionId - Collection ID * @returns {Promise} Array of collection items */ async getCollectionItems(collectionId) { try { const collectionsUrl = this.getCollectionsUrl(); const response = await fetch(`${collectionsUrl}/${collectionId}/items?site_id=${this.siteId}`); if (response.ok) { const result = await response.json(); return result.items || []; } else { console.warn(`⚠️ Failed to fetch collection items (${response.status}): ${collectionId}`); return []; } } catch (error) { console.error('Failed to fetch collection items:', collectionId, error); return []; } } /** * Reorder collection items in bulk * @param {string} collectionId - Collection ID * @param {Array} itemOrder - Array of {itemId, position} objects * @returns {Promise} Success status */ async reorderCollection(collectionId, itemOrder) { try { const collectionsUrl = this.getCollectionsUrl(); const payload = { items: itemOrder }; const response = await fetch(`${collectionsUrl}/${collectionId}/reorder?site_id=${this.siteId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.getAuthToken()}` }, body: JSON.stringify(payload) }); if (response.ok) { return true; } else { const errorText = await response.text(); console.error(`❌ Failed to reorder collection (${response.status}): ${errorText}`); return false; } } catch (error) { console.error('❌ Error reordering collection:', error); return false; } } /** * Trigger site enhancement after collection changes * @returns {Promise} Success status */ async enhanceSite() { try { const enhanceUrl = this.baseUrl.replace('/api/content', '/api/enhance'); const response = await fetch(`${enhanceUrl}?site_id=${this.siteId}`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.getAuthToken()}` } }); if (response.ok) { const result = await response.json(); console.log('✅ Files enhanced successfully:', result); return true; } else { console.error(`❌ Failed to enhance files (${response.status})`); return false; } } catch (error) { console.error('❌ Error enhancing files:', error); return false; } } // ============================================================================= /** * Get authentication token for API requests * @returns {string} JWT token or mock token for development */ getAuthToken() { // Check if we have a real JWT token from OAuth const realToken = this.getStoredToken(); if (realToken && !this.isTokenExpired(realToken)) { return realToken; } // Development/mock token for when no real auth is present return this.getMockToken(); } /** * Get current user information from token * @returns {string} User identifier */ getCurrentUser() { const token = this.getAuthToken(); // If it's a mock token, return mock user if (token.startsWith('mock-')) { return 'anonymous'; } // Parse real JWT token for user info try { const payload = this.parseJWT(token); return payload.sub || payload.user_id || payload.email || 'anonymous'; } catch (error) { console.warn('Failed to parse JWT token:', error); return 'anonymous'; } } /** * Get stored JWT token from localStorage/sessionStorage * @returns {string|null} Stored JWT token */ getStoredToken() { // Try localStorage first (persistent), then sessionStorage (session-only) return localStorage.getItem('insertr_auth_token') || sessionStorage.getItem('insertr_auth_token') || null; } /** * Store JWT token for future requests * @param {string} token - JWT token from OAuth provider * @param {boolean} persistent - Whether to use localStorage (true) or sessionStorage (false) */ setStoredToken(token, persistent = true) { const storage = persistent ? localStorage : sessionStorage; storage.setItem('insertr_auth_token', token); // Clear the other storage to avoid conflicts const otherStorage = persistent ? sessionStorage : localStorage; otherStorage.removeItem('insertr_auth_token'); } /** * Clear stored authentication token */ clearStoredToken() { localStorage.removeItem('insertr_auth_token'); sessionStorage.removeItem('insertr_auth_token'); } /** * Generate mock JWT token for development/testing * @returns {string} Mock JWT token */ getMockToken() { // Create a mock JWT-like token for development // Format: mock-{user}-{timestamp}-{random} const user = 'anonymous'; const timestamp = Date.now(); const random = Math.random().toString(36).substr(2, 9); return `mock-${user}-${timestamp}-${random}`; } /** * Parse JWT token payload * @param {string} token - JWT token * @returns {object} Parsed payload */ parseJWT(token) { if (token.startsWith('mock-')) { // Return mock payload for development tokens return { sub: 'anonymous', user_id: 'anonymous', email: 'anonymous@localhost', iss: 'insertr-dev', exp: Date.now() + 24 * 60 * 60 * 1000 // 24 hours from now }; } try { // Parse real JWT token const parts = token.split('.'); if (parts.length !== 3) { throw new Error('Invalid JWT format'); } const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/'))); return payload; } catch (error) { throw new Error(`Failed to parse JWT token: ${error.message}`); } } /** * Check if JWT token is expired * @param {string} token - JWT token * @returns {boolean} True if token is expired */ isTokenExpired(token) { try { const payload = this.parseJWT(token); const now = Math.floor(Date.now() / 1000); return payload.exp && payload.exp < now; } catch (error) { // If we can't parse the token, consider it expired return true; } } /** * Initialize OAuth flow with provider (Google, GitHub, etc.) * @param {string} provider - OAuth provider ('google', 'github', etc.) * @returns {Promise} Success status */ async initiateOAuth(provider = 'google') { // This will be implemented when we add real OAuth integration console.log(`🔐 OAuth flow with ${provider} not yet implemented`); console.log('💡 For now, using mock authentication in development'); // Store a mock token for development const mockToken = this.getMockToken(); this.setStoredToken(mockToken, true); return true; } /** * Handle OAuth callback after user returns from provider * @param {URLSearchParams} urlParams - URL parameters from OAuth callback * @returns {Promise} Success status */ async handleOAuthCallback(urlParams) { // This will be implemented when we add real OAuth integration const code = urlParams.get('code'); const state = urlParams.get('state'); if (code) { console.log('🔐 OAuth callback received, exchanging code for token...'); // TODO: Exchange authorization code for JWT token // const token = await this.exchangeCodeForToken(code, state); // this.setStoredToken(token, true); return true; } return false; } /** * Handle missing site_id configuration * @returns {string} Site ID or throws error */ handleMissingSiteId() { console.error('❌ No site_id configured for Insertr.'); console.log('💡 Add data-site-id attribute to your script tag:'); console.log(' '); console.log(''); console.log('🚀 For demos under demos/, site_id is auto-derived from directory name.'); console.log('🏭 For production sites, you must explicitly set your site_id.'); // Return a placeholder that will cause API calls to fail gracefully return '__MISSING_SITE_ID__'; } /** * Get current file path from URL for consistent ID generation * @returns {string} File path like "index.html", "about.html" */ getCurrentFilePath() { const path = window.location.pathname; if (path === '/' || path === '') { return 'index.html'; } // Remove leading slash: "/about.html" → "about.html" return path.replace(/^\//, ''); } }