From d261a7446139d6a08c29cd051e963d42398dab28 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Sun, 11 Jan 2026 22:07:20 -0500 Subject: [PATCH] fix: fallback --- src/server/session-helpers.ts | 152 ++++++++++++++++++++++++++++++++-- 1 file changed, 144 insertions(+), 8 deletions(-) diff --git a/src/server/session-helpers.ts b/src/server/session-helpers.ts index e195574..1b20538 100644 --- a/src/server/session-helpers.ts +++ b/src/server/session-helpers.ts @@ -1,7 +1,7 @@ import { v4 as uuidV4 } from "uuid"; import { createHash, randomBytes, timingSafeEqual } from "crypto"; import type { H3Event } from "vinxi/http"; -import { useSession, clearSession, getSession, getCookie } from "vinxi/http"; +import { clearSession, getSession, getCookie, setCookie } from "vinxi/http"; import { ConnectionFactory } from "./database"; import { env } from "~/env/server"; import { AUTH_CONFIG, expiryToSeconds, CACHE_CONFIG } from "~/config"; @@ -203,16 +203,27 @@ export async function createAuthSession( maxAge: configWithMaxAge.maxAge }); - // Use useSession API to update session - const session = await useSession(event, configWithMaxAge); - await session.update(sessionData); + // Use updateSession to set session data directly + const { updateSession } = await import("vinxi/http"); + const session = await updateSession(event, configWithMaxAge, sessionData); - console.log("[Session Create] Session created via useSession API:", { - id: session.id, + console.log("[Session Create] Session created via updateSession API:", { + sessionId: session.id, hasData: !!session.data, dataKeys: session.data ? Object.keys(session.data) : [] }); + // Set a separate sessionId cookie for DB fallback (in case main session cookie fails) + setCookie(event, "session_id", sessionId, { + httpOnly: true, + secure: env.NODE_ENV === "production", + sameSite: "lax", + path: "/", + maxAge: configWithMaxAge.maxAge + }); + + console.log("[Session Create] Set session_id fallback cookie:", sessionId); + // Verify session was actually set by reading it back try { const cookieName = sessionConfig.name || "session"; @@ -223,8 +234,11 @@ export async function createAuthSession( cookieLength: cookieValue?.length || 0 }); - // Try reading back the session immediately - const verifySession = await getSession(event, sessionConfig); + // Try reading back the session immediately using the same config + const verifySession = await getSession( + event, + configWithMaxAge + ); console.log("[Session Create] Read-back verification:", { hasData: !!verifySession.data, dataMatches: verifySession.data?.userId === sessionData.userId, @@ -302,6 +316,24 @@ export async function getAuthSession( if (!data.userId || !data.sessionId) { console.log("[Session Get] Missing userId or sessionId"); + + // Fallback: Try to restore from DB using session_id cookie + const sessionIdCookie = getCookie(event, "session_id"); + if (sessionIdCookie) { + console.log( + "[Session Get] Attempting DB fallback (skipUpdate path) with session_id:", + sessionIdCookie + ); + const restored = await restoreSessionFromDB(event, sessionIdCookie); + if (restored) { + console.log( + "[Session Get] Successfully restored session from DB (skipUpdate path)" + ); + return restored; + } + console.log("[Session Get] DB fallback failed (skipUpdate path)"); + } + return null; } @@ -333,6 +365,24 @@ export async function getAuthSession( console.log( "[Session Get] Missing data or userId/sessionId in normal path" ); + + // Fallback: Try to restore from DB using session_id cookie + const sessionIdCookie = getCookie(event, "session_id"); + if (sessionIdCookie) { + console.log( + "[Session Get] Attempting DB fallback with session_id:", + sessionIdCookie + ); + const restored = await restoreSessionFromDB(event, sessionIdCookie); + if (restored) { + console.log("[Session Get] Successfully restored session from DB"); + return restored; + } + console.log( + "[Session Get] DB fallback failed - session not found or invalid" + ); + } + return null; } @@ -373,6 +423,83 @@ export async function getAuthSession( } } +/** + * Restore session from database when cookie data is empty/corrupt + * This provides a fallback mechanism for session recovery + * @param event - H3Event + * @param sessionId - Session ID from fallback cookie + * @returns Session data or null if cannot restore + */ +async function restoreSessionFromDB( + event: H3Event, + sessionId: string +): Promise { + try { + const conn = ConnectionFactory(); + + // Query DB for session with all necessary data + const result = await conn.execute({ + sql: `SELECT s.id, s.user_id, s.token_family, s.refresh_token_hash, + s.revoked, s.expires_at, u.isAdmin + FROM Session s + JOIN User u ON s.user_id = u.id + WHERE s.id = ?`, + args: [sessionId] + }); + + if (result.rows.length === 0) { + console.log("[Session Restore] Session not found in DB:", sessionId); + return null; + } + + const dbSession = result.rows[0]; + + // Validate session is still valid + if (dbSession.revoked === 1) { + console.log("[Session Restore] Session is revoked"); + return null; + } + + const expiresAt = new Date(dbSession.expires_at as string); + if (expiresAt < new Date()) { + console.log("[Session Restore] Session expired"); + return null; + } + + // We can't restore the refresh token (it's hashed in DB) + // So we need to generate a new one and rotate the session + console.log( + "[Session Restore] Session valid but refresh token lost - rotating session" + ); + + // Get IP and user agent + const { getRequestIP } = await import("vinxi/http"); + const ipAddress = getRequestIP(event) || "unknown"; + const userAgent = event.node?.req?.headers["user-agent"] || "unknown"; + + // Create a new session (this will be a rotation) + const newSession = await createAuthSession( + event, + dbSession.user_id as string, + dbSession.isAdmin === 1, + true, // Assume rememberMe=true for restoration + ipAddress, + userAgent, + sessionId, // Parent session + dbSession.token_family as string // Reuse family + ); + + console.log( + "[Session Restore] Created new session via rotation:", + newSession.sessionId + ); + return newSession; + } catch (error) { + console.error("[Session Restore] Error restoring session:", error); + return null; + } +} + /** * Validate session against database * Checks if session exists, not revoked, not expired, and refresh token matches @@ -456,6 +583,15 @@ export async function invalidateAuthSession( }); await clearSession(event, sessionConfig); + + // Also clear the session_id fallback cookie + setCookie(event, "session_id", "", { + httpOnly: true, + secure: env.NODE_ENV === "production", + sameSite: "lax", + path: "/", + maxAge: 0 // Expire immediately + }); } /**