refactor: enhance ProtectedRoute and dashboard components for improved access control and user experience

- Updated the ProtectedRoute component to optimize access checking logic, preventing unnecessary re-checks and improving authentication flow.
- Enhanced the dashboard page to automatically open the login modal for authentication errors and refined loading states for better user feedback.
- Improved UI elements across various components for consistency and visual appeal, including updated tab styles and enhanced alert messages.
- Removed redundant footer from the vacation management page to streamline the interface.
This commit is contained in:
2025-11-18 06:34:55 -03:00
parent 3420872a37
commit 422dc6f022
5 changed files with 398 additions and 541 deletions

View File

@@ -21,11 +21,26 @@
let isChecking = $state(true); let isChecking = $state(true);
let hasAccess = $state(false); let hasAccess = $state(false);
let timeoutId: ReturnType<typeof setTimeout> | null = null; let timeoutId: ReturnType<typeof setTimeout> | null = null;
let hasCheckedOnce = $state(false);
let lastUserState = $state<typeof currentUser | undefined>(undefined);
const currentUser = useQuery(api.auth.getCurrentUser, {}); const currentUser = useQuery(api.auth.getCurrentUser, {});
// Usar $effect para reagir às mudanças na query // Usar $effect para reagir apenas às mudanças na query currentUser
$effect(() => { $effect(() => {
checkAccess(); // Não verificar novamente se já tem acesso concedido e usuário está autenticado
if (hasAccess && currentUser?.data) {
lastUserState = currentUser;
return;
}
// Evitar loop: só verificar se currentUser realmente mudou
// Comparar dados, não o objeto proxy
const currentData = currentUser?.data;
const lastData = lastUserState?.data;
if (currentData !== lastData || (currentUser === undefined) !== (lastUserState === undefined)) {
lastUserState = currentUser;
checkAccess();
}
}); });
function checkAccess() { function checkAccess() {
@@ -42,6 +57,9 @@
return; return;
} }
// Marcar que já verificou pelo menos uma vez
hasCheckedOnce = true;
// Se a query retornou dados, verificar autenticação // Se a query retornou dados, verificar autenticação
if (currentUser?.data) { if (currentUser?.data) {
// Verificar roles // Verificar roles
@@ -67,20 +85,29 @@
return; return;
} }
// Se não tem dados e requer autenticação, aguardar um pouco antes de redirecionar // Se não tem dados e requer autenticação
// (pode estar carregando ainda)
if (requireAuth && !currentUser?.data) { if (requireAuth && !currentUser?.data) {
// Se a query já retornou (não está mais undefined), finalizar estado
if (currentUser !== undefined) {
const currentPath = window.location.pathname;
// Evitar redirecionamento em loop - verificar se já está na URL de erro
const urlParams = new URLSearchParams(window.location.search);
if (!urlParams.has('error')) {
// Só redirecionar se não estiver em loop
if (!hasCheckedOnce || currentUser === null) {
window.location.href = `${redirectTo}?error=auth_required&redirect=${encodeURIComponent(currentPath)}`;
return;
}
}
// Se já tem erro na URL, permitir renderização para mostrar o alerta
isChecking = false;
hasAccess = true;
return;
}
// Se ainda está carregando (undefined), aguardar
isChecking = true; isChecking = true;
hasAccess = false; hasAccess = false;
// Aguardar 3 segundos antes de redirecionar (dar tempo para a query carregar)
timeoutId = setTimeout(() => {
// Verificar novamente antes de redirecionar
if (!currentUser?.data) {
const currentPath = window.location.pathname;
window.location.href = `${redirectTo}?error=auth_required&redirect=${encodeURIComponent(currentPath)}`;
}
}, 3000);
return; return;
} }

View File

@@ -9,12 +9,13 @@
import { UserPlus, Mail } from "lucide-svelte"; import { UserPlus, Mail } from "lucide-svelte";
import { useAuth } from "@mmailaender/convex-better-auth-svelte/svelte"; import { useAuth } from "@mmailaender/convex-better-auth-svelte/svelte";
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte"; import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
import { loginModalStore } from "$lib/stores/loginModal.svelte";
let { data } = $props(); let { data } = $props();
const auth = useAuth(); const auth = useAuth();
const isLoading = $derived(auth.isLoading && !data.currentUser); const isLoading = $derived(auth.isLoading && !data?.currentUser);
const isAuthenticated = $derived(auth.isAuthenticated || !!data.currentUser); const isAuthenticated = $derived(auth.isAuthenticated || !!data?.currentUser);
$inspect({ isLoading, isAuthenticated }); $inspect({ isLoading, isAuthenticated });
@@ -56,6 +57,11 @@
redirectRoute = route; redirectRoute = route;
showAlert = true; showAlert = true;
// Se for erro de autenticação, abrir modal de login automaticamente
if (error === "auth_required") {
loginModalStore.open(route || to.url.pathname);
}
// Limpar URL usando SvelteKit (após router estar inicializado) // Limpar URL usando SvelteKit (após router estar inicializado)
try { try {
replaceState(to.url.pathname, {}); replaceState(to.url.pathname, {});
@@ -75,6 +81,17 @@
onMount(() => { onMount(() => {
mounted = true; mounted = true;
// Verificar se há erro na URL ao carregar a página
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has("error")) {
const error = urlParams.get("error");
const route = urlParams.get("route") || urlParams.get("redirect") || "";
if (error === "auth_required") {
loginModalStore.open(route || window.location.pathname);
}
}
// Atualizar relógio e forçar refresh das queries a cada segundo // Atualizar relógio e forçar refresh das queries a cada segundo
const interval = setInterval(() => { const interval = setInterval(() => {
currentTime = new Date(); currentTime = new Date();

File diff suppressed because it is too large Load Diff

View File

@@ -1993,12 +1993,6 @@
{/if} {/if}
{/await} {/await}
{/if} {/if}
<footer
class="border-base-300/60 bg-base-100 text-base-content/70 mt-8 border-t py-6 text-center text-sm"
>
SGSE - Sistema de Gerenciamento de Secretaria.
</footer>
<style> <style>
/* Calendário de Férias */ /* Calendário de Férias */

View File

@@ -636,7 +636,7 @@ export const getStatusSistema = query({
/** /**
* Atividade do banco no último minuto (agregada em buckets) * Atividade do banco no último minuto (agregada em buckets)
* Usa mensagensPorMinuto como proxy de atividade quando disponível. * Usa logsAtividades e systemMetrics para calcular atividade real.
*/ */
export const getAtividadeBancoDados = query({ export const getAtividadeBancoDados = query({
args: {}, args: {},
@@ -652,6 +652,14 @@ export const getAtividadeBancoDados = query({
const agora = Date.now(); const agora = Date.now();
const haUmMinuto = agora - 60 * 1000; const haUmMinuto = agora - 60 * 1000;
// Buscar atividades reais do sistema
const atividadesRecentes = await ctx.db
.query('logsAtividades')
.withIndex('by_timestamp', (q) => q.gte('timestamp', haUmMinuto))
.order('asc')
.collect();
// Buscar métricas também (para mensagens se houver)
const metricasRecentes = await ctx.db const metricasRecentes = await ctx.db
.query('systemMetrics') .query('systemMetrics')
.withIndex('by_timestamp', (q) => q.gte('timestamp', haUmMinuto)) .withIndex('by_timestamp', (q) => q.gte('timestamp', haUmMinuto))
@@ -666,15 +674,30 @@ export const getAtividadeBancoDados = query({
for (let i = 0; i < numBuckets; i++) { for (let i = 0; i < numBuckets; i++) {
const inicio = haUmMinuto + i * bucketSizeMs; const inicio = haUmMinuto + i * bucketSizeMs;
const fim = inicio + bucketSizeMs; const fim = inicio + bucketSizeMs;
// Contar atividades de criação/inserção (entradas)
const atividadesBucket = atividadesRecentes.filter(
(a) => a.timestamp >= inicio && a.timestamp < fim
);
const entradasAtividades = atividadesBucket.filter(
a => a.acao === 'criar' || a.acao === 'inserir' || a.acao === 'cadastrar'
).length;
// Contar atividades de exclusão/remoção (saídas)
const saidasAtividades = atividadesBucket.filter(
a => a.acao === 'excluir' || a.acao === 'remover' || a.acao === 'deletar'
).length;
// Usar mensagensPorMinuto como adicional se disponível
const bucketMetricas = metricasRecentes.filter( const bucketMetricas = metricasRecentes.filter(
(m) => m.timestamp >= inicio && m.timestamp < fim (m) => m.timestamp >= inicio && m.timestamp < fim
); );
// Usar mensagensPorMinuto como proxy de "entradas"; "saídas" como fração
const somaMensagens = const somaMensagens =
bucketMetricas.reduce((acc, m) => acc + (m.mensagensPorMinuto ?? 0), 0) || 0; bucketMetricas.reduce((acc, m) => acc + (m.mensagensPorMinuto ?? 0), 0) || 0;
const entradas = Math.max(0, Math.round(somaMensagens));
const saidas = Math.max(0, Math.round(entradas * 0.6)); // Combinar atividades reais com métricas de mensagens
const entradas = Math.max(0, Math.round(entradasAtividades + somaMensagens * 0.3));
const saidas = Math.max(0, Math.round(saidasAtividades + somaMensagens * 0.2));
historico.push({ entradas, saidas }); historico.push({ entradas, saidas });
} }
@@ -684,7 +707,7 @@ export const getAtividadeBancoDados = query({
}); });
/** /**
* Distribuição de operações (estimada a partir das métricas) * Distribuição de operações (calculada a partir de logsAtividades e métricas)
*/ */
export const getDistribuicaoRequisicoes = query({ export const getDistribuicaoRequisicoes = query({
args: {}, args: {},
@@ -696,21 +719,43 @@ export const getDistribuicaoRequisicoes = query({
}), }),
handler: async (ctx) => { handler: async (ctx) => {
const umaHoraAtras = Date.now() - 60 * 60 * 1000; const umaHoraAtras = Date.now() - 60 * 60 * 1000;
// Buscar atividades reais do sistema
const atividades = await ctx.db
.query('logsAtividades')
.withIndex('by_timestamp', (q) => q.gte('timestamp', umaHoraAtras))
.collect();
// Buscar métricas também
const metricas = await ctx.db const metricas = await ctx.db
.query('systemMetrics') .query('systemMetrics')
.withIndex('by_timestamp', (q) => q.gte('timestamp', umaHoraAtras)) .withIndex('by_timestamp', (q) => q.gte('timestamp', umaHoraAtras))
.order('desc') .order('desc')
.take(100); .take(100);
const totalOps = Math.max( // Contar operações de leitura (consultas, visualizações)
const leituras = atividades.filter(
a => a.acao === 'consultar' || a.acao === 'visualizar' || a.acao === 'listar' || a.acao === 'buscar'
).length;
// Contar operações de escrita (criar, editar, excluir)
const escritas = atividades.filter(
a => a.acao === 'criar' || a.acao === 'editar' || a.acao === 'excluir' ||
a.acao === 'inserir' || a.acao === 'atualizar' || a.acao === 'deletar' ||
a.acao === 'cadastrar' || a.acao === 'remover'
).length;
// Adicionar estimativa baseada em mensagens se disponível
const totalMensagens = Math.max(
0, 0,
Math.round(metricas.reduce((acc, m) => acc + (m.mensagensPorMinuto ?? 0), 0)) Math.round(metricas.reduce((acc, m) => acc + (m.mensagensPorMinuto ?? 0), 0))
); );
const queries = Math.round(totalOps * 0.7); // Queries são leituras + parte das mensagens (como consultas de chat)
const mutations = Math.max(0, totalOps - queries); const queries = leituras + Math.round(totalMensagens * 0.5);
const leituras = queries;
const escritas = mutations; // Mutations são escritas + parte das mensagens (como envio de mensagens)
const mutations = escritas + Math.round(totalMensagens * 0.3);
return { queries, mutations, leituras, escritas }; return { queries, mutations, leituras, escritas };
} }