Files
flxn-app/src/utils/supertokens-core.server.ts
T
2026-08-23 17:35:45 -07:00

140 lines
3.9 KiB
TypeScript

import { redirect } from "@tanstack/react-router";
import UserRoles from "supertokens-node/recipe/userroles";
import UserMetadata from "supertokens-node/recipe/usermetadata";
import { getSessionForStart } from "@/lib/supertokens/recipes/start-session";
import { Logger } from "@/lib/logger";
import { pbAdmin } from "@/lib/pocketbase/client";
import { PUBLIC_ROUTES } from "./supertokens";
import type { Player } from "@/features/players/types";
const logger = new Logger("Middleware");
const ROLES_METADATA_TTL_MS = 60 * 1000;
const rolesMetadataCache = new Map<
string,
{ roles: string[]; metadata: any; expiresAt: number }
>();
export const invalidateUserCache = (userAuthId: string) => {
rolesMetadataCache.delete(userAuthId);
};
export const fetchUserRoles = async (userAuthId: string) => {
const response = await UserRoles.getRolesForUser("public", userAuthId);
return response;
};
const getRolesAndMetadata = async (userAuthId: string) => {
const now = Date.now();
const cached = rolesMetadataCache.get(userAuthId);
if (cached && cached.expiresAt > now) {
return cached;
}
const [{ roles }, { metadata }] = await Promise.all([
fetchUserRoles(userAuthId),
UserMetadata.getUserMetadata(userAuthId),
]);
const entry = { roles, metadata, expiresAt: now + ROLES_METADATA_TTL_MS };
rolesMetadataCache.set(userAuthId, entry);
return entry;
};
export const updateUserMetadataFields = async (
userAuthId: string,
fields: Record<string, any>
) => {
await UserMetadata.updateUserMetadata(userAuthId, fields);
invalidateUserCache(userAuthId);
};
const verifySuperTokensSession = async (request: Request) => {
let session = await getSessionForStart(request, { sessionRequired: false });
if (session?.needsRefresh) {
logger.info("Session needs refresh - redirecting to client");
return { context: { session: { tryRefresh: true } } };
}
const userAuthId = session?.userId;
if (!userAuthId || !session) {
return { context: { userAuthId: null, roles: [] } };
}
const { roles, metadata } = await getRolesAndMetadata(userAuthId);
return {
context: {
userAuthId,
roles,
metadata,
phone: session.phone,
session: {
accessTokenPayload: session.accessTokenPayload,
sessionHandle: session.sessionHandle,
},
},
};
};
const LAST_ACTIVITY_THROTTLE_MS = 5 * 60 * 1000;
const lastActivityWriteAt = new Map<string, number>();
export const getSessionContextCore = async (
request: Request,
options?: { isServerFunction?: boolean }
) => {
const session = await verifySuperTokensSession(request);
if (session.context.session?.tryRefresh) {
if (options?.isServerFunction) {
throw new Error("SESSION_REFRESH_REQUIRED");
}
const url = new URL(request.url);
if (PUBLIC_ROUTES.some((route) => url.pathname.startsWith(route))) {
throw new Error("Unauthenticated");
}
const from = url.pathname + url.search;
throw redirect({
to: "/refresh-session",
search: from === "/" ? {} : { redirect: from },
});
}
if (!session.context.userAuthId) {
throw new Error("Unauthenticated");
}
let player: Player | null = null;
try {
player = await pbAdmin.getPlayerByAuthId(session.context.userAuthId);
if (player) {
const now = Date.now();
const lastWrite = lastActivityWriteAt.get(session.context.userAuthId) ?? 0;
if (now - lastWrite > LAST_ACTIVITY_THROTTLE_MS) {
lastActivityWriteAt.set(session.context.userAuthId, now);
await pbAdmin.updatePlayer(player.id, {
last_activity: new Date().toISOString(),
});
}
}
} catch (error) {
logger.error("Failed to update player last_activity", error);
}
return {
userAuthId: session.context.userAuthId,
roles: session.context.roles,
metadata: session.context.metadata,
phone: session.context.phone,
player,
};
};