278 lines
9.3 KiB
JavaScript
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(); |