From 010dc62159f6845beb1c6ebe69a401c7d20010eb Mon Sep 17 00:00:00 2001 From: Oksamies Date: Tue, 9 Dec 2025 05:46:55 +0200 Subject: [PATCH] Add centralized invalid session cleanup and wire Dapper to it - expose a clearInvalidSession helper from ts-api-react that handles storage, cookies, and stale flags with proper error reporting - update Dapper instantiations (root app, singleton, client loaders) to rely on the shared cleanup hook instead of duplicating logic --- apps/cyberstorm-remix/app/root.tsx | 20 +++++++--- .../cyberstorm/utils/dapperClientLoaders.ts | 13 ++++--- .../cyberstorm/utils/dapperSingleton.ts | 5 ++- packages/dapper-ts/src/methods/currentUser.ts | 35 +++++++++++++++--- packages/ts-api-react/src/SessionContext.tsx | 37 +++++++++++++++++-- packages/ts-api-react/src/index.ts | 1 + 6 files changed, 90 insertions(+), 21 deletions(-) diff --git a/apps/cyberstorm-remix/app/root.tsx b/apps/cyberstorm-remix/app/root.tsx index 94937957f..4eef67010 100644 --- a/apps/cyberstorm-remix/app/root.tsx +++ b/apps/cyberstorm-remix/app/root.tsx @@ -46,6 +46,7 @@ import { } from "@thunderstore/ts-api-react/src/SessionContext"; import { getPublicEnvVariables, + getSessionTools, type publicEnvVariablesType, } from "cyberstorm/security/publicEnvVariables"; import { StorageManager } from "@thunderstore/ts-api-react/src/storage"; @@ -580,12 +581,19 @@ const TooltipProvider = memo(function TooltipProvider({ function App() { const data = useLoaderData(); - const dapper = new DapperTs(() => { - return { - apiHost: data?.publicEnvVariables.VITE_API_URL, - sessionId: data?.config.sessionId, - }; - }); + const sessionTools = getSessionTools(); + const dapper = new DapperTs( + () => { + return { + apiHost: data?.publicEnvVariables.VITE_API_URL, + sessionId: data?.config.sessionId, + }; + }, + () => + sessionTools.clearInvalidSession( + data?.publicEnvVariables.VITE_COOKIE_DOMAIN + ) + ); return ( ( const setupDapper = () => { const tools = getSessionTools(); - const config = tools?.getConfig(); - return new DapperTs(() => ({ - apiHost: config?.apiHost, - sessionId: config?.sessionId, - })); + const config = tools.getConfig(); + return new DapperTs( + () => ({ + apiHost: config.apiHost, + sessionId: config.sessionId, + }), + () => tools.clearInvalidSession() + ); }; diff --git a/apps/cyberstorm-remix/cyberstorm/utils/dapperSingleton.ts b/apps/cyberstorm-remix/cyberstorm/utils/dapperSingleton.ts index 0d1c0f735..d64a48c82 100644 --- a/apps/cyberstorm-remix/cyberstorm/utils/dapperSingleton.ts +++ b/apps/cyberstorm-remix/cyberstorm/utils/dapperSingleton.ts @@ -36,7 +36,10 @@ export function initializeClientDapper(factory?: ConfigFactory) { if (!window.Dapper) { const resolvedFactory = resolveConfigFactory(); - window.Dapper = new DapperTs(resolvedFactory); + const tools = getSessionTools(); + window.Dapper = new DapperTs(resolvedFactory, () => + tools.clearInvalidSession() + ); } } diff --git a/packages/dapper-ts/src/methods/currentUser.ts b/packages/dapper-ts/src/methods/currentUser.ts index ee80db2f5..2a513f78a 100644 --- a/packages/dapper-ts/src/methods/currentUser.ts +++ b/packages/dapper-ts/src/methods/currentUser.ts @@ -6,6 +6,31 @@ import { import { DapperTsInterface } from "../index"; +function isInvalidTokenError(error: ApiError): boolean { + const detail = extractErrorDetail(error.responseJson); + return ( + typeof detail === "string" && detail.toLowerCase().includes("invalid token") + ); +} + +function extractErrorDetail(payload: unknown): string | undefined { + if (!payload) { + return undefined; + } + if (typeof payload === "string") { + return payload; + } + if ( + typeof payload === "object" && + payload !== null && + "detail" in payload && + typeof (payload as { detail?: unknown }).detail === "string" + ) { + return (payload as { detail: string }).detail; + } + return undefined; +} + export async function getCurrentUser(this: DapperTsInterface) { try { const data = await fetchCurrentUser({ @@ -17,13 +42,13 @@ export async function getCurrentUser(this: DapperTsInterface) { return data; } catch (error) { if (error instanceof ApiError && error.response.status === 401) { - // If the user is not authenticated, we remove the session hook - this.removeSessionHook?.(); + if (isInvalidTokenError(error)) { + // If the token is invalid, clear any persisted session data + this.removeSessionHook?.(); + } return null; - } else { - // If it's another error, we throw it - throw error; } + throw error; } } diff --git a/packages/ts-api-react/src/SessionContext.tsx b/packages/ts-api-react/src/SessionContext.tsx index b943cc8be..c5cc31c81 100644 --- a/packages/ts-api-react/src/SessionContext.tsx +++ b/packages/ts-api-react/src/SessionContext.tsx @@ -17,6 +17,8 @@ export interface ContextInterface { clearSession: (clearApiHost?: boolean) => void; /** Remove session cookies. */ clearCookies: (domain: string) => void; + /** Clear all persisted session data and flag as stale. */ + clearInvalidSession: (cookieDomainOverride?: string) => void; /** Set SessionData in storage */ setSession: (sessionData: SessionData) => void; /** Set session stale state */ @@ -99,6 +101,28 @@ export const clearCookies = (domain: string) => { deleteCookie("sessionid", domain); }; +export const clearInvalidSession = ( + _storage: StorageManager, + cookieDomainOverride?: string +) => { + if (typeof window === "undefined") { + return; + } + try { + clearSession(_storage, true); + const cookieDomain = + cookieDomainOverride || + _storage.safeGetValue(COOKIE_DOMAIN_KEY) || + undefined; + if (cookieDomain) { + clearCookies(cookieDomain); + } + setSessionStale(_storage, true); + } catch (error) { + console.error("Failed to clear invalid session", error); + } +}; + export const getConfig = ( _storage: StorageManager, domain?: string @@ -178,10 +202,7 @@ export const updateCurrentUser = async ( customClearSession ? customClearSession : () => { - // This function gets called when the dapper getCurrentUser gets 401 as a response - clearSession(_storage, false); - // We want to clear the sessionid cookie if it's invalid. - clearCookies(_storage.safeGetValue(COOKIE_DOMAIN_KEY) || ""); + clearInvalidSession(_storage); } ); const currentUser = await dapper.getCurrentUser(); @@ -247,6 +268,13 @@ export const getSessionContext = ( clearCookies(domain); }; + const _clearInvalidSession = (cookieDomainOverride?: string) => { + clearInvalidSession( + _storage, + cookieDomainOverride || cookieDomain || undefined + ); + }; + const _getConfig = (domain?: string): RequestConfig => { return getConfig(_storage, domain); }; @@ -289,6 +317,7 @@ export const getSessionContext = ( return { clearSession: _clearSession, clearCookies: _clearCookies, + clearInvalidSession: _clearInvalidSession, getConfig: _getConfig, runSessionValidationCheck: _runSessionValidationCheck, updateCurrentUser: _updateCurrentUser, diff --git a/packages/ts-api-react/src/index.ts b/packages/ts-api-react/src/index.ts index 4c85be554..020f4c6e8 100644 --- a/packages/ts-api-react/src/index.ts +++ b/packages/ts-api-react/src/index.ts @@ -3,6 +3,7 @@ export { CURRENT_USER_KEY, setSession, clearSession, + clearInvalidSession, getConfig, runSessionValidationCheck, storeCurrentUser,