/** * Cliente HTTP usando Axios * Chamadas diretas ao Supabase */ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; import { API_CONFIG } from "./config"; class ApiClient { private client: AxiosInstance; constructor() { this.client = axios.create({ baseURL: API_CONFIG.REST_URL, timeout: API_CONFIG.TIMEOUT, headers: { "Content-Type": "application/json", apikey: API_CONFIG.SUPABASE_ANON_KEY, }, }); this.setupInterceptors(); } private setupInterceptors() { // Request interceptor - adiciona token automaticamente this.client.interceptors.request.use( (config) => { // Não adicionar token se a flag _skipAuth estiver presente if ((config as any)._skipAuth) { delete (config as any)._skipAuth; return config; } const token = localStorage.getItem( API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN ); if (token && config.headers) { config.headers.Authorization = `Bearer ${token}`; console.log( `[ApiClient] Request: ${config.method?.toUpperCase()} ${ config.url } - Token presente: ${token.substring(0, 20)}...` ); } else { console.warn( `[ApiClient] Request: ${config.method?.toUpperCase()} ${ config.url } - SEM TOKEN!` ); } return config; }, (error) => { return Promise.reject(error); } ); // Response interceptor - trata erros globalmente this.client.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; console.error(`[ApiClient] Erro na requisição:`, { url: originalRequest?.url, status: error.response?.status, message: error.message, }); // Se retornar 401 e não for uma requisição de refresh/login if ( error.response?.status === 401 && !originalRequest._retry && !originalRequest.url?.includes("auth-refresh") && !originalRequest.url?.includes("auth-login") ) { originalRequest._retry = true; console.log("[ApiClient] Recebeu 401, tentando renovar token..."); try { // Tenta renovar o token const refreshToken = localStorage.getItem( API_CONFIG.STORAGE_KEYS.REFRESH_TOKEN ); if (refreshToken) { console.log("[ApiClient] Refresh token encontrado, renovando..."); // Chama Supabase diretamente para renovar token const response = await axios.post( `${API_CONFIG.AUTH_URL}/token?grant_type=refresh_token`, { refresh_token: refreshToken, }, { headers: { "Content-Type": "application/json", apikey: API_CONFIG.SUPABASE_ANON_KEY, }, } ); const { access_token, refresh_token: newRefreshToken, user, } = response.data; console.log("[ApiClient] Token renovado com sucesso!"); // Atualiza tokens localStorage.setItem( API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN, access_token ); localStorage.setItem( API_CONFIG.STORAGE_KEYS.REFRESH_TOKEN, newRefreshToken ); localStorage.setItem( API_CONFIG.STORAGE_KEYS.USER, JSON.stringify(user) ); // Atualiza o header da requisição original originalRequest.headers.Authorization = `Bearer ${access_token}`; // Refaz a requisição original return this.client(originalRequest); } else { console.warn("[ApiClient] Nenhum refresh token disponível!"); } } catch (refreshError) { console.error( "[ApiClient] Falha ao renovar token, fazendo logout...", refreshError ); // Se refresh falhar, limpa tudo e redireciona para home localStorage.removeItem(API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN); localStorage.removeItem(API_CONFIG.STORAGE_KEYS.REFRESH_TOKEN); localStorage.removeItem(API_CONFIG.STORAGE_KEYS.USER); // Redireciona para home ao invés de login específico if ( !window.location.pathname.includes("/login") && window.location.pathname !== "/" ) { window.location.href = "/"; } return Promise.reject(refreshError); } } // Se não conseguir renovar, apenas loga o erro mas NÃO limpa a sessão // Deixa o componente decidir o que fazer if (error.response?.status === 401) { console.error( "[ApiClient] 401 não tratado - requisição:", originalRequest?.url ); } return Promise.reject(error); } ); } // Métodos HTTP async get( url: string, config?: AxiosRequestConfig ): Promise> { console.log( "[ApiClient] GET Request:", url, "Params:", JSON.stringify(config?.params) ); const response = await this.client.get(url, config); console.log("[ApiClient] GET Response:", { status: response.status, dataType: typeof response.data, isArray: Array.isArray(response.data), dataLength: Array.isArray(response.data) ? response.data.length : "not array", }); console.log( "[ApiClient] Response Data:", JSON.stringify(response.data, null, 2) ); return response; } async post( url: string, data?: unknown, config?: AxiosRequestConfig ): Promise> { console.log("[ApiClient] POST Request:", { url, fullUrl: `${this.client.defaults.baseURL}${url}`, data, config, }); try { const response = await this.client.post(url, data, config); console.log("[ApiClient] POST Response:", { status: response.status, data: response.data, }); return response; } catch (error: any) { console.error("[ApiClient] POST Error:", { url, fullUrl: `${this.client.defaults.baseURL}${url}`, status: error?.response?.status, statusText: error?.response?.statusText, data: error?.response?.data, message: error?.message, }); throw error; } } /** * POST público - não envia token de autenticação * Usado para endpoints de auto-registro */ async postPublic( url: string, data?: unknown, config?: AxiosRequestConfig ): Promise> { const configWithoutAuth = { ...config, headers: { ...config?.headers, // Remove Authorization header }, // Flag especial para o interceptor saber que não deve adicionar token _skipAuth: true, }; return this.client.post(url, data, configWithoutAuth); } async patch( url: string, data?: unknown, config?: AxiosRequestConfig ): Promise> { console.log("[ApiClient] PATCH Request:", { url, data, config, }); try { // Adicionar header Prefer para Supabase retornar os dados atualizados const configWithPrefer = { ...config, headers: { ...config?.headers, Prefer: "return=representation", }, }; const response = await this.client.patch(url, data, configWithPrefer); console.log("[ApiClient] PATCH Response:", { status: response.status, data: response.data, }); return response; } catch (error: any) { console.error("[ApiClient] PATCH Error:", { url, status: error?.response?.status, statusText: error?.response?.statusText, data: error?.response?.data, message: error?.message, }); throw error; } } async delete( url: string, config?: AxiosRequestConfig ): Promise> { return this.client.delete(url, config); } async put( url: string, data?: unknown, config?: AxiosRequestConfig ): Promise> { return this.client.put(url, data, config); } /** * Chama uma Edge Function do Supabase * Usa a baseURL de Functions em vez de REST */ async callFunction( functionName: string, data?: unknown, config?: AxiosRequestConfig ): Promise> { const fullUrl = `${API_CONFIG.FUNCTIONS_URL}/${functionName}`; console.log("[ApiClient] Calling Edge Function:", { functionName, fullUrl, data, }); // Cria uma requisição sem baseURL const functionsClient = axios.create({ timeout: API_CONFIG.TIMEOUT, headers: { "Content-Type": "application/json", apikey: API_CONFIG.SUPABASE_ANON_KEY, }, }); // Adiciona token se disponível const token = localStorage.getItem(API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN); if (token) { functionsClient.defaults.headers.common[ "Authorization" ] = `Bearer ${token}`; } try { const response = await functionsClient.post(fullUrl, data, config); console.log("[ApiClient] Edge Function Response:", { functionName, status: response.status, data: response.data, }); return response; } catch (error: any) { console.error("[ApiClient] Edge Function Error:", { functionName, fullUrl, status: error?.response?.status, statusText: error?.response?.statusText, data: error?.response?.data, message: error?.message, }); throw error; } } } export const apiClient = new ApiClient();