import { createMiddleware, createServerFn, createServerOnlyFn, } from "@tanstack/react-start"; import { getRequest, setResponseHeader } from "@tanstack/react-start/server"; import { redirect as 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 z from "zod"; const logger = new Logger("Middleware"); 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) { logger.info("No authenticated user"); return { context: { userAuthId: null, roles: [] } }; } const { roles } = await fetchUserRoles(userAuthId); const { metadata } = await UserMetadata.getUserMetadata(userAuthId); return { context: { userAuthId, roles, metadata, phone: session.phone, session: { accessTokenPayload: session.accessTokenPayload, sessionHandle: session.sessionHandle, }, }, }; }; export const getSessionContext = createServerOnlyFn(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); const from = encodeURIComponent(url.pathname + url.search); throw redirect({ to: "/refresh-session", search: { redirect: from } }); } if (!session.context.userAuthId) { logger.error("Unauthenticated user", session.context); throw new Error("Unauthenticated"); } const context = { userAuthId: session.context.userAuthId, roles: session.context.roles, metadata: session.context.metadata, phone: session.context.phone }; return context; }); export const superTokensRequestMiddleware = createMiddleware({ type: "request", }).server(async ({ next, request }) => { const context = await getSessionContext(request); return next({ context }); }); export const superTokensFunctionMiddleware = createMiddleware({ type: "function", }).server(async ({ next }) => { const request = getRequest(); try { const context = await getSessionContext(request, { isServerFunction: true }); return next({ context }); } catch (error: any) { if (error.message === "SESSION_REFRESH_REQUIRED") { throw new Response( JSON.stringify({ error: "SESSION_REFRESH_REQUIRED", message: "Session needs to be refreshed" }), { status: 401, headers: { "Content-Type": "application/json" } } ); } throw error; } }); export const superTokensAdminFunctionMiddleware = createMiddleware({ type: "function", }).server(async ({ next }) => { const request = getRequest(); try { const context = await getSessionContext(request, { isServerFunction: true }); if (context.roles?.includes("Admin")) { return next({ context }); } logger.error("Unauthorized user in admin function.", context); throw new Error("Unauthorized"); } catch (error: any) { if (error.message === "SESSION_REFRESH_REQUIRED") { throw new Response( JSON.stringify({ error: "SESSION_REFRESH_REQUIRED", message: "Session needs to be refreshed" }), { status: 401, headers: { "Content-Type": "application/json" } } ); } throw error; } }); export const fetchUserRoles = async (userAuthId: string) => { const response = await UserRoles.getRolesForUser("public", userAuthId); return response; }; export const setUserMetadata = createServerFn({ method: "POST" }) .inputValidator( z .object({ first_name: z .string() .min(2) .max(20) .regex( /^[a-zA-Z0-9\s]+$/, "First name must be 2-20 characters long and contain only letters and spaces" ), last_name: z .string() .min(2) .max(20) .regex( /^[a-zA-Z0-9\s]+$/, "Last name must be 2-20 characters long and contain only letters and spaces" ), player_id: z.string(), }) .partial() ) .middleware([superTokensFunctionMiddleware]) .handler(async ({ context, data }) => { const { userAuthId, metadata } = context; if (!userAuthId) return; await UserMetadata.updateUserMetadata(userAuthId, { first_name: data.first_name, last_name: data.last_name, player_id: data.player_id, }); return { metadata: { ...metadata, ...data, }, }; }); export const updateUserColorScheme = createServerFn({ method: "POST" }) .inputValidator((data: string) => data) .middleware([superTokensFunctionMiddleware]) .handler(async ({ context, data }) => { const { userAuthId, metadata } = context; if (!userAuthId) return; await UserMetadata.updateUserMetadata(userAuthId, { colorScheme: data, }); return { metadata: { ...metadata, colorScheme: data, }, }; }); export const updateUserAccentColor = createServerFn({ method: "POST" }) .inputValidator((data: string) => data) .middleware([superTokensFunctionMiddleware]) .handler(async ({ context, data }) => { const { userAuthId, metadata } = context; if (!userAuthId) return; await UserMetadata.updateUserMetadata(userAuthId, { accentColor: data, }); return { metadata: { ...metadata, accentColor: data, }, }; });