import axios from 'axios'; import { getCurrentModuleAuth, clearModuleToken, getModuleFromPath, getSetorFromModule } from '@/utils/tokenManager'; import { decryptToken, validateTokenExpiration, maskTokenForLogging, secureLog, isDevelopment } from '@/utils/tokenSecurity'; import { generateCSRFToken, getCSRFToken, setCSRFToken } from '@/utils/tokenSecurity'; /** * Instância centralizada do Axios para todas as comunicações com o backend. * A base URL é configurada via variável de ambiente VITE_API_URL. * * Exemplo de configuração no .env: * VITE_API_URL=https://dev.workspace.itguys.com.br/api */ const api = axios.create({ baseURL: import.meta.env.VITE_API_URL || 'https://dev.workspace.itguys.com.br/api', headers: { 'Content-Type': 'application/json', }, timeout: 1200000, // 20 minutos (1200 segundos) para lidar com a lentidão do backend withCredentials: true, // Habilita envio de cookies (necessário para remember_2fa) }); // Cache síncrono para tokens descriptografados (atualizado periodicamente) let tokenCache = { token: null, setor: null, timestamp: 0 }; const CACHE_DURATION = 5000; // 5 segundos /** * Obtém token e setor do cache ou tenta descriptografar síncronamente * Versão síncrona para uso no interceptor do axios */ const getCachedAuthSync = () => { const now = Date.now(); // Se cache é válido, retorna if (tokenCache.token && (now - tokenCache.timestamp) < CACHE_DURATION) { return { token: tokenCache.token, setor: tokenCache.setor }; } // Tenta obter token do módulo atual (versão síncrona com fallback) const module = getModuleFromPath(); let token = null; let setor = null; if (module) { // Tenta obter token criptografado do módulo const encryptedToken = localStorage.getItem(`token_${module}`); if (encryptedToken && encryptedToken.startsWith('enc_')) { // Descriptografa de forma síncrona (pode ser lento, mas necessário) try { const obfuscated = encryptedToken.replace('enc_', ''); const base64 = obfuscated.split('').reverse().join(''); const decrypted = atob(base64); if (decrypted && validateTokenExpiration(decrypted)) { token = decrypted; setor = localStorage.getItem(`setor_${module}`) || getSetorFromModule(module); } } catch (e) { // Erro ao descriptografar, tenta fallback } } else if (encryptedToken && !encryptedToken.startsWith('enc_')) { // Token não criptografado (compatibilidade com tokens antigos) if (validateTokenExpiration(encryptedToken)) { token = encryptedToken; setor = localStorage.getItem(`setor_${module}`) || getSetorFromModule(module); } } } // Fallback: token global if (!token) { const globalEncrypted = localStorage.getItem('x-access-token'); if (globalEncrypted) { if (globalEncrypted.startsWith('enc_')) { try { const obfuscated = globalEncrypted.replace('enc_', ''); const base64 = obfuscated.split('').reverse().join(''); const decrypted = atob(base64); if (decrypted && validateTokenExpiration(decrypted)) { token = decrypted; const module = getModuleFromPath(); setor = module ? getSetorFromModule(module) : null; } } catch (e) { // Erro ao descriptografar } } else { // Token não criptografado (compatibilidade) if (validateTokenExpiration(globalEncrypted)) { token = globalEncrypted; const module = getModuleFromPath(); setor = module ? getSetorFromModule(module) : null; } } } } // Atualiza cache if (token) { tokenCache = { token, setor, timestamp: Date.now() }; } // Atualiza cache de forma assíncrona em background para próxima vez getCurrentModuleAuth().then(({ token: asyncToken, setor: asyncSetor }) => { if (asyncToken) { tokenCache = { token: asyncToken, setor: asyncSetor, timestamp: Date.now() }; } }).catch(() => { // Ignora erros em background }); return { token, setor }; }; // Interceptor para adicionar o token e setor em cada requisição api.interceptors.request.use( async (config) => { // Não adiciona token em requisições de autenticação (Passo 1 e 2) // O backend espera x-access-token: null nessas requisições const isAuthRequest = config.url === '/auth' || config.url?.includes('/auth') || config.url === '/auth_pralog' || config.url === '/auth_oestepan'; if (!isAuthRequest) { // Obtém token e setor de forma síncrona (interceptor precisa ser síncrono) const { token, setor } = getCachedAuthSync(); if (token) { config.headers['x-access-token'] = token; // Adiciona setor via header (backend agora requer ambos) if (setor) { config.headers['x-setor'] = setor; } // Adiciona token CSRF para requisições mutantes const mutatingMethods = ['POST', 'PUT', 'PATCH', 'DELETE']; if (mutatingMethods.includes(config.method?.toUpperCase())) { let csrfToken = getCSRFToken(); if (!csrfToken) { csrfToken = generateCSRFToken(); setCSRFToken(csrfToken); } config.headers['x-csrf-token'] = csrfToken; } } } else { // Para requisições de autenticação, garante que os headers sejam removidos if (config.headers['x-access-token']) { delete config.headers['x-access-token']; } if (config.headers['x-setor']) { delete config.headers['x-setor']; } if (config.headers['x-csrf-token']) { delete config.headers['x-csrf-token']; } } // Log seguro apenas em desenvolvimento if (isDevelopment()) { secureLog('[API] Requisição', { method: config.method?.toUpperCase(), url: config.url, baseURL: config.baseURL, isAuthRequest, hasToken: !!token, hasSetor: !!setor, headers: Object.keys(config.headers || {}) }); } return config; }, (error) => { if (isDevelopment()) { secureLog('[API] Erro no interceptor de request', { error: error.message }); } return Promise.reject(error); } ); // Interceptor para tratamento de erros de resposta api.interceptors.response.use( (response) => { return response; }, async (error) => { const currentPath = window.location.pathname; const isUnauthorized = error.response?.status === 401; const isForbidden = error.response?.status === 403; // Log apenas do erro necessário if (isDevelopment()) { secureLog('[API] Erro na resposta:', { status: error.response?.status, url: error.config?.url, method: error.config?.method?.toUpperCase(), data: error.response?.data }); } // Tratamento de erro 401 (não autorizado) ou 403 (proibido): redireciona para login if (isUnauthorized || isForbidden) { // SEGURANÇA: Limpa TODO o localStorage (conforme solicitado para GR) // Isso força logout completo de todos os módulos localStorage.clear(); // Limpar cache local do interceptor tokenCache = { token: null, setor: null, timestamp: 0 }; // Redirecionar para login se não estiver já na página de login if (!currentPath.includes('/login') && !currentPath.includes('/auth/login')) { if (currentPath.includes('/plataforma/prafrot')) { window.location.href = '/plataforma/prafrot/login'; } else if (currentPath.includes('/plataforma/financeiro-cnab')) { window.location.href = '/plataforma/auth/login-cnab'; } else if (currentPath.includes('/plataforma/financeiro-v2')) { window.location.href = '/plataforma/auth/login-finance'; } else if (currentPath.includes('/plataforma/fleet-v2')) { window.location.href = '/plataforma/auth/login'; } else if (currentPath.includes('/plataforma/hr')) { window.location.href = '/plataforma/hr/login'; } else if (currentPath.includes('/plataforma/fleet')) { window.location.href = '/plataforma/fleet/login'; } else if (currentPath.includes('/plataforma/workspace')) { window.location.href = '/plataforma/workspace/login'; } else if (currentPath.includes('/plataforma/gr')) { window.location.href = '/plataforma/gr/login'; } else if (currentPath.includes('/plataforma/oest-pan')) { window.location.href = '/plataforma/oest-pan/login'; } else { window.location.href = '/plataforma/auth/login'; } } } return Promise.reject(error); } ); export default api;