// API Client for making requests to the backend class APIClient { constructor() { this.baseURL = '/api'; this.csrfToken = null; this.csrfTokenPromise = null; } /** * Fetch CSRF token from the server */ async fetchCsrfToken() { // If we're already fetching, return the existing promise if (this.csrfTokenPromise) { return this.csrfTokenPromise; } this.csrfTokenPromise = (async () => { try { console.log('Fetching CSRF token from server...'); const response = await fetch(`${this.baseURL}/csrf-token`, { credentials: 'include' // Important: include cookies }); const data = await response.json(); this.csrfToken = data.csrfToken; console.log('CSRF token received:', this.csrfToken ? 'Token obtained' : 'No token'); return this.csrfToken; } catch (error) { console.error('Failed to fetch CSRF token:', error); this.csrfToken = null; throw error; } finally { this.csrfTokenPromise = null; } })(); return this.csrfTokenPromise; } /** * Ensure we have a valid CSRF token */ async ensureCsrfToken() { if (!this.csrfToken) { await this.fetchCsrfToken(); } return this.csrfToken; } async makeRequest(endpoint, options = {}, isRetry = false) { // For state-changing methods, ensure we have a CSRF token const needsCsrf = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(options.method); if (needsCsrf) { await this.ensureCsrfToken(); } const config = { headers: { 'Content-Type': 'application/json', ...(needsCsrf && this.csrfToken ? { 'X-CSRF-Token': this.csrfToken } : {}), ...options.headers }, credentials: 'include', // Important: include cookies for CSRF ...options }; try { const response = await fetch(`${this.baseURL}${endpoint}`, config); const data = await response.json(); // If response includes a new CSRF token, update it if (data.csrfToken) { this.csrfToken = data.csrfToken; } if (!response.ok) { // If CSRF token is invalid and we haven't retried yet, fetch a new one and retry once if (!isRetry && response.status === 403 && data.error && data.error.includes('CSRF')) { console.log('CSRF token invalid, fetching new token and retrying...'); this.csrfToken = null; await this.fetchCsrfToken(); // Retry the request once with new token return this.makeRequest(endpoint, options, true); } // Create enhanced error with response data for better error handling const error = new Error(data.error || data.message || `HTTP ${response.status}`); error.status = response.status; error.data = data; throw error; } return data; } catch (error) { console.error('API request failed:', error); throw error; } } async get(endpoint) { return this.makeRequest(endpoint, { method: 'GET' }); } async post(endpoint, data) { return this.makeRequest(endpoint, { method: 'POST', body: JSON.stringify(data) }); } async put(endpoint, data) { return this.makeRequest(endpoint, { method: 'PUT', body: JSON.stringify(data) }); } async patch(endpoint, data) { return this.makeRequest(endpoint, { method: 'PATCH', body: JSON.stringify(data) }); } async delete(endpoint) { return this.makeRequest(endpoint, { method: 'DELETE' }); } async postFormData(endpoint, formData, isRetry = false) { // Ensure we have a CSRF token for POST requests await this.ensureCsrfToken(); console.log('Sending FormData with CSRF token:', this.csrfToken ? 'Token present' : 'No token'); // Add CSRF token to form data AND headers if (this.csrfToken) { formData.set('_csrf', this.csrfToken); } // Don't set Content-Type header - browser will set it with boundary for multipart/form-data // But DO set CSRF token header const config = { method: 'POST', headers: { ...(this.csrfToken ? { 'X-CSRF-Token': this.csrfToken } : {}) }, body: formData, credentials: 'include' // Important: include cookies for CSRF }; try { const response = await fetch(`${this.baseURL}${endpoint}`, config); const data = await response.json(); // If response includes a new CSRF token, update it if (data.csrfToken) { this.csrfToken = data.csrfToken; } if (!response.ok) { // If CSRF token is invalid and we haven't retried yet, fetch a new one and retry once if (!isRetry && response.status === 403 && data.error && data.error.includes('CSRF')) { console.log('CSRF token invalid, fetching new token and retrying...'); this.csrfToken = null; await this.fetchCsrfToken(); // Update form data with new token formData.set('_csrf', this.csrfToken); // Retry the request once with new token return this.postFormData(endpoint, formData, true); } const error = new Error(data.error || data.message || `HTTP ${response.status}`); error.status = response.status; error.data = data; throw error; } return data; } catch (error) { console.error('API request failed:', error); throw error; } } async putFormData(endpoint, formData, isRetry = false) { // Ensure we have a CSRF token for PUT requests await this.ensureCsrfToken(); // Add CSRF token to form data AND headers if (this.csrfToken) { formData.set('_csrf', this.csrfToken); } // Don't set Content-Type header - browser will set it with boundary for multipart/form-data // But DO set CSRF token header const config = { method: 'PUT', headers: { ...(this.csrfToken ? { 'X-CSRF-Token': this.csrfToken } : {}) }, body: formData, credentials: 'include' // Important: include cookies for CSRF }; try { const response = await fetch(`${this.baseURL}${endpoint}`, config); const data = await response.json(); // If response includes a new CSRF token, update it if (data.csrfToken) { this.csrfToken = data.csrfToken; } if (!response.ok) { // If CSRF token is invalid and we haven't retried yet, fetch a new one and retry once if (!isRetry && response.status === 403 && data.error && data.error.includes('CSRF')) { console.log('CSRF token invalid, fetching new token and retrying...'); this.csrfToken = null; await this.fetchCsrfToken(); // Update form data with new token formData.set('_csrf', this.csrfToken); // Retry the request once with new token return this.putFormData(endpoint, formData, true); } const error = new Error(data.error || data.message || `HTTP ${response.status}`); error.status = response.status; error.data = data; throw error; } return data; } catch (error) { console.error('API request failed:', error); throw error; } } // Health check async checkHealth() { return this.get('/health'); } // Test Represent API connection async testRepresent() { return this.get('/test-represent'); } // Get representatives by postal code async getRepresentativesByPostalCode(postalCode) { const cleanPostalCode = postalCode.replace(/\s/g, '').toUpperCase(); return this.get(`/representatives/by-postal/${cleanPostalCode}`); } // Refresh representatives for postal code async refreshRepresentatives(postalCode) { const cleanPostalCode = postalCode.replace(/\s/g, '').toUpperCase(); return this.post(`/representatives/refresh-postal/${cleanPostalCode}`); } // Send email to representative async sendEmail(emailData) { return this.post('/emails/send', emailData); } // Preview email before sending async previewEmail(emailData) { return this.post('/emails/preview', emailData); } } // Create global instance window.apiClient = new APIClient();