- Added session manager to handle token refresh and user activity tracking. - Introduced session timeout warning dialog for user inactivity. - Updated authentication token handling in cookies with security policies. - Created server-side endpoint for silent refresh of access tokens using HttpOnly cookies. - Added silent check SSO HTML page for Keycloak integration.
122 lines
4.7 KiB
Svelte
122 lines
4.7 KiB
Svelte
<script lang="ts">
|
|
import { setContext, onMount, onDestroy } from 'svelte';
|
|
import { invalidateAll } from '$app/navigation';
|
|
import type { LayoutData } from './$types';
|
|
import AppSidebar from '$lib/components/sidebar/app-sidebar.svelte';
|
|
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
|
|
import { Separator } from '$lib/components/ui/separator/index.js';
|
|
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
|
import { companyStore } from '$lib/stores/company.svelte';
|
|
import ExchangeRateGuard from '$lib/components/dashboard/exchange_rate/exchange-rate-guard.svelte';
|
|
import SessionTimeoutWarning from '$lib/components/session-timeout-warning.svelte';
|
|
import { page } from '$app/state';
|
|
import {
|
|
createSessionManager,
|
|
destroySessionManager,
|
|
SESSION_EXPIRED_EVENT
|
|
} from '$lib/session-manager';
|
|
import type { SessionExpiredDetail } from '$lib/session-manager';
|
|
import { authStore } from '$lib/auth';
|
|
import { logout, getKeycloakInstance } from '$lib/auth';
|
|
|
|
let { data, children }: { data: LayoutData; children: any } = $props();
|
|
|
|
// Hacer disponible el usuario en el contexto para los componentes hijos
|
|
setContext('user', data.user);
|
|
|
|
// ── Manejar expiración de sesión ────────────────────────────────────────
|
|
function handleSessionExpired(e: Event) {
|
|
const { reason } = (e as CustomEvent<SessionExpiredDetail>).detail;
|
|
console.info(`[Dashboard] Sesión expirada (motivo: ${reason}) — cerrando sesión`);
|
|
void logout();
|
|
}
|
|
|
|
onMount(() => {
|
|
// ── Inicializar el token en el authStore desde los datos del servidor ──
|
|
// El servidor valida la cookie y pasa el access_token a través de data.user.token.
|
|
// Lo almacenamos en memoria (authStore) sin tocar localStorage.
|
|
if (data.user?.token) {
|
|
authStore.setToken(data.user.token);
|
|
authStore.setAuthenticated(true);
|
|
}
|
|
|
|
// ── Inicializar el SessionManager ─────────────────────────────────────
|
|
if (data.user?.token) {
|
|
const mgr = createSessionManager({
|
|
refreshBeforeExpirySeconds: 60, // Refrescar 60s antes de que expire
|
|
idleTimeoutMs: 30 * 60 * 1000, // Idle timeout: 30 minutos
|
|
warningBeforeIdleMs: 5 * 60 * 1000, // Advertencia: 5 min antes del idle
|
|
ssoCheckIntervalMs: 5 * 60 * 1000, // Verificar sesión SSO cada 5 min
|
|
getKeycloakInstance,
|
|
onTokenRefreshed: (newToken) => {
|
|
authStore.setToken(newToken);
|
|
},
|
|
onSessionExpired: (reason) => {
|
|
void logout();
|
|
}
|
|
});
|
|
|
|
mgr.start(data.user.token);
|
|
}
|
|
|
|
// ── Escuchar evento global de expiración de sesión ────────────────────
|
|
window.addEventListener(SESSION_EXPIRED_EVENT, handleSessionExpired);
|
|
|
|
// ── Inicializar compañías ─────────────────────────────────────────────
|
|
if (data.companies) {
|
|
companyStore.initialize(data.companies);
|
|
}
|
|
|
|
// ── Escuchar cambios de compañía y recargar datos ─────────────────────
|
|
const handleCompanyChange = () => invalidateAll();
|
|
window.addEventListener('companyChanged', handleCompanyChange);
|
|
|
|
return () => {
|
|
window.removeEventListener('companyChanged', handleCompanyChange);
|
|
window.removeEventListener(SESSION_EXPIRED_EVENT, handleSessionExpired);
|
|
};
|
|
});
|
|
|
|
onDestroy(() => {
|
|
destroySessionManager();
|
|
});
|
|
</script>
|
|
|
|
<Sidebar.Provider>
|
|
<AppSidebar />
|
|
<Sidebar.Inset class="overflow-x-hidden">
|
|
<header
|
|
class="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12"
|
|
>
|
|
<div class="flex items-center gap-2 px-4">
|
|
<Sidebar.Trigger class="-ml-1" />
|
|
<Separator orientation="vertical" class="mr-2 data-[orientation=vertical]:h-4" />
|
|
<!--
|
|
<Breadcrumb.Root>
|
|
<Breadcrumb.List>
|
|
<Breadcrumb.Item class="hidden md:block">
|
|
<Breadcrumb.Link href="/dashboard">Dashboard</Breadcrumb.Link>
|
|
</Breadcrumb.Item>
|
|
<Breadcrumb.Separator class="hidden md:block" />
|
|
<Breadcrumb.Item>
|
|
<Breadcrumb.Page>Inicio</Breadcrumb.Page>
|
|
</Breadcrumb.Item>
|
|
</Breadcrumb.List>
|
|
</Breadcrumb.Root>
|
|
-->
|
|
</div>
|
|
</header>
|
|
<div class="flex flex-1 flex-col gap-4 overflow-x-hidden p-4 pt-0">
|
|
<!-- Contenido de cada página -->
|
|
{@render children?.()}
|
|
</div>
|
|
</Sidebar.Inset>
|
|
</Sidebar.Provider>
|
|
|
|
{#if !page.url.pathname.includes('/dashboard/invoices') && !page.url.pathname.includes('/dashboard/pedimentos')}
|
|
<ExchangeRateGuard overlayClass="bg-black/5" />
|
|
{/if}
|
|
|
|
<!-- Diálogo de advertencia de sesión por inactividad -->
|
|
<SessionTimeoutWarning />
|