talk2me/static/js/src/apiClient.ts
Adolfo Delorenzo b08574efe5 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>
2025-06-02 23:51:27 -06:00

155 lines
5.4 KiB
TypeScript

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