feat: implementar autenticação com token e refatorar serviços de métricas
parent
cb01415dfa
commit
5dfe4570e6
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const localStorageKeys = {
|
||||||
|
ACCESS_TOKEN: '@review:token',
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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&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>© {ano} Ágape Sistemas e Tecnologia</p>
|
<p>© {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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue