/** * API Client for Mailing List Manager * Handles all communication with the FastAPI backend */ class APIClient { constructor() { this.baseURL = this.getBaseURL(); this.token = null; this.headers = { 'Content-Type': 'application/json' }; } /** * Get the base URL for API calls * Automatically detects if running in container or development */ getBaseURL() { const protocol = window.location.protocol; const hostname = window.location.hostname; // If running on localhost, assume development mode if (hostname === 'localhost' || hostname === '127.0.0.1') { return `${protocol}//${hostname}:8000`; } // If running in production behind a reverse proxy, use /api path return `${protocol}//${hostname}/api`; } /** * Set authentication token */ setToken(token) { this.token = token; this.headers['Authorization'] = `Bearer ${token}`; } /** * Clear authentication token */ clearToken() { this.token = null; delete this.headers['Authorization']; } /** * Make HTTP request to API */ async request(endpoint, options = {}) { const url = `${this.baseURL}${endpoint}`; const config = { headers: { ...this.headers }, ...options }; try { const response = await fetch(url, config); // Handle different response types if (response.status === 204) { return null; // No content } const contentType = response.headers.get('content-type'); let data; if (contentType && contentType.includes('application/json')) { data = await response.json(); } else { data = await response.text(); } if (!response.ok) { throw new APIError( data.detail || data || `HTTP ${response.status}`, response.status, data ); } return data; } catch (error) { if (error instanceof APIError) { throw error; } // Network or other errors throw new APIError( 'Network error or API unavailable', 0, { originalError: error.message } ); } } // Health and authentication methods async checkHealth() { return this.request('/health'); } async testAuth() { return this.request('/'); } async login(username, password) { // Don't include Authorization header for login const tempHeaders = { ...this.headers }; delete tempHeaders['Authorization']; const response = await this.request('/auth/login', { method: 'POST', headers: tempHeaders, body: JSON.stringify({ username, password }) }); // Set the token from the response if (response.access_token) { this.setToken(response.access_token); } return response; } async logout() { try { await this.request('/auth/logout', { method: 'POST' }); } finally { // Clear token even if logout fails this.clearToken(); } } async getCurrentUser() { return this.request('/auth/me'); } // Mailing Lists API async getLists() { return this.request('/lists'); } async getList(listId) { return this.request(`/lists/${listId}`); } async createList(listData) { return this.request('/lists', { method: 'POST', body: JSON.stringify(listData) }); } async updateList(listId, listData) { return this.request(`/lists/${listId}`, { method: 'PATCH', body: JSON.stringify(listData) }); } async deleteList(listId) { return this.request(`/lists/${listId}`, { method: 'DELETE' }); } // Members API async getMembers() { return this.request('/members'); } async getMember(memberId) { return this.request(`/members/${memberId}`); } async createMember(memberData) { return this.request('/members', { method: 'POST', body: JSON.stringify(memberData) }); } async updateMember(memberId, memberData) { return this.request(`/members/${memberId}`, { method: 'PATCH', body: JSON.stringify(memberData) }); } async deleteMember(memberId) { return this.request(`/members/${memberId}`, { method: 'DELETE' }); } // Subscriptions API async getListMembers(listId) { return this.request(`/lists/${listId}/members`); } async createSubscription(subscriptionData) { return this.request('/subscriptions', { method: 'POST', body: JSON.stringify(subscriptionData) }); } async deleteSubscription(listEmail, memberEmail) { const params = new URLSearchParams({ list_email: listEmail, member_email: memberEmail }); return this.request(`/subscriptions?${params}`, { method: 'DELETE' }); } /** * Bulk import members from CSV data */ async bulkImportMembers(csvData, listIds) { return this.request('/bulk-import', { method: 'POST', body: JSON.stringify({ csv_data: csvData, list_ids: listIds }) }); } // User Management API async getUsers() { return this.request('/users'); } async createUser(userData) { return this.request('/users', { method: 'POST', body: JSON.stringify(userData) }); } async updateUser(userId, userData) { return this.request(`/users/${userId}`, { method: 'PATCH', body: JSON.stringify(userData) }); } async deleteUser(userId) { return this.request(`/users/${userId}`, { method: 'DELETE' }); } // Bounce Management API async getMemberBounces(memberId) { return this.request(`/members/${memberId}/bounces`); } async resetBounceStatus(memberId) { return this.request(`/members/${memberId}/bounce-status`, { method: 'PATCH' }); } } /** * Custom API Error class */ class APIError extends Error { constructor(message, status = 0, details = null) { super(message); this.name = 'APIError'; this.status = status; this.details = details; } isAuthError() { return this.status === 401; } isNotFound() { return this.status === 404; } isBadRequest() { return this.status === 400; } isServerError() { return this.status >= 500; } } // Create global API client instance window.apiClient = new APIClient(); window.APIError = APIError;