fix refresh issue
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,3 +20,4 @@ yarn.lock
|
|||||||
/scripts/
|
/scripts/
|
||||||
/pb_data/
|
/pb_data/
|
||||||
/.tanstack/
|
/.tanstack/
|
||||||
|
/dist/
|
||||||
@@ -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>,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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!`);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user