feat: gestionar sesión web con idle timeout y ruta /app protegida

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
webui
brobert 2 weeks ago
parent b7c8e37a85
commit ccbc9413d8

@ -1,13 +1,14 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
/* See https://svelte.dev/docs/kit/types#app.d.ts */
declare global {
namespace App {
interface Locals {
userId?: string | null;
}
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

@ -1,8 +1,73 @@
import type { Handle } from '@sveltejs/kit';
import { getDb } from '$lib/server/db';
import { sha256Hex } from '$lib/server/crypto';
import { isProd, sessionIdleTtlMs } from '$lib/server/env';
function toIsoSql(d: Date): string {
return d.toISOString().replace('T', ' ').replace('Z', '');
}
export const handle: Handle = async ({ event, resolve }) => {
// Handler mínimo (sin sesión aún). Añadimos cabeceras de seguridad básicas.
// Sesión por cookie 'sid'
const sid = event.cookies.get('sid');
if (sid) {
try {
const db = await getDb();
const hash = await sha256Hex(sid);
// Validar sesión vigente
const row = db
.prepare(
`SELECT user_id FROM web_sessions
WHERE session_hash = ?
AND expires_at > strftime('%Y-%m-%d %H:%M:%f','now')
LIMIT 1`
)
.get(hash) as { user_id: string } | undefined;
if (row?.user_id) {
event.locals.userId = row.user_id;
// Renovar expiración por inactividad y last_seen_at
const newExpIso = toIsoSql(new Date(Date.now() + sessionIdleTtlMs));
try {
db.prepare(
`UPDATE web_sessions
SET last_seen_at = strftime('%Y-%m-%d %H:%M:%f','now'),
expires_at = ?
WHERE session_hash = ?`
).run(newExpIso, hash);
} catch {
// Si no existe last_seen_at en el esquema, al menos renovar expires_at
try {
db.prepare(
`UPDATE web_sessions
SET expires_at = ?
WHERE session_hash = ?`
).run(newExpIso, hash);
} catch {}
}
// Refrescar cookie (idle)
event.cookies.set('sid', sid, {
path: '/',
httpOnly: true,
sameSite: 'lax',
secure: isProd(),
maxAge: Math.floor(sessionIdleTtlMs / 1000)
});
} else {
// Sesión inválida/expirada
event.cookies.delete('sid', { path: '/' });
}
} catch {
// En caso de error de DB, no romper la request; continuar sin sesión
}
}
const response = await resolve(event);
// Cabeceras de seguridad básicas
try {
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('Referrer-Policy', 'no-referrer');

@ -1,2 +1,4 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
<p><a href="/app">Ir al panel</a></p>

@ -0,0 +1,28 @@
import type { RequestHandler } from './$types';
import { getDb } from '$lib/server/db';
import { sha256Hex } from '$lib/server/crypto';
export const POST: RequestHandler = async (event) => {
const sid = event.cookies.get('sid');
if (sid) {
try {
const db = await getDb();
const hash = await sha256Hex(sid);
// Intentar borrar; si falla, expirar
try {
db.prepare(`DELETE FROM web_sessions WHERE session_hash = ?`).run(hash);
} catch {
db.prepare(
`UPDATE web_sessions
SET expires_at = strftime('%Y-%m-%d %H:%M:%f','now')
WHERE session_hash = ?`
).run(hash);
}
} catch {
// Ignorar errores de DB en logout
}
}
// Limpiar cookie
event.cookies.delete('sid', { path: '/' });
return new Response(null, { status: 204 });
};

@ -0,0 +1,11 @@
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async (event) => {
const userId = event.locals.userId ?? null;
if (!userId) {
// No hay sesión: redirigir a la home
throw redirect(303, '/');
}
return { userId };
};

@ -0,0 +1,7 @@
<script lang="ts">
export let data: { userId: string };
</script>
<h1>Panel</h1>
<p>Sesión iniciada como: <strong>{data.userId}</strong></p>
<p>Esta es una página protegida. La cookie de sesión se renueva con cada visita (idle timeout).</p>
Loading…
Cancel
Save