feat: implementar autenticação com token e refatorar serviços de métricas

main
guilherme 2025-12-15 09:54:39 -03:00
parent cb01415dfa
commit 5dfe4570e6
9 changed files with 144 additions and 46 deletions

View File

@ -1,12 +1,10 @@
import { Navigate, Route, Routes } from 'react-router-dom'; import { Navigate, Route, Routes } from 'react-router-dom';
import { LoginSlide } from './views/components/login-slide';
import { RetrospectiveSlides } from './views/components/retrospective-slides'; import { RetrospectiveSlides } from './views/components/retrospective-slides';
export function App() { export function App() {
return ( return (
<Routes> <Routes>
<Route path="/" element={<LoginSlide />} /> <Route path="/:token" element={<RetrospectiveSlides />} />
<Route path="/retrospectiva" element={<RetrospectiveSlides />} />
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>
); );

View File

@ -0,0 +1,3 @@
export const localStorageKeys = {
ACCESS_TOKEN: '@review:token',
};

View File

@ -0,0 +1,78 @@
/* eslint-disable react-refresh/only-export-components */
import { api } from '@/app/services';
import { createContext, useCallback, useContext, useState } from 'react';
import { localStorageKeys } from '../config/local-storage-keys';
type AuthState = {
token: string;
};
type AuthContextValue = {
signedIn: boolean;
authenticate(token: string): void;
signOut(): void;
};
export const AuthContext = createContext({} as AuthContextValue);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [isSignedIn, setIsSignedIn] = useState<boolean>(false);
const [, setAuthState] = useState<AuthState>(() => {
const token = localStorage.getItem(localStorageKeys.ACCESS_TOKEN);
if (token) {
api.defaults.headers.Authorization = `Bearer ${token}`;
setIsSignedIn(true);
return { token };
}
return {} as AuthState;
});
const authenticate = useCallback((token: string) => {
try {
localStorage.setItem(localStorageKeys.ACCESS_TOKEN, token);
api.defaults.headers.Authorization = `Bearer ${token}`;
setIsSignedIn(true);
setAuthState({ token });
} catch (error) {
console.error(error);
}
}, []);
const signOut = useCallback(() => {
localStorage.removeItem(localStorageKeys.ACCESS_TOKEN);
setIsSignedIn(false);
setAuthState({} as AuthState);
api.defaults.headers.Authorization = '';
api.defaults.headers['Cache-Control'] = 'no-cache';
}, []);
return (
<AuthContext.Provider
value={{
signedIn: isSignedIn,
authenticate,
signOut,
}}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

View File

@ -1,18 +1,18 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { metricsService } from '../services/metrics'; import { metricsService } from '../services/metrics';
export function useMetricsByPortal(cpf: string, ano: number) { export function useMetricsByPortal(ano: number) {
return useQuery({ return useQuery({
queryKey: ['metricsByPortal', cpf, ano], queryKey: ['metricsByPortal', ano],
queryFn: () => metricsService.getMetricsByPortal({ cpf, ano }), queryFn: () => metricsService.getMetricsByPortal({ ano }),
enabled: !!cpf && !!ano, enabled: !!ano,
}); });
} }
export function useMetricsBySystems(cpf: string, ano: number) { export function useMetricsBySystems(ano: number) {
return useQuery({ return useQuery({
queryKey: ['metricsBySystems', cpf, ano], queryKey: ['metricsBySystems', ano],
queryFn: () => metricsService.getMetricsBySystems({ cpf, ano }), queryFn: () => metricsService.getMetricsBySystems({ ano }),
enabled: !!cpf && !!ano, enabled: !!ano,
}); });
} }

View File

@ -6,10 +6,12 @@ const api = axios.create({
baseURL: import.meta.env.VITE_API_URL, baseURL: import.meta.env.VITE_API_URL,
}); });
// Adicionar token temporariamente aos headers export function setAuthToken(newToken: string | null) {
const token = import.meta.env.VITE_API_TOKEN || ''; if (newToken) {
if (token) { api.defaults.headers.common['Authorization'] = `Bearer ${newToken}`;
api.defaults.headers.common['Authorization'] = `Bearer ${token}`; } else {
delete api.defaults.headers.common['Authorization'];
}
} }
export const queryClient = new QueryClient({ export const queryClient = new QueryClient({

View File

@ -8,15 +8,13 @@ type MetricsByPortalProps = {
type GetMetricsByPortalParams = { type GetMetricsByPortalParams = {
ano: number; ano: number;
cpf: string;
}; };
export async function getMetricsByPortal({ export async function getMetricsByPortal({
ano, ano,
cpf,
}: GetMetricsByPortalParams): Promise<MetricsByPortalProps[]> { }: GetMetricsByPortalParams): Promise<MetricsByPortalProps[]> {
const { data } = await api.get( const { data } = await api.get(
`/userAccessMetricsByPortal?ano=${ano}&cpf=${cpf}`, `/userAccessMetricsByPortal?ano=${ano}`,
); );
return data; return data;

View File

@ -8,16 +8,12 @@ type MetricsBySystemsProps = {
type GetMetricsBySystemsParams = { type GetMetricsBySystemsParams = {
ano: number; ano: number;
cpf: string;
}; };
export async function getMetricsBySystems({ export async function getMetricsBySystems({
ano, ano,
cpf,
}: GetMetricsBySystemsParams): Promise<MetricsBySystemsProps[]> { }: GetMetricsBySystemsParams): Promise<MetricsBySystemsProps[]> {
const { data } = await api.get( const { data } = await api.get(`/userAccessMetricsBySystem?ano=${ano}`);
`/userAccessMetricsBySystem?ano=${ano}&cpf=${cpf}`,
);
return data; return data;
} }

View File

@ -14,13 +14,16 @@ import { ptBR } from 'date-fns/locale';
setDefaultOptions({ locale: ptBR }); setDefaultOptions({ locale: ptBR });
import '@/styles/index.css'; import '@/styles/index.css';
import { AuthProvider } from './app/hooks/use-auth';
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<BrowserRouter> <BrowserRouter>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<AuthProvider>
<StrictMode> <StrictMode>
<App /> <App />
</StrictMode> </StrictMode>
</AuthProvider>
</QueryClientProvider> </QueryClientProvider>
</BrowserRouter>, </BrowserRouter>,
); );

View File

@ -1,3 +1,5 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { useAuth } from '@/app/hooks/use-auth';
import { import {
useMetricsByPortal, useMetricsByPortal,
useMetricsBySystems, useMetricsBySystems,
@ -6,28 +8,50 @@ import logoAImg from '@/assets/images/a-agape.png';
import logoImg from '@/assets/images/agape-logo.png'; import logoImg from '@/assets/images/agape-logo.png';
import { ChartLineUpIcon, SpinnerIcon } from '@phosphor-icons/react'; import { ChartLineUpIcon, SpinnerIcon } from '@phosphor-icons/react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useSearchParams } from 'react-router-dom'; import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import ProfileCard from './ProfileCard'; import ProfileCard from './ProfileCard';
import { StorySlide } from './story-slide'; import { StorySlide } from './story-slide';
export function RetrospectiveSlides() { export function RetrospectiveSlides() {
const [searchParams] = useSearchParams(); const { token } = useParams();
const cpf = searchParams.get('cpf') || ''; const { authenticate } = useAuth();
const ano = searchParams.get('ano')
? parseInt(searchParams.get('ano')!) useEffect(() => {
: new Date().getFullYear(); if (token) {
authenticate(token);
}
}, [token, authenticate]);
const year = new Date().getFullYear();
if (!token) {
return (
<div className="h-screen w-full bg-black flex items-center justify-center">
<div className="text-center space-y-4 max-w-md">
<p className="text-white text-lg font-semibold">
Token inválido ou ausente
</p>
<p className="text-white/70">
Esta aplicação deve ser acessada com o parâmetro <code>token</code>{' '}
na URL. Exemplo: <code>?token=SEU_TOKEN&amp;cpf=00000000000</code>
</p>
</div>
</div>
);
}
const { const {
data: metricsPortal, data: metricsPortal,
isLoading: isLoadingPortal, isLoading: isLoadingPortal,
error: errorPortal, error: errorPortal,
} = useMetricsByPortal(cpf, ano); } = useMetricsByPortal(year);
const { const {
data: metricsSystems, data: metricsSystems,
isLoading: isLoadingSystems, isLoading: isLoadingSystems,
error: errorSystems, error: errorSystems,
} = useMetricsBySystems(cpf, ano); } = useMetricsBySystems(year);
const slides: number[] = []; const slides: number[] = [];
slides.push(0); slides.push(0);
@ -108,7 +132,7 @@ export function RetrospectiveSlides() {
transition={{ duration: 0.6, delay: 0.2 }}> transition={{ duration: 0.6, delay: 0.2 }}>
Sua Retrospectiva Sua Retrospectiva
<br /> <br />
Ágape {ano} Ágape {year}
</motion.h1> </motion.h1>
<motion.p <motion.p
@ -139,12 +163,7 @@ export function RetrospectiveSlides() {
{metricsPortal && metricsPortal.length > 0 && ( {metricsPortal && metricsPortal.length > 0 && (
<> <>
<StorySlide className="bg-white text-gray-900 overflow-hidden relative"> <StorySlide className="bg-linear-to-br from-slate-50 to-blue-50">
<div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-24 -right-24 w-96 h-96 bg-gradient-to-br from-blue-50 to-indigo-50 rounded-full opacity-70"></div>
<div className="absolute -bottom-32 -left-32 w-80 h-80 bg-gradient-to-tr from-blue-50 to-cyan-50 rounded-full opacity-60"></div>
</div>
<div className="max-w-7xl mx-auto w-full h-full px-6 py-8 md:py-16 relative z-10"> <div className="max-w-7xl mx-auto w-full h-full px-6 py-8 md:py-16 relative z-10">
<div className="flex flex-col lg:flex-row items-center justify-between h-full"> <div className="flex flex-col lg:flex-row items-center justify-between h-full">
<motion.div <motion.div
@ -201,7 +220,7 @@ export function RetrospectiveSlides() {
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-lg md:text-xl text-muted-foreground"> <span className="text-lg md:text-xl text-muted-foreground">
Acessos em {ano} Acessos em {year}
</span> </span>
</div> </div>
</div> </div>
@ -301,7 +320,8 @@ export function RetrospectiveSlides() {
}}> }}>
<p className="text-xl md:text-2xl font-bold text-yellow-300 mb-1"> <p className="text-xl md:text-2xl font-bold text-yellow-300 mb-1">
{( {(
(item.totalAcessos / (totalAcessos || 1)) * (item.totalAcessos /
(totalAcessos || 1)) *
100 100
).toFixed(0)} ).toFixed(0)}
% %
@ -349,7 +369,7 @@ export function RetrospectiveSlides() {
</motion.div> </motion.div>
<h2 className="text-4xl md:text-5xl font-bold text-[#0e233d]"> <h2 className="text-4xl md:text-5xl font-bold text-[#0e233d]">
A Ágape deseja a você um {ano + 1} repleto de sucesso! {'<'} O futuro é programado aqui! Feliz {year + 1}! {'/>'}
</h2> </h2>
<p className="text-xl text-muted-foreground max-w-xl"> <p className="text-xl text-muted-foreground max-w-xl">
@ -365,7 +385,7 @@ export function RetrospectiveSlides() {
whileInView={{ opacity: 1 }} whileInView={{ opacity: 1 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.4 }}> transition={{ duration: 0.6, delay: 0.4 }}>
<p>&copy; {ano} Ágape Sistemas e Tecnologia</p> <p>&copy; {year} Ágape Sistemas e Tecnologia</p>
<p className="mt-2"> <p className="mt-2">
O Futuro da Gestão Pública começa aqui! O Futuro da Gestão Pública começa aqui!
</p> </p>