feat: agregar canje de token magico en GET /login y crear sesion

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
webui
brobert 2 weeks ago
parent 9347d86065
commit 6d7d203465

@ -0,0 +1,19 @@
/**
* Genera un token aleatorio en base64url (sin padding).
*/
export function randomTokenBase64Url(bytes: number = 32): string {
const arr = new Uint8Array(bytes);
crypto.getRandomValues(arr);
const b64 = Buffer.from(arr).toString('base64');
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
}
/**
* Calcula SHA-256 en hexadecimal (minúsculas).
*/
export async function sha256Hex(input: string): Promise<string> {
const data = new TextEncoder().encode(input);
const hashBuf = await crypto.subtle.digest('SHA-256', data);
const bytes = new Uint8Array(hashBuf);
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
}

@ -0,0 +1,85 @@
import type { RequestHandler } from './$types';
import { redirect } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { sha256Hex, randomTokenBase64Url } from '$lib/server/crypto';
import { sessionIdleTtlMs, isProd } from '$lib/server/env';
function toIsoSql(d: Date): string {
return d.toISOString().replace('T', ' ').replace('Z', '');
}
export const GET: RequestHandler = async (event) => {
const token = event.url.searchParams.get('token')?.trim();
if (!token) {
return new Response('Falta el token', { status: 400 });
}
// Hash del token (no loguear el valor en claro)
const tokenHash = await sha256Hex(token);
const nowIso = toIsoSql(new Date());
// Intentar canjear el token: marcarlo como usado si está vigente y no usado
const res = db
.prepare(
`UPDATE web_tokens
SET used_at = ?
WHERE token_hash = ?
AND used_at IS NULL
AND expires_at > ?`
)
.run(nowIso, tokenHash, nowIso);
const changes = Number(res?.changes || 0);
if (changes < 1) {
return new Response('Enlace inválido o caducado', { status: 400 });
}
// Recuperar el user_id asociado
const row = db
.prepare(`SELECT user_id FROM web_tokens WHERE token_hash = ?`)
.get(tokenHash) as { user_id: string } | null;
const userId = row?.user_id?.trim();
if (!userId) {
return new Response('Token canjeado pero usuario no encontrado', { status: 500 });
}
// Crear sesión
const sessionToken = randomTokenBase64Url(32);
const sessionHash = await sha256Hex(sessionToken);
const sessionId = randomTokenBase64Url(16);
const expiresAtIso = toIsoSql(new Date(Date.now() + sessionIdleTtlMs));
// Datos de agente e IP (best-effort)
const userAgent = event.request.headers.get('user-agent') || null;
let ip: string | null = null;
try {
// SvelteKit 2: getClientAddress en adapters compatibles
// @ts-ignore
if (typeof event.getClientAddress === 'function') {
// @ts-ignore
ip = event.getClientAddress() || null;
}
} catch {}
if (!ip) {
const fwd = event.request.headers.get('x-forwarded-for');
ip = fwd ? fwd.split(',')[0].trim() : null;
}
db.prepare(
`INSERT INTO web_sessions (id, user_id, session_hash, expires_at, user_agent, ip)
VALUES (?, ?, ?, ?, ?, ?)`
).run(sessionId, userId, sessionHash, expiresAtIso, userAgent, ip);
// Cookie de sesión
event.cookies.set('sid', sessionToken, {
path: '/',
httpOnly: true,
sameSite: 'lax',
secure: isProd(),
maxAge: Math.floor(sessionIdleTtlMs / 1000)
});
// Redirigir a /app
throw redirect(303, '/app');
};
Loading…
Cancel
Save