Загрузка данных


# Переход на страницу входа сразу после окончания сессии

## Постановка

Пользователь вошёл в админку (`js/apps/admin-ui`) или в личный кабинет (`js/apps/account-ui`) и отошёл от экрана. Вкладка остаётся открытой, запросов к API нет.

Сейчас сессия на сервере становится недействительной по таймауту простоя (по умолчанию 30 минут) или по максимальному сроку (по умолчанию 10 часов), но страница об этом не узнаёт. Переход на страницу входа происходит только при следующем действии: перед запросом вызывается `keycloak.updateToken()`, обмен refresh-токена не проходит, и интерфейс вызывает `keycloak.login()`.

Нужно, чтобы в обеих консолях в момент окончания пользовательской SSO-сессии открытая вкладка сама переходила на страницу входа, без клика и без ожидания следующего запроса.

Срок по-прежнему задаётся настройками области (вкладка «Сессии»), включая «Запомнить меня» и отдельные таймауты клиентов `security-admin-console` и `account`. Интерфейс не должен считать 30 минут у себя.

## Решение на SSE

Сервер держит с открытой вкладкой поток `text/event-stream`. Пока сессия действительна, в потоке только служебные комментарии, чтобы промежуточные прокси не рвали соединение. Как только сессия перестаёт быть действительной, сервер присылает одно событие и закрывает поток. Клиент по этому событию вызывает `keycloak.login()`.

Поток не продлевает сессию. Обработчик только читает её и не обновляет время последнего обновления. Иначе открытая вкладка сама по себе отодвигала бы таймаут простоя, а задача в том, чтобы заметить его окончание.

Access-токен живёт около 5 минут и кончается раньше сессии. Поток на это не завязан. Пока он открыт, клиент не вызывает `keycloak.updateToken()`: такой вызов как раз обменял бы refresh-токен и начал бы отсчёт простоя заново. Авторизация потока выполняется один раз в начале, по текущему access-токену. Дальше сервер помнит идентификатор пользовательской сессии из токена и проверяет уже её.

Объект `EventSource` в браузере не умеет заголовок `Authorization`, а обе консоли ходят с bearer-токеном. Поэтому поток открывается через `fetch` и читается как текст SSE. Формат тот же: строки `event:` и `data:`.

### Сервер

Один метод на область, общий для админки и личного кабинета. Каждая консоль вызывает его со своим access-токеном.

При подключении сервер проверяет токен, находит пользовательскую сессию и больше её не продлевает. Дальше в цикле:

1. Перечитывает сессию из хранилища. Так учитывается другая вкладка: если там был обмен refresh-токена, срок простоя сдвинулся.
2. Проверяет сессию той же функцией, что и обмен refresh-токена (`AuthenticationManager.isSessionValid`). В ней уже сидят простой, максимальный срок и «Запомнить меня».
3. Если сессия действительна, ждёт до ближайшего из двух моментов: её расчётного окончания или короткого интервала heartbeat (порядка 15–20 секунд). В поток уходит комментарий `: ping`.
4. Если сессия недействительна, в поток уходит событие и соединение закрывается.

```text
event: session-expired
data: {"reason":"idle"}
```

`reason` — `idle` или `max`, по тому пределу, который сработал. Клиенту для перехода на вход достаточно самого события.

Если соединение оборвалось раньше окончания сессии, клиенту нужен способ открыть поток снова, не вызывая `keycloak.updateToken()`. При первом подключении сервер выдаёт билет на прослушивание этой сессии. Билет не является refresh-токеном и не сдвигает простой. Повторное подключение идёт с этим билетом. Когда сессия уже мертва, сервер сразу отвечает событием `session-expired`.

### Клиент

И в `admin-ui`, и в `account-ui` после входа открывается один поток на вкладку. Опрос по таймеру не добавляется.

- Событие `session-expired` — вызвать `keycloak.login()`, как сейчас делается в `catch` после неудачного `keycloak.updateToken()`.
- Обрыв сети при живой сессии — открыть поток снова по билету, без `keycloak.updateToken()`.
- Обычные запросы к API не меняются. Пока человек работает, `keycloak.updateToken()` по-прежнему продлевает сессию, а ждущий поток видит новый срок и молчит.

Две открытые консоли одного пользователя — два потока на одну SSO-сессию. Событие придёт в обе, и обе уйдут на страницу входа. Если в одной консоли человек продолжает работать, обмен refresh-токена сдвигает простой, и вторая консоль событие не получает.

## Что сознательно не делается

- Клиентский таймер на 30 минут. Он разойдётся с настройками области, с «Запомнить меня» и с фактической проверкой на сервере.
- Вызов `keycloak.updateToken()` ради поддержания потока. Он продлевает сессию.
- Переход на вход в момент истечения access-токена. Через 5 минут простоя сессия ещё жива; на вход нужно уходить тогда, когда заканчивается она.