/** * Authentication API Service * Handles communication with backend auth endpoints */ import axios, { AxiosInstance } from 'axios'; import { TokenManager } from '../utils/tokenManager'; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api/v1'; // Create axios instance with default config const apiClient: AxiosInstance = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json', }, withCredentials: true, // Important for cookie-based auth in localhost }); // Request interceptor to add access token apiClient.interceptors.request.use( (config) => { const token = TokenManager.getAccessToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => { return Promise.reject(error); } ); // Response interceptor to handle token refresh apiClient.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; // If error is 401 and we haven't retried yet if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; try { // Attempt to refresh token const refreshToken = TokenManager.getRefreshToken(); if (!refreshToken) { throw new Error('No refresh token available'); } const response = await axios.post( `${API_BASE_URL}/auth/refresh`, { refreshToken }, { withCredentials: true } ); const { accessToken } = response.data.data || response.data; if (accessToken) { TokenManager.setAccessToken(accessToken); originalRequest.headers.Authorization = `Bearer ${accessToken}`; return apiClient(originalRequest); } } catch (refreshError) { // Refresh failed, clear tokens and redirect to login TokenManager.clearAll(); window.location.href = '/'; return Promise.reject(refreshError); } } return Promise.reject(error); } ); export interface TokenExchangeResponse { user: { userId: string; employeeId: string; email: string; firstName: string; lastName: string; displayName: string; department?: string; designation?: string; isAdmin: boolean; }; accessToken: string; refreshToken: string; idToken?: string; // ID token from Okta for logout } export interface RefreshTokenResponse { accessToken: string; } /** * Exchange authorization code for tokens (localhost only) */ export async function exchangeCodeForTokens( code: string, redirectUri: string ): Promise { console.log('🔄 Exchange Code for Tokens:', { code: code ? `${code.substring(0, 10)}...` : 'MISSING', redirectUri, endpoint: `${API_BASE_URL}/auth/token-exchange`, }); try { const response = await apiClient.post( '/auth/token-exchange', { code, redirectUri, }, { responseType: 'json', // Explicitly set response type to JSON headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, } ); console.log('✅ Token exchange successful', { status: response.status, statusText: response.statusText, headers: response.headers, contentType: response.headers['content-type'], hasData: !!response.data, dataType: typeof response.data, dataIsArray: Array.isArray(response.data), dataPreview: Array.isArray(response.data) ? `Array[${response.data.length}]` : typeof response.data === 'object' ? JSON.stringify(response.data).substring(0, 100) : String(response.data).substring(0, 100), }); // Check if response is an array (buffer issue) if (Array.isArray(response.data)) { console.error('❌ Response is an array (buffer issue):', { arrayLength: response.data.length, firstFew: response.data.slice(0, 10), rawResponse: response, }); throw new Error('Invalid response format: received array instead of JSON. Check Content-Type header.'); } const data = response.data as any; const result = data.data || data; // Tokens are set as httpOnly cookies by backend, but we also store them here for client access if (result.accessToken && result.refreshToken) { TokenManager.setAccessToken(result.accessToken); TokenManager.setRefreshToken(result.refreshToken); TokenManager.setUserData(result.user); // Store id_token if available (needed for proper Okta logout) if (result.idToken) { TokenManager.setIdToken(result.idToken); console.log('✅ ID token stored for logout'); } console.log('✅ Tokens stored successfully'); } else { console.warn('⚠️ Tokens missing in response', { result }); } return result; } catch (error: any) { console.error('❌ Token exchange failed:', { message: error.message, response: error.response?.data, status: error.response?.status, code: code ? `${code.substring(0, 10)}...` : 'MISSING', redirectUri, }); throw error; } } /** * Refresh access token using refresh token */ export async function refreshAccessToken(): Promise { const refreshToken = TokenManager.getRefreshToken(); if (!refreshToken) { throw new Error('No refresh token available'); } const response = await apiClient.post('/auth/refresh', { refreshToken, }); const data = response.data as any; const accessToken = data.data?.accessToken || data.accessToken; if (accessToken) { TokenManager.setAccessToken(accessToken); return accessToken; } throw new Error('Failed to refresh token'); } /** * Get current user profile */ export async function getCurrentUser() { const response = await apiClient.get('/auth/me'); const data = response.data as any; return data.data || data; } /** * Logout user * CRITICAL: This endpoint MUST clear httpOnly cookies set by backend * Note: TokenManager.clearAll() is called in AuthContext.logout() * We don't call it here to avoid double clearing */ export async function logout(): Promise { try { console.log('📡 Calling backend logout endpoint to clear httpOnly cookies...'); // Use withCredentials to ensure cookies are sent const response = await apiClient.post('/auth/logout', {}, { withCredentials: true, // Ensure cookies are sent with request }); console.log('📡 Backend logout response:', response.status, response.statusText); console.log('📡 Response headers (check Set-Cookie):', response.headers); } catch (error: any) { console.error('📡 Logout API error:', error); console.error('📡 Error details:', { message: error.message, status: error.response?.status, data: error.response?.data, }); // Even if API call fails, cookies might still be cleared // Don't throw - let the caller handle cleanup } } export default apiClient;