|
|
|
|
@ -17,8 +17,8 @@ function escapeHtml(s: string): string {
|
|
|
|
|
.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.
|
|
|
|
|
// GET: página intermedia que requiere una interacción mínima y establece una cookie "login_intent" vía JS.
|
|
|
|
|
// Evita que bots de previsualización canjeen el token antes de que el usuario haga clic.
|
|
|
|
|
export const GET: RequestHandler = async (event) => {
|
|
|
|
|
const token = event.url.searchParams.get('token')?.trim();
|
|
|
|
|
if (!token) {
|
|
|
|
|
@ -26,26 +26,45 @@ export const GET: RequestHandler = async (event) => {
|
|
|
|
|
return new Response('Falta el token', { status: 400 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Nonce para "gate de JS"
|
|
|
|
|
const nonce = randomTokenBase64Url(18);
|
|
|
|
|
|
|
|
|
|
const html = `<!doctype html>
|
|
|
|
|
<html lang="es">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="utf-8" />
|
|
|
|
|
<title>Accediendo…</title>
|
|
|
|
|
<title>Acceder</title>
|
|
|
|
|
<meta name="robots" content="noindex,nofollow" />
|
|
|
|
|
<meta http-equiv="refresh" content="1" />
|
|
|
|
|
<meta name="referrer" content="no-referrer" />
|
|
|
|
|
<style>
|
|
|
|
|
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial, 'Apple Color Emoji', 'Segoe UI Emoji'; padding: 2rem; }
|
|
|
|
|
.card { max-width: 480px; margin: 0 auto; border: 1px solid #ddd; border-radius: 8px; padding: 1.5rem; }
|
|
|
|
|
button[disabled] { opacity: .6; cursor: not-allowed; }
|
|
|
|
|
</style>
|
|
|
|
|
</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>
|
|
|
|
|
<div class="card">
|
|
|
|
|
<h1>Acceso seguro</h1>
|
|
|
|
|
<p>Para continuar, pulsa “Continuar”. Si no funciona, asegúrate de abrir este enlace en tu navegador.</p>
|
|
|
|
|
<form method="POST" action="/login">
|
|
|
|
|
<input type="hidden" name="token" value="${escapeHtml(token)}" />
|
|
|
|
|
<input type="hidden" id="nonceInput" name="nonce" value="${escapeHtml(nonce)}" />
|
|
|
|
|
<button id="continueBtn" type="submit" disabled>Continuar</button>
|
|
|
|
|
</form>
|
|
|
|
|
<noscript>
|
|
|
|
|
<p><strong>JavaScript está deshabilitado.</strong> Actívalo para continuar.</p>
|
|
|
|
|
</noscript>
|
|
|
|
|
</div>
|
|
|
|
|
<script>
|
|
|
|
|
// Auto-submit para navegadores con JS; los bots de preview no realizan POST.
|
|
|
|
|
try { document.forms[0].submit(); } catch {}
|
|
|
|
|
// Establecer cookie de intención con el nonce y habilitar el botón.
|
|
|
|
|
try {
|
|
|
|
|
var nonce = ${JSON.stringify(nonce)};
|
|
|
|
|
var cookie = 'login_intent=' + encodeURIComponent(nonce) + '; Path=/; Max-Age=600; SameSite=Lax';
|
|
|
|
|
if (location.protocol === 'https:') cookie += '; Secure';
|
|
|
|
|
document.cookie = cookie;
|
|
|
|
|
var btn = document.getElementById('continueBtn');
|
|
|
|
|
if (btn) btn.removeAttribute('disabled');
|
|
|
|
|
} catch {}
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>`;
|
|
|
|
|
@ -68,6 +87,14 @@ export const POST: RequestHandler = async (event) => {
|
|
|
|
|
return new Response('Falta el token', { status: 400 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validación del "gate de JS": cookie + nonce deben coincidir
|
|
|
|
|
const nonce = String(form.get('nonce') || '').trim();
|
|
|
|
|
const loginIntent = event.cookies.get('login_intent') || '';
|
|
|
|
|
if (!nonce || !loginIntent || nonce !== loginIntent) {
|
|
|
|
|
console.warn('[web/login] Falta o no cuadra login_intent; posible previsualizador sin JS.');
|
|
|
|
|
return new Response('Solicitud inválida', { status: 400 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const tokenHash = await sha256Hex(token);
|
|
|
|
|
const db = await getDb();
|
|
|
|
|
|
|
|
|
|
@ -134,6 +161,9 @@ export const POST: RequestHandler = async (event) => {
|
|
|
|
|
maxAge: Math.floor(sessionIdleTtlMs / 1000)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Eliminar cookie de intento (ya no es necesaria)
|
|
|
|
|
event.cookies.delete('login_intent', { path: '/' });
|
|
|
|
|
|
|
|
|
|
// Redirigir a /app
|
|
|
|
|
throw redirect(303, '/app');
|
|
|
|
|
};
|
|
|
|
|
|