feat: agregar canje de token magico en GET /login y crear sesion
Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>webui
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…
Reference in New Issue