From 81329e435400df4aed5f854381ae9f56b0210477 Mon Sep 17 00:00:00 2001 From: yohlo Date: Wed, 24 Sep 2025 12:20:36 -0500 Subject: [PATCH] fix refresh issue --- .gitignore | 3 +- src/app/routes/__root.tsx | 20 +++++--- src/app/routes/refresh-session.tsx | 34 ++++++------- src/contexts/auth-context.tsx | 8 +-- src/lib/supertokens/client.ts | 34 ++++++++++--- .../hooks/use-server-mutation.ts | 12 ++++- src/utils/supertokens.ts | 50 +------------------ 7 files changed, 74 insertions(+), 87 deletions(-) diff --git a/.gitignore b/.gitignore index 597e91c..bba97bb 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,5 @@ yarn.lock /playwright/.cache/ /scripts/ /pb_data/ -/.tanstack/ \ No newline at end of file +/.tanstack/ +/dist/ \ No newline at end of file diff --git a/src/app/routes/__root.tsx b/src/app/routes/__root.tsx index 92266a2..485b9f9 100644 --- a/src/app/routes/__root.tsx +++ b/src/app/routes/__root.tsx @@ -83,12 +83,20 @@ export const Route = createRootRouteWithContext<{ return {}; } - // https://github.com/TanStack/router/discussions/3531 - const auth = await ensureServerQueryData( - context.queryClient, - playerQueries.auth() - ); - return { auth }; + if (location.pathname === '/login' || location.pathname === '/logout') { + return {}; + } + + try { + // https://github.com/TanStack/router/discussions/3531 + const auth = await ensureServerQueryData( + context.queryClient, + playerQueries.auth() + ); + return { auth }; + } catch (error) { + return {}; + } }, pendingComponent: () => , }); diff --git a/src/app/routes/refresh-session.tsx b/src/app/routes/refresh-session.tsx index 47bc840..dad66b0 100644 --- a/src/app/routes/refresh-session.tsx +++ b/src/app/routes/refresh-session.tsx @@ -1,38 +1,33 @@ import { createFileRoute } from '@tanstack/react-router' -import { useEffect } from 'react' +import { useEffect, useRef } from 'react' import FullScreenLoader from '@/components/full-screen-loader' import { attemptRefreshingSession } from 'supertokens-web-js/recipe/session' +import { resetRefreshFlag } from '@/lib/supertokens/client' export const Route = createFileRoute('/refresh-session')({ component: RouteComponent, }) -// https://supertokens.com/docs/additional-verification/session-verification/ssr?uiType=custom function RouteComponent() { + const hasAttemptedRef = useRef(false); + useEffect(() => { + if (hasAttemptedRef.current) return; + hasAttemptedRef.current = true; + const handleRefresh = async () => { - try { + try { + resetRefreshFlag(); const refreshed = await attemptRefreshingSession() - + if (refreshed) { const urlParams = new URLSearchParams(window.location.search) const redirect = urlParams.get('redirect') - - const isServerFunction = redirect && ( - redirect.startsWith('_serverFn') || - redirect.startsWith('api/') || - redirect.includes('_serverFn') - ); - - if (redirect && !isServerFunction) { + + if (redirect && !redirect.includes('_serverFn') && !redirect.includes('/api/')) { window.location.href = decodeURIComponent(redirect) } else { - const referrer = document.referrer; - const referrerUrl = referrer && !referrer.includes('/_serverFn') && !referrer.includes('/api/') - ? referrer - : '/'; - - window.location.href = referrerUrl; + window.location.href = '/'; } } else { window.location.href = '/login' @@ -42,8 +37,7 @@ function RouteComponent() { } } - const timeout = setTimeout(handleRefresh, 100) - return () => clearTimeout(timeout) + setTimeout(handleRefresh, 100) }, []) return diff --git a/src/contexts/auth-context.tsx b/src/contexts/auth-context.tsx index 1a897c3..1f13d16 100644 --- a/src/contexts/auth-context.tsx +++ b/src/contexts/auth-context.tsx @@ -58,13 +58,13 @@ export const AuthProvider: React.FC = ({ children }) => { const value = useMemo( () => ({ - user: data?.user || defaultAuthData.user, - metadata: data?.metadata || defaultAuthData.metadata, - roles: data?.roles || defaultAuthData.roles, + user: data?.user, + metadata: data?.metadata || { accentColor: "blue" as MantineColor, colorScheme: "dark" as MantineColorScheme }, + roles: data?.roles || [], phone: data?.phone || "", set, }), - [data, defaultAuthData] + [data, set] ); return {children}; diff --git a/src/lib/supertokens/client.ts b/src/lib/supertokens/client.ts index 5318114..5bf046d 100644 --- a/src/lib/supertokens/client.ts +++ b/src/lib/supertokens/client.ts @@ -4,6 +4,31 @@ import Passwordless from "supertokens-web-js/recipe/passwordless"; import { appInfo } from "./config"; import { logger } from "./"; +let refreshAttemptCount = 0; + +export const resetRefreshFlag = () => { + refreshAttemptCount = 0; +}; + +const setupFetchInterceptor = () => { + if (typeof window === 'undefined') return; + + const originalFetch = window.fetch; + window.fetch = async (resource: RequestInfo | URL, options?: RequestInit) => { + const url = typeof resource === 'string' ? resource : + resource instanceof URL ? resource.toString() : resource.url; + + if (url.includes('/api/auth/session/refresh')) { + refreshAttemptCount++; + if (refreshAttemptCount > 1) { + throw new Error('Duplicate refresh attempt blocked'); + } + } + + return originalFetch.call(window, resource, options); + }; +}; + export const frontendConfig = () => { return { appInfo, @@ -12,7 +37,6 @@ export const frontendConfig = () => { Session.init({ tokenTransferMethod: "cookie", sessionTokenBackendDomain: undefined, - preAPIHook: async (context) => { context.requestInit.credentials = "include"; return context; @@ -23,16 +47,14 @@ export const frontendConfig = () => { }; let initialized = false; + export function ensureSuperTokensFrontend() { if (typeof window === "undefined") return; if (!initialized) { + setupFetchInterceptor(); SuperTokens.init(frontendConfig()); initialized = true; - logger.info("Initialized"); - - Session.doesSessionExist().then((exists) => { - logger.info(`Session does${exists ? "" : "NOT"} exist on load!`); - }); + logger.info("SuperTokens initialized"); } } diff --git a/src/lib/tanstack-query/hooks/use-server-mutation.ts b/src/lib/tanstack-query/hooks/use-server-mutation.ts index c6de452..673768c 100644 --- a/src/lib/tanstack-query/hooks/use-server-mutation.ts +++ b/src/lib/tanstack-query/hooks/use-server-mutation.ts @@ -2,6 +2,8 @@ import { useMutation, UseMutationOptions } from "@tanstack/react-query"; import { ServerResult } from "../types"; import toast from '@/lib/sonner' +let isMutationRefreshingSession = false; + export function useServerMutation( options: Omit, 'mutationFn'> & { mutationFn: (variables: TVariables) => Promise>; @@ -42,8 +44,14 @@ export function useServerMutation( : error.response.data; if (errorData?.error === "SESSION_REFRESH_REQUIRED") { - const currentUrl = window.location.pathname + window.location.search; - window.location.href = `/refresh-session?redirect=${encodeURIComponent(currentUrl)}`; + if (!isMutationRefreshingSession) { + isMutationRefreshingSession = true; + const currentUrl = window.location.pathname + window.location.search; + setTimeout(() => { + isMutationRefreshingSession = false; + }, 1000); + window.location.href = `/refresh-session?redirect=${encodeURIComponent(currentUrl)}`; + } throw new Error("SESSION_REFRESH_REQUIRED"); } } catch (parseError) {} diff --git a/src/utils/supertokens.ts b/src/utils/supertokens.ts index f9d3819..620d5bf 100644 --- a/src/utils/supertokens.ts +++ b/src/utils/supertokens.ts @@ -10,62 +10,16 @@ import UserMetadata from "supertokens-node/recipe/usermetadata"; import { getSessionForStart } from "@/lib/supertokens/recipes/start-session"; import { Logger } from "@/lib/logger"; import z from "zod"; -import { refreshSession } from "supertokens-node/recipe/session"; - const logger = new Logger("Middleware"); -function createNodeRequest(request: Request) { - const cookies = request.headers.get('cookie') || ''; - - return { - getHeaderValue: (key: string) => { - return request.headers.get(key) || undefined; - }, - getCookieValue: (key: string) => { - const match = cookies.match(new RegExp(`(^| )${key}=([^;]+)`)); - return match ? match[2] : undefined; - }, - getMethod: () => request.method, - getOriginalURL: () => request.url, - }; -} - const verifySuperTokensSession = async ( request: Request ) => { let session = await getSessionForStart(request, { sessionRequired: false }); if (session?.needsRefresh) { - logger.info("Session needs refresh"); - - try { - - const nodeRequest = createNodeRequest(request); - const nodeResponse = { - setHeader: (key: string, value: string) => { - setResponseHeader(key, value); - }, - setCookie: (cookie: string) => { - setResponseHeader('Set-Cookie', cookie); - } - }; - - const refreshedSession = await refreshSession(nodeRequest, nodeResponse); - if (refreshedSession) { - session = await getSessionForStart(request, { sessionRequired: false }); - } - if (session?.needsRefresh) { - return { context: { session: { tryRefresh: true } } }; - } - } catch (error: any) { - logger.error("Session refresh error", error); - - if (error.type === 'UNAUTHORISED' || error.type === 'TOKEN_THEFT_DETECTED') { - return { context: { userAuthId: null, roles: [] } }; - } - - return { context: { session: { tryRefresh: true } } }; - } + logger.info("Session needs refresh - redirecting to client"); + return { context: { session: { tryRefresh: true } } }; } const userAuthId = session?.userId;