testes/src/services/api.js

239 lines
8.6 KiB
JavaScript

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;