Implement proper CORS configuration for secure cross-origin usage
- Add flask-cors dependency and configure CORS with security best practices - Support configurable CORS origins via environment variables - Separate admin endpoint CORS configuration for enhanced security - Create comprehensive CORS configuration documentation - Add apiClient utility for CORS-aware frontend requests - Include CORS test page for validation - Update README with CORS configuration instructions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		
							
								
								
									
										155
									
								
								static/js/src/apiClient.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								static/js/src/apiClient.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | ||||
| // 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<ApiClientConfig>): void { | ||||
|         this.config = { ...this.config, ...config }; | ||||
|     } | ||||
|      | ||||
|     // Make a fetch request with CORS support | ||||
|     async fetch(url: string, options: RequestInit = {}): Promise<Response> { | ||||
|         // 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<Response> { | ||||
|         return this.fetch(url, { ...options, method: 'GET' }); | ||||
|     } | ||||
|      | ||||
|     async post(url: string, body?: any, options?: RequestInit): Promise<Response> { | ||||
|         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<T>(url: string, options?: RequestInit): Promise<T> { | ||||
|         const response = await this.get(url, options); | ||||
|         if (!response.ok) { | ||||
|             throw new Error(`HTTP error! status: ${response.status}`); | ||||
|         } | ||||
|         return response.json(); | ||||
|     } | ||||
|      | ||||
|     async postJSON<T>(url: string, body?: any, options?: RequestInit): Promise<T> { | ||||
|         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(); | ||||
| @@ -21,10 +21,15 @@ import { Validator } from './validator'; | ||||
| import { StreamingTranslation } from './streamingTranslation'; | ||||
| import { PerformanceMonitor } from './performanceMonitor'; | ||||
| import { SpeakerManager } from './speakerManager'; | ||||
| // import { apiClient } from './apiClient'; // Available for cross-origin requests | ||||
|  | ||||
| // Initialize error boundary | ||||
| const errorBoundary = ErrorBoundary.getInstance(); | ||||
|  | ||||
| // Configure API client if needed for cross-origin requests | ||||
| // import { apiClient } from './apiClient'; | ||||
| // apiClient.configure({ baseUrl: 'https://api.talk2me.com', credentials: 'include' }); | ||||
|  | ||||
| document.addEventListener('DOMContentLoaded', function() { | ||||
|     // Set up global error handler | ||||
|     errorBoundary.setGlobalErrorHandler((error, errorInfo) => { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user