docs(spec): sessions API, device fields, profile sessions UI. Refs EventHub/EventHubBack#74
This commit is contained in:
+25
-11
@@ -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
@@ -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).
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user