// API Client with CORS support export interface ApiClientConfig { baseUrl?: string; credentials?: RequestCredentials; headers?: HeadersInit; } export class ApiClient { private static instance: ApiClient; private config: ApiClientConfig; private constructor() { // Default configuration this.config = { baseUrl: '', // Use same origin by default credentials: 'same-origin', // Change to 'include' for cross-origin requests headers: { 'X-Requested-With': 'XMLHttpRequest' // Identify as AJAX request } }; // Check if we're in a cross-origin context this.detectCrossOrigin(); } static getInstance(): ApiClient { if (!ApiClient.instance) { ApiClient.instance = new ApiClient(); } return ApiClient.instance; } // Detect if we're making cross-origin requests private detectCrossOrigin(): void { // Check if the app is loaded from a different origin const currentScript = document.currentScript as HTMLScriptElement | null; const scriptSrc = currentScript?.src || ''; if (scriptSrc && !scriptSrc.startsWith(window.location.origin)) { // We're likely in a cross-origin context this.config.credentials = 'include'; console.log('Cross-origin context detected, enabling credentials'); } // Also check for explicit configuration in meta tags const corsOrigin = document.querySelector('meta[name="cors-origin"]'); if (corsOrigin) { const origin = corsOrigin.getAttribute('content'); if (origin && origin !== window.location.origin) { this.config.baseUrl = origin; this.config.credentials = 'include'; console.log(`Using CORS origin: ${origin}`); } } } // Configure the API client configure(config: Partial): void { this.config = { ...this.config, ...config }; } // Make a fetch request with CORS support async fetch(url: string, options: RequestInit = {}): Promise { // Construct full URL const fullUrl = this.config.baseUrl ? `${this.config.baseUrl}${url}` : url; // Merge headers const headers = new Headers(options.headers); if (this.config.headers) { const configHeaders = new Headers(this.config.headers); configHeaders.forEach((value, key) => { if (!headers.has(key)) { headers.set(key, value); } }); } // Merge options with defaults const fetchOptions: RequestInit = { ...options, headers, credentials: options.credentials || this.config.credentials }; // Add CORS mode if cross-origin if (this.config.baseUrl && this.config.baseUrl !== window.location.origin) { fetchOptions.mode = 'cors'; } try { const response = await fetch(fullUrl, fetchOptions); // Check for CORS errors if (!response.ok && response.type === 'opaque') { throw new Error('CORS request failed - check server CORS configuration'); } return response; } catch (error) { // Enhanced error handling for CORS issues if (error instanceof TypeError && error.message.includes('Failed to fetch')) { console.error('CORS Error: Failed to fetch. Check that:', { requestedUrl: fullUrl, origin: window.location.origin, credentials: fetchOptions.credentials, mode: fetchOptions.mode }); throw new Error('CORS request failed. The server may not allow requests from this origin.'); } throw error; } } // Convenience methods async get(url: string, options?: RequestInit): Promise { return this.fetch(url, { ...options, method: 'GET' }); } async post(url: string, body?: any, options?: RequestInit): Promise { const init: RequestInit = { ...options, method: 'POST' }; if (body) { if (body instanceof FormData) { init.body = body; } else { init.headers = { ...init.headers, 'Content-Type': 'application/json' }; init.body = JSON.stringify(body); } } return this.fetch(url, init); } // JSON convenience methods async getJSON(url: string, options?: RequestInit): Promise { const response = await this.get(url, options); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } async postJSON(url: string, body?: any, options?: RequestInit): Promise { const response = await this.post(url, body, options); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } } // Export a singleton instance export const apiClient = ApiClient.getInstance();