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 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} />
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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');
|
||||||
@ -38,10 +42,13 @@ export const WhatsappAdminPage = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
<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">
|
||||||
<p className="mb-2 text-gray-600">Escaneie o QR Code abaixo com seu WhatsApp:</p>
|
<p className="mb-2 text-gray-600">Escaneie o QR Code abaixo com seu WhatsApp:</p>
|
||||||
|
|||||||
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 { 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 />),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
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