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:
1259
static/js/app.js
1259
static/js/app.js
File diff suppressed because it is too large
Load Diff
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