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:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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 */
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user