import type { Handle } from '@sveltejs/kit'; import { getDb } from '$lib/server/db'; import { sha256Hex } from '$lib/server/crypto'; import { isProd, sessionIdleTtlMs, isDev, DEV_BYPASS_AUTH, DEV_DEFAULT_USER } from '$lib/server/env'; function toIsoSql(d: Date): string { return d.toISOString().replace('T', ' ').replace('Z', ''); } export const handle: Handle = async ({ event, resolve }) => { // Bypass de auth en desarrollo (siempre activo en dev para entorno de pruebas) const bypass = isDev(); if (bypass) { const qp = event.url.searchParams.get('__as')?.trim(); const current = event.cookies.get('dev_as') || ''; const user = qp && qp.length ? qp : (current || DEV_DEFAULT_USER); if (qp && qp.length && qp !== current) { event.cookies.set('dev_as', user, { path: '/', httpOnly: true, sameSite: 'lax', secure: isProd(), maxAge: 60 * 60 * 24 * 30 // 30 días }); } event.locals.userId = user; } // Sesión por cookie 'sid' const isLogout = event.url.pathname === '/api/logout' || event.url.pathname.startsWith('/api/logout/'); const sid = event.cookies.get('sid'); if (!bypass && 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) excepto durante /api/logout if (!isLogout) { 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: '/', httpOnly: true, sameSite: 'lax', secure: isProd() }); } } catch { // En caso de error de DB, no romper la request; continuar sin sesión } } const response = await resolve(event); // Cabeceras de seguridad y caché: solo para HTML try { const ct = response.headers.get('content-type') || ''; if (ct.includes('text/html')) { response.headers.set('cache-control', 'no-store'); response.headers.set('X-Frame-Options', 'DENY'); response.headers.set('Referrer-Policy', 'no-referrer'); response.headers.set('X-Content-Type-Options', 'nosniff'); // Mitigar aviso de “preload no usado” en CSS: // Filtrar del header Link los preloads con as=style (dejamos modulepreload para JS). const link = response.headers.get('Link') || response.headers.get('link'); if (link) { const filtered = link .split(',') .map((s) => s.trim()) .filter((seg) => !/;\s*as=style\b/i.test(seg)); if (filtered.length > 0) { response.headers.set('Link', filtered.join(', ')); } else { response.headers.delete('Link'); } } } } catch { // Ignorar si la implementación de Response no permite set() } // Indicador de bypass en respuestas (útil en dev) try { if (bypass) { response.headers.set('X-Dev-Auth', 'bypass'); } } catch {} return response; };