diff --git a/EventHubBackSpec.md b/EventHubBackSpec.md index 3d0d3e9..b968eae 100644 --- a/EventHubBackSpec.md +++ b/EventHubBackSpec.md @@ -260,7 +260,10 @@ Legacy: прямой `POST /v1/calendars/:id/specialists` **не использ - `calendar_share` (`read` | `write` | `admin`): соредактор personal / заместитель commercial; invite/accept/revoke + grants; personal **не** в search/discovery. Не путать с follow, specialist_invite и платной subscription. -- `client_type=mobile` — хвост, когда появится нативный клиент. +- Управление своими сессиями + `client_type`/`device_name` — **сделано** (Back#74): + `GET/DELETE /v1/sessions`, `POST /v1/sessions/revoke-others`; login принимает + `client_type` (`web`|`mobile`) и `device_name`, сохраняет `User-Agent`. + UI списка — Front#72. Kick-on-login / нативный клиент — по-прежнему future. - **Не в фазе 2:** оплата услуги клиентом (B2C) — вне горизонта продукта. ### 2.1.3. Share / приглашения (соредактор и заместитель) @@ -574,11 +577,18 @@ src/ `/invites?token=`, `/reset-password?token=` (бренд CalenTIQ). - `POST /v1/reset-password` — `{token, password}` (пароль ≥ 8 символов); успех → новый hash, токен удалён, refresh-сессии user отозваны; `404`/`410`/`400`/`403`. -- `POST /v1/login` — вход. -- `POST /v1/refresh` — обновление токена. +- `POST /v1/login` — вход; опционально `client_type` (`web`|`mobile`, default `web`), + `device_name` (max 120); `User-Agent` пишется в сессию. Ответ: + `{ token, refresh_token, session_id, user }`. Неверный `client_type` → `400`. +- `POST /v1/refresh` — обновление токена; ответ `{ token, refresh_token, session_id }`. - `POST /v1/logout` — `{ refresh_token }` → отзыв текущей `auth_session`; после этого refresh той же сессии → `401`. Access JWT до истечения TTL не отзывается. Невалидный refresh → `401`; отсутствие поля → `400`. Клиент чистит storage даже при ошибке сети. +- `GET /v1/sessions` — список активных своих user-сессий (Bearer): + `session_id`, `client_type`, `device_name`, `user_agent`, `created_at`, `updated_at`, `expires_at`. +- `DELETE /v1/sessions/:id` — отзыв своей сессии; чужая/нет → `404`. +- `POST /v1/sessions/revoke-others` — Bearer + `{ refresh_token }`: оставить сессию + из refresh, отозвать остальные; чужой subject → `403`; невалидный refresh → `401`. - `GET /v1/user/me` — профиль пользователя. - `PATCH /v1/user/me` — частичное обновление своего профиля: `language` (`ru`|`en`), `nickname`, `timezone`, `phone`, `avatar_url`, `preferences`; смена пароля — @@ -721,43 +731,47 @@ src/ | `current_jti` | Актуальный jti refresh JWT | | `expires_at` | Срок жизни сессии (30 дней) | | `revoked` | Флаг отзыва | +| `device_name` | Человекочитаемая метка устройства (с login; может быть пустой) | +| `user_agent` | `User-Agent` с login (может быть пустой) | +| `created_at` / `updated_at` | Создание / последняя ротация или отзыв | **Refresh JWT (admin)** — claims: - `typ=refresh`, `aud=admin`, `sub=` - `sid=`, `fid=`, `jti=` - `client=admin`, `exp`, `iat` -**Refresh JWT (user, фаза 2)** — claims: +**Refresh JWT (user)** — claims: - `typ=refresh`, `aud=user`, `sub=` - `sid`, `fid`, `jti` — как у admin -- `client=web` (фаза 3: `mobile`), `exp`, `iat` +- `client=web` \| `mobile` (из сессии; refresh не меняет device/UA) - Подпись: `JWT_SECRET` (user JWK), отдельно от admin **Поток login admin** (`POST /v1/admin/login`): 1. Проверка email/password. 2. Создание записи `auth_session`. -3. Ответ: `{ token, refresh_token, user }`. +3. Ответ: `{ token, refresh_token, session_id, user }`. **Поток refresh admin** (`POST /v1/admin/refresh`): 1. Верификация подписи и срока refresh JWT. 2. Сверка `jti` из JWT с `current_jti` в Mnesia. -3. При совпадении — ротация: новый `jti`, новая пара токенов. +3. При совпадении — ротация: новый `jti`, новая пара токенов + `session_id`. 4. При несовпадении (reuse) — `revoke_family`, ответ 401. **Поток login user** (`POST /v1/login`): 1. Проверка email/password. -2. Создание записи `auth_session` (`subject_type=user`, `client_type=web`). -3. Ответ: `{ token, refresh_token, user }`. +2. Создание `auth_session` (`subject_type=user`, `client_type`, `device_name`, `user_agent`). +3. Ответ: `{ token, refresh_token, session_id, user }`. **Поток refresh user** (`POST /v1/refresh`): 1. Верификация refresh JWT (`aud=user`). -2. Сверка `jti` с `current_jti` в Mnesia, ротация при успехе. +2. Сверка `jti` с `current_jti` в Mnesia, ротация при успехе; `client` берётся из сессии. 3. Reuse — `revoke_family`, 401. **Фазы внедрения:** - Фаза 1 (#23): admin API — реализовано. - Фаза 2 (#26): user login + `/v1/refresh` — реализовано. -- Фаза 3: client web + mobile (явный `client_type`, тот же контракт). +- Фаза 3 (Back#74): явный `client_type` web|mobile, device meta, list/revoke sessions — реализовано. + Kick-on-login / concurrent policy по `client_type` — не сделано. ### Legacy (не используется login/refresh user) - Таблица `session` (`ram_copies`) и opaque refresh в `core_session` — оставлены в кодовой базе, hot path user API переведён на `auth_session`. diff --git a/EventHubFrontSpec.md b/EventHubFrontSpec.md index 77c1f05..5169a92 100644 --- a/EventHubFrontSpec.md +++ b/EventHubFrontSpec.md @@ -21,15 +21,19 @@ Deep-link на календарь, владельцем которого явл Эндпоинты: - `POST /v1/register` — регистрация (`email`, `password`); статус пользователя `pending` до верификации. - `POST /v1/verify` — подтверждение email `{ token }`; после успеха у пользователя есть дефолтный personal-календарь. -- `POST /v1/login` — вход; ответ `{ token, refresh_token, user }`. Сессия `auth_session` с `client_type=web`. -- `POST /v1/refresh` — ротация пары токенов по `{ refresh_token }`. +- `POST /v1/login` — вход; тело: `email`, `password`, `client_type=web`, `device_name` + (метка браузера/ОС). Ответ `{ token, refresh_token, session_id, user }`. +- `POST /v1/refresh` — ротация по `{ refresh_token }`; ответ включает `session_id`. +- `POST /v1/logout` — `{ refresh_token }` до очистки storage. +- `GET /v1/sessions`, `DELETE /v1/sessions/:id`, `POST /v1/sessions/revoke-others` — + активные сессии в профиле (устройство, «текущая», отзыв / выйти на других). - `GET /v1/user/me` — профиль текущего пользователя. Реализация: -- JWT в `localStorage` под ключами, отличными от Admin UI (избежать коллизии при общем origin). +- JWT + `session_id` в `localStorage` (ключи, отличные от Admin UI). - Axios: `Authorization: Bearer `; при 401 — single-flight refresh; при неудаче — редирект на `/login`. - При старте приложения — `GET /v1/user/me` при наличии токена. -- Logout — только клиентский (очистка storage); серверного revoke нет. +- Logout — `POST /v1/logout`, затем clear storage. - WebSocket: `ws://…:8081/ws?token=`; subscribe/unsubscribe по `calendar_id`. ## 3. Технологический стек @@ -376,7 +380,9 @@ E2E: `e2e/TESTIDS.md`. Моки (`npm run test:e2e`, `project=mock`) — тол legal stubs; позиция по подписке **владельца**; DNS/SPF без покупки домена без явного ок. **Продукт (фаза 2):** -- серверный logout / revoke session (`POST /v1/logout` + `{ refresh_token }`; клиент зовёт до clear storage) +- серверный logout / revoke session (`POST /v1/logout` + `{ refresh_token }`; клиент зовёт до clear storage) — сделано +- список/отзыв сессий в профиле (Back#74 / Front#72): устройство (`device_name`/UA), + revoke одной и revoke-others - загрузка файлов: avatar `POST /v1/user/me/avatar`, cover `POST /v1/calendars/:id/cover` (Back#71); UI — Front#69; вложения — позже - полноценный push / reminders (email-reminder: Back#70; Web Push + prefs: @@ -386,7 +392,7 @@ legal stubs; позиция по подписке **владельца**; DNS/SP карточке события при полном слоте — Front#70 - шаринг календаря с правами (`calendar_share`) — Back#73 API + Front list/invites/mirror; не путать со specialist_invite / Following -- явный `client_type=mobile` — хвост (фаза 3 бэка / нативный клиент) +- нативный `client_type=mobile` клиент — хвост (API уже принимает `mobile`) **Вне горизонта (не фаза 2):** оплата услуги клиентом (B2C).