- 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>
155 lines
5.4 KiB
TypeScript
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(); |