fix refresh issue
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,3 +20,4 @@ yarn.lock
|
||||
/scripts/
|
||||
/pb_data/
|
||||
/.tanstack/
|
||||
/dist/
|
||||
@@ -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: () => <Providers><FullScreenLoader /></Providers>,
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
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 <FullScreenLoader />
|
||||
|
||||
@@ -58,13 +58,13 @@ export const AuthProvider: React.FC<PropsWithChildren> = ({ 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 <AuthContext value={value}>{children}</AuthContext>;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TData, TVariables = unknown>(
|
||||
options: Omit<UseMutationOptions<TData, Error, TVariables>, 'mutationFn'> & {
|
||||
mutationFn: (variables: TVariables) => Promise<ServerResult<TData>>;
|
||||
@@ -42,8 +44,14 @@ export function useServerMutation<TData, TVariables = unknown>(
|
||||
: 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) {}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user