AURORA-1877: сессия обрывается через 60 минут — нет обновления access-токена #256

Open
aep wants to merge 1 commits from feat/248-aurora-1877-token-refresh into test
Owner

Closes #248.

Проблема

Access-токен (60 мин) не обновлялся: отсутствовал response-interceptor на 401, refresh-токен лежал в localStorage неиспользуемым, WebSocket-сокеты (форма, статусы выгрузки) не переподключались после закрытия с auth-кодом, а после редиректа на /login пользователь терял исходную страницу и возвращался на /tasks.

Что сделано

  • web/src/api/utils/tokenRefresh.js — чистые getTokenExp/msUntilRefresh/createSingleFlight, написаны через TDD (tokenRefresh.test.js).
  • web/src/api/auth.js — refreshTokens(): обновление через «голый» axios в обход интерсепторов api (иначе рекурсия на 401 от /auth/refresh); централизованно чистит токены и шлёт auth:expired при неудаче.
  • web/src/api/client.js — response-interceptor на 401 повторяет исходный запрос после обновления токена.
  • web/src/app/context/AuthProvider.jsx — при старте и просроченном access пробует refreshTokens() прежде чем разлогинивать; проактивный таймер обновления (exp − 2 мин), пересборка по visibilitychange; на auth:expired — toast «Сессия истекла, войдите снова».
  • web/src/app/Routes.jsx / LoginPage.jsx — PrivateRoute сохраняет location в state.from, после входа — возврат туда же вместо жёсткого /tasks; убрана мёртвая ветка loading (бралась из контекста, которого там не было).
  • web/src/components/RealtimeTable/hooks/useWebSocket.js и web/src/api/exports.js — при закрытии сокета с auth-кодом (1008 / 4401) одна попытка refreshTokens() и переподключение, без неё — как раньше.

Rulings (решения без обсуждения)

  1. Удалил неиспользуемый stub updateToken из token.js вместо реализации — вызывающих мест не было (проверено grep).
  2. Централизовал очистку токенов + auth:expired внутри refreshTokens(), а не дублировал в interceptor/AuthProvider/WebSocket/exports.js по отдельности.
  3. Разорвал потенциальный цикл client.js ↔ auth.js динамическим import('./auth') внутри response-interceptor.
  4. AuthProvider.jsx не покрыт модульными тестами — в проекте нет инфраструктуры для тестирования React-компонентов (ни jsdom, ни testing-library); добавлять её ради одного файла — вне объёма bounded-фикса. Проверено только вручную.
  5. Эффект авторедиректа на /login для уже залогиненных (isTestMode-ветка) не трогал — тикет описывает только потерю страницы после ручного входа.

Тесты

  • cd web && node --test src/components/RealtimeTable/utils/*.test.js src/api/utils/*.test.js — 14/14 зелёных.
  • npm run build — без ошибок.
  • Biome на изменённых файлах — новых ошибок не внесено (сверено построчно с версией в test).

Ручная проверка (acceptance criteria, не а��томатизировано)

  • При ACCESS_TOKEN_EXPIRE_MINUTES=2 на бэке: лист открыт 5 минут, переход на другой лист — без разлогина, в DevTools виден /auth/refresh и повтор исходного запроса.
  • Удалить refresh_token из localStorage, дождаться истечения access — переход на /login, после входа — возврат на тот же лист.
  • Дождаться истечения токена, перезапустить API — WebSocket переподключается сам.
  • Две вкладки одновременно — обе продолжают работать.

Не в объёме

AURORA-1878 (тип токена, expires_in) — отдельная задача.

🤖 Generated with Claude Code

Closes #248. ## Проблема Access-токен (60 мин) не обновлялся: отсутствовал response-interceptor на 401, refresh-токен лежал в localStorage неиспользуемым, WebSocket-сокеты (форма, статусы выгрузки) не переподключались после закрытия с auth-кодом, а после редиректа на /login пользователь терял исходную страницу и возвращался на /tasks. ## Что сделано - `web/src/api/utils/tokenRefresh.js` — чистые `getTokenExp`/`msUntilRefresh`/`createSingleFlight`, написаны через TDD (`tokenRefresh.test.js`). - `web/src/api/auth.js` — `refreshTokens()`: обновление через «голый» axios в обход интерсепторов `api` (иначе рекурсия на 401 от /auth/refresh); централизованно чистит токены и шлёт `auth:expired` при неудаче. - `web/src/api/client.js` — response-interceptor на 401 повторяет исходный запрос после обновления токена. - `web/src/app/context/AuthProvider.jsx` — при старте и просроченном access пробует `refreshTokens()` прежде чем разлогинивать; проактивный таймер обновления (exp − 2 мин), пересборка по `visibilitychange`; на `auth:expired` — toast «Сессия истекла, войдите снова». - `web/src/app/Routes.jsx` / `LoginPage.jsx` — `PrivateRoute` сохраняет `location` в `state.from`, после входа — возврат туда же вместо жёсткого `/tasks`; убрана мёртвая ветка `loading` (бралась из контекста, которого там не было). - `web/src/components/RealtimeTable/hooks/useWebSocket.js` и `web/src/api/exports.js` — при закрытии сокета с auth-кодом (1008 / 4401) одна попытка `refreshTokens()` и переподключение, без неё — как раньше. ## Rulings (решения без обсуждения) 1. Удалил неиспользуемый stub `updateToken` из `token.js` вместо реализации — вызывающих мест не было (проверено grep). 2. Централизовал очистку токенов + `auth:expired` внутри `refreshTokens()`, а не дублировал в interceptor/AuthProvider/WebSocket/exports.js по отдельности. 3. Разорвал потенциальный цикл `client.js` ↔ `auth.js` динамическим `import('./auth')` внутри response-interceptor. 4. `AuthProvider.jsx` не покрыт модульными тестами — в проекте нет инфраструктуры для тестирования React-компонентов (ни jsdom, ни testing-library); добавлять её ради одного файла — вне объёма bounded-фикса. Проверено только вручную. 5. Эффект авторедиректа на `/login` для уже залогиненных (`isTestMode`-ветка) не трогал — тикет описывает только потерю страницы после ручного входа. ## Тесты - `cd web && node --test src/components/RealtimeTable/utils/*.test.js src/api/utils/*.test.js` — 14/14 зелёных. - `npm run build` — без ошибок. - Biome на изменённых файлах — новых ошибок не внесено (сверено построчно с версией в `test`). ## Ручная проверка (acceptance criteria, не а��томатизировано) - [ ] При `ACCESS_TOKEN_EXPIRE_MINUTES=2` на бэке: лист открыт 5 минут, переход на другой лист — без разлогина, в DevTools виден `/auth/refresh` и повтор исходного запроса. - [ ] Удалить `refresh_token` из localStorage, дождаться истечения access — переход на `/login`, после входа — возврат на тот же лист. - [ ] Дождаться истечения токена, перезапустить API — WebSocket переподключается сам. - [ ] Две вкладки одновременно — обе продолжают работать. ## Не в объёме AURORA-1878 (тип токена, `expires_in`) — отдельная задача. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
aep added 1 commit 2026-10-30 16:42:59 +03:00
Access-токен жил 60 минут без обновления: фронт не слал response-interceptor
на 401, refresh-токен лежал в localStorage неиспользуемым, WebSocket-сокеты
(форма, статусы выгрузки) не переподключались после закрытия с auth-кодом,
а после редиректа на /login пользователь терял исходную страницу.

- tokenRefresh.js: чистые getTokenExp/msUntilRefresh/createSingleFlight (TDD).
- auth.js: refreshTokens — обновление токена вне интерсепторов api (без
  рекурсии), централизованно чистит токены и шлёт auth:expired при неудаче.
- client.js: response-interceptor повторяет запрос после обновления токена.
- AuthProvider: при старте и по 'auth:expired'/'auth:refreshed' не выкидывает
  из системы, пока жив refresh; проактивный таймер обновления + visibilitychange.
- Routes/LoginPage: PrivateRoute сохраняет исходный location, вход возвращает
  туда же вместо /tasks; убрана мёртвая ветка loading.
- useWebSocket/exports: переподключение после 1008/4401 с одной попыткой
  refreshTokens() перед тем как сдаться.

Issue: #248 (AURORA-1877, эпик AURORA-1876, Критичный приоритет)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This pull request can be merged automatically.
You are not authorized to merge this pull request.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/248-aurora-1877-token-refresh:feat/248-aurora-1877-token-refresh
git checkout feat/248-aurora-1877-token-refresh
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RSHB/DFiP_Budget_planing#256
No description provided.