- 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.
129 lines
4.4 KiB
Svelte
129 lines
4.4 KiB
Svelte
<script lang="ts">
|
|
import { onMount, onDestroy } from 'svelte';
|
|
import { browser } from '$app/environment';
|
|
import * as Dialog from '$lib/components/ui/dialog';
|
|
import { Button } from '$lib/components/ui/button';
|
|
import {
|
|
SESSION_WARNING_EVENT,
|
|
SESSION_EXPIRED_EVENT,
|
|
SESSION_EXTENDED_EVENT,
|
|
getSessionManager
|
|
} from '$lib/session-manager';
|
|
import type { SessionWarningDetail } from '$lib/session-manager';
|
|
|
|
// ─── State ────────────────────────────────────────────────────────────────
|
|
let open = $state(false);
|
|
let remainingSeconds = $state(300);
|
|
let countdownId: ReturnType<typeof setInterval> | null = null;
|
|
|
|
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
function formatTime(secs: number): string {
|
|
const m = Math.floor(secs / 60);
|
|
const s = secs % 60;
|
|
return `${m}:${s.toString().padStart(2, '0')}`;
|
|
}
|
|
|
|
function clearCountdown() {
|
|
if (countdownId !== null) {
|
|
clearInterval(countdownId);
|
|
countdownId = null;
|
|
}
|
|
}
|
|
|
|
function startCountdown() {
|
|
clearCountdown();
|
|
countdownId = setInterval(() => {
|
|
remainingSeconds = Math.max(0, remainingSeconds - 1);
|
|
if (remainingSeconds === 0) clearCountdown();
|
|
}, 1000);
|
|
}
|
|
|
|
// ─── Event handlers ───────────────────────────────────────────────────────
|
|
function onWarning(e: Event) {
|
|
const { remainingMs } = (e as CustomEvent<SessionWarningDetail>).detail;
|
|
remainingSeconds = Math.floor(remainingMs / 1000);
|
|
open = true;
|
|
startCountdown();
|
|
}
|
|
|
|
function onExpired() {
|
|
open = false;
|
|
clearCountdown();
|
|
}
|
|
|
|
function onExtended() {
|
|
open = false;
|
|
clearCountdown();
|
|
}
|
|
|
|
// ─── User actions ─────────────────────────────────────────────────────────
|
|
function continueSession() {
|
|
const mgr = getSessionManager();
|
|
mgr?.extendSession();
|
|
open = false;
|
|
clearCountdown();
|
|
}
|
|
|
|
function logoutNow() {
|
|
open = false;
|
|
clearCountdown();
|
|
// Dispara el evento de sesión expirada para que el layout gestione el logout
|
|
window.dispatchEvent(
|
|
new CustomEvent(SESSION_EXPIRED_EVENT, { detail: { reason: 'manual' } })
|
|
);
|
|
}
|
|
|
|
// ─── Lifecycle ────────────────────────────────────────────────────────────
|
|
onMount(() => {
|
|
if (!browser) return;
|
|
window.addEventListener(SESSION_WARNING_EVENT, onWarning);
|
|
window.addEventListener(SESSION_EXPIRED_EVENT, onExpired);
|
|
window.addEventListener(SESSION_EXTENDED_EVENT, onExtended);
|
|
});
|
|
|
|
onDestroy(() => {
|
|
if (!browser) return;
|
|
clearCountdown();
|
|
window.removeEventListener(SESSION_WARNING_EVENT, onWarning);
|
|
window.removeEventListener(SESSION_EXPIRED_EVENT, onExpired);
|
|
window.removeEventListener(SESSION_EXTENDED_EVENT, onExtended);
|
|
});
|
|
</script>
|
|
|
|
<!--
|
|
session-timeout-warning.svelte
|
|
Diálogo que avisa al usuario cuando su sesión está a punto de expirar
|
|
por inactividad. Se controla completamente a través de eventos DOM.
|
|
-->
|
|
<Dialog.Root bind:open>
|
|
<Dialog.Portal>
|
|
<Dialog.Overlay class="fixed inset-0 z-[9998] bg-black/40 backdrop-blur-sm" />
|
|
<Dialog.Content
|
|
class="fixed left-1/2 top-1/2 z-[9999] w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg bg-background p-6 shadow-xl"
|
|
>
|
|
<Dialog.Header>
|
|
<Dialog.Title class="flex items-center gap-2 text-lg font-semibold">
|
|
⚠️ Sesión por expirar
|
|
</Dialog.Title>
|
|
<Dialog.Description class="mt-2 text-sm text-muted-foreground">
|
|
Tu sesión cerrará automáticamente por inactividad en
|
|
<span class="font-mono font-bold text-foreground">
|
|
{formatTime(remainingSeconds)}
|
|
</span>.
|
|
<br />
|
|
¿Deseas continuar trabajando?
|
|
</Dialog.Description>
|
|
</Dialog.Header>
|
|
|
|
<Dialog.Footer class="mt-6 flex gap-3">
|
|
<Button variant="outline" class="flex-1" onclick={logoutNow}>
|
|
Cerrar sesión
|
|
</Button>
|
|
<Button class="flex-1" onclick={continueSession}>
|
|
Continuar sesión
|
|
</Button>
|
|
</Dialog.Footer>
|
|
</Dialog.Content>
|
|
</Dialog.Portal>
|
|
</Dialog.Root>
|