diff --git a/apps/server/src/modules/account/repositories/index.ts b/apps/server/src/modules/account/repositories/index.ts index 732fb0d..c2e3484 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)]); } } diff --git a/apps/web/app/(protected)/dashboard/[subdomain]/layout.tsx b/apps/web/app/(protected)/dashboard/[subdomain]/layout.tsx index 9287049..ba1333f 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 faa2648..1890dcf 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); }