You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

125 lines
3.8 KiB
TypeScript

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;
};