fix: fallback
This commit is contained in:
@@ -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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user