fix: fallback

This commit is contained in:
Michael Freno
2026-01-11 22:07:20 -05:00
parent b81a3a69d2
commit d261a74461

View File

@@ -1,7 +1,7 @@
import { v4 as uuidV4 } from "uuid"; import { v4 as uuidV4 } from "uuid";
import { createHash, randomBytes, timingSafeEqual } from "crypto"; import { createHash, randomBytes, timingSafeEqual } from "crypto";
import type { H3Event } from "vinxi/http"; 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 { ConnectionFactory } from "./database";
import { env } from "~/env/server"; import { env } from "~/env/server";
import { AUTH_CONFIG, expiryToSeconds, CACHE_CONFIG } from "~/config"; import { AUTH_CONFIG, expiryToSeconds, CACHE_CONFIG } from "~/config";
@@ -203,16 +203,27 @@ export async function createAuthSession(
maxAge: configWithMaxAge.maxAge maxAge: configWithMaxAge.maxAge
}); });
// Use useSession API to update session // Use updateSession to set session data directly
const session = await useSession<SessionData>(event, configWithMaxAge); const { updateSession } = await import("vinxi/http");
await session.update(sessionData); const session = await updateSession(event, configWithMaxAge, sessionData);
console.log("[Session Create] Session created via useSession API:", { console.log("[Session Create] Session created via updateSession API:", {
id: session.id, sessionId: session.id,
hasData: !!session.data, hasData: !!session.data,
dataKeys: session.data ? Object.keys(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 // Verify session was actually set by reading it back
try { try {
const cookieName = sessionConfig.name || "session"; const cookieName = sessionConfig.name || "session";
@@ -223,8 +234,11 @@ export async function createAuthSession(
cookieLength: cookieValue?.length || 0 cookieLength: cookieValue?.length || 0
}); });
// Try reading back the session immediately // Try reading back the session immediately using the same config
const verifySession = await getSession<SessionData>(event, sessionConfig); const verifySession = await getSession<SessionData>(
event,
configWithMaxAge
);
console.log("[Session Create] Read-back verification:", { console.log("[Session Create] Read-back verification:", {
hasData: !!verifySession.data, hasData: !!verifySession.data,
dataMatches: verifySession.data?.userId === sessionData.userId, dataMatches: verifySession.data?.userId === sessionData.userId,
@@ -302,6 +316,24 @@ export async function getAuthSession(
if (!data.userId || !data.sessionId) { if (!data.userId || !data.sessionId) {
console.log("[Session Get] Missing userId or 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; return null;
} }
@@ -333,6 +365,24 @@ export async function getAuthSession(
console.log( console.log(
"[Session Get] Missing data or userId/sessionId in normal path" "[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; 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<SessionData | null> {
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 * Validate session against database
* Checks if session exists, not revoked, not expired, and refresh token matches * Checks if session exists, not revoked, not expired, and refresh token matches
@@ -456,6 +583,15 @@ export async function invalidateAuthSession(
}); });
await clearSession(event, sessionConfig); 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
});
} }
/** /**