export interface ApiResponse { error?: string; success: boolean; data?: T; message?: string; requires2FA?: boolean; } class ApiClient { private getBaseURL(): string { if (typeof window === 'undefined') { // Server-side calls downstream Spring Boot return process.env.API_URL_SERVER || 'http://localhost:8080'; } // Client-side calls Next.js API Routes proxy return process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api'; } private async request( method: string, endpoint: string, body?: any, config?: RequestInit & { timeoutMs?: number } ): Promise { const baseURL = this.getBaseURL(); const url = `${baseURL}${endpoint}`; const headers: Record = { 'Content-Type': 'application/json', ...(config?.headers as any), }; const controller = new AbortController(); const id = setTimeout(() => controller.abort(), config?.timeoutMs || 10000); const options: RequestInit = { method, headers, signal: controller.signal, ...config, }; if (body !== undefined) { options.body = JSON.stringify(body); } try { const res = await fetch(url, options); clearTimeout(id); if (!res.ok && res.status !== 400 && res.status !== 401 && res.status !== 403) { throw new Error(`HTTP error! status: ${res.status}`); } return await res.json(); } catch (err: any) { clearTimeout(id); if (err.name === 'AbortError') { throw new Error('Request timeout'); } throw err; } } get(endpoint: string, config?: RequestInit & { timeoutMs?: number }) { return this.request('GET', endpoint, undefined, config); } post(endpoint: string, data?: any, config?: RequestInit & { timeoutMs?: number }) { return this.request('POST', endpoint, data, config); } put(endpoint: string, data?: any, config?: RequestInit & { timeoutMs?: number }) { return this.request('PUT', endpoint, data, config); } delete(endpoint: string, config?: RequestInit & { timeoutMs?: number }) { return this.request('DELETE', endpoint, undefined, config); } } export const apiClient = new ApiClient();