feat: Implement dedicated login page and public/dashboard layouts, refactoring authentication flow and removing the todos page.

This commit is contained in:
2025-12-12 14:22:28 -03:00
parent b47a317c33
commit b771322b24
18 changed files with 665 additions and 802 deletions

View File

@@ -1,14 +1,21 @@
import { createConvexHttpClient } from '@mmailaender/convex-better-auth-svelte/sveltekit';
import { api } from '@sgse-app/backend/convex/_generated/api';
import { error, redirect } from '@sveltejs/kit';
import type { FunctionReference } from 'convex/server';
export const load = async ({ locals }) => {
if (!locals.token) {
throw redirect(302, '/login');
}
try {
const client = createConvexHttpClient({ token: locals.token });
const currentUser = await client.query(api.auth.getCurrentUser, {});
const currentUser = await client.query(api.auth.getCurrentUser as FunctionReference<'query'>);
if (!currentUser) {
throw redirect(302, '/login');
}
return { currentUser };
} catch (error) {
console.error('Erro ao carregar usuário atual no layout do dashboard:', error);
// Evita quebrar toda a área logada em caso de falha transitória na API/auth
return { currentUser: null };
} catch {
return error(500, 'Ops! Ocorreu um erro, tente novamente mais tarde.');
}
};

View File

@@ -1,100 +1,17 @@
<script lang="ts">
import { page } from '$app/state';
import { useQuery } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import ActionGuard from '$lib/components/ActionGuard.svelte';
import { Toaster } from 'svelte-sonner';
import PushNotificationManager from '$lib/components/PushNotificationManager.svelte';
import Sidebar from '$lib/components/Sidebar.svelte';
const { children } = $props();
// Usuário atual e consentimento LGPD
const currentUser = useQuery(api.auth.getCurrentUser, {});
const consentimentoLGPD = useQuery(api.lgpd.verificarConsentimento, { tipo: 'termo_uso' });
// Redirecionar para o termo de consentimento se obrigatório e não aceito
$effect(() => {
const p = page.url.pathname;
// Rotas públicas/que não exigem termo
if (
p === '/' ||
p === '/abrir-chamado' ||
p === '/termo-consentimento' ||
p.startsWith('/privacidade') ||
p.startsWith('/api/')
) {
return;
}
// Precisa estar autenticado para exigir LGPD
if (!currentUser?.data) {
return;
}
// Query ainda carregando ou sem dados
if (!consentimentoLGPD?.data) {
return;
}
const data = consentimentoLGPD.data;
if (data.termoObrigatorio && !data.aceito) {
const redirect = encodeURIComponent(p);
window.location.href = `/termo-consentimento?redirect=${redirect}`;
}
});
// Resolver recurso/ação a partir da rota
const routeAction = $derived.by(() => {
const p = page.url.pathname;
if (p === '/' || p === '/abrir-chamado') return null;
// Funcionários
if (p.startsWith('/recursos-humanos/funcionarios')) {
if (p.includes('/cadastro')) return { recurso: 'funcionarios', acao: 'criar' };
if (p.includes('/excluir')) return { recurso: 'funcionarios', acao: 'excluir' };
if (p.includes('/editar') || p.includes('/funcionarioId'))
return { recurso: 'funcionarios', acao: 'editar' };
return { recurso: 'funcionarios', acao: 'listar' };
}
// Símbolos
if (p.startsWith('/recursos-humanos/simbolos')) {
if (p.includes('/cadastro')) return { recurso: 'simbolos', acao: 'criar' };
if (p.includes('/excluir')) return { recurso: 'simbolos', acao: 'excluir' };
if (p.includes('/editar') || p.includes('/simboloId'))
return { recurso: 'simbolos', acao: 'editar' };
return { recurso: 'simbolos', acao: 'listar' };
}
// Outras áreas (uso genérico: ver)
if (p.startsWith('/financeiro')) return { recurso: 'financeiro', acao: 'ver' };
if (p.startsWith('/controladoria')) return { recurso: 'controladoria', acao: 'ver' };
if (p.startsWith('/licitacoes')) return { recurso: 'licitacoes', acao: 'ver' };
if (p.startsWith('/compras')) return { recurso: 'compras', acao: 'ver' };
if (p.startsWith('/juridico')) return { recurso: 'juridico', acao: 'ver' };
if (p.startsWith('/comunicacao')) return { recurso: 'comunicacao', acao: 'ver' };
if (p.startsWith('/programas-esportivos'))
return { recurso: 'programas_esportivos', acao: 'ver' };
if (p.startsWith('/secretaria-executiva'))
return { recurso: 'secretaria_executiva', acao: 'ver' };
if (p.startsWith('/gestao-pessoas')) return { recurso: 'gestao_pessoas', acao: 'ver' };
return null;
});
</script>
{#if routeAction}
<ActionGuard recurso={routeAction.recurso} acao={routeAction.acao}>
<div class="flex flex-col">
<Sidebar>
<main id="container-central" class="w-full max-w-none px-3 py-4 lg:px-4">
{@render children()}
</main>
</ActionGuard>
{:else}
<main id="container-central" class="w-full max-w-none px-3 py-4 lg:px-4">
{@render children()}
</main>
{/if}
</Sidebar>
</div>
<!-- Toast Notifications (Sonner) -->
<Toaster position="top-right" richColors closeButton expand={true} />

View File

@@ -6,7 +6,6 @@
import { resolve } from '$app/paths';
import { UserPlus, Mail, Clock, Award, TrendingUp, Zap, Users, Database } from 'lucide-svelte';
import ProtectedRoute from '$lib/components/ProtectedRoute.svelte';
import { loginModalStore } from '$lib/stores/loginModal.svelte';
// Queries para dados do dashboard
const statsQuery = useQuery(api.dashboard.getStats, {});
@@ -36,7 +35,12 @@
// Se for erro de autenticação, abrir modal de login automaticamente
if (error === 'auth_required') {
loginModalStore.open(route || to.url.pathname);
const redirectTo = route || to.url.pathname;
goto(`${resolve('/login')}?redirect=${encodeURIComponent(redirectTo)}`, {
replaceState: true,
noScroll: true
});
return;
}
// Limpar URL usando SvelteKit (após router estar inicializado)
@@ -65,7 +69,12 @@
const route = urlParams.get('route') || urlParams.get('redirect') || '';
if (error === 'auth_required') {
loginModalStore.open(route || window.location.pathname);
const redirectTo = route || window.location.pathname;
goto(`${resolve('/login')}?redirect=${encodeURIComponent(redirectTo)}`, {
replaceState: true,
noScroll: true
});
return;
}
}