diff --git a/apps/infoalloggi/src/components/Layout.tsx b/apps/infoalloggi/src/components/Layout.tsx index ba8fc1e..0505412 100644 --- a/apps/infoalloggi/src/components/Layout.tsx +++ b/apps/infoalloggi/src/components/Layout.tsx @@ -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 (
); - if (session.status !== "success" || !session.user) { + if (status !== "AUTHENTICATED") { window.location.reload(); return (
+ + +
+
+ +
+
+
+ ); + } return (
)} - + {children}
@@ -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 ; } - if (session.status === "error" || !session.user) { + if (status !== "AUTHENTICATED") { window.location.reload(); return ; } @@ -174,7 +194,7 @@ export const AreaRiservataLayoutUserView = ({
- + User View - Infoalloggi.it diff --git a/apps/infoalloggi/src/components/annuncio-interactions/annuncio_interactions.tsx b/apps/infoalloggi/src/components/annuncio-interactions/annuncio_interactions.tsx index e95293d..f960c3a 100644 --- a/apps/infoalloggi/src/components/annuncio-interactions/annuncio_interactions.tsx +++ b/apps/infoalloggi/src/components/annuncio-interactions/annuncio_interactions.tsx @@ -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 (
@@ -32,12 +28,12 @@ export const AnnuncioInteractions = ({ } return (
- {session.user ? ( + {user ? ( tipo ? ( ) : ( {locale === "it" ? "Errore" : "Error"} diff --git a/apps/infoalloggi/src/components/navbar/login-button.tsx b/apps/infoalloggi/src/components/navbar/login-button.tsx index d8b45de..a140211 100644 --- a/apps/infoalloggi/src/components/navbar/login-button.tsx +++ b/apps/infoalloggi/src/components/navbar/login-button.tsx @@ -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 (
@@ -61,7 +60,7 @@ export const UserHeaderSection = ({ return (
- {!session || !session.user ? ( + {status !== "AUTHENTICATED" ? ( - {session.user.isAdmin ? ( + {user.isAdmin ? ( <>

- {session.user.username} + {user.username}

Amministratore @@ -136,10 +133,10 @@ export const UserHeaderSection = ({

- {session.user.username} + {user.username}

- {session.user.email} + {user.email}

diff --git a/apps/infoalloggi/src/pages/annuncio/[cod].tsx b/apps/infoalloggi/src/pages/annuncio/[cod].tsx index 2e7f196..d8e83f3 100644 --- a/apps/infoalloggi/src/pages/annuncio/[cod].tsx +++ b/apps/infoalloggi/src/pages/annuncio/[cod].tsx @@ -121,14 +121,14 @@ const AnnuncioDettaglio: NextPage = ({ }; 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 ; @@ -150,7 +150,7 @@ const AnnuncioView = ({ data, flag }: AnnuncioProps) => {
- {session.user?.isAdmin ? ( + {user?.isAdmin ? ( <> { ) : ( - + )} {data.tipo && diff --git a/apps/infoalloggi/src/pages/api/trpc/[trpc].ts b/apps/infoalloggi/src/pages/api/trpc/[trpc].ts index 2009e26..4522c56 100644 --- a/apps/infoalloggi/src/pages/api/trpc/[trpc].ts +++ b/apps/infoalloggi/src/pages/api/trpc/[trpc].ts @@ -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 ?? ""}, msg: ${error.message}`, diff --git a/apps/infoalloggi/src/pages/area-riservata/admin/chats.tsx b/apps/infoalloggi/src/pages/area-riservata/admin/chats.tsx index 6965372..7d3422c 100644 --- a/apps/infoalloggi/src/pages/area-riservata/admin/chats.tsx +++ b/apps/infoalloggi/src/pages/area-riservata/admin/chats.tsx @@ -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 ; - if (session.status !== "success" || !session.user) { + if (status !== "AUTHENTICATED") { window.location.reload(); return ; } diff --git a/apps/infoalloggi/src/pages/auth/aggiorna-password.tsx b/apps/infoalloggi/src/pages/auth/aggiorna-password.tsx index e2032ba..1d2abb8 100644 --- a/apps/infoalloggi/src/pages/auth/aggiorna-password.tsx +++ b/apps/infoalloggi/src/pages/auth/aggiorna-password.tsx @@ -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 ; } - if (session.status === "error" || !session.user) { + if (status !== "AUTHENTICATED") { window.location.reload(); return ; } diff --git a/apps/infoalloggi/src/providers/SessionProvider.tsx b/apps/infoalloggi/src/providers/SessionProvider.tsx index b3681e2..5dc6114 100644 --- a/apps/infoalloggi/src/providers/SessionProvider.tsx +++ b/apps/infoalloggi/src/providers/SessionProvider.tsx @@ -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; }; +type InvalidStatus = Exclude | "LOADING"; +export type InvalidSession = { + status: InvalidStatus; + user: null; +}; + +export type SessionContextType = ValidSession | InvalidSession; + const SessionContext = createContext({ - 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 ( - - {children} + + {isLoading ? : children} ); }; diff --git a/apps/infoalloggi/src/proxies/api.ts b/apps/infoalloggi/src/proxies/api.ts index c8bf3f8..d965bbb 100644 --- a/apps/infoalloggi/src/proxies/api.ts +++ b/apps/infoalloggi/src/proxies/api.ts @@ -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 }); } diff --git a/apps/infoalloggi/src/proxies/auth.ts b/apps/infoalloggi/src/proxies/auth.ts index d295fdb..2787008 100644 --- a/apps/infoalloggi/src/proxies/auth.ts +++ b/apps/infoalloggi/src/proxies/auth.ts @@ -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", ); diff --git a/apps/infoalloggi/src/proxies/psw_change.ts b/apps/infoalloggi/src/proxies/psw_change.ts index 15668d9..20f02e3 100644 --- a/apps/infoalloggi/src/proxies/psw_change.ts +++ b/apps/infoalloggi/src/proxies/psw_change.ts @@ -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), ); diff --git a/apps/infoalloggi/src/server/api/routers/auth.ts b/apps/infoalloggi/src/server/api/routers/auth.ts index cfc8938..b961874 100644 --- a/apps/infoalloggi/src/server/api/routers/auth.ts +++ b/apps/infoalloggi/src/server/api/routers/auth.ts @@ -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"; }), diff --git a/apps/infoalloggi/src/server/api/trpc.ts b/apps/infoalloggi/src/server/api/trpc.ts index a11d0aa..68c2e2b 100644 --- a/apps/infoalloggi/src/server/api/trpc.ts +++ b/apps/infoalloggi/src/server/api/trpc.ts @@ -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; -export type ValidSession = { - user: Session; - status: "success"; -}; +export type SessionStatus = + | "AUTHENTICATED" + | "EXPIRED" + | "INVALID" + | "NO_SESSION"; interface CreateInnerContextOptions extends Partial { - 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>; +export type Context = Awaited>; 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", diff --git a/apps/infoalloggi/src/server/auth/configs.ts b/apps/infoalloggi/src/server/auth/configs.ts index 2b3a199..ee795fc 100644 --- a/apps/infoalloggi/src/server/auth/configs.ts +++ b/apps/infoalloggi/src/server/auth/configs.ts @@ -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, diff --git a/apps/infoalloggi/src/server/auth/jwt.ts b/apps/infoalloggi/src/server/auth/jwt.ts index 2ccaeb8..513ba99 100644 --- a/apps/infoalloggi/src/server/auth/jwt.ts +++ b/apps/infoalloggi/src/server/auth/jwt.ts @@ -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 { +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 { 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 }; } } diff --git a/apps/infoalloggi/src/server/controllers/auth.controller.ts b/apps/infoalloggi/src/server/controllers/auth.controller.ts index bfa53ea..1405379 100644 --- a/apps/infoalloggi/src/server/controllers/auth.controller.ts +++ b/apps/infoalloggi/src/server/controllers/auth.controller.ts @@ -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 }); diff --git a/apps/infoalloggi/src/server/utils/ssgHelper.ts b/apps/infoalloggi/src/server/utils/ssgHelper.ts index 94c2c3f..747fdb1 100644 --- a/apps/infoalloggi/src/server/utils/ssgHelper.ts +++ b/apps/infoalloggi/src/server/utils/ssgHelper.ts @@ -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 }; diff --git a/apps/infoalloggi/src/utils/api.ts b/apps/infoalloggi/src/utils/api.ts index ed8ca93..7684425 100644 --- a/apps/infoalloggi/src/utils/api.ts +++ b/apps/infoalloggi/src/utils/api.ts @@ -27,6 +27,11 @@ const errorLink: TRPCLink = () => { 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 = () => { 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({ 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({