239 lines
8.6 KiB
JavaScript
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;
|