FEAT: Integrada autenticação segura no frontend

- Token passa de localStorage para sessionStorage
- Removido fallback de perfis demo
- Fetch passa a enviar Bearer Token automaticamente
- Socket.IO do WhatsApp envia token no handshake
- Login Microsoft lê payload por fragmento #auth e limpa a URL
- Acesso a /home sem autenticação redireciona para /login
- Rotas protegidas com componente ProtectedRoute
This commit is contained in:
Rafael Alves Lopes 2026-05-29 12:27:37 -03:00
parent dfc47ce7e8
commit eb6e4faf2c
9 changed files with 114 additions and 31 deletions

View File

@ -2,8 +2,11 @@ import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { RouterProvider } from 'react-router-dom'; import { RouterProvider } from 'react-router-dom';
import { router } from './routes/router'; import { router } from './routes/router';
import { installAuthFetchInterceptor } from './shared/services/authFetch';
import './shared/styles/global.css'; import './shared/styles/global.css';
installAuthFetchInterceptor();
ReactDOM.createRoot(document.getElementById('root')).render( ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode> <React.StrictMode>
<RouterProvider router={router} /> <RouterProvider router={router} />

View File

@ -7,6 +7,13 @@ import {
storeAuthSession, storeAuthSession,
} from '../services/authService'; } from '../services/authService';
function decodeAuthPayload(payload) {
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
const paddedBase64 = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');
return JSON.parse(atob(paddedBase64));
}
export function useLogin() { export function useLogin() {
const navigate = useNavigate(); const navigate = useNavigate();
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
@ -20,6 +27,21 @@ export function useLogin() {
}, []); }, []);
useEffect(() => { useEffect(() => {
const hashParams = new URLSearchParams(window.location.hash.replace(/^#/, ''));
const authPayload = hashParams.get('auth');
if (authPayload) {
try {
storeAuthSession(decodeAuthPayload(authPayload));
window.history.replaceState({}, document.title, window.location.pathname);
navigate('/home', { replace: true });
return;
} catch {
window.history.replaceState({}, document.title, window.location.pathname);
setError('Não foi possível concluir o login Microsoft.');
}
}
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const token = params.get('token'); const token = params.get('token');
const rawUser = params.get('user'); const rawUser = params.get('user');

View File

@ -30,6 +30,8 @@ export function startMicrosoftLogin() {
} }
export function storeAuthSession(authResult) { export function storeAuthSession(authResult) {
window.localStorage.setItem('authToken', authResult.token); window.sessionStorage.setItem('authToken', authResult.token);
window.localStorage.setItem('authUser', JSON.stringify(authResult.user)); window.sessionStorage.setItem('authUser', JSON.stringify(authResult.user));
window.localStorage.removeItem('authToken');
window.localStorage.removeItem('authUser');
} }

View File

@ -8,16 +8,8 @@ const PROFILE_ALIASES = {
agent: 'agent', agent: 'agent',
}; };
const DEMO_PROFILE_BY_USERNAME = {
admin: 'admin',
'lucas.admin': 'admin',
supervisor: 'supervisor',
'marina.alves': 'supervisor',
'rafael.nunes': 'supervisor',
};
function readStoredUser() { function readStoredUser() {
const rawUser = window.localStorage.getItem('authUser'); const rawUser = window.sessionStorage.getItem('authUser');
if (!rawUser) { if (!rawUser) {
return null; return null;
@ -68,6 +60,14 @@ export function getCurrentUser() {
return readStoredUser(); return readStoredUser();
} }
export function getAuthToken() {
return window.sessionStorage.getItem('authToken');
}
export function isAuthenticated() {
return Boolean(getAuthToken() && getCurrentUser());
}
export function getCurrentUserDisplay() { export function getCurrentUserDisplay() {
const user = getCurrentUser(); const user = getCurrentUser();
const fullName = user?.name || user?.nome || user?.username || 'Ana Camolesi'; const fullName = user?.name || user?.nome || user?.username || 'Ana Camolesi';
@ -102,7 +102,7 @@ export function getCurrentUserProfile() {
const user = getCurrentUser(); const user = getCurrentUser();
if (!user) { if (!user) {
return 'agent'; return null;
} }
if (user.accessStatus === 'unassigned') { if (user.accessStatus === 'unassigned') {
@ -119,17 +119,12 @@ export function getCurrentUserProfile() {
return backendProfile; return backendProfile;
} }
const username = String(user.username || user.email || user.name || '').trim().toLowerCase();
const demoProfile = DEMO_PROFILE_BY_USERNAME[username];
if (demoProfile) {
return demoProfile;
}
return 'agent'; return 'agent';
} }
export function clearSession() { export function clearSession() {
window.sessionStorage.removeItem('authToken');
window.sessionStorage.removeItem('authUser');
window.localStorage.removeItem('authToken'); window.localStorage.removeItem('authToken');
window.localStorage.removeItem('authUser'); window.localStorage.removeItem('authUser');
} }

View File

@ -1,14 +1,18 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { io } from 'socket.io-client'; import { io } from 'socket.io-client';
import { API_BASE_URL, WHATSAPP_SOCKET_URL } from '../../../shared/services/apiConfig'; import { API_BASE_URL, WHATSAPP_SOCKET_URL } from '../../../shared/services/apiConfig';
import { getAuthToken } from '../../auth/services/sessionService';
export const WhatsappAdminPage = () => { export const WhatsappAdminPage = () => {
const [qrCode, setQrCode] = useState(null); const [qrCode, setQrCode] = useState(null);
const [status, setStatus] = useState('DISCONNECTED'); const [status, setStatus] = useState('DISCONNECTED');
useEffect(() => { useEffect(() => {
// Conecta ao namespace /whatsapp const socket = io(WHATSAPP_SOCKET_URL, {
const socket = io(WHATSAPP_SOCKET_URL); auth: {
token: getAuthToken(),
},
});
socket.on('connect', () => { socket.on('connect', () => {
console.log('Connected to WhatsApp WebSocket'); console.log('Connected to WhatsApp WebSocket');
@ -40,7 +44,10 @@ export const WhatsappAdminPage = () => {
<h1 className="text-2xl font-bold mb-4">Configuração do WhatsApp</h1> <h1 className="text-2xl font-bold mb-4">Configuração do WhatsApp</h1>
<div className="bg-white p-6 rounded-lg shadow-md max-w-md"> <div className="bg-white p-6 rounded-lg shadow-md max-w-md">
<h2 className="text-lg font-semibold mb-2">Status da Conexão: <span className={status === 'CONNECTED' ? 'text-green-600' : 'text-red-600'}>{status}</span></h2> <h2 className="text-lg font-semibold mb-2">
Status da Conexão:{' '}
<span className={status === 'CONNECTED' ? 'text-green-600' : 'text-red-600'}>{status}</span>
</h2>
{status === 'AWAITING_QR' && qrCode && ( {status === 'AWAITING_QR' && qrCode && (
<div className="mt-4 flex flex-col items-center"> <div className="mt-4 flex flex-col items-center">

View File

@ -0,0 +1,10 @@
import { Navigate } from 'react-router-dom';
import { isAuthenticated } from '../modules/auth/services/sessionService';
export function ProtectedRoute({ children }) {
if (!isAuthenticated()) {
return <Navigate to="/login" replace />;
}
return children;
}

View File

@ -7,6 +7,11 @@ import { ChatPage } from '../modules/chat/pages/ChatPage';
import { CallPage } from '../modules/call/pages/CallPage'; import { CallPage } from '../modules/call/pages/CallPage';
import { AgentNewAttendancePage } from '../modules/attendance/pages/AgentNewAttendancePage'; import { AgentNewAttendancePage } from '../modules/attendance/pages/AgentNewAttendancePage';
import { WhatsappAdminPage } from '../modules/management/pages/WhatsappAdminPage'; import { WhatsappAdminPage } from '../modules/management/pages/WhatsappAdminPage';
import { ProtectedRoute } from './ProtectedRoute';
function privatePage(page) {
return <ProtectedRoute>{page}</ProtectedRoute>;
}
export const router = createBrowserRouter([ export const router = createBrowserRouter([
{ {
@ -19,30 +24,30 @@ export const router = createBrowserRouter([
}, },
{ {
path: '/home', path: '/home',
element: <ProfileHomePage />, element: privatePage(<ProfileHomePage />),
}, },
{ {
path: '/chat', path: '/chat',
element: <ChatPage />, element: privatePage(<ChatPage />),
}, },
{ {
path: '/call', path: '/call',
element: <CallPage />, element: privatePage(<CallPage />),
}, },
{ {
path: '/new-attendance', path: '/new-attendance',
element: <AgentNewAttendancePage />, element: privatePage(<AgentNewAttendancePage />),
}, },
{ {
path: '/mass-message', path: '/mass-message',
element: <AgentMassMessagePage />, element: privatePage(<AgentMassMessagePage />),
}, },
{ {
path: '/contacts', path: '/contacts',
element: <ContactsPage />, element: privatePage(<ContactsPage />),
}, },
{ {
path: '/admin/whatsapp', path: '/admin/whatsapp',
element: <WhatsappAdminPage />, element: privatePage(<WhatsappAdminPage />),
}, },
]); ]);

View File

@ -1,6 +1,7 @@
import { useEffect, useState, useRef } from 'react'; import { useEffect, useState, useRef } from 'react';
import io from 'socket.io-client'; import io from 'socket.io-client';
import { API_BASE_URL, WHATSAPP_SOCKET_URL } from '../services/apiConfig'; import { API_BASE_URL, WHATSAPP_SOCKET_URL } from '../services/apiConfig';
import { getAuthToken } from '../../modules/auth/services/sessionService';
export function useWhatsappSocket() { export function useWhatsappSocket() {
const [socket, setSocket] = useState(null); const [socket, setSocket] = useState(null);
@ -15,6 +16,9 @@ export function useWhatsappSocket() {
// Conectar ao namespace /whatsapp // Conectar ao namespace /whatsapp
const newSocket = io(WHATSAPP_SOCKET_URL, { const newSocket = io(WHATSAPP_SOCKET_URL, {
reconnectionAttempts: 5, reconnectionAttempts: 5,
auth: {
token: getAuthToken(),
},
}); });
socketRef.current = newSocket; socketRef.current = newSocket;

View File

@ -0,0 +1,35 @@
import { API_BASE_URL } from './apiConfig';
function getStoredToken() {
return window.sessionStorage.getItem('authToken');
}
function shouldAttachToken(resource) {
const url = typeof resource === 'string' ? resource : resource?.url;
return Boolean(url && url.startsWith(API_BASE_URL) && getStoredToken());
}
export function installAuthFetchInterceptor() {
window.localStorage.removeItem('authToken');
window.localStorage.removeItem('authUser');
const nativeFetch = window.fetch.bind(window);
window.fetch = (resource, options = {}) => {
if (!shouldAttachToken(resource)) {
return nativeFetch(resource, options);
}
const headers = new Headers(options.headers || {});
if (!headers.has('Authorization')) {
headers.set('Authorization', `Bearer ${getStoredToken()}`);
}
return nativeFetch(resource, {
...options,
headers,
});
};
}