|
|
|
@ -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('&', '&')
|
|
|
|
|
|
|
|
.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) => {
|
|
|
|
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
|
|
|
|
|