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:
parent
dfc47ce7e8
commit
eb6e4faf2c
@ -2,8 +2,11 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { router } from './routes/router';
|
||||
import { installAuthFetchInterceptor } from './shared/services/authFetch';
|
||||
import './shared/styles/global.css';
|
||||
|
||||
installAuthFetchInterceptor();
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
|
||||
@ -7,6 +7,13 @@ import {
|
||||
storeAuthSession,
|
||||
} 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() {
|
||||
const navigate = useNavigate();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
@ -20,6 +27,21 @@ export function useLogin() {
|
||||
}, []);
|
||||
|
||||
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 token = params.get('token');
|
||||
const rawUser = params.get('user');
|
||||
|
||||
@ -30,6 +30,8 @@ export function startMicrosoftLogin() {
|
||||
}
|
||||
|
||||
export function storeAuthSession(authResult) {
|
||||
window.localStorage.setItem('authToken', authResult.token);
|
||||
window.localStorage.setItem('authUser', JSON.stringify(authResult.user));
|
||||
window.sessionStorage.setItem('authToken', authResult.token);
|
||||
window.sessionStorage.setItem('authUser', JSON.stringify(authResult.user));
|
||||
window.localStorage.removeItem('authToken');
|
||||
window.localStorage.removeItem('authUser');
|
||||
}
|
||||
|
||||
@ -8,16 +8,8 @@ const PROFILE_ALIASES = {
|
||||
agent: 'agent',
|
||||
};
|
||||
|
||||
const DEMO_PROFILE_BY_USERNAME = {
|
||||
admin: 'admin',
|
||||
'lucas.admin': 'admin',
|
||||
supervisor: 'supervisor',
|
||||
'marina.alves': 'supervisor',
|
||||
'rafael.nunes': 'supervisor',
|
||||
};
|
||||
|
||||
function readStoredUser() {
|
||||
const rawUser = window.localStorage.getItem('authUser');
|
||||
const rawUser = window.sessionStorage.getItem('authUser');
|
||||
|
||||
if (!rawUser) {
|
||||
return null;
|
||||
@ -68,6 +60,14 @@ export function getCurrentUser() {
|
||||
return readStoredUser();
|
||||
}
|
||||
|
||||
export function getAuthToken() {
|
||||
return window.sessionStorage.getItem('authToken');
|
||||
}
|
||||
|
||||
export function isAuthenticated() {
|
||||
return Boolean(getAuthToken() && getCurrentUser());
|
||||
}
|
||||
|
||||
export function getCurrentUserDisplay() {
|
||||
const user = getCurrentUser();
|
||||
const fullName = user?.name || user?.nome || user?.username || 'Ana Camolesi';
|
||||
@ -102,7 +102,7 @@ export function getCurrentUserProfile() {
|
||||
const user = getCurrentUser();
|
||||
|
||||
if (!user) {
|
||||
return 'agent';
|
||||
return null;
|
||||
}
|
||||
|
||||
if (user.accessStatus === 'unassigned') {
|
||||
@ -119,17 +119,12 @@ export function getCurrentUserProfile() {
|
||||
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';
|
||||
}
|
||||
|
||||
export function clearSession() {
|
||||
window.sessionStorage.removeItem('authToken');
|
||||
window.sessionStorage.removeItem('authUser');
|
||||
window.localStorage.removeItem('authToken');
|
||||
window.localStorage.removeItem('authUser');
|
||||
}
|
||||
|
||||
@ -1,14 +1,18 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { io } from 'socket.io-client';
|
||||
import { API_BASE_URL, WHATSAPP_SOCKET_URL } from '../../../shared/services/apiConfig';
|
||||
import { getAuthToken } from '../../auth/services/sessionService';
|
||||
|
||||
export const WhatsappAdminPage = () => {
|
||||
const [qrCode, setQrCode] = useState(null);
|
||||
const [status, setStatus] = useState('DISCONNECTED');
|
||||
|
||||
useEffect(() => {
|
||||
// Conecta ao namespace /whatsapp
|
||||
const socket = io(WHATSAPP_SOCKET_URL);
|
||||
const socket = io(WHATSAPP_SOCKET_URL, {
|
||||
auth: {
|
||||
token: getAuthToken(),
|
||||
},
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
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>
|
||||
|
||||
<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 && (
|
||||
<div className="mt-4 flex flex-col items-center">
|
||||
|
||||
10
src/routes/ProtectedRoute.jsx
Normal file
10
src/routes/ProtectedRoute.jsx
Normal 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;
|
||||
}
|
||||
@ -7,6 +7,11 @@ import { ChatPage } from '../modules/chat/pages/ChatPage';
|
||||
import { CallPage } from '../modules/call/pages/CallPage';
|
||||
import { AgentNewAttendancePage } from '../modules/attendance/pages/AgentNewAttendancePage';
|
||||
import { WhatsappAdminPage } from '../modules/management/pages/WhatsappAdminPage';
|
||||
import { ProtectedRoute } from './ProtectedRoute';
|
||||
|
||||
function privatePage(page) {
|
||||
return <ProtectedRoute>{page}</ProtectedRoute>;
|
||||
}
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
@ -19,30 +24,30 @@ export const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
element: <ProfileHomePage />,
|
||||
element: privatePage(<ProfileHomePage />),
|
||||
},
|
||||
{
|
||||
path: '/chat',
|
||||
element: <ChatPage />,
|
||||
element: privatePage(<ChatPage />),
|
||||
},
|
||||
{
|
||||
path: '/call',
|
||||
element: <CallPage />,
|
||||
element: privatePage(<CallPage />),
|
||||
},
|
||||
{
|
||||
path: '/new-attendance',
|
||||
element: <AgentNewAttendancePage />,
|
||||
element: privatePage(<AgentNewAttendancePage />),
|
||||
},
|
||||
{
|
||||
path: '/mass-message',
|
||||
element: <AgentMassMessagePage />,
|
||||
element: privatePage(<AgentMassMessagePage />),
|
||||
},
|
||||
{
|
||||
path: '/contacts',
|
||||
element: <ContactsPage />,
|
||||
element: privatePage(<ContactsPage />),
|
||||
},
|
||||
{
|
||||
path: '/admin/whatsapp',
|
||||
element: <WhatsappAdminPage />,
|
||||
element: privatePage(<WhatsappAdminPage />),
|
||||
},
|
||||
]);
|
||||
|
||||
@ -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 { getAuthToken } from '../../modules/auth/services/sessionService';
|
||||
|
||||
export function useWhatsappSocket() {
|
||||
const [socket, setSocket] = useState(null);
|
||||
@ -15,6 +16,9 @@ export function useWhatsappSocket() {
|
||||
// Conectar ao namespace /whatsapp
|
||||
const newSocket = io(WHATSAPP_SOCKET_URL, {
|
||||
reconnectionAttempts: 5,
|
||||
auth: {
|
||||
token: getAuthToken(),
|
||||
},
|
||||
});
|
||||
|
||||
socketRef.current = newSocket;
|
||||
|
||||
35
src/shared/services/authFetch.js
Normal file
35
src/shared/services/authFetch.js
Normal 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,
|
||||
});
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user