23 Commits

Author SHA1 Message Date
deabcedff7 Merge pull request 'try this' (#21) from development into main
All checks were successful
CI/CD Pipeline / Build and Push App Docker Image (push) Successful in 3m10s
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 8s
CI/CD Pipeline / Deploy to Kubernetes (push) Successful in 48s
Reviewed-on: #21
2026-03-03 09:13:47 -06:00
yohlo
2e7098e566 try this
Some checks failed
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Has been cancelled
CI/CD Pipeline / Deploy to Kubernetes (push) Has been cancelled
CI/CD Pipeline / Build and Push App Docker Image (push) Has been cancelled
2026-03-03 09:13:22 -06:00
6e7ef894bf Merge pull request 'try this' (#20) from development into main
All checks were successful
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 7s
CI/CD Pipeline / Deploy to Kubernetes (push) Successful in 46s
CI/CD Pipeline / Build and Push App Docker Image (push) Successful in 3m3s
Reviewed-on: #20
2026-03-03 08:52:55 -06:00
yohlo
562d8294da try this
Some checks failed
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Has been cancelled
CI/CD Pipeline / Deploy to Kubernetes (push) Has been cancelled
CI/CD Pipeline / Build and Push App Docker Image (push) Has been cancelled
2026-03-03 08:52:06 -06:00
ca5bafff46 Merge pull request 'try this' (#19) from development into main
All checks were successful
CI/CD Pipeline / Build and Push App Docker Image (push) Successful in 3m40s
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 8s
CI/CD Pipeline / Deploy to Kubernetes (push) Successful in 50s
Reviewed-on: #19
2026-03-03 08:43:29 -06:00
yohlo
12dcf00d5f try this
Some checks failed
CI/CD Pipeline / Build and Push App Docker Image (push) Failing after 17s
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 7s
CI/CD Pipeline / Deploy to Kubernetes (push) Has been skipped
2026-03-03 08:42:47 -06:00
70d591f925 Merge pull request 'more auth ree' (#18) from development into main
All checks were successful
CI/CD Pipeline / Build and Push App Docker Image (push) Successful in 2m24s
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 8s
CI/CD Pipeline / Deploy to Kubernetes (push) Successful in 43s
Reviewed-on: #18
2026-03-02 23:17:54 -06:00
yohlo
fda8751642 more auth ree
Some checks failed
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Has been cancelled
CI/CD Pipeline / Build and Push App Docker Image (push) Has been cancelled
CI/CD Pipeline / Deploy to Kubernetes (push) Has been cancelled
2026-03-02 23:17:16 -06:00
bccadd18e2 Merge pull request 'more auth ree' (#17) from development into main
All checks were successful
CI/CD Pipeline / Build and Push App Docker Image (push) Successful in 3m4s
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 8s
CI/CD Pipeline / Deploy to Kubernetes (push) Successful in 51s
Reviewed-on: #17
2026-03-02 23:00:16 -06:00
yohlo
41cfcc0260 more auth ree
Some checks failed
CI/CD Pipeline / Build and Push App Docker Image (push) Successful in 3m1s
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 9s
CI/CD Pipeline / Deploy to Kubernetes (push) Has been cancelled
2026-03-02 22:59:46 -06:00
71641f61bf Merge pull request 'more auth ree' (#16) from development into main
All checks were successful
CI/CD Pipeline / Build and Push App Docker Image (push) Successful in 2m37s
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 8s
CI/CD Pipeline / Deploy to Kubernetes (push) Successful in 47s
Reviewed-on: #16
2026-03-02 22:50:26 -06:00
yohlo
1f1de2e04b more auth ree
Some checks failed
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Has been cancelled
CI/CD Pipeline / Deploy to Kubernetes (push) Has been cancelled
CI/CD Pipeline / Build and Push App Docker Image (push) Has been cancelled
2026-03-02 22:49:49 -06:00
9353fa8492 Merge pull request 'more auth ree' (#15) from development into main
All checks were successful
CI/CD Pipeline / Build and Push App Docker Image (push) Successful in 3m11s
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 9s
CI/CD Pipeline / Deploy to Kubernetes (push) Successful in 48s
Reviewed-on: #15
2026-03-02 22:38:34 -06:00
yohlo
d2e1e5d4f0 more auth ree
Some checks failed
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Has been cancelled
CI/CD Pipeline / Deploy to Kubernetes (push) Has been cancelled
CI/CD Pipeline / Build and Push App Docker Image (push) Has been cancelled
2026-03-02 22:38:09 -06:00
957ff79033 Merge pull request 'more auth ree' (#14) from development into main
All checks were successful
CI/CD Pipeline / Build and Push App Docker Image (push) Successful in 2m39s
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 8s
CI/CD Pipeline / Deploy to Kubernetes (push) Successful in 46s
Reviewed-on: #14
2026-03-02 22:25:29 -06:00
yohlo
2551ff8bb3 more auth ree
Some checks failed
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Has been cancelled
CI/CD Pipeline / Deploy to Kubernetes (push) Has been cancelled
CI/CD Pipeline / Build and Push App Docker Image (push) Has been cancelled
2026-03-02 22:25:01 -06:00
ef06665fbc Merge pull request 'more auth ree' (#13) from development into main
All checks were successful
CI/CD Pipeline / Build and Push App Docker Image (push) Successful in 2m38s
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 8s
CI/CD Pipeline / Deploy to Kubernetes (push) Successful in 43s
Reviewed-on: #13
2026-03-02 22:17:35 -06:00
yohlo
76306cc937 more auth ree
Some checks failed
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Has been cancelled
CI/CD Pipeline / Build and Push App Docker Image (push) Has been cancelled
CI/CD Pipeline / Deploy to Kubernetes (push) Has been cancelled
2026-03-02 22:17:05 -06:00
42263c2e7b Merge pull request 'more auth ree' (#12) from development into main
All checks were successful
CI/CD Pipeline / Build and Push App Docker Image (push) Successful in 2m30s
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 8s
CI/CD Pipeline / Deploy to Kubernetes (push) Successful in 48s
Reviewed-on: #12
2026-03-02 20:29:51 -06:00
yohlo
152235dd14 more auth ree
All checks were successful
CI/CD Pipeline / Build and Push App Docker Image (push) Successful in 2m36s
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 10s
CI/CD Pipeline / Deploy to Kubernetes (push) Successful in 45s
2026-03-02 20:28:49 -06:00
0665521a7c Merge pull request 'development' (#11) from development into main
All checks were successful
CI/CD Pipeline / Build and Push App Docker Image (push) Successful in 3m7s
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 7s
CI/CD Pipeline / Deploy to Kubernetes (push) Successful in 45s
Reviewed-on: #11
2026-03-02 10:02:53 -06:00
yohlo
6fddbbab68 test auth fix idk
All checks were successful
CI/CD Pipeline / Build and Push App Docker Image (push) Successful in 2m34s
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 8s
CI/CD Pipeline / Deploy to Kubernetes (push) Successful in 47s
2026-03-02 10:02:13 -06:00
yohlo
3909fbc966 test auth stuff 2026-03-02 09:43:46 -06:00
17 changed files with 311 additions and 223 deletions

View File

@@ -122,25 +122,9 @@ export const Route = createRootRouteWithContext<{
context.queryClient,
playerQueries.auth()
);
return { auth };
} catch (error: any) {
if (typeof window !== 'undefined') {
const { doesSessionExist, attemptRefreshingSession } = await import('supertokens-web-js/recipe/session');
const sessionExists = await doesSessionExist();
if (sessionExists) {
try {
await attemptRefreshingSession();
const auth = await ensureServerQueryData(
context.queryClient,
playerQueries.auth()
);
return { auth };
} catch {
return {};
}
}
}
return {};
}
},

View File

@@ -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,

View File

@@ -2,6 +2,7 @@ import LoginLayout from "@/features/login/components/layout";
import LoginFlow from "@/features/login/components/login-flow";
import { redirect, createFileRoute } from "@tanstack/react-router";
import z from "zod";
import { useEffect } from "react";
const loginSearchSchema = z.object({
stage: z.enum(["code", "name"]).optional(),
@@ -9,6 +10,36 @@ const loginSearchSchema = z.object({
callback: z.string().optional(),
});
function LoginComponent() {
useEffect(() => {
if (typeof window !== 'undefined') {
const cookies = document.cookie.split(';');
const accessTokenCookies = cookies.filter(c => c.trim().startsWith('sAccessToken='));
if (accessTokenCookies.length > 0) {
console.log('[Login] Clearing old SuperTokens cookies');
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}`;
}
});
}
}
}, []);
return (
<LoginLayout>
<LoginFlow />
</LoginLayout>
);
}
export const Route = createFileRoute("/login")({
validateSearch: loginSearchSchema,
beforeLoad: async ({ context }) => {
@@ -16,11 +47,5 @@ export const Route = createFileRoute("/login")({
throw redirect({ to: "/" });
}
},
component: () => {
return (
<LoginLayout>
<LoginFlow />
</LoginLayout>
);
},
component: LoginComponent,
});

View File

@@ -1,8 +1,7 @@
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')({
@@ -20,9 +19,7 @@ function RouteComponent() {
try {
logger.info("Refresh session route: starting refresh");
const refreshed = await getOrCreateRefreshPromise(async () => {
return await attemptRefreshingSession();
});
const refreshed = await refreshManager.refresh();
if (refreshed) {
logger.info("Refresh session route: refresh successful");

View File

@@ -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';
import { ensureSuperTokensFrontend } from '@/lib/supertokens/client';
export function SessionMonitor() {
const navigate = useNavigate();
const lastRefreshTimeRef = useRef<number>(0);
const REFRESH_COOLDOWN = 30 * 1000;
const REFRESH_COOLDOWN = 5 * 1000;
useEffect(() => {
if (typeof window === 'undefined') return;
@@ -22,23 +19,27 @@ 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 {
ensureSuperTokensFrontend();
const { doesSessionExist } = await import('supertokens-web-js/recipe/session');
const sessionExists = await doesSessionExist();
if (!sessionExists) {
logger.info('Session monitor: no session exists, skipping refresh');
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();
@@ -46,19 +47,17 @@ export function SessionMonitor() {
} else {
logger.warn('Session monitor: refresh returned false');
}
} catch (error) {
logger.error('Session monitor: error refreshing session', error);
} catch (error: any) {
logger.error('Session monitor: error refreshing session', error?.message);
}
};
handleVisibilityChange();
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [navigate]);
}, []);
return null;
}

View File

@@ -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<void> | 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<void>((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,
},
});
};

View File

@@ -10,29 +10,41 @@ 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") {
throw error;
}
logger.info("FetchMe: Error caught", {
message: error?.message,
isResponse: error instanceof Response,
status: error instanceof Response ? error.status : error?.response?.status,
hasOptions: !!error?.options,
redirectTo: error?.options?.to
});
if (error?.options?.to && error?.options?.statusCode) {
logger.info("FetchMe: Redirect detected, re-throwing", error.options);
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 };
}
})

View File

@@ -4,34 +4,15 @@ import Passwordless from "supertokens-web-js/recipe/passwordless";
import { appInfo } from "./config";
import { logger } from "./";
let refreshPromise: Promise<boolean> | null = null;
export const resetRefreshFlag = () => {
refreshPromise = null;
logger.warn("resetRefreshFlag is deprecated. Use refreshManager.reset() instead.");
};
export const getOrCreateRefreshPromise = (refreshFn: () => Promise<boolean>): Promise<boolean> => {
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 = () => {

View File

@@ -0,0 +1,77 @@
import { attemptRefreshingSession } from 'supertokens-web-js/recipe/session';
import { logger } from './index';
class SessionRefreshManager {
private refreshPromise: Promise<boolean> | null = null;
private redirectPromise: Promise<void> | null = null;
private lastRefreshTime: number = 0;
private readonly MIN_REFRESH_INTERVAL = 1000;
async refresh(): Promise<boolean> {
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<void> {
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<void>((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();

View File

@@ -25,9 +25,29 @@ export const backendConfig = (): TypeInput => {
cookieSameSite: "lax",
cookieSecure: process.env.NODE_ENV === "production",
cookieDomain: process.env.COOKIE_DOMAIN || undefined,
olderCookieDomain: undefined,
olderCookieDomain: process.env.COOKIE_DOMAIN || 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",
}),

View File

@@ -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<void> | null = null;
import { handleQueryError } from '../utils/global-error-handler';
export function useServerMutation<TData, TVariables = unknown>(
options: Omit<UseMutationOptions<TData, Error, TVariables>, 'mutationFn'> & {
@@ -14,14 +11,14 @@ export function useServerMutation<TData, TVariables = unknown>(
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<TData, TVariables = unknown>(
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<void>((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;
}
},

View File

@@ -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<TData>(
options: {
@@ -17,16 +18,21 @@ export function useServerQuery<TData>(
...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;
}
});
}

View File

@@ -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<TData>(
options: {
@@ -16,16 +17,21 @@ export function useServerSuspenseQuery<TData>(
...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;
}
});

View File

@@ -11,13 +11,20 @@ export async function ensureServerQueryData<TData>(
return queryClient.ensureQueryData({
queryKey: query.queryKey,
queryFn: async () => {
const result = await query.queryFn();
if (!result.success) {
throw new Error(result.error.userMessage);
try {
const result = await query.queryFn();
if (!result.success) {
throw new Error(result.error.userMessage);
}
return result.data;
} catch (error: any) {
if (error?.options?.to && error?.options?.statusCode) {
throw error;
}
throw error;
}
return result.data;
}
});
}

View File

@@ -0,0 +1,65 @@
import { refreshManager } from '@/lib/supertokens/refresh-manager';
import { logger } from '@/lib/supertokens';
export async function handleQueryError(error: any): Promise<void> {
if (typeof window === 'undefined') {
throw error;
}
if (!error || typeof error !== 'object') {
throw error;
}
if (error.options?.to && error.options?.statusCode) {
logger.info('handleQueryError: Re-throwing TanStack Router redirect', error.options);
throw error;
}
if (error instanceof Response) {
const status = error.status;
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);
throw new Error('Redirecting to refresh session');
}
} catch (parseError) {
}
}
throw error;
}

View File

@@ -26,6 +26,14 @@ export const toServerResult = async <T>(
const data = await serverFn();
return { success: true, data };
} catch (error) {
if (error && typeof error === 'object' && 'options' in error) {
const redirectError = error as any;
if (redirectError.options?.to && redirectError.options?.statusCode) {
logger.info('toServerResult: Re-throwing TanStack Router redirect', redirectError.options);
throw error;
}
}
const duration = Date.now() - startTime;
logger.error('Server Fn Error', error);

View File

@@ -4,7 +4,6 @@ import {
createServerOnlyFn,
} from "@tanstack/react-start";
import { getRequest, setResponseHeader } from "@tanstack/react-start/server";
import { redirect as redirect } from "@tanstack/react-router";
import UserRoles from "supertokens-node/recipe/userroles";
import UserMetadata from "supertokens-node/recipe/usermetadata";
import { getSessionForStart } from "@/lib/supertokens/recipes/start-session";
@@ -48,20 +47,12 @@ const verifySuperTokensSession = async (
};
};
export const getSessionContext = createServerOnlyFn(async (request: Request, options?: { isServerFunction?: boolean }) => {
export const getSessionContext = createServerOnlyFn(async (request: Request) => {
const session = await verifySuperTokensSession(request);
if (session.context.session?.tryRefresh) {
if (options?.isServerFunction) {
throw new Error("SESSION_REFRESH_REQUIRED");
}
const url = new URL(request.url);
const from = encodeURIComponent(url.pathname + url.search);
throw redirect({
to: "/refresh-session",
search: { redirect: from }
});
logger.info("Session needs refresh - treating as unauthenticated");
throw new Error("Unauthenticated");
}
if (!session.context.userAuthId) {
@@ -101,23 +92,11 @@ export const superTokensFunctionMiddleware = createMiddleware({
type: "function",
}).server(async ({ next }) => {
const request = getRequest();
try {
const context = await getSessionContext(request, { isServerFunction: true });
const context = await getSessionContext(request);
return next({ context });
} catch (error: any) {
if (error.message === "SESSION_REFRESH_REQUIRED") {
throw new Response(
JSON.stringify({
error: "SESSION_REFRESH_REQUIRED",
message: "Session needs to be refreshed"
}),
{
status: 401,
headers: { "Content-Type": "application/json" }
}
);
}
throw error;
}
});
@@ -126,9 +105,9 @@ export const superTokensAdminFunctionMiddleware = createMiddleware({
type: "function",
}).server(async ({ next }) => {
const request = getRequest();
try {
const context = await getSessionContext(request, { isServerFunction: true });
const context = await getSessionContext(request);
if (context.roles?.includes("Admin")) {
return next({ context });
@@ -137,18 +116,6 @@ export const superTokensAdminFunctionMiddleware = createMiddleware({
logger.error("Unauthorized user in admin function.", context);
throw new Error("Unauthorized");
} catch (error: any) {
if (error.message === "SESSION_REFRESH_REQUIRED") {
throw new Response(
JSON.stringify({
error: "SESSION_REFRESH_REQUIRED",
message: "Session needs to be refreshed"
}),
{
status: 401,
headers: { "Content-Type": "application/json" }
}
);
}
throw error;
}
});