feat: añade página intermedia de login y flujo de canje de token

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

@ -8,6 +8,17 @@ function toIsoSql(d: Date): string {
return d.toISOString().replace('T', ' ').replace('Z', ''); return d.toISOString().replace('T', ' ').replace('Z', '');
} }
function escapeHtml(s: string): string {
return s
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
// 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) => { export const GET: RequestHandler = async (event) => {
const token = event.url.searchParams.get('token')?.trim(); const token = event.url.searchParams.get('token')?.trim();
if (!token) { if (!token) {
@ -15,12 +26,52 @@ export const GET: RequestHandler = async (event) => {
return new Response('Falta el token', { status: 400 }); return new Response('Falta el token', { status: 400 });
} }
// Hash del token (no loguear el valor en claro) const html = `<!doctype html>
const tokenHash = await sha256Hex(token); <html lang="es">
<head>
<meta charset="utf-8" />
<title>Accediendo</title>
<meta name="robots" content="noindex,nofollow" />
<meta http-equiv="refresh" content="1" />
<meta name="referrer" content="no-referrer" />
</head>
<body>
<noscript>
<p>Para continuar, pulsa el botón:</p>
</noscript>
<form method="POST" action="/login">
<input type="hidden" name="token" value="${escapeHtml(token)}" />
<button type="submit">Continuar</button>
</form>
<script>
// Auto-submit para navegadores con JS; los bots de preview no realizan POST.
try { document.forms[0].submit(); } catch {}
</script>
</body>
</html>`;
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(); 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 const res = db
.prepare( .prepare(
`UPDATE web_tokens `UPDATE web_tokens

@ -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. - 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 - Devolver URL del tipo: https://app.example.com/login?token=XYZ
- Canje (web): - Canje (web):
- Validar hash y caducidad; si ok, invalidar token (marcar usado). - GET /login muestra una página intermedia con formulario (y auto-submit JS) para evitar que los “link preview bots” canjeen el token.
- Crear sesión en DB (web_sessions) y emitir cookie de sesión (solo cookie de sesión, sin persistencia en disco). - POST /login valida hash y caducidad; si ok, invalida el token (marcar usado).
- Redirigir a /app (sin token en la URL). - 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: - Expiración:
- Idle timeout: 2 horas de inactividad. Si excede, pedir un nuevo token /t web. - Idle timeout: 2 horas de inactividad. Si excede, pedir un nuevo token /t web.
- Seguridad: - Seguridad:

Loading…
Cancel
Save