diff --git a/apps/web/src/routes/login/+server.ts b/apps/web/src/routes/login/+server.ts index 78a3bc4..785108f 100644 --- a/apps/web/src/routes/login/+server.ts +++ b/apps/web/src/routes/login/+server.ts @@ -8,6 +8,17 @@ function toIsoSql(d: Date): string { return d.toISOString().replace('T', ' ').replace('Z', ''); } +function escapeHtml(s: string): string { + return s + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +// GET: página intermedia para evitar que los “link preview bots” canjeen el token. +// Muestra un formulario que hace POST automático para canjear el token. export const GET: RequestHandler = async (event) => { const token = event.url.searchParams.get('token')?.trim(); if (!token) { @@ -15,12 +26,52 @@ export const GET: RequestHandler = async (event) => { return new Response('Falta el token', { status: 400 }); } - // Hash del token (no loguear el valor en claro) - const tokenHash = await sha256Hex(token); + const html = ` + + + + Accediendo… + + + + + + +
+ + +
+ + +`; + + return new Response(html, { + status: 200, + headers: { + 'content-type': 'text/html; charset=utf-8', + 'cache-control': 'no-store, max-age=0' + } + }); +}; + +// POST: canje real del token (uso único). Crea sesión y redirige a /app. +export const POST: RequestHandler = async (event) => { + const form = await event.request.formData(); + const token = String(form.get('token') || '').trim(); + if (!token) { + console.warn('[web/login] POST sin token'); + return new Response('Falta el token', { status: 400 }); + } + const tokenHash = await sha256Hex(token); const db = await getDb(); - // Intentar canjear el token: marcarlo como usado si está vigente y no usado (usar tiempo de SQLite) + // Intentar canjear el token (un solo uso, no caducado) const res = db .prepare( `UPDATE web_tokens diff --git a/docs/plan-interfaz-web.md b/docs/plan-interfaz-web.md index fee7b98..1fdae28 100644 --- a/docs/plan-interfaz-web.md +++ b/docs/plan-interfaz-web.md @@ -45,9 +45,10 @@ Este documento define el plan para añadir una interfaz web al sistema, mantenie - En /t web por DM: crear token aleatorio, guardar hash (no el token en claro), TTL 10 min, uso único, rate-limit por usuario. - Devolver URL del tipo: https://app.example.com/login?token=XYZ - Canje (web): - - Validar hash y caducidad; si ok, invalidar token (marcar usado). - - Crear sesión en DB (web_sessions) y emitir cookie de sesión (solo cookie de sesión, sin persistencia en disco). - - Redirigir a /app (sin token en la URL). + - GET /login muestra una página intermedia con formulario (y auto-submit JS) para evitar que los “link preview bots” canjeen el token. + - POST /login valida hash y caducidad; si ok, invalida el token (marcar usado). + - Crea sesión en DB (web_sessions) y emite cookie de sesión (solo cookie de sesión, sin persistencia en disco). + - Redirige a /app (sin token en la URL). - Expiración: - Idle timeout: 2 horas de inactividad. Si excede, pedir un nuevo token /t web. - Seguridad: