diff --git a/src/app/routeTree.gen.ts b/src/app/routeTree.gen.ts
index 043e7ca..e10dfeb 100644
--- a/src/app/routeTree.gen.ts
+++ b/src/app/routeTree.gen.ts
@@ -11,6 +11,7 @@
import { createServerRootRoute } from '@tanstack/react-start/server'
import { Route as rootRouteImport } from './routes/__root'
+import { Route as RefreshSessionRouteImport } from './routes/refresh-session'
import { Route as LogoutRouteImport } from './routes/logout'
import { Route as LoginRouteImport } from './routes/login'
import { Route as AuthedRouteImport } from './routes/_authed'
@@ -33,6 +34,11 @@ import { ServerRoute as ApiFilesCollectionRecordIdFileServerRouteImport } from '
const rootServerRouteImport = createServerRootRoute()
+const RefreshSessionRoute = RefreshSessionRouteImport.update({
+ id: '/refresh-session',
+ path: '/refresh-session',
+ getParentRoute: () => rootRouteImport,
+} as any)
const LogoutRoute = LogoutRouteImport.update({
id: '/logout',
path: '/logout',
@@ -137,6 +143,7 @@ const ApiFilesCollectionRecordIdFileServerRoute =
export interface FileRoutesByFullPath {
'/login': typeof LoginRoute
'/logout': typeof LogoutRoute
+ '/refresh-session': typeof RefreshSessionRoute
'/admin': typeof AuthedAdminRouteWithChildren
'/settings': typeof AuthedSettingsRoute
'/': typeof AuthedIndexRoute
@@ -153,6 +160,7 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo {
'/login': typeof LoginRoute
'/logout': typeof LogoutRoute
+ '/refresh-session': typeof RefreshSessionRoute
'/settings': typeof AuthedSettingsRoute
'/': typeof AuthedIndexRoute
'/admin/preview': typeof AuthedAdminPreviewRoute
@@ -170,6 +178,7 @@ export interface FileRoutesById {
'/_authed': typeof AuthedRouteWithChildren
'/login': typeof LoginRoute
'/logout': typeof LogoutRoute
+ '/refresh-session': typeof RefreshSessionRoute
'/_authed/admin': typeof AuthedAdminRouteWithChildren
'/_authed/settings': typeof AuthedSettingsRoute
'/_authed/': typeof AuthedIndexRoute
@@ -188,6 +197,7 @@ export interface FileRouteTypes {
fullPaths:
| '/login'
| '/logout'
+ | '/refresh-session'
| '/admin'
| '/settings'
| '/'
@@ -204,6 +214,7 @@ export interface FileRouteTypes {
to:
| '/login'
| '/logout'
+ | '/refresh-session'
| '/settings'
| '/'
| '/admin/preview'
@@ -220,6 +231,7 @@ export interface FileRouteTypes {
| '/_authed'
| '/login'
| '/logout'
+ | '/refresh-session'
| '/_authed/admin'
| '/_authed/settings'
| '/_authed/'
@@ -238,6 +250,7 @@ export interface RootRouteChildren {
AuthedRoute: typeof AuthedRouteWithChildren
LoginRoute: typeof LoginRoute
LogoutRoute: typeof LogoutRoute
+ RefreshSessionRoute: typeof RefreshSessionRoute
}
export interface FileServerRoutesByFullPath {
'/api/auth/$': typeof ApiAuthSplatServerRoute
@@ -288,6 +301,13 @@ export interface RootServerRouteChildren {
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
+ '/refresh-session': {
+ id: '/refresh-session'
+ path: '/refresh-session'
+ fullPath: '/refresh-session'
+ preLoaderRoute: typeof RefreshSessionRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/logout': {
id: '/logout'
path: '/logout'
@@ -475,6 +495,7 @@ const rootRouteChildren: RootRouteChildren = {
AuthedRoute: AuthedRouteWithChildren,
LoginRoute: LoginRoute,
LogoutRoute: LogoutRoute,
+ RefreshSessionRoute: RefreshSessionRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
diff --git a/src/app/routes/__root.tsx b/src/app/routes/__root.tsx
index 0207b7d..feda413 100644
--- a/src/app/routes/__root.tsx
+++ b/src/app/routes/__root.tsx
@@ -19,6 +19,7 @@ import { HeaderConfig } from "@/features/core/types/header-config";
import { playerQueries } from "@/features/players/queries";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
+import FullScreenLoader from "@/components/full-screen-loader";
export const Route = createRootRouteWithContext<{
queryClient: QueryClient;
@@ -70,7 +71,12 @@ export const Route = createRootRouteWithContext<{
},
component: RootComponent,
notFoundComponent: () => ,
- beforeLoad: async ({ context }) => {
+ beforeLoad: async ({ context, location }) => {
+ // Skip auth check for refresh-session route to avoid infinite loops
+ if (location.pathname === '/refresh-session') {
+ return {};
+ }
+
// https://github.com/TanStack/router/discussions/3531
const auth = await ensureServerQueryData(
context.queryClient,
@@ -78,7 +84,7 @@ export const Route = createRootRouteWithContext<{
);
return { auth };
},
- pendingComponent: () =>
Loading...
,
+ pendingComponent: () => ,
});
function RootComponent() {
diff --git a/src/app/routes/refresh-session.tsx b/src/app/routes/refresh-session.tsx
new file mode 100644
index 0000000..615318c
--- /dev/null
+++ b/src/app/routes/refresh-session.tsx
@@ -0,0 +1,39 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { useEffect } from 'react'
+import FullScreenLoader from '@/components/full-screen-loader'
+import { attemptRefreshingSession } from 'supertokens-web-js/recipe/session'
+
+export const Route = createFileRoute('/refresh-session')({
+ component: RouteComponent,
+})
+
+// https://supertokens.com/docs/additional-verification/session-verification/ssr?uiType=custom
+function RouteComponent() {
+ useEffect(() => {
+ const handleRefresh = async () => {
+ try {
+ const refreshed = await attemptRefreshingSession()
+
+ if (refreshed) {
+ const urlParams = new URLSearchParams(window.location.search)
+ const redirect = urlParams.get('redirect')
+
+ if (redirect) {
+ window.location.href = decodeURIComponent(redirect)
+ } else {
+ window.location.href = '/'
+ }
+ } else {
+ window.location.href = '/login'
+ }
+ } catch (error) {
+ window.location.href = '/login'
+ }
+ }
+
+ const timeout = setTimeout(handleRefresh, 100)
+ return () => clearTimeout(timeout)
+ }, [])
+
+ return
+}
\ No newline at end of file
diff --git a/src/components/full-screen-loader.tsx b/src/components/full-screen-loader.tsx
new file mode 100644
index 0000000..af88ba5
--- /dev/null
+++ b/src/components/full-screen-loader.tsx
@@ -0,0 +1,13 @@
+import { Center, Container, Loader, Stack } from "@mantine/core";
+
+const FullScreenLoader = () => (
+
+
+
+
+
+
+
+);
+
+export default FullScreenLoader;
diff --git a/src/features/players/server.ts b/src/features/players/server.ts
index 92543d3..ec2f2ee 100644
--- a/src/features/players/server.ts
+++ b/src/features/players/server.ts
@@ -1,4 +1,4 @@
-import { setUserMetadata, superTokensFunctionMiddleware, verifySuperTokensSession } from "@/utils/supertokens";
+import { setUserMetadata, superTokensFunctionMiddleware, getSessionContext } from "@/utils/supertokens";
import { createServerFn } from "@tanstack/react-start";
import { Player, playerInputSchema, playerUpdateSchema } from "@/features/players/types";
import { pbAdmin } from "@/lib/pocketbase/client";
@@ -8,20 +8,25 @@ import { getWebRequest } from "@tanstack/react-start/server";
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
export const fetchMe = createServerFn()
- .handler(async ({ response }) =>
+ .handler(async () =>
toServerResult(async () => {
const request = getWebRequest();
- const { context } = await verifySuperTokensSession(request, response);
-
- if (!context || !context.userAuthId) return { user: undefined, roles: [], metadata: {} };
-
- await pbAdmin.authPromise;
- const result = await pbAdmin.getPlayerByAuthId(context.userAuthId);
- return {
- user: result || undefined,
- roles: context.roles,
- metadata: context.metadata
- };
+
+ try {
+ const context = await getSessionContext(request);
+
+ await pbAdmin.authPromise;
+ const result = await pbAdmin.getPlayerByAuthId(context.userAuthId);
+ return {
+ user: result || undefined,
+ roles: context.roles,
+ metadata: context.metadata
+ };
+ } catch (error: any) {
+ // If getSessionContext throws (unauthenticated or redirect), return empty state
+ logger.info('fetchMe: No authenticated user or redirect needed');
+ return { user: undefined, roles: [], metadata: {} };
+ }
})
);
diff --git a/src/lib/supertokens/recipes/start-session.ts b/src/lib/supertokens/recipes/start-session.ts
index cb3f7fc..9ba9500 100644
--- a/src/lib/supertokens/recipes/start-session.ts
+++ b/src/lib/supertokens/recipes/start-session.ts
@@ -6,11 +6,7 @@ export async function getSessionForStart(request: Request, options?: { sessionRe
ensureSuperTokensBackend();
try {
- console.log("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=");
- console.log("Getting session for request:", request);
const session = await getSessionForSSR(request);
- console.log("Session from getSessionForSSR:", session);
- console.log("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=");
if (session.hasToken) {
if (session.accessTokenPayload?.sub === undefined || session.accessTokenPayload?.sessionHandle === undefined) {
diff --git a/src/utils/supertokens.ts b/src/utils/supertokens.ts
index f3589f8..13d2509 100644
--- a/src/utils/supertokens.ts
+++ b/src/utils/supertokens.ts
@@ -4,7 +4,7 @@ import {
ServerFnResponseType,
} from "@tanstack/react-start";
import { getWebRequest } from "@tanstack/react-start/server";
-import { getSessionForSSR } from "supertokens-node/custom";
+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";
@@ -21,9 +21,32 @@ export const verifySuperTokensSession = async (
let session = await getSessionForStart(request, { sessionRequired: false });
if (session?.needsRefresh) {
- logger.info("Session refreshing...");
- session = await getSessionForStart(request, { sessionRequired: false });
+ logger.info("Session needs refresh");
+
+ try {
+ // attempt refresh on backend
+ if (response) {
+ const refreshedSession = await refreshSession(request, response);
+ if (refreshedSession) {
+ session = await getSessionForStart(request, { sessionRequired: false });
+ }
+ }
+
+ if (session?.needsRefresh) {
+ // tryRefresh on frontend
+ 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;
if (!userAuthId || !session) {
@@ -47,13 +70,20 @@ export const verifySuperTokensSession = async (
};
};
-export const superTokensRequestMiddleware = createMiddleware({
- type: "request",
-}).server(async ({ next, request }) => {
+export const getSessionContext = async (request: Request): Promise => {
const session = await verifySuperTokensSession(request);
+ if (session.context.session?.tryRefresh) {
+ const url = new URL(request.url);
+ const from = encodeURIComponent(url.pathname + url.search);
+ throw redirect({
+ to: "/refresh-session",
+ search: { redirect: from }
+ });
+ }
+
if (!session.context.userAuthId) {
- logger.error("Unauthenticated user in API call.", session.context);
+ logger.error("Unauthenticated user", session.context);
throw new Error("Unauthenticated");
}
@@ -63,6 +93,13 @@ export const superTokensRequestMiddleware = createMiddleware({
metadata: session.context.metadata,
};
+ return context;
+};
+
+export const superTokensRequestMiddleware = createMiddleware({
+ type: "request",
+}).server(async ({ next, request }) => {
+ const context = await getSessionContext(request);
return next({ context });
});
@@ -70,37 +107,15 @@ export const superTokensFunctionMiddleware = createMiddleware({
type: "function",
}).server(async ({ next, response }) => {
const request = getWebRequest();
- const session = await verifySuperTokensSession(request, response);
-
- if (!session.context.userAuthId) {
- logger.error("Unauthenticated user in server function.", session.context);
- throw new Error("Unauthenticated");
- }
-
- const context = {
- userAuthId: session.context.userAuthId,
- roles: session.context.roles,
- metadata: session.context.metadata,
- };
+ const context = await getSessionContext(request);
return next({ context });
});
export const superTokensAdminFunctionMiddleware = createMiddleware({
type: "function",
-}).server(async ({ next, response }) => {
+}).server(async ({ next }) => {
const request = getWebRequest();
- const session = await verifySuperTokensSession(request, response);
-
- if (!session.context.userAuthId) {
- logger.error("Unauthenticated user in admin function.", session.context);
- throw new Error("Unauthenticated");
- }
-
- const context = {
- userAuthId: session.context.userAuthId,
- roles: session.context.roles,
- metadata: session.context.metadata,
- };
+ const context = await getSessionContext(request);
if (context.roles?.includes("Admin")) {
return next({ context });