admin 4d8b9effd0 feat(blog): add detailed update on Influence and Map app developments since August
A bunch of udpates to the listmonk sync to add influence to it
2025-10-25 12:45:35 -06:00

278 lines
9.3 KiB
JavaScript

// 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();