integratedhub/services/stateMiddleware.ts
2026-01-20 09:02:26 -06:00

193 lines
6.2 KiB
TypeScript
Executable File

import { define } from "../utils.ts";
import { Context } from "fresh";
import * as Iron from "iron";
import { deleteStateKey, getStateKeys, setStateKey } from "./userService.ts";
import { getCookies, setCookie } from "$std/http/cookie.ts";
import { runStateKeyCleanup } from "./dbCleanup.ts";
import {
generatePassword,
isSecureRequest,
persistWithEncryptedCookie,
SessionCookie,
} from "../routes/auth/callback.ts";
import { getE2ESession, isE2EEnabled } from "./e2eFixtures.ts";
const useDb = 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;
}
export function readSessionCookiesFromCookies(
headers: Headers,
): SessionCookieParams {
const cookies: Record<string, string> = getCookies(headers);
return {
sessionCookie: cookies["session"] ?? null,
stateId: cookies["xx_state_id"] ?? null,
};
}
export async function hashText(text: string): Promise<string> {
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<any>) => {
const url = new URL(ctx.req.url);
if (isE2EEnabled()) {
const e2eUser = ctx.req.headers.get("x-e2e-user") ??
url.searchParams.get("e2e-user");
const e2eSession = getE2ESession(e2eUser);
if (e2eSession) {
ctx.state.photoUrl = "";
ctx.state.data = e2eSession;
ctx.state.host = url.href;
ctx.state.urlRoute = url.pathname;
ctx.state.stateId = "e2e-state";
return await ctx.next();
}
}
if (url.hostname === "cyberhybridhub.com") {
return Response.redirect("https://portal.cyberhybridhub.com", 302);
}
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 (url.searchParams.get("popstate") === "true") {
console.log("popstate");
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);
stateKeys.sort((a: any, b: any) => b.created_on - a.created_on);
const timestamp = timestampNow - (1000 * 60 * 60);
for (const stateKey of stateKeys) {
if (stateKey.created_on < timestamp) {
deleteStateKey(stateKey.state_id);
} 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) {
console.log("DEBUG stateMiddleware: error unsealing cookie", _e);
return new Response(null, {
status: 302,
headers: { Location: "/auth/logout" },
});
}
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;
break;
} else {
console.log(
"DEBUG stateMiddleware: hash mismatch, cookie invalid",
);
}
} else {
console.log(
"DEBUG stateMiddleware: stateData missing or no user_id",
{
hasStateData: !!stateData,
user_id: stateData?.user_id,
},
);
}
}
}
}
}
ctx.state.photoUrl = "";
ctx.state.data = sessionData;
ctx.state.host = url.href;
ctx.state.urlRoute = url.pathname;
ctx.state.stateId = sessionCookieParams.stateId;
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;
});
}