import { define, type State } from "../utils.ts"; import { Context } from "fresh"; import * as Iron from "iron"; import { deleteStateKey, getStateKeys, getUser, setStateKey, User, } from "./userService.ts"; import { getCookies } from "$std/http/cookie.ts"; import { runStateKeyCleanup } from "./dbCleanup.ts"; import { generatePassword, isSecureRequest, persistWithEncryptedCookie, SessionCookie, } from "../routes/auth/callback.ts"; const useDb = true; const authDebug = Deno.env.get("AUTH_DEBUG") === "true"; export function generateNonce(length = 32) { const array = new Uint8Array(length); crypto.getRandomValues(array); return btoa(String.fromCharCode(...array)).replace(/[^a-zA-Z0-9]/g, ""); } export interface SessionCookieParams { sessionCookie: string | null; stateId: string | null; } const emptySession: SessionCookie = { started_on: 0, user_name: "", user_encrypted: false, user_id: "", role: "", user_type: "", }; export function readSessionCookiesFromCookies( headers: Headers, ): SessionCookieParams { const cookies: Record = getCookies(headers); return { sessionCookie: cookies["session"] ?? null, stateId: cookies["xx_state_id"] ?? null, }; } export async function hashText(text: string): Promise { const encoder = new TextEncoder(); const data = encoder.encode(text); const hashBuffer = await crypto.subtle.digest("SHA-256", data); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join( "", ); return hashHex; } export function InitiPGSessionMiddleware() { return define.middleware(async (ctx: Context) => { const url = new URL(ctx.req.url); //console.log("Pathname:", ctx.req.method, url.pathname); if ( url.pathname.startsWith("/api/ai-images/file/") || url.pathname.startsWith("/api/audio-blob/") || url.pathname.startsWith("/api/image-blob/") || url.pathname === "/health" || url.pathname.startsWith("/auth/") || url.pathname.startsWith("/node_modules/") || url.pathname.startsWith("/.") || url.pathname.startsWith("/favicon.ico") ) { return await ctx.next(); } const timestampNow = Date.now(); let sessionData: SessionCookie | null = null; const sessionCookieParams = readSessionCookiesFromCookies(ctx.req.headers); if (authDebug) { console.log("DEBUG stateMiddleware: cookies", { path: url.pathname, hasSession: !!sessionCookieParams.sessionCookie, hasStateId: !!sessionCookieParams.stateId, }); } if (url.searchParams.get("popstate") === "true") { url.searchParams.delete("popstate"); if (sessionCookieParams.sessionCookie) { url.pathname = "/auth/logout"; return Response.redirect(url.toString(), 302); } else { return Response.redirect(url.toString(), 302); } } const xxStateId = sessionCookieParams.stateId; let theStateKey: string | null = null; if (xxStateId) { if (useDb) { const stateKeys = await getStateKeys(xxStateId); let unsealErrorCount = 0; stateKeys.sort((a, b) => b.created_on - a.created_on); const timestamp = timestampNow - (1000 * 60 * 60); for (const stateKey of stateKeys) { if (stateKey.created_on < timestamp) { deleteStateKey(stateKey.seal_key); } else if (sessionCookieParams.sessionCookie) { let stateData: SessionCookie | null = null; try { stateData = await Iron.unseal( sessionCookieParams.sessionCookie, stateKey.seal_key, Iron.defaults, ) as SessionCookie; } catch (_e) { unsealErrorCount++; ctx.state.authError = "invalid_session"; continue; } if (stateData && stateData.user_id) { const thisHash = await hashText(JSON.stringify(stateData)); if (thisHash === stateKey.cookie_hash) { const renewWindow = timestampNow - (5 * 60 * 1000); const newKey = generatePassword(); if (stateKey.created_on < renewWindow) { stateData.started_on = timestampNow; const cookieHash = await hashText(JSON.stringify(stateData)); await setStateKey(newKey, xxStateId, cookieHash); theStateKey = newKey; } else { theStateKey = stateKey.seal_key; } sessionData = stateData; if (authDebug) { console.log("DEBUG stateMiddleware: session validated", { user_id: sessionData.user_id, state_id: xxStateId, }); } break; } else { console.log( "DEBUG stateMiddleware: hash mismatch, cookie invalid", ); ctx.state.authError = "invalid_session"; } } else { console.log( "DEBUG stateMiddleware: stateData missing or no user_id", { hasStateData: !!stateData, user_id: stateData?.user_id, }, ); ctx.state.authError = "invalid_session"; } } } if (authDebug && unsealErrorCount > 0 && !sessionData) { console.log( "DEBUG stateMiddleware: failed to unseal session cookie", { state_id: xxStateId, attempts: unsealErrorCount }, ); } } } ctx.state.photoUrl = ""; ctx.state.data = sessionData ?? emptySession; ctx.state.host = url.href; ctx.state.urlRoute = url.pathname; ctx.state.stateId = sessionCookieParams.stateId ?? ""; ctx.state.sealKey = theStateKey ?? undefined; // Store the validated seal key for logout const themeCookie = getCookies(ctx.req.headers)["cyberhybrid-theme"]; if (themeCookie === "light" || themeCookie === "dark") { ctx.state.themePreference = themeCookie; } // Inject full user data into ctx.state.user for route handlers let userData: User | null = null; if (sessionData && sessionData.user_id) { try { userData = await getUser(sessionData.user_id); } catch (error) { console.error("Error fetching user data:", error); // Continue without user data - session is valid but user lookup failed } } ctx.state.user = userData; const preResp = await ctx.next(); if (sessionCookieParams.sessionCookie && !sessionData) { const expiredDate = new Date(0).toUTCString(); const isSecure = isSecureRequest(ctx.req); const deleteCookie = `session=; Expires=${expiredDate}; Path=/; HttpOnly=true; Secure=${isSecure}; SameSite=Lax`; preResp.headers.append("Set-Cookie", deleteCookie); } runStateKeyCleanup().catch((err) => { console.error("State key cleanup error:", err); }); return theStateKey && sessionData ? persistWithEncryptedCookie( sessionData as SessionCookie, theStateKey, preResp, ctx.req, xxStateId || "", ) : preResp; }); }