/** * API service with automatic token refresh functionality */ interface ApiRequestInit extends RequestInit { skipRetry?: boolean // Flag to prevent infinite retry loops } class ApiService { private baseURL = 'http://localhost:5000' private isRefreshing = false private refreshPromise: Promise | null = null /** * Main API request method with automatic token refresh */ async request(endpoint: string, options: ApiRequestInit = {}): Promise { const url = `${this.baseURL}${endpoint}` const config: RequestInit = { credentials: 'include', headers: { 'Content-Type': 'application/json', ...options.headers, }, ...options, } try { const response = await fetch(url, config) // If request succeeds or it's not a 401, return response if (response.ok || response.status !== 401 || options.skipRetry) { return response } // Handle 401 - try to refresh token const refreshed = await this.handleTokenRefresh() if (refreshed) { // Retry the original request once return this.request(endpoint, { ...options, skipRetry: true }) } else { // Refresh failed, return the original 401 response return response } } catch (error) { console.error('API request failed:', error) throw error } } /** * Handle token refresh with deduplication */ private async handleTokenRefresh(): Promise { // If already refreshing, wait for the current refresh to complete if (this.isRefreshing && this.refreshPromise) { return this.refreshPromise } // Start new refresh process this.isRefreshing = true this.refreshPromise = this.performTokenRefresh() try { const result = await this.refreshPromise return result } finally { this.isRefreshing = false this.refreshPromise = null } } /** * Perform the actual token refresh */ private async performTokenRefresh(): Promise { try { const response = await fetch(`${this.baseURL}/api/auth/refresh`, { method: 'POST', credentials: 'include', }) if (response.ok) { console.log('Token refreshed successfully') return true } else { console.log('Token refresh failed:', response.status) // If refresh token is also expired (401), trigger logout if (response.status === 401) { this.handleLogout() } return false } } catch (error) { console.error('Token refresh error:', error) return false } } /** * Handle logout when refresh token expires */ private handleLogout() { // Clear any local storage or state if needed console.log('Refresh token expired, user needs to login again') // Dispatch a custom event that the AuthContext can listen to window.dispatchEvent(new CustomEvent('auth:refresh-token-expired')) } /** * Convenience methods for common HTTP verbs */ async get(endpoint: string, options: ApiRequestInit = {}) { return this.request(endpoint, { ...options, method: 'GET' }) } async post(endpoint: string, data?: unknown, options: ApiRequestInit = {}) { return this.request(endpoint, { ...options, method: 'POST', body: data ? JSON.stringify(data) : undefined, }) } async patch(endpoint: string, data?: unknown, options: ApiRequestInit = {}) { return this.request(endpoint, { ...options, method: 'PATCH', body: data ? JSON.stringify(data) : undefined, }) } async put(endpoint: string, data?: unknown, options: ApiRequestInit = {}) { return this.request(endpoint, { ...options, method: 'PUT', body: data ? JSON.stringify(data) : undefined, }) } async delete(endpoint: string, options: ApiRequestInit = {}) { return this.request(endpoint, { ...options, method: 'DELETE' }) } } // Export singleton instance export const apiService = new ApiService()