docs(spec): sessions API, device fields, profile sessions UI. Refs EventHub/EventHubBack#74

This commit is contained in:
2026-08-16 18:30:33 +03:00
parent 4dd741ba48
commit 9b4499e884
2 changed files with 37 additions and 17 deletions
+25 -11
View File
@@ -260,7 +260,10 @@ Legacy: прямой `POST /v1/calendars/:id/specialists` **не использ
- `calendar_share` (`read` | `write` | `admin`): соредактор personal / заместитель commercial; - `calendar_share` (`read` | `write` | `admin`): соредактор personal / заместитель commercial;
invite/accept/revoke + grants; personal **не** в search/discovery. Не путать с follow, invite/accept/revoke + grants; personal **не** в search/discovery. Не путать с follow,
specialist_invite и платной subscription. 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:** оплата услуги клиентом (B2C) — вне горизонта продукта.
### 2.1.3. Share / приглашения (соредактор и заместитель) ### 2.1.3. Share / приглашения (соредактор и заместитель)
@@ -574,11 +577,18 @@ src/
`/invites?token=`, `/reset-password?token=` (бренд CalenTIQ). `/invites?token=`, `/reset-password?token=` (бренд CalenTIQ).
- `POST /v1/reset-password``{token, password}` (пароль ≥ 8 символов); успех → новый hash, - `POST /v1/reset-password``{token, password}` (пароль ≥ 8 символов); успех → новый hash,
токен удалён, refresh-сессии user отозваны; `404`/`410`/`400`/`403`. токен удалён, refresh-сессии user отозваны; `404`/`410`/`400`/`403`.
- `POST /v1/login` — вход. - `POST /v1/login` — вход; опционально `client_type` (`web`|`mobile`, default `web`),
- `POST /v1/refresh` — обновление токена. `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`; после этого - `POST /v1/logout``{ refresh_token }` → отзыв текущей `auth_session`; после этого
refresh той же сессии → `401`. Access JWT до истечения TTL не отзывается. refresh той же сессии → `401`. Access JWT до истечения TTL не отзывается.
Невалидный refresh → `401`; отсутствие поля → `400`. Клиент чистит storage даже при ошибке сети. Невалидный 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` — профиль пользователя. - `GET /v1/user/me` — профиль пользователя.
- `PATCH /v1/user/me` — частичное обновление своего профиля: `language` (`ru`|`en`), - `PATCH /v1/user/me` — частичное обновление своего профиля: `language` (`ru`|`en`),
`nickname`, `timezone`, `phone`, `avatar_url`, `preferences`; смена пароля — `nickname`, `timezone`, `phone`, `avatar_url`, `preferences`; смена пароля —
@@ -721,43 +731,47 @@ src/
| `current_jti` | Актуальный jti refresh JWT | | `current_jti` | Актуальный jti refresh JWT |
| `expires_at` | Срок жизни сессии (30 дней) | | `expires_at` | Срок жизни сессии (30 дней) |
| `revoked` | Флаг отзыва | | `revoked` | Флаг отзыва |
| `device_name` | Человекочитаемая метка устройства (с login; может быть пустой) |
| `user_agent` | `User-Agent` с login (может быть пустой) |
| `created_at` / `updated_at` | Создание / последняя ротация или отзыв |
**Refresh JWT (admin)** — claims: **Refresh JWT (admin)** — claims:
- `typ=refresh`, `aud=admin`, `sub=<admin_id>` - `typ=refresh`, `aud=admin`, `sub=<admin_id>`
- `sid=<session_id>`, `fid=<family_id>`, `jti=<current_jti>` - `sid=<session_id>`, `fid=<family_id>`, `jti=<current_jti>`
- `client=admin`, `exp`, `iat` - `client=admin`, `exp`, `iat`
**Refresh JWT (user, фаза 2)** — claims: **Refresh JWT (user)** — claims:
- `typ=refresh`, `aud=user`, `sub=<user_id>` - `typ=refresh`, `aud=user`, `sub=<user_id>`
- `sid`, `fid`, `jti` — как у admin - `sid`, `fid`, `jti` — как у admin
- `client=web` (фаза 3: `mobile`), `exp`, `iat` - `client=web` \| `mobile` (из сессии; refresh не меняет device/UA)
- Подпись: `JWT_SECRET` (user JWK), отдельно от admin - Подпись: `JWT_SECRET` (user JWK), отдельно от admin
**Поток login admin** (`POST /v1/admin/login`): **Поток login admin** (`POST /v1/admin/login`):
1. Проверка email/password. 1. Проверка email/password.
2. Создание записи `auth_session`. 2. Создание записи `auth_session`.
3. Ответ: `{ token, refresh_token, user }`. 3. Ответ: `{ token, refresh_token, session_id, user }`.
**Поток refresh admin** (`POST /v1/admin/refresh`): **Поток refresh admin** (`POST /v1/admin/refresh`):
1. Верификация подписи и срока refresh JWT. 1. Верификация подписи и срока refresh JWT.
2. Сверка `jti` из JWT с `current_jti` в Mnesia. 2. Сверка `jti` из JWT с `current_jti` в Mnesia.
3. При совпадении — ротация: новый `jti`, новая пара токенов. 3. При совпадении — ротация: новый `jti`, новая пара токенов + `session_id`.
4. При несовпадении (reuse) — `revoke_family`, ответ 401. 4. При несовпадении (reuse) — `revoke_family`, ответ 401.
**Поток login user** (`POST /v1/login`): **Поток login user** (`POST /v1/login`):
1. Проверка email/password. 1. Проверка email/password.
2. Создание записи `auth_session` (`subject_type=user`, `client_type=web`). 2. Создание `auth_session` (`subject_type=user`, `client_type`, `device_name`, `user_agent`).
3. Ответ: `{ token, refresh_token, user }`. 3. Ответ: `{ token, refresh_token, session_id, user }`.
**Поток refresh user** (`POST /v1/refresh`): **Поток refresh user** (`POST /v1/refresh`):
1. Верификация refresh JWT (`aud=user`). 1. Верификация refresh JWT (`aud=user`).
2. Сверка `jti` с `current_jti` в Mnesia, ротация при успехе. 2. Сверка `jti` с `current_jti` в Mnesia, ротация при успехе; `client` берётся из сессии.
3. Reuse — `revoke_family`, 401. 3. Reuse — `revoke_family`, 401.
**Фазы внедрения:** **Фазы внедрения:**
- Фаза 1 (#23): admin API — реализовано. - Фаза 1 (#23): admin API — реализовано.
- Фаза 2 (#26): user login + `/v1/refresh` — реализовано. - Фаза 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) ### Legacy (не используется login/refresh user)
- Таблица `session` (`ram_copies`) и opaque refresh в `core_session` — оставлены в кодовой базе, hot path user API переведён на `auth_session`. - Таблица `session` (`ram_copies`) и opaque refresh в `core_session` — оставлены в кодовой базе, hot path user API переведён на `auth_session`.
+12 -6
View File
@@ -21,15 +21,19 @@ Deep-link на календарь, владельцем которого явл
Эндпоинты: Эндпоинты:
- `POST /v1/register` — регистрация (`email`, `password`); статус пользователя `pending` до верификации. - `POST /v1/register` — регистрация (`email`, `password`); статус пользователя `pending` до верификации.
- `POST /v1/verify` — подтверждение email `{ token }`; после успеха у пользователя есть дефолтный personal-календарь. - `POST /v1/verify` — подтверждение email `{ token }`; после успеха у пользователя есть дефолтный personal-календарь.
- `POST /v1/login` — вход; ответ `{ token, refresh_token, user }`. Сессия `auth_session` с `client_type=web`. - `POST /v1/login` — вход; тело: `email`, `password`, `client_type=web`, `device_name`
- `POST /v1/refresh` — ротация пары токенов по `{ refresh_token }`. (метка браузера/ОС). Ответ `{ 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` — профиль текущего пользователя. - `GET /v1/user/me` — профиль текущего пользователя.
Реализация: Реализация:
- JWT в `localStorage` под ключами, отличными от Admin UI (избежать коллизии при общем origin). - JWT + `session_id` в `localStorage` (ключи, отличные от Admin UI).
- Axios: `Authorization: Bearer <access>`; при 401 — single-flight refresh; при неудаче — редирект на `/login`. - Axios: `Authorization: Bearer <access>`; при 401 — single-flight refresh; при неудаче — редирект на `/login`.
- При старте приложения — `GET /v1/user/me` при наличии токена. - При старте приложения — `GET /v1/user/me` при наличии токена.
- Logout — только клиентский (очистка storage); серверного revoke нет. - Logout — `POST /v1/logout`, затем clear storage.
- WebSocket: `ws://…:8081/ws?token=<access_jwt>`; subscribe/unsubscribe по `calendar_id`. - WebSocket: `ws://…:8081/ws?token=<access_jwt>`; subscribe/unsubscribe по `calendar_id`.
## 3. Технологический стек ## 3. Технологический стек
@@ -376,7 +380,9 @@ E2E: `e2e/TESTIDS.md`. Моки (`npm run test:e2e`, `project=mock`) — тол
legal stubs; позиция по подписке **владельца**; DNS/SPF без покупки домена без явного ок. legal stubs; позиция по подписке **владельца**; DNS/SPF без покупки домена без явного ок.
**Продукт (фаза 2):** **Продукт (фаза 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 - загрузка файлов: avatar `POST /v1/user/me/avatar`, cover
`POST /v1/calendars/:id/cover` (Back#71); UI — Front#69; вложения — позже `POST /v1/calendars/:id/cover` (Back#71); UI — Front#69; вложения — позже
- полноценный push / reminders (email-reminder: Back#70; Web Push + prefs: - полноценный push / reminders (email-reminder: Back#70; Web Push + prefs:
@@ -386,7 +392,7 @@ legal stubs; позиция по подписке **владельца**; DNS/SP
карточке события при полном слоте — Front#70 карточке события при полном слоте — Front#70
- шаринг календаря с правами (`calendar_share`) — Back#73 API + Front list/invites/mirror; - шаринг календаря с правами (`calendar_share`) — Back#73 API + Front list/invites/mirror;
не путать со specialist_invite / Following не путать со specialist_invite / Following
- явный `client_type=mobile` — хвост (фаза 3 бэка / нативный клиент) - нативный `client_type=mobile` клиент — хвост (API уже принимает `mobile`)
**Вне горизонта (не фаза 2):** оплата услуги клиентом (B2C). **Вне горизонта (не фаза 2):** оплата услуги клиентом (B2C).