From 482b0be1fa449f01c64f357383e3349f21fb8562 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Wed, 5 Aug 2026 15:03:40 +0500 Subject: [PATCH] =?UTF-8?q?fix(tradein/ui):=20=D1=87=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20TanStack=20Query=20cache=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=20=D1=81=D0=BC=D0=B5=D0=BD=D0=B5=20identity=20(#2567)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Логин, логаут и 401-редирект инвалидировали только ME_QUERY_KEY — данные предыдущего пользователя (team/employees, история оценок, квоты) оставались в кэше и могли отрендериться следующему в той же SPA-сессии до рефетча. queryClient.clear() во всех трёх точках смены identity. clear() вместо точечного removeQueries: allowlist ключей пришлось бы синхронизировать с каждым новым запросом приложения, один промах — снова утечка. In-flight запросы безопасны: удалённый Query получает новый инстанс при следующей подписке, поздний ответ старого пользователя пишет в осиротевший объект. Легаси basic-auth logout (hard reload) не тронут — там весь JS-heap и так уничтожается. Persist-слоя у QueryClient нет. Refs #2567 --- tradein-mvp/frontend/src/app/login/page.tsx | 7 +++++-- .../frontend/src/components/auth/GuardedRoute.tsx | 11 ++++++++++- tradein-mvp/frontend/src/lib/useLogout.ts | 11 +++++++++-- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/tradein-mvp/frontend/src/app/login/page.tsx b/tradein-mvp/frontend/src/app/login/page.tsx index ad74d876..3f90c98e 100644 --- a/tradein-mvp/frontend/src/app/login/page.tsx +++ b/tradein-mvp/frontend/src/app/login/page.tsx @@ -20,7 +20,6 @@ import { useRouter } from "next/navigation"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiFetch, HTTPError } from "@/lib/api"; -import { ME_QUERY_KEY } from "@/lib/useMe"; import { AnonSupportWidget } from "@/components/auth/AnonSupportWidget"; interface LoginInput { @@ -140,7 +139,11 @@ export default function LoginPage() { const loginMutation = useMutation({ mutationFn: loginRequest, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ME_QUERY_KEY }); + // #2567: вход под другим юзером без предварительного logout (тот же + // девайс/вкладка) — снимаем ВЕСЬ кэш, не только /me, иначе + // team/employees, история оценок и т.п. предыдущего юзера переживают + // login и могут отрисоваться до первого рефетча. + queryClient.clear(); router.push(sanitizeNext(readNextParam())); }, }); diff --git a/tradein-mvp/frontend/src/components/auth/GuardedRoute.tsx b/tradein-mvp/frontend/src/components/auth/GuardedRoute.tsx index 937e0bbc..7d97a9f7 100644 --- a/tradein-mvp/frontend/src/components/auth/GuardedRoute.tsx +++ b/tradein-mvp/frontend/src/components/auth/GuardedRoute.tsx @@ -42,6 +42,7 @@ * задвоил бы префикс в `/trade-in/trade-in/history`. */ +import { useQueryClient } from "@tanstack/react-query"; import { useRouter } from "next/navigation"; import { useEffect } from "react"; @@ -62,6 +63,7 @@ export default function GuardedRoute({ children: React.ReactNode; }) { const router = useRouter(); + const queryClient = useQueryClient(); // Абсолютный путь сайта: BASE_PATH + rawPath. Аккуратно с двойным слэшем // на `/`: `BASE_PATH = "/trade-in"` + `"/"` → `/trade-in/` (ок). const absolutePath = BASE_PATH @@ -85,6 +87,13 @@ export default function GuardedRoute({ useEffect(() => { if (!shouldRedirectToLogin) return; + // #2567: сессия протухла (401) — снимаем кэш ДО редиректа на /login. Тот + // же самый девайс/вкладка может через секунду залогиниться другим + // юзером; без clear() здесь team/employees, история оценок и т.п. + // предыдущего юзера дожидались бы login-точки (её `clear()` тоже + // сработает, но лучше не полагаться на единственную точку сброса — + // 401 — самостоятельный путь смены identity, не только logout/login). + queryClient.clear(); // PR #2562 review finding 3: deep-links carry их state в query (`/v2?id= // ` — см. next.config.ts redirect comment про restore-by-id). Без // `window.location.search` юзер, чья сессия истекла mid-session на такой @@ -93,7 +102,7 @@ export default function GuardedRoute({ // поэтому `window` тут безопасен без typeof-guard. const next = `${rawPath}${window.location.search}`; router.push(`/login?next=${encodeURIComponent(next)}`); - }, [shouldRedirectToLogin, rawPath, router]); + }, [shouldRedirectToLogin, rawPath, router, queryClient]); // #801: preview-страница самодостаточна (свой QueryClient с фейковым me), // RBAC к ней не применяем. Только под флагом — в проде по умолчанию выключено. diff --git a/tradein-mvp/frontend/src/lib/useLogout.ts b/tradein-mvp/frontend/src/lib/useLogout.ts index 93de98c4..d5d03d3e 100644 --- a/tradein-mvp/frontend/src/lib/useLogout.ts +++ b/tradein-mvp/frontend/src/lib/useLogout.ts @@ -14,13 +14,20 @@ * локальный logout (кэш + редирект) выполняется в `onSettled`, а не только * `onSuccess`: сетевой сбой / уже-протухшая сессия не должны запирать юзера * на странице без возможности разлогиниться. + * + * #2567: `clear()`, а не точечный invalidate ME_QUERY_KEY — иначе кэш + * остального (team/employees, история оценок, квоты) переживает logout и + * может отрисоваться следующему юзеру той же вкладки до первого рефетча. + * `clear()` безопасен для in-flight запросов: TanStack Query создаёт для + * снятого из кэша ключа новый `Query`-инстанс при следующей подписке, так + * что поздний resolve старого fetch пишет в осиротевший объект, а не в + * видимый кэш. */ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useRouter } from "next/navigation"; import { apiFetch } from "@/lib/api"; -import { ME_QUERY_KEY } from "@/lib/useMe"; async function logoutRequest(): Promise { await apiFetch<{ ok: boolean }>("/api/v1/auth/logout", { method: "POST" }); @@ -33,7 +40,7 @@ export function useLogout() { return useMutation({ mutationFn: logoutRequest, onSettled: () => { - queryClient.invalidateQueries({ queryKey: ME_QUERY_KEY }); + queryClient.clear(); router.push("/login"); }, }); -- 2.45.3