fix refresh issue

This commit is contained in:
yohlo
2025-09-24 12:20:36 -05:00
parent 36f3bb77d4
commit 81329e4354
7 changed files with 74 additions and 87 deletions

1
.gitignore vendored
View File

@@ -20,3 +20,4 @@ yarn.lock
/scripts/ /scripts/
/pb_data/ /pb_data/
/.tanstack/ /.tanstack/
/dist/

View File

@@ -83,12 +83,20 @@ export const Route = createRootRouteWithContext<{
return {}; return {};
} }
if (location.pathname === '/login' || location.pathname === '/logout') {
return {};
}
try {
// https://github.com/TanStack/router/discussions/3531 // https://github.com/TanStack/router/discussions/3531
const auth = await ensureServerQueryData( const auth = await ensureServerQueryData(
context.queryClient, context.queryClient,
playerQueries.auth() playerQueries.auth()
); );
return { auth }; return { auth };
} catch (error) {
return {};
}
}, },
pendingComponent: () => <Providers><FullScreenLoader /></Providers>, pendingComponent: () => <Providers><FullScreenLoader /></Providers>,
}); });

View File

@@ -1,38 +1,33 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { useEffect } from 'react' import { useEffect, useRef } from 'react'
import FullScreenLoader from '@/components/full-screen-loader' import FullScreenLoader from '@/components/full-screen-loader'
import { attemptRefreshingSession } from 'supertokens-web-js/recipe/session' import { attemptRefreshingSession } from 'supertokens-web-js/recipe/session'
import { resetRefreshFlag } from '@/lib/supertokens/client'
export const Route = createFileRoute('/refresh-session')({ export const Route = createFileRoute('/refresh-session')({
component: RouteComponent, component: RouteComponent,
}) })
// https://supertokens.com/docs/additional-verification/session-verification/ssr?uiType=custom
function RouteComponent() { function RouteComponent() {
const hasAttemptedRef = useRef(false);
useEffect(() => { useEffect(() => {
if (hasAttemptedRef.current) return;
hasAttemptedRef.current = true;
const handleRefresh = async () => { const handleRefresh = async () => {
try { try {
resetRefreshFlag();
const refreshed = await attemptRefreshingSession() const refreshed = await attemptRefreshingSession()
if (refreshed) { if (refreshed) {
const urlParams = new URLSearchParams(window.location.search) const urlParams = new URLSearchParams(window.location.search)
const redirect = urlParams.get('redirect') const redirect = urlParams.get('redirect')
const isServerFunction = redirect && ( if (redirect && !redirect.includes('_serverFn') && !redirect.includes('/api/')) {
redirect.startsWith('_serverFn') ||
redirect.startsWith('api/') ||
redirect.includes('_serverFn')
);
if (redirect && !isServerFunction) {
window.location.href = decodeURIComponent(redirect) window.location.href = decodeURIComponent(redirect)
} else { } else {
const referrer = document.referrer; window.location.href = '/';
const referrerUrl = referrer && !referrer.includes('/_serverFn') && !referrer.includes('/api/')
? referrer
: '/';
window.location.href = referrerUrl;
} }
} else { } else {
window.location.href = '/login' window.location.href = '/login'
@@ -42,8 +37,7 @@ function RouteComponent() {
} }
} }
const timeout = setTimeout(handleRefresh, 100) setTimeout(handleRefresh, 100)
return () => clearTimeout(timeout)
}, []) }, [])
return <FullScreenLoader /> return <FullScreenLoader />

View File

@@ -58,13 +58,13 @@ export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
const value = useMemo( const value = useMemo(
() => ({ () => ({
user: data?.user || defaultAuthData.user, user: data?.user,
metadata: data?.metadata || defaultAuthData.metadata, metadata: data?.metadata || { accentColor: "blue" as MantineColor, colorScheme: "dark" as MantineColorScheme },
roles: data?.roles || defaultAuthData.roles, roles: data?.roles || [],
phone: data?.phone || "", phone: data?.phone || "",
set, set,
}), }),
[data, defaultAuthData] [data, set]
); );
return <AuthContext value={value}>{children}</AuthContext>; return <AuthContext value={value}>{children}</AuthContext>;

View File

@@ -4,6 +4,31 @@ import Passwordless from "supertokens-web-js/recipe/passwordless";
import { appInfo } from "./config"; import { appInfo } from "./config";
import { logger } from "./"; 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 = () => { export const frontendConfig = () => {
return { return {
appInfo, appInfo,
@@ -12,7 +37,6 @@ export const frontendConfig = () => {
Session.init({ Session.init({
tokenTransferMethod: "cookie", tokenTransferMethod: "cookie",
sessionTokenBackendDomain: undefined, sessionTokenBackendDomain: undefined,
preAPIHook: async (context) => { preAPIHook: async (context) => {
context.requestInit.credentials = "include"; context.requestInit.credentials = "include";
return context; return context;
@@ -23,16 +47,14 @@ export const frontendConfig = () => {
}; };
let initialized = false; let initialized = false;
export function ensureSuperTokensFrontend() { export function ensureSuperTokensFrontend() {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
if (!initialized) { if (!initialized) {
setupFetchInterceptor();
SuperTokens.init(frontendConfig()); SuperTokens.init(frontendConfig());
initialized = true; initialized = true;
logger.info("Initialized"); logger.info("SuperTokens initialized");
Session.doesSessionExist().then((exists) => {
logger.info(`Session does${exists ? "" : "NOT"} exist on load!`);
});
} }
} }

View File

@@ -2,6 +2,8 @@ import { useMutation, UseMutationOptions } from "@tanstack/react-query";
import { ServerResult } from "../types"; import { ServerResult } from "../types";
import toast from '@/lib/sonner' import toast from '@/lib/sonner'
let isMutationRefreshingSession = false;
export function useServerMutation<TData, TVariables = unknown>( export function useServerMutation<TData, TVariables = unknown>(
options: Omit<UseMutationOptions<TData, Error, TVariables>, 'mutationFn'> & { options: Omit<UseMutationOptions<TData, Error, TVariables>, 'mutationFn'> & {
mutationFn: (variables: TVariables) => Promise<ServerResult<TData>>; mutationFn: (variables: TVariables) => Promise<ServerResult<TData>>;
@@ -42,8 +44,14 @@ export function useServerMutation<TData, TVariables = unknown>(
: error.response.data; : error.response.data;
if (errorData?.error === "SESSION_REFRESH_REQUIRED") { if (errorData?.error === "SESSION_REFRESH_REQUIRED") {
if (!isMutationRefreshingSession) {
isMutationRefreshingSession = true;
const currentUrl = window.location.pathname + window.location.search; const currentUrl = window.location.pathname + window.location.search;
setTimeout(() => {
isMutationRefreshingSession = false;
}, 1000);
window.location.href = `/refresh-session?redirect=${encodeURIComponent(currentUrl)}`; window.location.href = `/refresh-session?redirect=${encodeURIComponent(currentUrl)}`;
}
throw new Error("SESSION_REFRESH_REQUIRED"); throw new Error("SESSION_REFRESH_REQUIRED");
} }
} catch (parseError) {} } catch (parseError) {}

View File

@@ -10,63 +10,17 @@ import UserMetadata from "supertokens-node/recipe/usermetadata";
import { getSessionForStart } from "@/lib/supertokens/recipes/start-session"; import { getSessionForStart } from "@/lib/supertokens/recipes/start-session";
import { Logger } from "@/lib/logger"; import { Logger } from "@/lib/logger";
import z from "zod"; import z from "zod";
import { refreshSession } from "supertokens-node/recipe/session";
const logger = new Logger("Middleware"); 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 ( const verifySuperTokensSession = async (
request: Request request: Request
) => { ) => {
let session = await getSessionForStart(request, { sessionRequired: false }); let session = await getSessionForStart(request, { sessionRequired: false });
if (session?.needsRefresh) { if (session?.needsRefresh) {
logger.info("Session needs refresh"); logger.info("Session needs refresh - redirecting to client");
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 } } }; 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 } } };
}
}
const userAuthId = session?.userId; const userAuthId = session?.userId;