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;