refactor: update session handling across components to use new session structure and improve loading states

This commit is contained in:
Marco Pedone 2026-04-09 11:09:25 +02:00
parent 621d227760
commit 3f477ca3b8
18 changed files with 226 additions and 132 deletions

View file

@ -16,7 +16,6 @@ import {
useSession,
} from "~/providers/SessionProvider";
import type { UsersId } from "~/schemas/public/Users";
import type { ValidSession } from "~/server/api/trpc";
import { api } from "~/utils/api";
import { UserViewHeader } from "./area-riservata/userViewHeader";
@ -73,9 +72,9 @@ export const AreaRiservataLayout = ({
const { data: bannerData } = api.banners.getBannerData.useQuery({
area_riservata: true,
});
const session = useSession();
const { status, user } = useSession();
if (!ignoreSessionCheck) {
if (session.status === "pending")
if (status === "LOADING")
return (
<div
className={cn(
@ -97,7 +96,7 @@ export const AreaRiservataLayout = ({
</main>
</div>
);
if (session.status !== "success" || !session.user) {
if (status !== "AUTHENTICATED") {
window.location.reload();
return (
<div
@ -122,6 +121,27 @@ export const AreaRiservataLayout = ({
);
}
}
if (status !== "AUTHENTICATED") {
window.location.reload();
return (
<div
className={cn(
"flex h-full min-h-screen flex-col text-clip",
bodyClassName,
)}
>
<SiteHeader />
<main
className={cn("flex h-full flex-1 overflow-auto", containerClassName)}
>
<div className="flex h-auto w-full flex-col md:flex-row">
<LoadingPage />
</div>
</main>
</div>
);
}
return (
<div
@ -143,10 +163,10 @@ export const AreaRiservataLayout = ({
{noSidebar ? null : (
<Sidebar
defaultOpen={getCookie(SIDEBAR_COOKIE_NAME) === "true"}
isAdmin={session.user?.isAdmin || false}
isAdmin={user?.isAdmin || false}
/>
)}
<EnforcedSessionContext.Provider value={session as ValidSession}>
<EnforcedSessionContext.Provider value={{ user, status }}>
{children}
</EnforcedSessionContext.Provider>
</div>
@ -160,12 +180,12 @@ export const AreaRiservataLayoutUserView = ({
}: {
children: ReactNode & { props: { userId: UsersId } };
}) => {
const session = useSession();
const { status, user } = useSession();
const props = children.props as { userId: UsersId };
if (session.status === "pending") {
if (status === "LOADING") {
return <LoadingPage />;
}
if (session.status === "error" || !session.user) {
if (status !== "AUTHENTICATED") {
window.location.reload();
return <LoadingPage />;
}
@ -174,7 +194,7 @@ export const AreaRiservataLayoutUserView = ({
<SiteHeader />
<main className="flex h-full flex-1 overflow-auto">
<UserViewProvider userId={props.userId}>
<EnforcedSessionContext.Provider value={session as ValidSession}>
<EnforcedSessionContext.Provider value={{ user, status }}>
<Head>
<title>User View - Infoalloggi.it</title>
</Head>

View file

@ -7,20 +7,16 @@ import { Button } from "~/components/ui/button";
import { useAnnuncio } from "~/providers/AnnuncioProvider";
import { useTranslation } from "~/providers/I18nProvider";
import { useServizio } from "~/providers/ServizioProvider";
import type { SessionContextType } from "~/providers/SessionProvider";
import { useSession } from "~/providers/SessionProvider";
import type { AnnunciId } from "~/schemas/public/Annunci";
import type { UsersId } from "~/schemas/public/Users";
import { api } from "~/utils/api";
export const AnnuncioInteractions = ({
session,
disabled,
}: {
session: SessionContextType;
disabled?: boolean;
}) => {
export const AnnuncioInteractions = ({ disabled }: { disabled?: boolean }) => {
const { user } = useSession();
const { id, tipo } = useAnnuncio();
const { locale } = useTranslation();
if (!user) return null;
if (disabled) {
return (
<div className="w-full rounded-md bg-red-500 p-2 text-center font-semibold text-base text-white">
@ -32,12 +28,12 @@ export const AnnuncioInteractions = ({
}
return (
<div className="flex w-full flex-col justify-around gap-3 md:gap-5">
{session.user ? (
{user ? (
tipo ? (
<ServizioInteraction
annuncioId={id}
tipologia={tipo}
userId={session.user.id}
userId={user.id}
/>
) : (
<span>{locale === "it" ? "Errore" : "Error"}</span>

View file

@ -1,4 +1,3 @@
"use client";
import { LogIn, LogOut } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
@ -35,7 +34,7 @@ export const UserHeaderSection = ({
inSidebar,
}: LoginButtonProps) => {
const router = useRouter();
const session = useSession();
const { status, user } = useSession();
const { t } = useTranslation();
const path = usePathname();
const utils = api.useUtils();
@ -51,7 +50,7 @@ export const UserHeaderSection = ({
},
});
if (session.status === "pending") {
if (status === "LOADING") {
return (
<div className="flex items-center px-3 py-2">
<LoadingSpinner className="size-5" />
@ -61,7 +60,7 @@ export const UserHeaderSection = ({
return (
<div className="flex items-center">
{!session || !session.user ? (
{status !== "AUTHENTICATED" ? (
<Button
aria-label="Login Button"
className="flex h-11.5 w-full items-center justify-center gap-x-1 rounded-lg p-2"
@ -89,25 +88,23 @@ export const UserHeaderSection = ({
>
<UserAvatar
className="size-10"
userId={session.user.id}
username={session.user.username}
userId={user.id}
username={user.username}
/>
{inSidebar ? (
<span>{session.user.username}</span>
<span>{user.username}</span>
) : (
<span className="navqry:block hidden">
{session.user.username}
</span>
<span className="navqry:block hidden">{user.username}</span>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56" forceMount>
{session.user.isAdmin ? (
{user.isAdmin ? (
<>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="font-medium text-sm leading-none">
{session.user.username}
{user.username}
</p>
<p className="text-muted-foreground text-xs leading-none">
Amministratore
@ -136,10 +133,10 @@ export const UserHeaderSection = ({
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="font-medium text-sm leading-none">
{session.user.username}
{user.username}
</p>
<p className="text-muted-foreground text-xs leading-none">
{session.user.email}
{user.email}
</p>
</div>
</DropdownMenuLabel>

View file

@ -121,14 +121,14 @@ const AnnuncioDettaglio: NextPage<AnnuncioProps> = ({
};
const AnnuncioView = ({ data, flag }: AnnuncioProps) => {
const session = useSession();
const { status, user } = useSession();
useStaleImageReload();
const isDesktop = useMediaQuery("(min-width: 768px)");
if (
!data ||
session.status === "error" ||
status !== "AUTHENTICATED" ||
(data && data.stato === "Sospeso")
) {
return <FailedAnnuncioLoading />;
@ -150,7 +150,7 @@ const AnnuncioView = ({ data, flag }: AnnuncioProps) => {
<div className="mb-6 flex flex-col md:mb-0 md:w-1/3">
<div className="flex w-full flex-col flex-nowrap items-center justify-center gap-6">
<CardInfos />
{session.user?.isAdmin ? (
{user?.isAdmin ? (
<>
<Link
className="w-full"
@ -175,10 +175,7 @@ const AnnuncioView = ({ data, flag }: AnnuncioProps) => {
</Link>
</>
) : (
<AnnuncioInteractions
disabled={flag === "true"}
session={session}
/>
<AnnuncioInteractions disabled={flag === "true"} />
)}
<ComeFunziona />
{data.tipo &&

View file

@ -9,6 +9,9 @@ export default createNextApiHandler({
createContext: createTRPCContext,
onError: ({ path, error }) => {
if (error.message.startsWith("SESSION_EXPIRED")) {
return;
}
if (env.NODE_ENV === "development") {
console.error(
`❌ tRPC failed on ${path ?? "<no-path>"}, msg: ${error.message}`,

View file

@ -15,7 +15,7 @@ import { api } from "~/utils/api";
* Pagina di gestione chat per admin: /area-riservata/admin/chats
*/
const AdminChats: NextPageWithLayout = () => {
const session = useSession();
const { status } = useSession();
const { data: activeChats, isLoading: isLoadingActiveChats } =
api.chat.getActiveChats.useQuery();
@ -23,14 +23,10 @@ const AdminChats: NextPageWithLayout = () => {
const { data: inactiveUsers, isLoading: isLoadingInactiveUsers } =
api.users.getActiveUsersWithoutChat.useQuery();
if (
session.status === "pending" ||
isLoadingActiveChats ||
isLoadingInactiveUsers
)
if (status === "LOADING" || isLoadingActiveChats || isLoadingInactiveUsers)
return <LoadingPage />;
if (session.status !== "success" || !session.user) {
if (status !== "AUTHENTICATED") {
window.location.reload();
return <LoadingPage />;
}

View file

@ -11,11 +11,11 @@ import { useSession } from "~/providers/SessionProvider";
*/
const AggiornaPasswordPage: NextPage = () => {
const { t } = useTranslation();
const session = useSession();
if (session.status === "pending") {
const { status } = useSession();
if (status === "LOADING") {
return <LoadingPage />;
}
if (session.status === "error" || !session.user) {
if (status !== "AUTHENTICATED") {
window.location.reload();
return <Status500 />;
}

View file

@ -6,17 +6,25 @@ import {
useContext,
useEffect,
} from "react";
import type { Session, ValidSession } from "~/server/api/trpc";
import { LoadingPage } from "~/components/loading";
import type { Session, SessionStatus } from "~/server/api/trpc";
import { api } from "~/utils/api";
type SessionStatus = "pending" | "success" | "error";
export type SessionContextType = {
user: Session | null;
status: SessionStatus;
export type ValidSession = {
user: Session;
status: Extract<SessionStatus, "AUTHENTICATED">;
};
type InvalidStatus = Exclude<SessionStatus, "AUTHENTICATED"> | "LOADING";
export type InvalidSession = {
status: InvalidStatus;
user: null;
};
export type SessionContextType = ValidSession | InvalidSession;
const SessionContext = createContext<SessionContextType>({
status: "pending",
status: "LOADING",
user: null,
});
@ -24,24 +32,20 @@ export const SessionProvider: FC<{ children: ReactNode }> = ({ children }) => {
const router = useRouter();
const utils = api.useUtils();
const {
data: user,
status,
isLoading,
isError,
error,
} = api.auth.getSession.useQuery(undefined, {
const { data, isLoading } = api.auth.getSession.useQuery(undefined, {
refetchInterval: 30000,
refetchOnMount: true,
refetchOnReconnect: true,
//refetchOnMount: true,
//refetchOnReconnect: true,
refetchOnWindowFocus: true,
staleTime: 0,
staleTime: Infinity,
retry: false,
meta: {
isSessionQuery: true,
},
});
if (isError && error) {
console.error("Session fetch error: ", error);
window.location.reload();
}
const session = data?.session ?? null;
const sessionStatus = data?.sessionStatus ?? "LOADING";
// Invalidate session on route change
useEffect(() => {
@ -58,17 +62,28 @@ export const SessionProvider: FC<{ children: ReactNode }> = ({ children }) => {
}, [router, utils]);
// Invalidate session if user is null
useEffect(() => {
if (!user) {
utils.auth.getSession.invalidate().catch((error: Error) => {
console.error("Error during session invalidation:", error);
});
}
}, [user]);
// useEffect(() => {
// if (!session && status === "success") {
// utils.auth.getSession.invalidate().catch((error: Error) => {
// console.error("Error during session invalidation:", error);
// });
// }
// }, [session, status]);
const sessionValue: SessionContextType =
session && sessionStatus === "AUTHENTICATED"
? {
user: session, // TypeScript knows user is non-null here
status: "AUTHENTICATED", // matches ValidSession
}
: {
user: null, // matches InvalidSession
status: sessionStatus as InvalidStatus, // TypeScript knows this is not "AUTHENTICATED"
};
return (
<SessionContext.Provider value={{ status, user: user || null }}>
{children}
<SessionContext.Provider value={sessionValue}>
{isLoading ? <LoadingPage /> : children}
</SessionContext.Provider>
);
};

View file

@ -33,8 +33,8 @@ export const apisProxy: ProxyFn = async (req: NextRequest) => {
return new NextResponse("Unauthorized", { status: 401 });
}
const payload = await verifyToken(accessToken);
if (!payload) {
const { expired, session } = await verifyToken(accessToken);
if (!session || expired) {
console.log("Invalid token, referrer:", req.referrer, " url:", req.url);
return new NextResponse("Unauthorized", { status: 401 });
}

View file

@ -50,8 +50,12 @@ export const authProxy: ProxyFn = async (req: NextRequest) => {
}
// If access token is present, check if it's valid
const accessTokenPayload = await verifyToken(accessToken);
if (!accessTokenPayload) {
const { expired, session } = await verifyToken(accessToken);
if (expired && refreshToken) {
// If access token is expired but refresh token is present, attempt to refresh
return await refreshAuthToken(req, refreshToken, false);
}
if (!session) {
// If access token is invalid, delete it and redirect to login
const destination = new URL("/login", req.nextUrl);
destination.searchParams.set("redirect", path);
@ -61,7 +65,7 @@ export const authProxy: ProxyFn = async (req: NextRequest) => {
}
if (path.startsWith("/area-riservata/admin")) {
if (!accessTokenPayload.isAdmin) {
if (!session.isAdmin) {
return NextResponse.redirect(
new URL("/area-riservata/dashboard", req.nextUrl),
);
@ -87,9 +91,12 @@ export const authProxy: ProxyFn = async (req: NextRequest) => {
}
// If no redirect present, redirect to dashboard
const payload = await verifyToken(accessToken);
if (payload) {
if (payload.isAdmin) {
const { expired, session } = await verifyToken(accessToken);
if (expired && refreshToken) {
return await refreshAuthToken(req, refreshToken, true);
}
if (session && !expired) {
if (session.isAdmin) {
return NextResponse.redirect(
new URL("/area-riservata/dashboard", req.nextUrl),
);
@ -118,8 +125,8 @@ export const authProxy: ProxyFn = async (req: NextRequest) => {
return await refreshAuthToken(req, refreshToken, false);
}
} else {
const payload = await verifyToken(accessToken);
if (!payload) {
const { expired, session } = await verifyToken(accessToken);
if (!session || expired) {
if (refreshToken) {
return await refreshAuthToken(req, refreshToken, false);
}
@ -138,9 +145,10 @@ const refreshAuthToken = async (
refreshToken: string,
onSuccessDashboard: boolean,
) => {
//console.log("Attempting to refresh access token")
// If refresh token is present, verify it
const refreshTokenPayload = await verifyToken(refreshToken);
if (!refreshTokenPayload) {
const { expired, session } = await verifyToken(refreshToken);
if (!session || expired) {
// If refresh token is invalid, delete it and redirect to login
const destination = new URL("/login", req.nextUrl);
destination.searchParams.set("redirect", req.nextUrl.pathname);
@ -156,7 +164,7 @@ const refreshAuthToken = async (
const response = NextResponse.redirect(new URL(destination, req.nextUrl));
const newAccessToken = await signToken(
refreshTokenPayload,
session,
TOKEN_CONFIG.ACCESS_TOKEN_EXPIRY,
"accessToken",
);

View file

@ -16,11 +16,11 @@ export const pswChangeProxy: ProxyFn = async (req: NextRequest) => {
return;
}
const accessTokenPayload = await verifyToken(accessToken);
if (!accessTokenPayload) {
const { expired, session } = await verifyToken(accessToken);
if (!session || expired) {
return;
}
if (accessTokenPayload.mustChangePassword) {
if (session.mustChangePassword) {
return NextResponse.redirect(
new URL("/auth/aggiorna-password", req.nextUrl),
);

View file

@ -73,7 +73,7 @@ export const authRouter = createTRPCRouter({
getSession: publicProcedure.query(async ({ ctx }) => {
if (ctx.session?.isBlocked) return null;
return ctx.session;
return { session: ctx.session, sessionStatus: ctx.sessionStatus };
}),
loginUser: publicProcedure
.input(
@ -90,7 +90,7 @@ export const authRouter = createTRPCRouter({
}),
logoutUser: protectedProcedure.mutation(async ({ ctx }) => {
await logOut({ ctx: ctx });
await logOut({ req: ctx.req, res: ctx.res });
return "logout success";
}),

View file

@ -9,7 +9,6 @@
import { initTRPC, TRPCError } from "@trpc/server";
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
import type { NextApiRequest, NextApiResponse } from "next";
import superjson from "superjson";
import z, { ZodError } from "zod/v4";
import { env } from "~/env";
@ -36,23 +35,40 @@ export const sessionSchema = z.object({
export type Session = z.infer<typeof sessionSchema>;
export type ValidSession = {
user: Session;
status: "success";
};
export type SessionStatus =
| "AUTHENTICATED"
| "EXPIRED"
| "INVALID"
| "NO_SESSION";
interface CreateInnerContextOptions extends Partial<CreateNextContextOptions> {
session: Session | null;
res?: NextApiResponse;
req?: NextApiRequest;
accessToken?: string;
}
export const createInnerTRPCContext = async ({
accessToken,
...opts
}: CreateInnerContextOptions) => {
let sessionData: Session | null = null;
let sessionStatus: SessionStatus = "NO_SESSION";
if (accessToken) {
const { expired, session } = await verifyToken(accessToken);
if (expired) {
sessionStatus = "EXPIRED";
} else if (session) {
sessionData = session;
sessionStatus = "AUTHENTICATED";
} else {
sessionStatus = "INVALID";
}
}
export function createInnerTRPCContext(opts: CreateInnerContextOptions) {
return {
...opts,
session: opts.session,
session: sessionData,
sessionStatus,
};
}
};
export const createTRPCContext = async ({
req,
@ -63,19 +79,11 @@ export const createTRPCContext = async ({
"You are missing `req` or `res` in your call to createTRPCContext.",
);
}
const accessToken = req.cookies[TOKEN_CONFIG.ACCESS_TOKEN_COOKIE_NAME];
// Get the session from the server using the getServerSession wrapper function
const session = accessToken ? await verifyToken(accessToken) : null;
const contextInner = createInnerTRPCContext({ session });
return {
...contextInner,
req,
res,
};
return await createInnerTRPCContext({ accessToken, req, res });
};
export type Context = Awaited<ReturnType<typeof createInnerTRPCContext>>;
export type Context = Awaited<ReturnType<typeof createTRPCContext>>;
export const addApiAuthToContext = (ctx: Context) => {
if (!ctx.req || !ctx.res) {
throw new Error(
@ -146,6 +154,12 @@ export const publicProcedure = t.procedure;
/** Reusable middleware that enforces users are logged in before running the procedure. */
const enforceUserIsAuthed = t.middleware(({ ctx, next, path }) => {
if (ctx.sessionStatus === "EXPIRED") {
throw new TRPCError({
code: "UNAUTHORIZED",
message: `SESSION_EXPIRED, error in path: ${path} [${ctx.req?.url}]`,
});
}
if (!ctx.session) {
throw new TRPCError({
code: "UNAUTHORIZED",
@ -202,6 +216,12 @@ export const overridableProcedure = t.procedure.use(
/** Reusable middleware that enforces users are logged in before running the procedure. */
const enforceUserIsAdmin = t.middleware(({ ctx, next, path }) => {
if (ctx.sessionStatus === "EXPIRED") {
throw new TRPCError({
code: "UNAUTHORIZED",
message: `SESSION_EXPIRED, error in path: ${path} [${ctx.req?.url}]`,
});
}
if (!ctx.session || !ctx.session.isAdmin) {
throw new TRPCError({
code: "UNAUTHORIZED",

View file

@ -2,9 +2,9 @@ export const TOKEN_CONFIG = {
ACCESS_TOKEN_COOKIE_NAME: "access_token",
REFRESH_TOKEN_COOKIE_NAME: "refresh_token",
OVERRIDE_TOKEN_COOKIE_NAME: "override_token",
ACCESS_TOKEN_EXPIRY: "1h",
ACCESS_TOKEN_EXPIRY: "2h",
REFRESH_TOKEN_EXPIRY: "7d",
ACCESS_TOKEN_COOKIE_EXPIRY: 1,
ACCESS_TOKEN_COOKIE_EXPIRY: 2,
REFRESH_TOKEN_COOKIE_EXPIRY: 7,
COOKIE_OPTIONS: {
httpOnly: true,

View file

@ -1,5 +1,6 @@
import { TRPCError } from "@trpc/server";
import { jwtVerify, SignJWT } from "jose";
import { JWTExpired } from "jose/errors";
import { env } from "~/env";
import { type Session, sessionSchema } from "~/server/api/trpc";
@ -24,17 +25,40 @@ export async function signToken(
}
}
export async function verifyToken(token: string): Promise<Session | null> {
type VerifySuccess = {
expired: false;
session: Session;
};
type VerifyExpired = {
expired: true;
session: null;
};
type VerifyFailure = {
expired: boolean;
session: null;
};
type VerifyResult = VerifySuccess | VerifyExpired | VerifyFailure;
export async function verifyToken(token: string): Promise<VerifyResult> {
try {
const { payload } = await jwtVerify(token, secret);
const { payload } = await jwtVerify(token, secret, {
requiredClaims: ["exp"],
});
const parse = sessionSchema.safeParse(payload.payload);
if (!parse.success) {
//console.error("JWT payload invalid:", parse.error);
return null;
return { expired: false, session: null };
}
return parse.data;
return { expired: false, session: parse.data };
} catch (e) {
if (e instanceof JWTExpired) {
return { expired: true, session: null };
}
console.error("JWT verification Internal Error:", (e as Error).message);
return null;
return { expired: false, session: null };
}
}

View file

@ -1,6 +1,7 @@
import { TRPCError } from "@trpc/server";
import { deleteCookie, setCookie } from "cookies-next";
import { add } from "date-fns";
import type { NextApiRequest, NextApiResponse } from "next";
import type { UsersId } from "~/schemas/public/Users";
import type { Context } from "~/server/api/trpc";
import { signToken } from "~/server/auth/jwt";
@ -226,7 +227,13 @@ export const swapAuth = async ({
}
};
export const logOut = async ({ ctx: { req, res } }: { ctx: Context }) => {
export const logOut = async ({
req,
res,
}: {
req?: NextApiRequest;
res?: NextApiResponse;
}) => {
checkCtx({ req, res });
try {
await deleteCookie(TOKEN_CONFIG.ACCESS_TOKEN_COOKIE_NAME, { req, res });

View file

@ -6,14 +6,14 @@ import { verifyToken } from "~/server/auth/jwt";
export const generateSSGHelper = () =>
createServerSideHelpers({
ctx: { session: null },
ctx: { session: null, sessionStatus: "NO_SESSION" },
router: appRouter,
transformer: superjson, // optional - adds superjson serialization
});
const generateHelperWSession = (session: Session) =>
createServerSideHelpers({
ctx: { session: session },
ctx: { session: session, sessionStatus: "AUTHENTICATED" },
router: appRouter,
transformer: superjson, // optional - adds superjson serialization
});
@ -24,9 +24,9 @@ export const TrpcAuthedFetchingIstance = async ({
access_token: string | undefined;
}) => {
if (access_token) {
const session = await verifyToken(access_token);
const { expired, session } = await verifyToken(access_token);
if (session) {
if (!expired && session) {
const trpc = generateHelperWSession(session);
return { session, trpc };

View file

@ -27,6 +27,11 @@ const errorLink: TRPCLink<AppRouter> = () => {
const unsubscribe = next(op).subscribe({
complete: observer.complete,
error(err) {
if (err.message.startsWith("SESSION_EXPIRED")) {
// Silently reload — the auth proxy will refresh the token
window.location.reload();
return;
}
if (
(err instanceof TRPCClientError &&
err.data?.code === "UNAUTHORIZED") ||
@ -34,6 +39,10 @@ const errorLink: TRPCLink<AppRouter> = () => {
err.data?.code === "CONFLICT" ||
err.data?.code === "NOT_FOUND"
) {
console.warn(
`Received ${err.data?.code} error from tRPC API, reloading the page to trigger auth flow or show error page.`,
err,
);
window.location.reload();
}
observer.error(err);
@ -54,7 +63,9 @@ export const api = createTRPCNext<AppRouter>({
enabled: (opts) =>
(process.env.NODE_ENV === "development" &&
typeof window !== "undefined") ||
(opts.direction === "down" && opts.result instanceof Error),
(opts.direction === "down" &&
opts.result instanceof Error &&
!opts.result.message.startsWith("SESSION_EXPIRED")),
}),
splitLink({