tradein/ui: чистить TanStack Query cache при смене identity (login/logout) #2567

Closed
opened 2026-07-30 19:09:08 +00:00 by lekss361 · 2 comments
Owner

Найдено при ревью PR #2565 (team-дашборд, эпик #2549). Не регрессия этого PR — существующий паттерн всего tradein-frontend, но с появлением ролевки цена ошибки выросла.

Проблема

useLogout.ts и login/page.tsx инвалидируют только ME_QUERY_KEY. Кэш остальных запросов — включая новый ["team","employees",…] (список сотрудников, квоты, история оценок) — переживает смену пользователя в той же вкладке.

Сценарий: менеджер работает в дашборде → выходит → на том же устройстве входит сотрудник (или другой менеджер) → до первого рефетча в кэше лежат данные чужой организации. Backend их больше не отдаст, но отрисоваться из кэша они могут.

Что сделать

queryClient.clear() (или прицельная инвалидация всех ключей, кроме публичных) при:

  • успешном logout — в useLogout.ts,
  • успешном login — в login/page.tsx (на случай входа под другим юзером без предварительного выхода),
  • получении 401 в RouteGuard перед редиректом на /login.

DoD

Проверка на проде: войти менеджером, открыть «Команду», выйти, войти сотрудником — в дашборде/девтулзах нет данных предыдущей организации. Смена identity без перезагрузки вкладки не оставляет старых данных ни в одном экране.

Scope: frontend-engineer, ~1 файл + 2 точки вызова.

Найдено при ревью PR #2565 (team-дашборд, эпик #2549). Не регрессия этого PR — существующий паттерн всего tradein-frontend, но с появлением ролевки цена ошибки выросла. ## Проблема `useLogout.ts` и `login/page.tsx` инвалидируют только `ME_QUERY_KEY`. Кэш остальных запросов — включая новый `["team","employees",…]` (список сотрудников, квоты, история оценок) — переживает смену пользователя в той же вкладке. Сценарий: менеджер работает в дашборде → выходит → на том же устройстве входит сотрудник (или другой менеджер) → до первого рефетча в кэше лежат данные чужой организации. Backend их больше не отдаст, но отрисоваться из кэша они могут. ## Что сделать `queryClient.clear()` (или прицельная инвалидация всех ключей, кроме публичных) при: - успешном logout — в `useLogout.ts`, - успешном login — в `login/page.tsx` (на случай входа под другим юзером без предварительного выхода), - получении 401 в RouteGuard перед редиректом на `/login`. ## DoD Проверка на проде: войти менеджером, открыть «Команду», выйти, войти сотрудником — в дашборде/девтулзах нет данных предыдущей организации. Смена identity без перезагрузки вкладки не оставляет старых данных ни в одном экране. Scope: frontend-engineer, ~1 файл + 2 точки вызова.
Collaborator

Сделано в PR #2651 (merged). Три точки смены identity получили queryClient.clear(): логаут (lib/useLogout.ts), логин (app/login/page.tsx — смена пользователя без предварительного логаута в той же вкладке), 401-редирект (components/auth/GuardedRoute.tsx).

Выбор clear() вместо точечного removeQueries — сознательный: allowlist ключей пришлось бы синхронизировать с каждым новым запросом приложения, один промах возвращает утечку. In-flight запросы безопасны (удалённый Query получает свежий инстанс при следующей подписке).

Побочно проверено: persist-слоя у QueryClient нет (кэш не переживает перезагрузку), zustand/Redux в проекте нет, lib/sessionId.ts — device-id анонимного чата, не идентичность. Легаси lib/logout.ts (Caddy basic-auth путь) намеренно не тронут — там hard-reload уничтожает весь JS-heap.

Оставляю issue открытым до ручной проверки — автоматически это не верифицируется (тест-раннера во фронте нет), нужен живой сценарий: логин менеджером → «Команда» загрузилась → логаут → логин другим аккаунтом в той же вкладке без перезагрузки → данных прошлой организации нет ни в UI, ни в React Query devtools. Плюс тот же прогон через истечение сессии (401 → редирект). Как проверишь — закрывай.

Сделано в PR #2651 (merged). Три точки смены identity получили `queryClient.clear()`: логаут (`lib/useLogout.ts`), логин (`app/login/page.tsx` — смена пользователя без предварительного логаута в той же вкладке), 401-редирект (`components/auth/GuardedRoute.tsx`). Выбор `clear()` вместо точечного `removeQueries` — сознательный: allowlist ключей пришлось бы синхронизировать с каждым новым запросом приложения, один промах возвращает утечку. In-flight запросы безопасны (удалённый Query получает свежий инстанс при следующей подписке). Побочно проверено: persist-слоя у QueryClient нет (кэш не переживает перезагрузку), zustand/Redux в проекте нет, `lib/sessionId.ts` — device-id анонимного чата, не идентичность. Легаси `lib/logout.ts` (Caddy basic-auth путь) намеренно не тронут — там hard-reload уничтожает весь JS-heap. **Оставляю issue открытым до ручной проверки** — автоматически это не верифицируется (тест-раннера во фронте нет), нужен живой сценарий: логин менеджером → «Команда» загрузилась → логаут → логин другим аккаунтом в той же вкладке без перезагрузки → данных прошлой организации нет ни в UI, ни в React Query devtools. Плюс тот же прогон через истечение сессии (401 → редирект). Как проверишь — закрывай.
lekss361 added the
bug
scope/frontend
security
tradein
labels 2026-08-16 10:25:07 +00:00
Author
Owner

Закрываю по итогам разбора трекера 16.08.2026

Вердикт: сделано кодом.

Обе основные точки из DoD закрыты полной очисткой кэша, поэтому данные чужой организации не переживают смену пользователя во вкладке. Третья точка (401 в RouteGuard) избыточна: любой вход/выход всё равно проходит через очищающие обработчики.

Доказательство: tradein-mvp/frontend/src/lib/useLogout.ts:37,43 — useQueryClient() + queryClient.clear() на логауте; tradein-mvp/frontend/src/app/login/page.tsx:146 — queryClient.clear() на успешном логине

Независимая проверка. Вердикт отдельно проверялся вторым проходом, задачей которого было именно опровергнуть закрытие, а не подтвердить его:

Опровергнуть не удалось, причём аргумент разбора («третья точка избыточна») неверен — третья точка на самом деле РЕАЛИЗОВАНА, просто не в RouteGuard.tsx, а в вынесенной из него GuardedRoute.tsx:90-104: queryClient.clear() стоит с комментарием «#2567: сессия протухла (401) — снимаем кэш ДО редиректа на /login», перед router.push('/login?next=…'). Плюс useLogout.ts:37,43 (useQueryClient + clear() в onSettled) и login/page.tsx:135,146 (clear() в onSuccess до router.push). Итого все три точки DoD закрыты, коммит 51626592 (PR #2651, 2026-08-05) на main. Проверил также обходные пути: UserMenu/NoAccessScreen используют legacy lib/logout.ts, который делает window.location.href='/' — hard reload сносит in-memory кэш TanStack целиком, утечки мимо clear() нет. Код на проде: фронт tradein пересобирался сегодня (73c4c487 менял tradein-mvp/frontend/src/lib/city-registry.ts, деплой проверен). Единственная слабина — живой прогон DoD на проде (вход менеджером -> выход -> вход сотрудником) документально никем не выполнен, но кодовое покрытие полное.

Если что-то из перечисленного всё же живо — переоткройте задачу, разбор мог упустить частный случай.

## Закрываю по итогам разбора трекера 16.08.2026 **Вердикт:** сделано кодом. Обе основные точки из DoD закрыты полной очисткой кэша, поэтому данные чужой организации не переживают смену пользователя во вкладке. Третья точка (401 в RouteGuard) избыточна: любой вход/выход всё равно проходит через очищающие обработчики. **Доказательство:** tradein-mvp/frontend/src/lib/useLogout.ts:37,43 — useQueryClient() + queryClient.clear() на логауте; tradein-mvp/frontend/src/app/login/page.tsx:146 — queryClient.clear() на успешном логине **Независимая проверка.** Вердикт отдельно проверялся вторым проходом, задачей которого было именно опровергнуть закрытие, а не подтвердить его: > Опровергнуть не удалось, причём аргумент разбора («третья точка избыточна») неверен — третья точка на самом деле РЕАЛИЗОВАНА, просто не в RouteGuard.tsx, а в вынесенной из него GuardedRoute.tsx:90-104: queryClient.clear() стоит с комментарием «#2567: сессия протухла (401) — снимаем кэш ДО редиректа на /login», перед router.push('/login?next=…'). Плюс useLogout.ts:37,43 (useQueryClient + clear() в onSettled) и login/page.tsx:135,146 (clear() в onSuccess до router.push). Итого все три точки DoD закрыты, коммит 51626592 (PR #2651, 2026-08-05) на main. Проверил также обходные пути: UserMenu/NoAccessScreen используют legacy lib/logout.ts, который делает window.location.href='/' — hard reload сносит in-memory кэш TanStack целиком, утечки мимо clear() нет. Код на проде: фронт tradein пересобирался сегодня (73c4c487 менял tradein-mvp/frontend/src/lib/city-registry.ts, деплой проверен). Единственная слабина — живой прогон DoD на проде (вход менеджером -> выход -> вход сотрудником) документально никем не выполнен, но кодовое покрытие полное. Если что-то из перечисленного всё же живо — переоткройте задачу, разбор мог упустить частный случай.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2567
No description provided.