From ebde02d1b6b5ceba694957abd982b21fb0bcf1a9 Mon Sep 17 00:00:00 2001 From: Ricardo Amorim <102877738+risixdzn@users.noreply.github.com> Date: Fri, 18 Sep 2026 12:15:24 -0300 Subject: [PATCH 1/2] fix(Auth): Revalidate expired session token before crashing/logging out getSession() threw an unhandled ZodError when the session cookie was missing/expired, because the middleware refreshed the JWT but never forwarded the new cookie to the current request, so downstream Server Components still parsed the stale one. Forward the revalidated cookies into the request in middleware, make getSession() fail gracefully to null instead of throwing, and redirect to login in the dashboard layout only if revalidation truly failed. Fixes FIXR-49 Co-Authored-By: Claude Sonnet 5 --- .../dashboard/[subdomain]/layout.tsx | 5 +++++ apps/web/lib/auth/utils.ts | 18 ++++++++++-------- apps/web/middleware.ts | 18 +++++++++++++++++- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/apps/web/app/(protected)/dashboard/[subdomain]/layout.tsx b/apps/web/app/(protected)/dashboard/[subdomain]/layout.tsx index 92870492..ba1333fc 100644 --- a/apps/web/app/(protected)/dashboard/[subdomain]/layout.tsx +++ b/apps/web/app/(protected)/dashboard/[subdomain]/layout.tsx @@ -5,6 +5,7 @@ import { SessionProvider } from "@/lib/hooks/use-session"; import QueryClientWrapper from "@/lib/query-client"; import "../../../globals.css"; import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; import { Header } from "@/components/dashboard/sidebar/header"; import { ThemeProvider } from "@/components/theme-provider"; import { ThemedToaster } from "@/components/themed-toaster"; @@ -35,6 +36,10 @@ export default async function RootLayout({ const cookieStore = await cookies(); const session = getSession(cookieStore); + if (!session) { + redirect("/auth/login"); + } + return ( | null { + const jwt = cookies + ? parseJwt(cookies.get(cookieKey("session"))?.value) + : parseJwt(parseCookies()[cookieKey("session")]); - const cookieStore = parseCookies(); - const jwt = parseJwt(cookieStore[cookieKey("session")]); // Corrected access using cookieKey - return userJWT.parse(jwt); + // Missing/expired sessions are expected (the middleware revalidates the token + // before this runs); fail gracefully to null instead of throwing a ZodError. + const result = userJWT.safeParse(jwt); + return result.success ? result.data : null; } export const getClientSession = (): ReturnType | null => { diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index faa26483..1890dcf8 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -178,7 +178,23 @@ async function revalidate(request: NextRequest, isProtectedRoute: boolean) { if (revalidateResponse.ok) { const setCookies = revalidateResponse.headers.getSetCookie(); if (setCookies.length > 0) { - const res = NextResponse.next(); + /** + * Apply the refreshed cookies to the current request as well, so Server + * Components rendered right after this middleware (e.g. getSession) see + * the new session instead of the expired one that triggered the revalidation. + */ + for (const cookie of setCookies) { + const nameValue = cookie.split(";")[0] ?? ""; + const separatorIndex = nameValue.indexOf("="); + if (separatorIndex === -1) { + continue; + } + const name = nameValue.slice(0, separatorIndex).trim(); + const value = nameValue.slice(separatorIndex + 1).trim(); + request.cookies.set(name, value); + } + + const res = NextResponse.next({ request }); for (const cookie of setCookies) { res.headers.append("Set-Cookie", cookie); } From a159a4192e5f0bf869ab78a128f6fcddb8aef016 Mon Sep 17 00:00:00 2001 From: Ricardo Amorim <102877738+risixdzn@users.noreply.github.com> Date: Fri, 18 Sep 2026 12:15:48 -0300 Subject: [PATCH 2/2] fix(Account): Resolve unmerged conflict markers in account repository apps/server/src/modules/account/repositories/index.ts had unresolved <<<<<<< HEAD / ======= / >>>>>>> markers left over from an earlier merge, silently committed to develop. Resolved onto the Cached/ InvalidateCache infra (the current caching approach used by every other repository), migrating updateAvatarUrl off the now-deleted core/lib/cache helpers. Co-Authored-By: Claude Sonnet 5 --- .../src/modules/account/repositories/index.ts | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/apps/server/src/modules/account/repositories/index.ts b/apps/server/src/modules/account/repositories/index.ts index 732fb0d5..c2e34847 100644 --- a/apps/server/src/modules/account/repositories/index.ts +++ b/apps/server/src/modules/account/repositories/index.ts @@ -1,16 +1,7 @@ import { db, eq, sql } from "@fixr/db/connection"; import { clients, companies, employees, users } from "@fixr/db/schema"; import { accountSchema } from "@fixr/schemas/account"; -<<<<<<< HEAD -import { Cached } from "../../../shared/infra/cache"; -======= -import { redis } from "../../../config/redis"; -import { - accountCacheKey, - CACHE_TTL, - jwtPayloadCacheKey, -} from "../../../core/lib/cache"; ->>>>>>> 990eba6 (Feat(Server): Add avatar upload presign, update, and remove endpoints) +import { Cached, InvalidateCache } from "../../../shared/infra/cache"; /** @description Account data access layer */ export class AccountRepository { @@ -62,11 +53,8 @@ export class AccountRepository { * @param userId - The user ID * @param avatarUrl - The new avatar URL */ + @InvalidateCache({ patterns: ["account:*", "jwt:*"] }) static async updateAvatarUrl(userId: string, avatarUrl: string | null) { await db.update(users).set({ avatarUrl }).where(eq(users.id, userId)); - - const cacheKey = accountCacheKey(userId); - const jwtCacheKey = jwtPayloadCacheKey(userId); - await Promise.all([redis.del(cacheKey), redis.del(jwtCacheKey)]); } }