- {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({