From 3909fbc966256001bef15d496949b61c410bd835 Mon Sep 17 00:00:00 2001 From: yohlo Date: Mon, 2 Mar 2026 09:43:46 -0600 Subject: [PATCH 1/2] test auth stuff --- src/app/routes/__root.tsx | 5 +- src/app/routes/refresh-session.tsx | 30 ++++++-- src/components/session-monitor.tsx | 28 +++---- src/features/players/queries.ts | 35 +-------- src/lib/supertokens/client.ts | 27 +------ src/lib/supertokens/refresh-manager.ts | 77 +++++++++++++++++++ src/lib/supertokens/server.ts | 20 +++++ .../hooks/use-server-mutation.ts | 55 +++---------- .../tanstack-query/hooks/use-server-query.ts | 22 ++++-- .../hooks/use-server-suspense-query.ts | 22 ++++-- .../utils/global-error-handler.ts | 54 +++++++++++++ src/utils/supertokens.ts | 32 +++++--- 12 files changed, 255 insertions(+), 152 deletions(-) create mode 100644 src/lib/supertokens/refresh-manager.ts create mode 100644 src/lib/tanstack-query/utils/global-error-handler.ts diff --git a/src/app/routes/__root.tsx b/src/app/routes/__root.tsx index 91ae9de..e31f413 100644 --- a/src/app/routes/__root.tsx +++ b/src/app/routes/__root.tsx @@ -125,12 +125,13 @@ export const Route = createRootRouteWithContext<{ return { auth }; } catch (error: any) { if (typeof window !== 'undefined') { - const { doesSessionExist, attemptRefreshingSession } = await import('supertokens-web-js/recipe/session'); + const { doesSessionExist } = await import('supertokens-web-js/recipe/session'); + const { refreshManager } = await import('@/lib/supertokens/refresh-manager'); const sessionExists = await doesSessionExist(); if (sessionExists) { try { - await attemptRefreshingSession(); + await refreshManager.refresh(); const auth = await ensureServerQueryData( context.queryClient, playerQueries.auth() diff --git a/src/app/routes/refresh-session.tsx b/src/app/routes/refresh-session.tsx index 9d627ed..cbe35f9 100644 --- a/src/app/routes/refresh-session.tsx +++ b/src/app/routes/refresh-session.tsx @@ -1,14 +1,26 @@ import { createFileRoute } from '@tanstack/react-router' import { useEffect, useRef } from 'react' import FullScreenLoader from '@/components/full-screen-loader' -import { attemptRefreshingSession } from 'supertokens-web-js/recipe/session' -import { resetRefreshFlag, getOrCreateRefreshPromise } from '@/lib/supertokens/client' +import { refreshManager } from '@/lib/supertokens/refresh-manager' import { logger } from '@/lib/supertokens' export const Route = createFileRoute('/refresh-session')({ component: RouteComponent, }) +function clearSuperTokensCookies() { + const cookieNames = ['sAccessToken', 'sRefreshToken', 'sIdRefreshToken', 'sFrontToken']; + const cookieDomain = (window as any).__COOKIE_DOMAIN__ || undefined; + + cookieNames.forEach(name => { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; + + if (cookieDomain) { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${cookieDomain}`; + } + }); +} + function RouteComponent() { const hasAttemptedRef = useRef(false); @@ -20,9 +32,17 @@ function RouteComponent() { try { logger.info("Refresh session route: starting refresh"); - const refreshed = await getOrCreateRefreshPromise(async () => { - return await attemptRefreshingSession(); - }); + const cookies = document.cookie.split(';'); + const accessTokenCookies = cookies.filter(c => c.trim().startsWith('sAccessToken=')); + + if (accessTokenCookies.length > 1) { + logger.warn(`Found ${accessTokenCookies.length} access tokens, clearing all before refresh`); + clearSuperTokensCookies(); + + await new Promise(resolve => setTimeout(resolve, 100)); + } + + const refreshed = await refreshManager.refresh(); if (refreshed) { logger.info("Refresh session route: refresh successful"); diff --git a/src/components/session-monitor.tsx b/src/components/session-monitor.tsx index 9fc8963..0c53d45 100644 --- a/src/components/session-monitor.tsx +++ b/src/components/session-monitor.tsx @@ -1,14 +1,11 @@ import { useEffect, useRef } from 'react'; -import { useNavigate } from '@tanstack/react-router'; import { doesSessionExist } from 'supertokens-web-js/recipe/session'; -import { getOrCreateRefreshPromise } from '@/lib/supertokens/client'; -import { attemptRefreshingSession } from 'supertokens-web-js/recipe/session'; +import { refreshManager } from '@/lib/supertokens/refresh-manager'; import { logger } from '@/lib/supertokens'; export function SessionMonitor() { - const navigate = useNavigate(); const lastRefreshTimeRef = useRef(0); - const REFRESH_COOLDOWN = 30 * 1000; + const REFRESH_COOLDOWN = 5 * 1000; useEffect(() => { if (typeof window === 'undefined') return; @@ -22,32 +19,35 @@ export function SessionMonitor() { } const now = Date.now(); - if (now - lastRefreshTimeRef.current < REFRESH_COOLDOWN) { - logger.info('Session monitor: skipping refresh (cooldown)'); + const timeSinceLastRefresh = now - lastRefreshTimeRef.current; + + if (timeSinceLastRefresh < REFRESH_COOLDOWN) { + logger.info(`Session monitor: skipping refresh (refreshed ${timeSinceLastRefresh}ms ago)`); return; } try { const sessionExists = await doesSessionExist(); if (!sessionExists) { - logger.info('Session monitor: no session exists, skipping refresh'); + logger.info('Session monitor: no session exists, redirecting to login'); + window.location.href = '/login'; return; } - logger.info('Session monitor: tab became visible, refreshing session'); + logger.info('Session monitor: tab became visible, checking session freshness'); - const refreshed = await getOrCreateRefreshPromise(async () => { - return await attemptRefreshingSession(); - }); + const refreshed = await refreshManager.refresh(); if (refreshed) { lastRefreshTimeRef.current = Date.now(); logger.info('Session monitor: session refreshed successfully'); } else { - logger.warn('Session monitor: refresh returned false'); + logger.warn('Session monitor: refresh returned false, redirecting to login'); + window.location.href = '/login'; } } catch (error) { logger.error('Session monitor: error refreshing session', error); + window.location.href = '/login'; } }; @@ -58,7 +58,7 @@ export function SessionMonitor() { return () => { document.removeEventListener('visibilitychange', handleVisibilityChange); }; - }, [navigate]); + }, []); return null; } diff --git a/src/features/players/queries.ts b/src/features/players/queries.ts index 53f2b49..8ccc2a2 100644 --- a/src/features/players/queries.ts +++ b/src/features/players/queries.ts @@ -1,8 +1,5 @@ import { useServerSuspenseQuery } from "@/lib/tanstack-query/hooks"; import { listPlayers, getPlayer, getUnassociatedPlayers, fetchMe, getPlayerStats, getAllPlayerStats, getPlayerMatches, getUnenrolledPlayers, getPlayersActivity } from "./server"; -import { logger } from '@/lib/supertokens'; - -let queryRefreshRedirect: Promise | null = null; export const playerKeys = { auth: ['auth'], @@ -64,37 +61,7 @@ export const useMe = () => { staleTime: 30 * 1000, refetchOnMount: false, refetchOnWindowFocus: true, - retry: (failureCount, error: any) => { - if (error?.response?.status === 401) { - const errorData = error?.response?.data; - if (errorData?.error === 'SESSION_REFRESH_REQUIRED') { - logger.warn("Query detected SESSION_REFRESH_REQUIRED"); - - if (!queryRefreshRedirect) { - const currentUrl = window.location.pathname + window.location.search; - logger.info("Query initiating refresh redirect to:", currentUrl); - - queryRefreshRedirect = new Promise((resolve) => { - setTimeout(() => { - window.location.href = `/refresh-session?redirect=${encodeURIComponent(currentUrl)}`; - resolve(); - }, 100); - }); - - queryRefreshRedirect.finally(() => { - setTimeout(() => { - queryRefreshRedirect = null; - }, 1000); - }); - } else { - logger.info("Query: refresh redirect already in progress"); - } - - return false; - } - } - return failureCount < 3; - }, + retry: 3, }, }); }; diff --git a/src/lib/supertokens/client.ts b/src/lib/supertokens/client.ts index d994557..763bcae 100644 --- a/src/lib/supertokens/client.ts +++ b/src/lib/supertokens/client.ts @@ -4,34 +4,15 @@ import Passwordless from "supertokens-web-js/recipe/passwordless"; import { appInfo } from "./config"; import { logger } from "./"; -let refreshPromise: Promise | null = null; export const resetRefreshFlag = () => { - refreshPromise = null; + logger.warn("resetRefreshFlag is deprecated. Use refreshManager.reset() instead."); }; + export const getOrCreateRefreshPromise = (refreshFn: () => Promise): Promise => { - if (refreshPromise) { - logger.info("Reusing existing refresh promise"); - return refreshPromise; - } - - logger.info("Creating new refresh promise"); - refreshPromise = refreshFn() - .then((result) => { - logger.info("Refresh completed successfully:", result); - setTimeout(() => { - refreshPromise = null; - }, 500); - return result; - }) - .catch((error) => { - logger.error("Refresh failed:", error); - refreshPromise = null; - throw error; - }); - - return refreshPromise; + logger.warn("getOrCreateRefreshPromise is deprecated. Use refreshManager.refresh() instead."); + return refreshFn(); }; export const frontendConfig = () => { diff --git a/src/lib/supertokens/refresh-manager.ts b/src/lib/supertokens/refresh-manager.ts new file mode 100644 index 0000000..b66ce00 --- /dev/null +++ b/src/lib/supertokens/refresh-manager.ts @@ -0,0 +1,77 @@ +import { attemptRefreshingSession } from 'supertokens-web-js/recipe/session'; +import { logger } from './index'; + +class SessionRefreshManager { + private refreshPromise: Promise | null = null; + private redirectPromise: Promise | null = null; + private lastRefreshTime: number = 0; + private readonly MIN_REFRESH_INTERVAL = 1000; + + async refresh(): Promise { + if (this.refreshPromise) { + logger.info('RefreshManager: Reusing existing refresh promise'); + return this.refreshPromise; + } + + const timeSinceLastRefresh = Date.now() - this.lastRefreshTime; + if (timeSinceLastRefresh < this.MIN_REFRESH_INTERVAL) { + logger.info(`RefreshManager: Skipping refresh (last refresh ${timeSinceLastRefresh}ms ago)`); + return true; + } + + logger.info('RefreshManager: Starting new session refresh'); + this.refreshPromise = attemptRefreshingSession() + .then((result) => { + logger.info('RefreshManager: Refresh completed successfully:', result); + this.lastRefreshTime = Date.now(); + return result; + }) + .catch((error) => { + logger.error('RefreshManager: Refresh failed:', error); + throw error; + }) + .finally(() => { + setTimeout(() => { + this.refreshPromise = null; + }, 500); + }); + + return this.refreshPromise; + } + + async redirectToRefresh(currentPath: string): Promise { + if (this.redirectPromise) { + logger.info('RefreshManager: Redirect already in progress, waiting...'); + return this.redirectPromise; + } + + logger.info('RefreshManager: Initiating refresh redirect to:', currentPath); + this.redirectPromise = new Promise((resolve) => { + setTimeout(() => { + window.location.href = `/refresh-session?redirect=${encodeURIComponent(currentPath)}`; + resolve(); + }, 100); + }); + + this.redirectPromise.finally(() => { + setTimeout(() => { + this.redirectPromise = null; + }, 1000); + }); + + return this.redirectPromise; + } + + reset(): void { + logger.info('RefreshManager: Resetting state'); + this.refreshPromise = null; + this.redirectPromise = null; + this.lastRefreshTime = 0; + } + + getTimeSinceLastRefresh(): number { + return Date.now() - this.lastRefreshTime; + } +} + +export const refreshManager = new SessionRefreshManager(); diff --git a/src/lib/supertokens/server.ts b/src/lib/supertokens/server.ts index f5119ab..d26c455 100644 --- a/src/lib/supertokens/server.ts +++ b/src/lib/supertokens/server.ts @@ -28,6 +28,26 @@ export const backendConfig = (): TypeInput => { olderCookieDomain: undefined, antiCsrf: process.env.NODE_ENV === "production" ? "VIA_TOKEN" : "NONE", + sessionExpiredStatusCode: 440, + invalidClaimStatusCode: 403, + + override: { + functions: (originalImplementation) => ({ + ...originalImplementation, + refreshSession: async (input) => { + logger.info('Backend: Refresh session attempt'); + try { + const result = await originalImplementation.refreshSession(input); + logger.info('Backend: Refresh session successful'); + return result; + } catch (error) { + logger.error('Backend: Refresh session failed:', error); + throw error; + } + }, + }), + }, + // Debug only exposeAccessTokenToFrontendInCookieBasedAuth: process.env.NODE_ENV !== "production", }), diff --git a/src/lib/tanstack-query/hooks/use-server-mutation.ts b/src/lib/tanstack-query/hooks/use-server-mutation.ts index 9484785..c545cd5 100644 --- a/src/lib/tanstack-query/hooks/use-server-mutation.ts +++ b/src/lib/tanstack-query/hooks/use-server-mutation.ts @@ -1,10 +1,7 @@ import { useMutation, UseMutationOptions } from "@tanstack/react-query"; import { ServerResult } from "../types"; import toast from '@/lib/sonner' -import { logger } from '@/lib/supertokens' - - -let sessionRefreshRedirect: Promise | null = null; +import { handleQueryError } from '../utils/global-error-handler'; export function useServerMutation( options: Omit, 'mutationFn'> & { @@ -14,14 +11,14 @@ export function useServerMutation( showSuccessToast?: boolean; } ) { - const { - mutationFn, - successMessage, - showErrorToast = true, + const { + mutationFn, + successMessage, + showErrorToast = true, showSuccessToast = true, onSuccess, onError, - ...mutationOptions + ...mutationOptions } = options; return useMutation({ @@ -29,51 +26,17 @@ export function useServerMutation( mutationFn: async (variables: TVariables) => { try { const result = await mutationFn(variables); - + if (!result.success) { if (showErrorToast) { toast.error(result.error.userMessage); } throw new Error(result.error.userMessage); } - + return result.data; } catch (error: any) { - if (error?.response?.status === 401) { - try { - const errorData = typeof error.response.data === 'string' - ? JSON.parse(error.response.data) - : error.response.data; - - if (errorData?.error === "SESSION_REFRESH_REQUIRED") { - logger.warn("Mutation detected SESSION_REFRESH_REQUIRED"); - - if (!sessionRefreshRedirect) { - const currentUrl = window.location.pathname + window.location.search; - logger.info("Mutation initiating refresh redirect to:", currentUrl); - - sessionRefreshRedirect = new Promise((resolve) => { - setTimeout(() => { - window.location.href = `/refresh-session?redirect=${encodeURIComponent(currentUrl)}`; - resolve(); - }, 100); - }); - - sessionRefreshRedirect.finally(() => { - setTimeout(() => { - sessionRefreshRedirect = null; - }, 1000); - }); - } else { - logger.info("Mutation: refresh redirect already in progress, waiting..."); - await sessionRefreshRedirect; - } - - throw new Error("SESSION_REFRESH_REQUIRED"); - } - } catch (parseError) {} - } - + await handleQueryError(error); throw error; } }, diff --git a/src/lib/tanstack-query/hooks/use-server-query.ts b/src/lib/tanstack-query/hooks/use-server-query.ts index fe9deb8..45c5394 100644 --- a/src/lib/tanstack-query/hooks/use-server-query.ts +++ b/src/lib/tanstack-query/hooks/use-server-query.ts @@ -1,6 +1,7 @@ import { QueryKey, useQuery, UseQueryOptions } from "@tanstack/react-query"; import { ServerResult } from "../types"; import toast from '@/lib/sonner' +import { handleQueryError } from '../utils/global-error-handler'; export function useServerQuery( options: { @@ -17,16 +18,21 @@ export function useServerQuery( ...queryOptions, queryKey, queryFn: async () => { - const result = await queryFn(); - - if (!result.success) { - if (showErrorToast) { - toast.error(result.error.userMessage); + try { + const result = await queryFn(); + + if (!result.success) { + if (showErrorToast) { + toast.error(result.error.userMessage); + } + throw new Error(result.error.userMessage); } - throw new Error(result.error.userMessage); + + return result.data; + } catch (error: any) { + await handleQueryError(error); + throw error; } - - return result.data; } }); } \ No newline at end of file diff --git a/src/lib/tanstack-query/hooks/use-server-suspense-query.ts b/src/lib/tanstack-query/hooks/use-server-suspense-query.ts index df77117..bc71400 100644 --- a/src/lib/tanstack-query/hooks/use-server-suspense-query.ts +++ b/src/lib/tanstack-query/hooks/use-server-suspense-query.ts @@ -1,6 +1,7 @@ import { QueryKey, UseQueryOptions, useSuspenseQuery } from "@tanstack/react-query"; import { ServerResult } from "../types"; import toast from '@/lib/sonner' +import { handleQueryError } from '../utils/global-error-handler'; export function useServerSuspenseQuery( options: { @@ -16,16 +17,21 @@ export function useServerSuspenseQuery( ...queryOptions, queryKey, queryFn: async () => { - const result = await queryFn(); - - if (!result.success) { - if (showErrorToast) { - toast.error(result.error.userMessage); + try { + const result = await queryFn(); + + if (!result.success) { + if (showErrorToast) { + toast.error(result.error.userMessage); + } + throw new Error(result.error.userMessage); } - throw new Error(result.error.userMessage); + + return result.data; + } catch (error: any) { + await handleQueryError(error); + throw error; } - - return result.data; } }); diff --git a/src/lib/tanstack-query/utils/global-error-handler.ts b/src/lib/tanstack-query/utils/global-error-handler.ts new file mode 100644 index 0000000..ed9aa33 --- /dev/null +++ b/src/lib/tanstack-query/utils/global-error-handler.ts @@ -0,0 +1,54 @@ +import { refreshManager } from '@/lib/supertokens/refresh-manager'; +import { logger } from '@/lib/supertokens'; + +export async function handleQueryError(error: any): Promise { + if (typeof window === 'undefined') { + throw error; + } + + const isSessionExpired = + error?.response?.status === 440 || + error?.response?.headers?.get?.('X-Session-Expired') === 'true'; + + if (isSessionExpired) { + try { + const errorData = await error.response.json().catch(() => ({})); + + if (errorData.error === 'SESSION_REFRESH_REQUIRED' && errorData.shouldRetry) { + logger.warn('Query detected SESSION_REFRESH_REQUIRED, initiating redirect'); + + const currentUrl = window.location.pathname + window.location.search; + await refreshManager.redirectToRefresh(currentUrl); + + throw new Error('Redirecting to refresh session'); + } + } catch (parseError) { + if (error?.response?.status === 440) { + logger.warn('Session expired (440), redirecting to refresh'); + const currentUrl = window.location.pathname + window.location.search; + await refreshManager.redirectToRefresh(currentUrl); + throw new Error('Redirecting to refresh session'); + } + } + } + + if (error?.response?.status === 401) { + try { + const errorData = typeof error.response.data === 'string' + ? JSON.parse(error.response.data) + : error.response.data; + + if (errorData?.error === 'SESSION_REFRESH_REQUIRED') { + logger.warn('Query detected legacy SESSION_REFRESH_REQUIRED (401), initiating redirect'); + + const currentUrl = window.location.pathname + window.location.search; + await refreshManager.redirectToRefresh(currentUrl); + + throw new Error('Redirecting to refresh session'); + } + } catch (parseError) { + } + } + + throw error; +} diff --git a/src/utils/supertokens.ts b/src/utils/supertokens.ts index faeba78..bddc582 100644 --- a/src/utils/supertokens.ts +++ b/src/utils/supertokens.ts @@ -101,20 +101,24 @@ 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({ + JSON.stringify({ error: "SESSION_REFRESH_REQUIRED", - message: "Session needs to be refreshed" + message: "Session needs to be refreshed", + shouldRetry: true }), - { - status: 401, - headers: { "Content-Type": "application/json" } + { + status: 440, + headers: { + "Content-Type": "application/json", + "X-Session-Expired": "true" + } } ); } @@ -126,7 +130,7 @@ export const superTokensAdminFunctionMiddleware = createMiddleware({ type: "function", }).server(async ({ next }) => { const request = getRequest(); - + try { const context = await getSessionContext(request, { isServerFunction: true }); @@ -139,13 +143,17 @@ export const superTokensAdminFunctionMiddleware = createMiddleware({ } catch (error: any) { if (error.message === "SESSION_REFRESH_REQUIRED") { throw new Response( - JSON.stringify({ + JSON.stringify({ error: "SESSION_REFRESH_REQUIRED", - message: "Session needs to be refreshed" + message: "Session needs to be refreshed", + shouldRetry: true }), - { - status: 401, - headers: { "Content-Type": "application/json" } + { + status: 440, + headers: { + "Content-Type": "application/json", + "X-Session-Expired": "true" + } } ); } From 6fddbbab68af7d1fc93d4cfe6dc94b2336fc7f4f Mon Sep 17 00:00:00 2001 From: yohlo Date: Mon, 2 Mar 2026 10:02:13 -0600 Subject: [PATCH 2/2] test auth fix idk --- src/app/routes/__root.tsx | 22 ++---- src/app/routes/_authed.tsx | 4 ++ src/components/session-monitor.tsx | 19 +++--- src/features/players/server.ts | 39 ++++++++--- .../utils/global-error-handler.ts | 68 ++++++++++--------- 5 files changed, 84 insertions(+), 68 deletions(-) diff --git a/src/app/routes/__root.tsx b/src/app/routes/__root.tsx index e31f413..fae7445 100644 --- a/src/app/routes/__root.tsx +++ b/src/app/routes/__root.tsx @@ -122,26 +122,12 @@ export const Route = createRootRouteWithContext<{ context.queryClient, playerQueries.auth() ); + + console.log('__root beforeLoad auth data:', auth); + return { auth }; } catch (error: any) { - if (typeof window !== 'undefined') { - const { doesSessionExist } = await import('supertokens-web-js/recipe/session'); - const { refreshManager } = await import('@/lib/supertokens/refresh-manager'); - - const sessionExists = await doesSessionExist(); - if (sessionExists) { - try { - await refreshManager.refresh(); - const auth = await ensureServerQueryData( - context.queryClient, - playerQueries.auth() - ); - return { auth }; - } catch { - return {}; - } - } - } + console.error('__root beforeLoad error:', error); return {}; } }, diff --git a/src/app/routes/_authed.tsx b/src/app/routes/_authed.tsx index d024d51..5ffac39 100644 --- a/src/app/routes/_authed.tsx +++ b/src/app/routes/_authed.tsx @@ -5,10 +5,14 @@ import { Flex, Loader } from "@mantine/core"; export const Route = createFileRoute("/_authed")({ beforeLoad: ({ context }) => { + console.log('_authed beforeLoad context:', context.auth); + if (!context.auth?.user) { + console.log('_authed: No user in context, redirecting to login'); throw redirect({ to: "/login" }); } + console.log('_authed: User found, allowing access'); return { auth: { ...context.auth, diff --git a/src/components/session-monitor.tsx b/src/components/session-monitor.tsx index 0c53d45..bec0a9c 100644 --- a/src/components/session-monitor.tsx +++ b/src/components/session-monitor.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef } from 'react'; -import { doesSessionExist } from 'supertokens-web-js/recipe/session'; import { refreshManager } from '@/lib/supertokens/refresh-manager'; import { logger } from '@/lib/supertokens'; +import { ensureSuperTokensFrontend } from '@/lib/supertokens/client'; export function SessionMonitor() { const lastRefreshTimeRef = useRef(0); @@ -27,10 +27,13 @@ export function SessionMonitor() { } try { + ensureSuperTokensFrontend(); + + const { doesSessionExist } = await import('supertokens-web-js/recipe/session'); + const sessionExists = await doesSessionExist(); if (!sessionExists) { - logger.info('Session monitor: no session exists, redirecting to login'); - window.location.href = '/login'; + logger.info('Session monitor: no session exists, skipping refresh'); return; } @@ -42,17 +45,13 @@ export function SessionMonitor() { lastRefreshTimeRef.current = Date.now(); logger.info('Session monitor: session refreshed successfully'); } else { - logger.warn('Session monitor: refresh returned false, redirecting to login'); - window.location.href = '/login'; + logger.warn('Session monitor: refresh returned false'); } - } catch (error) { - logger.error('Session monitor: error refreshing session', error); - window.location.href = '/login'; + } catch (error: any) { + logger.error('Session monitor: error refreshing session', error?.message); } }; - handleVisibilityChange(); - document.addEventListener('visibilitychange', handleVisibilityChange); return () => { diff --git a/src/features/players/server.ts b/src/features/players/server.ts index 56874e1..c0c9969 100644 --- a/src/features/players/server.ts +++ b/src/features/players/server.ts @@ -10,29 +10,50 @@ import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result"; import { serverFnLoggingMiddleware } from "@/utils/activities"; export const fetchMe = createServerFn() - .handler(async () => + .handler(async () => toServerResult(async () => { const request = getRequest(); - + try { const context = await getSessionContext(request); - + await pbAdmin.authPromise; const result = await pbAdmin.getPlayerByAuthId(context.userAuthId); return { - user: result || undefined, - roles: context.roles, + user: result || undefined, + roles: context.roles, metadata: context.metadata, phone: context.phone }; } catch (error: any) { - // logger.info("FetchMe: Session error", error) - if (error?.response?.status === 401) { - const errorData = error?.response?.data; - if (errorData?.error === "SESSION_REFRESH_REQUIRED") { + logger.info("FetchMe: Error caught", { + message: error?.message, + isResponse: error instanceof Response, + status: error instanceof Response ? error.status : error?.response?.status + }); + + if (error instanceof Response) { + const status = error.status; + if (status === 440) { + logger.info("FetchMe: Session refresh required (440)"); throw error; } } + + if (error?.response?.status === 440 || error?.response?.status === 401) { + const errorData = error?.response?.data; + if (errorData?.error === "SESSION_REFRESH_REQUIRED") { + logger.info("FetchMe: Session refresh required (legacy)"); + throw error; + } + } + + if (error?.message === "Unauthenticated") { + logger.info("FetchMe: No authenticated user (expected when not logged in)"); + return { user: undefined, roles: [], metadata: {}, phone: undefined }; + } + + logger.warn("FetchMe: Unexpected error, returning default", error); return { user: undefined, roles: [], metadata: {}, phone: undefined }; } }) diff --git a/src/lib/tanstack-query/utils/global-error-handler.ts b/src/lib/tanstack-query/utils/global-error-handler.ts index ed9aa33..c4a5ec2 100644 --- a/src/lib/tanstack-query/utils/global-error-handler.ts +++ b/src/lib/tanstack-query/utils/global-error-handler.ts @@ -6,40 +6,46 @@ export async function handleQueryError(error: any): Promise { throw error; } - const isSessionExpired = - error?.response?.status === 440 || - error?.response?.headers?.get?.('X-Session-Expired') === 'true'; - - if (isSessionExpired) { - try { - const errorData = await error.response.json().catch(() => ({})); - - if (errorData.error === 'SESSION_REFRESH_REQUIRED' && errorData.shouldRetry) { - logger.warn('Query detected SESSION_REFRESH_REQUIRED, initiating redirect'); - - const currentUrl = window.location.pathname + window.location.search; - await refreshManager.redirectToRefresh(currentUrl); - - throw new Error('Redirecting to refresh session'); - } - } catch (parseError) { - if (error?.response?.status === 440) { - logger.warn('Session expired (440), redirecting to refresh'); - const currentUrl = window.location.pathname + window.location.search; - await refreshManager.redirectToRefresh(currentUrl); - throw new Error('Redirecting to refresh session'); - } - } + if (!error || typeof error !== 'object') { + throw error; } - if (error?.response?.status === 401) { - try { - const errorData = typeof error.response.data === 'string' - ? JSON.parse(error.response.data) - : error.response.data; + if (error instanceof Response) { + const status = error.status; - if (errorData?.error === 'SESSION_REFRESH_REQUIRED') { - logger.warn('Query detected legacy SESSION_REFRESH_REQUIRED (401), initiating redirect'); + if (status === 440) { + try { + const errorData = await error.json(); + + if (errorData?.error === 'SESSION_REFRESH_REQUIRED' && errorData?.shouldRetry === true) { + logger.warn('Query detected SESSION_REFRESH_REQUIRED (Response), initiating redirect'); + + const currentUrl = window.location.pathname + window.location.search; + await refreshManager.redirectToRefresh(currentUrl); + + throw new Error('Redirecting to refresh session'); + } + } catch (parseError) { + } + } + throw error; + } + + const status = error?.response?.status; + + if (status === 440) { + try { + let errorData = error?.response?.data; + + if (typeof errorData === 'string') { + try { + errorData = JSON.parse(errorData); + } catch { + } + } + + if (errorData?.error === 'SESSION_REFRESH_REQUIRED' && errorData?.shouldRetry === true) { + logger.warn('Query detected SESSION_REFRESH_REQUIRED (legacy format), initiating redirect'); const currentUrl = window.location.pathname + window.location.search; await refreshManager.redirectToRefresh(currentUrl);