test auth stuff
This commit is contained in:
@@ -125,12 +125,13 @@ export const Route = createRootRouteWithContext<{
|
||||
return { auth };
|
||||
} catch (error: any) {
|
||||
if (typeof window !== 'undefined') {
|
||||
const { doesSessionExist, attemptRefreshingSession } = await import('supertokens-web-js/recipe/session');
|
||||
const { doesSessionExist } = await import('supertokens-web-js/recipe/session');
|
||||
const { refreshManager } = await import('@/lib/supertokens/refresh-manager');
|
||||
|
||||
const sessionExists = await doesSessionExist();
|
||||
if (sessionExists) {
|
||||
try {
|
||||
await attemptRefreshingSession();
|
||||
await refreshManager.refresh();
|
||||
const auth = await ensureServerQueryData(
|
||||
context.queryClient,
|
||||
playerQueries.auth()
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
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')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function clearSuperTokensCookies() {
|
||||
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}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function RouteComponent() {
|
||||
const hasAttemptedRef = useRef(false);
|
||||
|
||||
@@ -20,9 +32,17 @@ function RouteComponent() {
|
||||
try {
|
||||
logger.info("Refresh session route: starting refresh");
|
||||
|
||||
const refreshed = await getOrCreateRefreshPromise(async () => {
|
||||
return await attemptRefreshingSession();
|
||||
});
|
||||
const cookies = document.cookie.split(';');
|
||||
const accessTokenCookies = cookies.filter(c => c.trim().startsWith('sAccessToken='));
|
||||
|
||||
if (accessTokenCookies.length > 1) {
|
||||
logger.warn(`Found ${accessTokenCookies.length} access tokens, clearing all before refresh`);
|
||||
clearSuperTokensCookies();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const refreshed = await refreshManager.refresh();
|
||||
|
||||
if (refreshed) {
|
||||
logger.info("Refresh session route: refresh successful");
|
||||
|
||||
@@ -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';
|
||||
|
||||
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,32 +19,35 @@ 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 {
|
||||
const sessionExists = await doesSessionExist();
|
||||
if (!sessionExists) {
|
||||
logger.info('Session monitor: no session exists, skipping refresh');
|
||||
logger.info('Session monitor: no session exists, redirecting to login');
|
||||
window.location.href = '/login';
|
||||
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();
|
||||
logger.info('Session monitor: session refreshed successfully');
|
||||
} else {
|
||||
logger.warn('Session monitor: refresh returned false');
|
||||
logger.warn('Session monitor: refresh returned false, redirecting to login');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Session monitor: error refreshing session', error);
|
||||
window.location.href = '/login';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -58,7 +58,7 @@ export function SessionMonitor() {
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, [navigate]);
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
77
src/lib/supertokens/refresh-manager.ts
Normal file
77
src/lib/supertokens/refresh-manager.ts
Normal 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();
|
||||
@@ -28,6 +28,26 @@ export const backendConfig = (): TypeInput => {
|
||||
olderCookieDomain: 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",
|
||||
}),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
54
src/lib/tanstack-query/utils/global-error-handler.ts
Normal file
54
src/lib/tanstack-query/utils/global-error-handler.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
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;
|
||||
}
|
||||
|
||||
const isSessionExpired =
|
||||
error?.response?.status === 440 ||
|
||||
error?.response?.headers?.get?.('X-Session-Expired') === 'true';
|
||||
|
||||
if (isSessionExpired) {
|
||||
try {
|
||||
const errorData = await error.response.json().catch(() => ({}));
|
||||
|
||||
if (errorData.error === 'SESSION_REFRESH_REQUIRED' && errorData.shouldRetry) {
|
||||
logger.warn('Query detected SESSION_REFRESH_REQUIRED, initiating redirect');
|
||||
|
||||
const currentUrl = window.location.pathname + window.location.search;
|
||||
await refreshManager.redirectToRefresh(currentUrl);
|
||||
|
||||
throw new Error('Redirecting to refresh session');
|
||||
}
|
||||
} catch (parseError) {
|
||||
if (error?.response?.status === 440) {
|
||||
logger.warn('Session expired (440), redirecting to refresh');
|
||||
const currentUrl = window.location.pathname + window.location.search;
|
||||
await refreshManager.redirectToRefresh(currentUrl);
|
||||
throw new Error('Redirecting to refresh session');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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('Query detected legacy SESSION_REFRESH_REQUIRED (401), 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;
|
||||
}
|
||||
@@ -101,20 +101,24 @@ export const superTokensFunctionMiddleware = createMiddleware({
|
||||
type: "function",
|
||||
}).server(async ({ next }) => {
|
||||
const request = getRequest();
|
||||
|
||||
|
||||
try {
|
||||
const context = await getSessionContext(request, { isServerFunction: true });
|
||||
return next({ context });
|
||||
} catch (error: any) {
|
||||
if (error.message === "SESSION_REFRESH_REQUIRED") {
|
||||
throw new Response(
|
||||
JSON.stringify({
|
||||
JSON.stringify({
|
||||
error: "SESSION_REFRESH_REQUIRED",
|
||||
message: "Session needs to be refreshed"
|
||||
message: "Session needs to be refreshed",
|
||||
shouldRetry: true
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
{
|
||||
status: 440,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Session-Expired": "true"
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -126,7 +130,7 @@ export const superTokensAdminFunctionMiddleware = createMiddleware({
|
||||
type: "function",
|
||||
}).server(async ({ next }) => {
|
||||
const request = getRequest();
|
||||
|
||||
|
||||
try {
|
||||
const context = await getSessionContext(request, { isServerFunction: true });
|
||||
|
||||
@@ -139,13 +143,17 @@ export const superTokensAdminFunctionMiddleware = createMiddleware({
|
||||
} catch (error: any) {
|
||||
if (error.message === "SESSION_REFRESH_REQUIRED") {
|
||||
throw new Response(
|
||||
JSON.stringify({
|
||||
JSON.stringify({
|
||||
error: "SESSION_REFRESH_REQUIRED",
|
||||
message: "Session needs to be refreshed"
|
||||
message: "Session needs to be refreshed",
|
||||
shouldRetry: true
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
{
|
||||
status: 440,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Session-Expired": "true"
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user