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"); }, });