diff --git a/apps/web/src/app.d.ts b/apps/web/src/app.d.ts index da08e6d..4bc652b 100644 --- a/apps/web/src/app.d.ts +++ b/apps/web/src/app.d.ts @@ -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 {}; diff --git a/apps/web/src/hooks.server.ts b/apps/web/src/hooks.server.ts index ea643aa..e342e33 100644 --- a/apps/web/src/hooks.server.ts +++ b/apps/web/src/hooks.server.ts @@ -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'); diff --git a/apps/web/src/routes/+page.svelte b/apps/web/src/routes/+page.svelte index cc88df0..1744fbe 100644 --- a/apps/web/src/routes/+page.svelte +++ b/apps/web/src/routes/+page.svelte @@ -1,2 +1,4 @@

Welcome to SvelteKit

Visit svelte.dev/docs/kit to read the documentation

+ +

Ir al panel

diff --git a/apps/web/src/routes/api/logout/+server.ts b/apps/web/src/routes/api/logout/+server.ts new file mode 100644 index 0000000..9b1f832 --- /dev/null +++ b/apps/web/src/routes/api/logout/+server.ts @@ -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 }); +}; diff --git a/apps/web/src/routes/app/+page.server.ts b/apps/web/src/routes/app/+page.server.ts new file mode 100644 index 0000000..57a97a1 --- /dev/null +++ b/apps/web/src/routes/app/+page.server.ts @@ -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 }; +}; diff --git a/apps/web/src/routes/app/+page.svelte b/apps/web/src/routes/app/+page.svelte new file mode 100644 index 0000000..4470e52 --- /dev/null +++ b/apps/web/src/routes/app/+page.svelte @@ -0,0 +1,7 @@ + + +

Panel

+

Sesión iniciada como: {data.userId}

+

Esta es una página protegida. La cookie de sesión se renueva con cada visita (idle timeout).