REFACTOR: Padronizado auth e services do frontend
- Centralizado client HTTP em apiClient - Padronizado parse de resposta e tratamento de erro - Mantido Bearer Token via interceptor global de fetch - Validado JWT pelo campo exp antes de autenticar sessão - Removidos fallbacks mockados de usuário - Bloqueado fluxo legado de login por query string - Preservada rota original após redirecionamento para login - Protegida rota /admin/whatsapp por perfil admin - Atualizada wiki frontend de autenticação, rotas e services
This commit is contained in:
parent
04971ca82e
commit
8b85971371
@ -2,7 +2,7 @@
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { BrandMark } from '../../../shared/components/BrandMark';
|
||||
import { useViewport } from '../../../shared/hooks/useViewport';
|
||||
import { API_BASE_URL } from '../../../shared/services/apiConfig';
|
||||
import { apiRequest } from '../../../shared/services/apiClient';
|
||||
import { getCurrentUser } from '../../auth/services/sessionService';
|
||||
import { listContactProfiles, saveContactProfile } from '../../chat/services/contactProfileService';
|
||||
import { getAccessOptions } from '../../management/services/adminAccessService';
|
||||
@ -177,19 +177,17 @@ function getUserAreas(user) {
|
||||
}
|
||||
|
||||
async function listWhatsappTemplates() {
|
||||
const response = await fetch(`${API_BASE_URL}/whatsapp/templates`);
|
||||
if (!response.ok) throw new Error('Falha ao carregar templates do WhatsApp.');
|
||||
return response.json();
|
||||
return apiRequest('/whatsapp/templates', {
|
||||
fallbackMessage: 'Falha ao carregar templates do WhatsApp.',
|
||||
});
|
||||
}
|
||||
|
||||
async function startWhatsappAttendance(payload) {
|
||||
const response = await fetch(`${API_BASE_URL}/whatsapp/start-attendance`, {
|
||||
return apiRequest('/whatsapp/start-attendance', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
fallbackMessage: 'Falha ao iniciar atendimento pelo WhatsApp.',
|
||||
});
|
||||
if (!response.ok) throw new Error('Falha ao iniciar atendimento pelo WhatsApp.');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export function NewAttendancePage({ embedded = false }) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
getAuthConfig,
|
||||
loginWithAd,
|
||||
@ -17,20 +17,22 @@ function decodeAuthPayload(payload) {
|
||||
|
||||
export function useLogin() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [providers, setProviders] = useState({ ldap: true, microsoft: false });
|
||||
const targetPath = typeof location.state?.from === 'string' ? location.state.from : '/home';
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.location.hash.includes('auth=') && isAuthenticated()) {
|
||||
navigate('/home', { replace: true });
|
||||
navigate(targetPath, { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
getAuthConfig()
|
||||
.then((config) => setProviders(config.providers || { ldap: true, microsoft: false }))
|
||||
.catch(() => setProviders({ ldap: true, microsoft: false }));
|
||||
}, [navigate]);
|
||||
}, [navigate, targetPath]);
|
||||
|
||||
useEffect(() => {
|
||||
const hashParams = new URLSearchParams(window.location.hash.replace(/^#/, ''));
|
||||
@ -40,7 +42,7 @@ export function useLogin() {
|
||||
try {
|
||||
storeAuthSession(decodeAuthPayload(authPayload));
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
navigate('/home', { replace: true });
|
||||
navigate(targetPath, { replace: true });
|
||||
return;
|
||||
} catch {
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
@ -52,7 +54,7 @@ export function useLogin() {
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
setError('Fluxo de login expirado. Tente novamente.');
|
||||
}
|
||||
}, [navigate]);
|
||||
}, [navigate, targetPath]);
|
||||
|
||||
async function login(credentials) {
|
||||
setIsSubmitting(true);
|
||||
@ -61,7 +63,7 @@ export function useLogin() {
|
||||
try {
|
||||
const authResult = await loginWithAd(credentials);
|
||||
storeAuthSession(authResult);
|
||||
navigate('/home');
|
||||
navigate(targetPath);
|
||||
} catch (loginError) {
|
||||
setError(loginError.message || 'Falha ao autenticar.');
|
||||
} finally {
|
||||
|
||||
@ -1,28 +1,18 @@
|
||||
import { API_BASE_URL } from '../../../shared/services/apiConfig';
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
const data = await response.json().catch(() => null);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.message || 'Não foi possível autenticar.');
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
import { apiRequest } from '../../../shared/services/apiClient';
|
||||
|
||||
export async function getAuthConfig() {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/config`);
|
||||
return parseJsonResponse(response);
|
||||
return apiRequest('/auth/config', {
|
||||
fallbackMessage: 'Nao foi possivel carregar configuracao de login.',
|
||||
});
|
||||
}
|
||||
|
||||
export async function loginWithAd(credentials) {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
||||
return apiRequest('/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(credentials),
|
||||
fallbackMessage: 'Nao foi possivel autenticar.',
|
||||
});
|
||||
|
||||
return parseJsonResponse(response);
|
||||
}
|
||||
|
||||
export function startMicrosoftLogin() {
|
||||
|
||||
@ -1,19 +1,10 @@
|
||||
import { API_BASE_URL } from '../../../shared/services/apiConfig';
|
||||
import { apiRequest } from '../../../shared/services/apiClient';
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
function request(path, options = {}) {
|
||||
return apiRequest(path, {
|
||||
fallbackMessage: 'Falha ao atualizar presenca do agente',
|
||||
...options,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Falha ao atualizar presenca do agente');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function getAgentPresence(userId) {
|
||||
|
||||
@ -1,23 +1,21 @@
|
||||
import { API_BASE_URL } from '../../../shared/services/apiConfig';
|
||||
import { apiRequest } from '../../../shared/services/apiClient';
|
||||
|
||||
export async function listContactProfiles() {
|
||||
const response = await fetch(`${API_BASE_URL}/contacts`);
|
||||
if (!response.ok) throw new Error('Falha ao carregar agenda.');
|
||||
return response.json();
|
||||
return apiRequest('/contacts', {
|
||||
fallbackMessage: 'Falha ao carregar agenda.',
|
||||
});
|
||||
}
|
||||
|
||||
export async function getContactProfile(chatId) {
|
||||
const response = await fetch(`${API_BASE_URL}/contacts/${encodeURIComponent(chatId)}`);
|
||||
if (!response.ok) throw new Error('Falha ao carregar contato.');
|
||||
return response.json();
|
||||
return apiRequest(`/contacts/${encodeURIComponent(chatId)}`, {
|
||||
fallbackMessage: 'Falha ao carregar contato.',
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveContactProfile(chatId, payload) {
|
||||
const response = await fetch(`${API_BASE_URL}/contacts/${encodeURIComponent(chatId)}`, {
|
||||
return apiRequest(`/contacts/${encodeURIComponent(chatId)}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
fallbackMessage: 'Falha ao salvar contato.',
|
||||
});
|
||||
if (!response.ok) throw new Error('Falha ao salvar contato.');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
@ -1,27 +1,24 @@
|
||||
import { API_BASE_URL } from '../../../shared/services/apiConfig';
|
||||
import { apiRequest } from '../../../shared/services/apiClient';
|
||||
|
||||
export async function listAgentNotes(userId) {
|
||||
if (!userId) return [];
|
||||
const response = await fetch(`${API_BASE_URL}/agent/notes?userId=${encodeURIComponent(userId)}`);
|
||||
if (!response.ok) throw new Error('Falha ao carregar anotações.');
|
||||
return response.json();
|
||||
|
||||
return apiRequest(`/agent/notes?userId=${encodeURIComponent(userId)}`, {
|
||||
fallbackMessage: 'Falha ao carregar anotacoes.',
|
||||
});
|
||||
}
|
||||
|
||||
export async function createAgentNote(userId, text) {
|
||||
const response = await fetch(`${API_BASE_URL}/agent/notes`, {
|
||||
return apiRequest('/agent/notes', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ userId, text }),
|
||||
fallbackMessage: 'Falha ao salvar anotacao.',
|
||||
});
|
||||
if (!response.ok) throw new Error('Falha ao salvar anotação.');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function deleteAgentNote(userId, noteId) {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/agent/notes/${encodeURIComponent(noteId)}?userId=${encodeURIComponent(userId)}`,
|
||||
{ method: 'DELETE' },
|
||||
);
|
||||
if (!response.ok) throw new Error('Falha ao excluir anotação.');
|
||||
return response.json();
|
||||
return apiRequest(`/agent/notes/${encodeURIComponent(noteId)}?userId=${encodeURIComponent(userId)}`, {
|
||||
method: 'DELETE',
|
||||
fallbackMessage: 'Falha ao excluir anotacao.',
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { API_BASE_URL } from '../../../shared/services/apiConfig';
|
||||
import { apiRequest } from '../../../shared/services/apiClient';
|
||||
import { getCurrentUser } from '../../auth/services/sessionService';
|
||||
import { listContactProfiles } from '../../chat/services/contactProfileService';
|
||||
import { DataPanel } from './DataPanel';
|
||||
@ -54,17 +54,11 @@ function renderPreview(content, variables) {
|
||||
}
|
||||
|
||||
async function startAttendance(payload) {
|
||||
const response = await fetch(`${API_BASE_URL}/whatsapp/start-attendance`, {
|
||||
return apiRequest('/whatsapp/start-attendance', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
fallbackMessage: 'Falha ao enviar disparo.',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Falha ao enviar disparo.');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export function MassMessagePanel({
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { io } from 'socket.io-client';
|
||||
import { API_BASE_URL, WHATSAPP_SOCKET_URL } from '../../../shared/services/apiConfig';
|
||||
import { WHATSAPP_SOCKET_URL } from '../../../shared/services/apiConfig';
|
||||
import { apiRequest } from '../../../shared/services/apiClient';
|
||||
import { getAuthToken } from '../../auth/services/sessionService';
|
||||
|
||||
export const WhatsappAdminPage = () => {
|
||||
@ -16,8 +17,9 @@ export const WhatsappAdminPage = () => {
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('Connected to WhatsApp WebSocket');
|
||||
fetch(`${API_BASE_URL}/whatsapp/status`)
|
||||
.then((response) => response.json())
|
||||
apiRequest('/whatsapp/status', {
|
||||
fallbackMessage: 'Falha ao consultar status do WhatsApp.',
|
||||
})
|
||||
.then((data) => setStatus(data.status))
|
||||
.catch(console.error);
|
||||
});
|
||||
|
||||
@ -1,26 +1,10 @@
|
||||
import { API_BASE_URL } from '../../../shared/services/apiConfig';
|
||||
import { apiRequest } from '../../../shared/services/apiClient';
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
function request(path, options = {}) {
|
||||
return apiRequest(path, {
|
||||
fallbackMessage: 'Falha ao consultar acessos',
|
||||
...options,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let message = 'Falha ao consultar acessos';
|
||||
try {
|
||||
const payload = await response.json();
|
||||
message = payload?.message || payload?.error || message;
|
||||
} catch {
|
||||
// Mantem mensagem padrao.
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function getAccessOptions() {
|
||||
|
||||
@ -1,28 +1,10 @@
|
||||
import { API_BASE_URL } from '../../../shared/services/apiConfig';
|
||||
import { apiRequest } from '../../../shared/services/apiClient';
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
function request(path, options = {}) {
|
||||
return apiRequest(path, {
|
||||
fallbackMessage: 'Falha ao consultar base de conhecimento.',
|
||||
...options,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let message = 'Falha ao consultar base de conhecimento.';
|
||||
try {
|
||||
const payload = await response.json();
|
||||
message = Array.isArray(payload?.message)
|
||||
? payload.message.join(' ')
|
||||
: payload?.message || payload?.error || message;
|
||||
} catch {
|
||||
// Mantem a mensagem padrao quando a API nao devolve JSON.
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export function listRoutingKeywords(areaId) {
|
||||
|
||||
@ -1,19 +1,10 @@
|
||||
import { API_BASE_URL } from '../../../shared/services/apiConfig';
|
||||
import { apiRequest } from '../../../shared/services/apiClient';
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
function request(path, options = {}) {
|
||||
return apiRequest(path, {
|
||||
fallbackMessage: 'Falha ao consultar templates.',
|
||||
...options,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Falha ao consultar templates.');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export function listTemplates() {
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
import { getCurrentUserProfile, isAuthenticated } from '../modules/auth/services/sessionService';
|
||||
|
||||
export function ProtectedRoute({ children, allowedProfiles = [] }) {
|
||||
const location = useLocation();
|
||||
|
||||
if (!isAuthenticated()) {
|
||||
return <Navigate to="/login" replace />;
|
||||
return <Navigate to="/login" replace state={{ from: location.pathname + location.search }} />;
|
||||
}
|
||||
|
||||
if (allowedProfiles.length && !allowedProfiles.includes(getCurrentUserProfile())) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import io from 'socket.io-client';
|
||||
import { API_BASE_URL, WHATSAPP_SOCKET_URL } from '../services/apiConfig';
|
||||
import { WHATSAPP_SOCKET_URL } from '../services/apiConfig';
|
||||
import { apiRequest } from '../services/apiClient';
|
||||
import { getAuthToken } from '../../modules/auth/services/sessionService';
|
||||
|
||||
export function useWhatsappSocket() {
|
||||
@ -27,8 +28,9 @@ export function useWhatsappSocket() {
|
||||
newSocket.on('connect', () => {
|
||||
console.log('Conectado ao WebSocket do WhatsApp');
|
||||
// Fetch status atual
|
||||
fetch(`${API_BASE_URL}/whatsapp/status`)
|
||||
.then(res => res.json())
|
||||
apiRequest('/whatsapp/status', {
|
||||
fallbackMessage: 'Falha ao consultar status do WhatsApp.',
|
||||
})
|
||||
.then(data => setStatus(data.status))
|
||||
.catch(console.error);
|
||||
});
|
||||
|
||||
62
src/shared/services/apiClient.js
Normal file
62
src/shared/services/apiClient.js
Normal file
@ -0,0 +1,62 @@
|
||||
import { API_BASE_URL } from './apiConfig';
|
||||
|
||||
function buildApiUrl(path) {
|
||||
if (/^https?:\/\//i.test(path)) {
|
||||
return path;
|
||||
}
|
||||
|
||||
return `${API_BASE_URL}${path}`;
|
||||
}
|
||||
|
||||
async function parseResponse(response) {
|
||||
const text = await response.text();
|
||||
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveErrorMessage(payload, fallbackMessage) {
|
||||
if (Array.isArray(payload?.message)) {
|
||||
return payload.message.join(' ');
|
||||
}
|
||||
|
||||
return payload?.message || payload?.error || fallbackMessage;
|
||||
}
|
||||
|
||||
export async function apiRequest(path, options = {}) {
|
||||
const {
|
||||
headers,
|
||||
fallbackMessage = 'Falha ao consultar API.',
|
||||
...requestOptions
|
||||
} = options;
|
||||
|
||||
const requestHeaders = new Headers(headers || {});
|
||||
const hasBody = requestOptions.body !== undefined && requestOptions.body !== null;
|
||||
const isFormData = typeof FormData !== 'undefined' && requestOptions.body instanceof FormData;
|
||||
|
||||
if (hasBody && !isFormData && !requestHeaders.has('Content-Type')) {
|
||||
requestHeaders.set('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
const response = await fetch(buildApiUrl(path), {
|
||||
...requestOptions,
|
||||
headers: requestHeaders,
|
||||
});
|
||||
const payload = await parseResponse(response);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = new Error(resolveErrorMessage(payload, fallbackMessage));
|
||||
error.status = response.status;
|
||||
error.payload = payload;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
@ -1,14 +1,10 @@
|
||||
import { API_BASE_URL } from './apiConfig';
|
||||
import { clearSession } from '../../modules/auth/services/sessionService';
|
||||
|
||||
function getStoredToken() {
|
||||
return window.sessionStorage.getItem('authToken');
|
||||
}
|
||||
import { clearSession, getAuthToken } from '../../modules/auth/services/sessionService';
|
||||
|
||||
function shouldAttachToken(resource) {
|
||||
const url = typeof resource === 'string' ? resource : resource?.url;
|
||||
|
||||
return Boolean(url && url.startsWith(API_BASE_URL) && getStoredToken());
|
||||
return Boolean(url && url.startsWith(API_BASE_URL) && getAuthToken());
|
||||
}
|
||||
|
||||
export function installAuthFetchInterceptor() {
|
||||
@ -25,7 +21,7 @@ export function installAuthFetchInterceptor() {
|
||||
const headers = new Headers(options.headers || {});
|
||||
|
||||
if (!headers.has('Authorization')) {
|
||||
headers.set('Authorization', `Bearer ${getStoredToken()}`);
|
||||
headers.set('Authorization', `Bearer ${getAuthToken()}`);
|
||||
}
|
||||
|
||||
const response = await nativeFetch(resource, {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user