Добавлены уведомления
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,554 @@
|
||||
# Бизнес-постановка: Пользователь (User)
|
||||
|
||||
**Статус:** v1 — консолидация принятых решений из `HAN_chat_specification` (`arch-00`…`arch-05`, `module-01`, `module-08`); открытые вопросы зафиксированы в §17
|
||||
**Продукт:** HAN Chat (клиентское приложение + `api-backend` + Keycloak)
|
||||
**Источники:** архитектура `HAN_chat_specification`; макет Figma (**не канон** — только визуализация; при расхождении приоритет у этого ТЗ и arch-документов)
|
||||
**Связанный backlog:** кнопка «Войти»; история устройств входа; дифференцированные ошибки OTP; debounce SMS; тестовый пользователь с фиксированным SMS-входом
|
||||
**Смежно:** уведомления (контуры G/P), чат, согласия, UX-сессия, CRM Contact через `bitrix-sync`
|
||||
|
||||
**Нормативная часть — §1–§16.** §17 — ненормативный журнал решений и открытых вопросов; при расхождении с §1–§16 приоритет у §1–§16. При расхождении этого документа с arch/module после их обновления — приоритет у arch/module до синхронизации.
|
||||
|
||||
---
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Дать клиенту устойчивую идентичность в HAN Chat: вход по подтверждённому телефону, локальную карточку пользователя в App DB, согласия, readonly-профиль в ЛК и связь с Contact в Bitrix24 — без смешения гостевого просмотра и персонального кабинета.
|
||||
|
||||
Гость изучает сервис без записи в App DB. Авторизованный клиент получает персональные данные, чат, персональные уведомления и профиль. Auth-идентичность принадлежит Keycloak; приложение владеет бизнес-карточкой пользователя, согласиями и кэшем профиля для UI.
|
||||
|
||||
---
|
||||
|
||||
## 2. Два режима клиента
|
||||
|
||||
| Режим | Кто это | Идентичность на бэкенде | Что доступно |
|
||||
|---|---|---|---|
|
||||
| **G. Гость** | Клиент без действующего JWT | Нет `UserIdentity`; опциональный локальный `guest_session_id` только на устройстве | UI + `GET /api/v1/public/*`; гостевые уведомления (контур G) |
|
||||
| **A. Авторизованный** | Клиент с валидным access token и выполненным `bootstrap` | `user_identities` (`keycloak_sub` ↔ JWT `sub`) | JWT API: чат, профиль, согласия, персональные уведомления, UX-сессия |
|
||||
|
||||
Правила:
|
||||
|
||||
1. До успешного OTP + `bootstrap` клиент — только гость. Write-endpoint (`consents`, `session-start`, чат, профиль и т.д.) **требуют JWT**.
|
||||
2. После авторизации гостевой контент **не** переносится в персональный (как в уведомлениях: G не мигрирует в P).
|
||||
3. `guest_session_id` **не** является auth и **не** открывает write API.
|
||||
4. Наличие refresh token в secure storage позволяет вернуться в режим **A** без OTP (§6.3); отсутствие/истечение refresh → снова гость до следующего защищённого действия.
|
||||
5. Access token проверяет `api-backend`; refresh выполняет **только frontend** через Keycloak. Backend refresh **не** делает.
|
||||
|
||||
---
|
||||
|
||||
## 3. Границы релиза
|
||||
|
||||
### 3.1. В scope
|
||||
|
||||
- OTP-only вход по номеру телефона (Keycloak; mock до controlled SMS cutover).
|
||||
- OIDC Authorization Code + PKCE; refresh / logout; silent return без OTP при валидном refresh.
|
||||
- Локальный `find-or-create` `UserIdentity` + минимальный `ClientProfile` через `POST /api/v1/auth/bootstrap`.
|
||||
- Согласия: обязательные `personal_data` + `user_agreement`, опциональный `marketing`; фиксация версий в `user_consents`.
|
||||
- Readonly блочный профиль `GET /api/v1/me`; зарезервированный `GET /api/v1/me/documents` (MVP может быть пустым).
|
||||
- Аналитическая `UxSession` для авторизованного клиента (`session-start`, заголовок `X-Ux-Session-Id`).
|
||||
- Асинхронный map/create Contact в Bitrix24 по телефону через `sync_queue` (не блокирует вход).
|
||||
- Нормализация телефона в E.164; phone claim только из JWT, не из body клиента.
|
||||
- Ownership: все пользовательские ресурсы адресуются через `user_id` из JWT.
|
||||
|
||||
### 3.2. Вне scope
|
||||
|
||||
- Пароль, email-OTP, social login, magic link.
|
||||
- Редактирование профиля клиентом (PATCH/PUT отсутствуют).
|
||||
- Доставка документов компании в блок «Документы» (post-MVP; API зарезервирован).
|
||||
- История устройств входа (backlog п.19) — модель и UI позже.
|
||||
- Смена телефона как продуктовый self-service flow (policy Keycloak есть; продуктовый UX — отдельно).
|
||||
- Merge/reassignment двух `sub` на один телефон — только administrative policy, не side effect login.
|
||||
- Гостевая запись согласий и UX-сессии в App DB.
|
||||
- Создание `UserIdentity` / `ClientProfile` сервисом `bitrix-sync` (sync не участвует в OTP-flow).
|
||||
|
||||
---
|
||||
|
||||
## 4. Изменения UI (относительно гостевого экрана)
|
||||
|
||||
| Место | Гость | Авторизованный |
|
||||
|---|---|---|
|
||||
| Главная | Публичный контент, гостевые уведомления | Персональные уведомления, чат доступен |
|
||||
| Отправка сообщения / популярный вопрос | Сначала согласия → OTP → bootstrap → отправка отложенного текста | Штатная отправка |
|
||||
| Центр уведомлений | Auth-gate «Авторизоваться» | Список персональных |
|
||||
| Профиль | Недоступен / ведёт на вход | Readonly блоки «Личные данные», «Документы» |
|
||||
| Кнопка «Войти» | Запускает поток авторизации | Скрыта / заменена профилем (по макету) |
|
||||
| Выход | — | Очистка tokens → гостевой UI |
|
||||
|
||||
Визуал — по Figma. Figma не канон поведения и состава полей профиля: канон — §5.5 и arch-01.
|
||||
|
||||
---
|
||||
|
||||
## 5. Бизнес-модель
|
||||
|
||||
### 5.1. Слои идентичности (не смешивать)
|
||||
|
||||
| Слой | Где живёт | Что хранит | Master |
|
||||
|---|---|---|---|
|
||||
| **IdP user** | Keycloak (`keycloak` schema) | Realm user, phone verified, sessions, tokens | Keycloak |
|
||||
| **UserIdentity** | App DB `user_identities` | Локальный `user_id`, связь `keycloak_sub`, кэш auth-телефона, `last_login_at` | Keycloak для телефона/`sub`; App DB для бизнес-FK |
|
||||
| **ClientProfile** | App DB `client_profiles` | Кэш полей UI + `bitrix_contact_id` | UI-поля — последнее успешно синхронизированное значение (входящий поток MVP — Bitrix24); auth-телефон инициирует sync, но master телефона — Keycloak |
|
||||
| **Bitrix Contact** | CRM Bitrix24 | Карточка клиента в CRM | Bitrix24 для CRM-полей; связь через `bitrix_contact_id` / `entity_external_mapping` |
|
||||
| **Гость** | Только устройство | UI-state, локальные согласия до OTP, опционально `guest_session_id` | Нет серверной записи |
|
||||
|
||||
**Инвариант:** один verified phone ↔ один active Keycloak `sub`. Один `sub` ↔ одна active `UserIdentity`. Один `user_id` ↔ один `ClientProfile`.
|
||||
|
||||
### 5.2. Связанные сущности (часть домена «пользователь», но не сам User)
|
||||
|
||||
| Сущность | Роль | Когда появляется |
|
||||
|---|---|---|
|
||||
| `UserConsent` | Факт принятия документа конкретной версии | `bootstrap` или `POST /consents` |
|
||||
| `UxSession` | Аналитический период активности | `session-start` **только** у авторизованного |
|
||||
| `Dialog` / `Message` | Чат с оператором | После auth, лениво при первом сообщении |
|
||||
| Notification (P) | Персональные уведомления | `user_id` NOT NULL |
|
||||
|
||||
`UxSession` **не** является механизмом авторизации и **не** заменяет JWT.
|
||||
|
||||
### 5.3. Согласия
|
||||
|
||||
| Тип | Обязательность (seed) | Документ / URL | Версия |
|
||||
|---|---|---|---|
|
||||
| `personal_data` | да (`consent.personal_data.required=true`) | `consent.personal_data.document_url` (+ политика `consent.privacy_policy.document_url`) | `consent.personal_data.version` |
|
||||
| `user_agreement` | да | `consent.user_agreement.document_url` | `consent.user_agreement.version` |
|
||||
| `marketing` | нет | `consent.marketing.document_url` | `consent.marketing.version` |
|
||||
|
||||
Правила:
|
||||
|
||||
1. Pop-up согласий показывается **до** OTP; до получения JWT факт принятия хранится **только на клиенте**.
|
||||
2. Серверная фиксация — в `bootstrap` (атомарно с созданием пользователя) или позже через `POST /api/v1/consents` при смене версий документов.
|
||||
3. Запись `UserConsent` **immutable**: unique `(user_id, consent_type, document_version)`; исправление — новая версия документа или administrative action с audit.
|
||||
4. Обязательные согласия без `accepted: true` → `403 consents_required`; вход в ЛК / write API с непринятыми актуальными обязательными версиями блокируется.
|
||||
5. Keycloak consent screen **не** заменяет продуктовые согласия API.
|
||||
|
||||
### 5.4. Auth-телефон
|
||||
|
||||
- Единственный канал MVP: номер телефона + OTP.
|
||||
- Нормализация: libphonenumber → canonical E.164.
|
||||
- В App DB телефон пишется **только** из JWT claims при `bootstrap` / обновлении identity, **никогда** из body клиента.
|
||||
- Порядок claim: `phone_number`, иначе `preferred_username` только если значение валидно как E.164.
|
||||
- Отсутствие/невалидность при bootstrap → `400 phone_claim_missing`.
|
||||
- Утечка существования номера запрещена на стороне Keycloak (одинаковый внешний ответ для нового/существующего).
|
||||
- В `ClientProfile` при создании копируется в `russian_phone` (минимальный профиль); дальнейшее обогащение — из CRM sync.
|
||||
|
||||
### 5.5. Профиль (UI)
|
||||
|
||||
Блочная модель. Редактирование клиентом **недоступно**.
|
||||
|
||||
**Блок «Личные данные»:**
|
||||
|
||||
| Поле | Источник отображения | Примечание |
|
||||
|---|---|---|
|
||||
| ФИО (`full_name`) | `client_profiles` | Может быть `null` до sync из Bitrix24 |
|
||||
| Гражданство (`citizenship`) | `client_profiles` | `null` до заполнения |
|
||||
| Телефон РФ (`russian_phone`) | `client_profiles` | При bootstrap = auth-телефон |
|
||||
| Зарубежный телефон (`foreign_phone`) | `client_profiles` | Опционально |
|
||||
| Email (`email`) | `client_profiles` | Опционально |
|
||||
|
||||
**Блок «Документы»:**
|
||||
|
||||
- перечень документов компании, дата, наименование, скачивание;
|
||||
- в MVP список может быть пустым; доставка из Bitrix24 — post-MVP;
|
||||
- API: `GET /api/v1/me/documents`, `GET /api/v1/documents/{id}`, `.../download-url` с audit.
|
||||
|
||||
Макет Figma может показывать дополнительные секции (патент, РВП и т.п.) — это **не** канон MVP-модели данных; расширение блоков — отдельное решение.
|
||||
|
||||
### 5.6. Жизненный цикл пользователя
|
||||
|
||||
| Состояние | Условие | Что видит клиент |
|
||||
|---|---|---|
|
||||
| Гость | Нет валидного JWT | Публичный UI |
|
||||
| OTP in progress | Идёт challenge в Keycloak | Экраны телефона / кода |
|
||||
| Authenticated, bootstrap pending | Есть JWT, нет local `UserIdentity` | Frontend обязан вызвать `bootstrap`; прочие protected → `409` «bootstrap required» |
|
||||
| Authenticated, ready | Есть `UserIdentity` + актуальные обязательные согласия | Полный ЛК |
|
||||
| Soft-deleted | `record_status='D'` на identity (админ) | Доступ запрещён; детали — operational policy |
|
||||
|
||||
Бизнес-«удаление аккаунта» клиентом в MVP **не** моделируется. Soft-delete — административный контур (arch-05).
|
||||
|
||||
### 5.7. Константы (`app_settings`)
|
||||
|
||||
| Ключ | Смысл | Default / seed |
|
||||
|---|---|---|
|
||||
| `auth.phone.enabled` | Вход по телефону | `true` |
|
||||
| `auth.password.enabled` | Пароль | `false` |
|
||||
| `otp.phone.max_send_attempts_per_24h` | Лимит отправок OTP | `3` |
|
||||
| `otp.phone.min_seconds_between_attempts` | Минимальный интервал между отправками | `30` |
|
||||
| `otp.phone.max_verify_attempts` | Лимит проверок кода | `5` |
|
||||
| `otp.phone.code_length` | Длина кода | `6` |
|
||||
| `otp.phone.ttl_seconds` | TTL кода | `60` |
|
||||
| `otp.phone.sms_order_timeout_ms` | Таймаут заказа SMS | `3000` |
|
||||
| `consent.*` | URL/версии/required флагов согласий | см. arch-04 |
|
||||
| `ux.session.idle_timeout_minutes` | Idle → новая UX-сессия | `30` |
|
||||
|
||||
Счётчики OTP ведёт **Keycloak/SPI**, не `api-backend`. Продуктовые `otp.phone.*` Keycloak читает через settings bridge `GET /internal/settings/v1/otp`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Поведение UI и сценарии
|
||||
|
||||
### 6.1. Гостевой режим
|
||||
|
||||
1. Клиент открывает приложение → гостевой UI.
|
||||
2. Доступен только `GET /api/v1/public/*` (+ статика).
|
||||
3. Согласия и `session-start` в App DB **не** пишутся.
|
||||
4. Попытка защищённого действия (сообщение, Центр уведомлений, профиль) → поток авторизации.
|
||||
|
||||
### 6.2. Поток первой авторизации (OTP)
|
||||
|
||||
1. Триггер: отправка сообщения / популярный вопрос / «Войти» / иное действие, требующее auth.
|
||||
2. Pop-up согласий; обязательные должны быть приняты локально.
|
||||
3. Форма телефона → Keycloak OTP-flow (mock или real SMS через `sms-service`).
|
||||
4. Успешная проверка OTP → tokens (Authorization Code + PKCE).
|
||||
5. `POST /api/v1/auth/bootstrap` с локальными согласиями и `device` metadata.
|
||||
6. `POST /api/v1/analytics/session-start` при необходимости новой UX-сессии.
|
||||
7. Триггер БД ставит `contact.map_or_create` в `sync_queue` (асинхронно; ошибка CRM **не** откатывает вход).
|
||||
8. Frontend продолжает исходное действие (в т.ч. отложенное сообщение / популярный вопрос).
|
||||
|
||||
**UX-инвариант (backlog п.21):** ошибка отправки отложенного сообщения после успешного bootstrap **не** должна выглядеть как «не удалось завершить вход». Вход завершён на шаге 5–6; ошибка Bitrix/чата показывается в контексте чата.
|
||||
|
||||
### 6.3. Возврат без OTP
|
||||
|
||||
1. Есть валидный refresh token → Refresh Token Grant → access token.
|
||||
2. При необходимости — `session-start`.
|
||||
3. OTP не показывается.
|
||||
4. Нет/истёк refresh → гость до следующего защищённого действия.
|
||||
|
||||
### 6.4. Поддержание сессии (tokens)
|
||||
|
||||
- Frontend проактивно обновляет access token (~60 с до `exp`), single-flight.
|
||||
- Успешный refresh **не** создаёт новую UX-сессию.
|
||||
- `401` от API → один refresh + retry исходного запроса; провал refresh → очистка tokens → гость.
|
||||
- То же для WebSocket `/api/v1/realtime`.
|
||||
|
||||
### 6.5. UX-сессия
|
||||
|
||||
Новая `UxSession` только при:
|
||||
|
||||
| `start_reason` | Когда |
|
||||
|---|---|
|
||||
| `first_launch` | В памяти нет `ux_session_id` |
|
||||
| `cold_start` | Kill app / закрытие вкладки |
|
||||
| `idle_timeout` | Простой > `ux.session.idle_timeout_minutes` |
|
||||
|
||||
`ux_session_id` хранится **только в памяти** (не в localStorage). Передаётся как `X-Ux-Session-Id`. Отсутствие заголовка API не блокирует (кроме endpoint, где id обязателен).
|
||||
|
||||
### 6.6. Профиль
|
||||
|
||||
- Открывается только авторизованным.
|
||||
- Данные — `GET /api/v1/me`; поля могут быть частично пустыми до CRM sync.
|
||||
- Редактирование недоступно; изменение ФИО/email и т.п. — через процессы компании (Bitrix24 → sync).
|
||||
- Документы — отдельный блок; скачивание с audit.
|
||||
|
||||
### 6.7. Выход
|
||||
|
||||
1. Frontend инициирует logout у Keycloak (revocation по policy модуля).
|
||||
2. Очищает access/refresh tokens и in-memory UX-сессию.
|
||||
3. UI переходит в гостевой режим.
|
||||
4. Локальный `UserIdentity` в App DB **не** удаляется.
|
||||
|
||||
---
|
||||
|
||||
## 7. Матрицы поведения
|
||||
|
||||
### 7.1. Что требует auth
|
||||
|
||||
| Действие | Гость | Авторизованный |
|
||||
|---|---|---|
|
||||
| `GET /api/v1/public/*` | да | да |
|
||||
| Просмотр главной / гостевых уведомлений | да | нет (после входа — только P) |
|
||||
| Отправка сообщения / вложение | нет → OTP | да |
|
||||
| `POST /auth/bootstrap` | нет (нужен JWT после OTP) | да (идемпотентно) |
|
||||
| `POST /consents`, `session-start` | нет | да |
|
||||
| `GET /me`, чат, персональные уведомления | нет | да |
|
||||
| `WS /api/v1/realtime` | нет | да |
|
||||
|
||||
### 7.2. Источник истины полей
|
||||
|
||||
| Поле / факт | Master | Куда кэшируется |
|
||||
|---|---|---|
|
||||
| `sub` / существование IdP user | Keycloak | `user_identities.keycloak_sub` |
|
||||
| Auth-телефон | Keycloak | `user_identities.phone_number`, seed `client_profiles.russian_phone` |
|
||||
| Согласия (версия + accepted) | App DB `user_consents` | — |
|
||||
| ФИО, гражданство, email, foreign_phone | Последний успешный sync (MVP: Bitrix → App) | `client_profiles` |
|
||||
| `bitrix_contact_id` | Результат `bitrix-sync` | `client_profiles` |
|
||||
| Tokens / auth session | Keycloak | secure storage на клиенте |
|
||||
| `ux_session_id` | App DB + память клиента | заголовок запросов |
|
||||
|
||||
### 7.3. Bootstrap — идемпотентность
|
||||
|
||||
| Повторный вызов | Результат |
|
||||
|---|---|
|
||||
| Тот же `sub`, те же версии согласий | `200`, тот же `user_id`; `last_login_at` обновляется; дублей consent нет |
|
||||
| Тот же `sub`, новые версии согласий | Новые immutable строки consent + update identity |
|
||||
| JWT без phone claim | `400 phone_claim_missing` |
|
||||
| Обязательные consents не accepted | `403 consents_required` |
|
||||
|
||||
Application-код **не** пишет в `sync_queue`: задачи создают триггеры на insert/update `UserIdentity` / `ClientProfile`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Идентификация
|
||||
|
||||
| Идентификатор | Назначение |
|
||||
|---|---|
|
||||
| `keycloak_sub` | Subject JWT; ключ find-or-create |
|
||||
| `user_id` | PK `user_identities`; FK всех персональных сущностей App DB |
|
||||
| `phone_number` | Auth-телефон E.164 |
|
||||
| `guest_session_id` | Локальный UUID устройства; не auth |
|
||||
| `ux_session_id` | Аналитическая сессия |
|
||||
| `bitrix_contact_id` | Contact в CRM (после sync) |
|
||||
| `device_id` | Opaque id устройства в bootstrap / session-start; в audit/log не копируется как PII |
|
||||
|
||||
Публичные id — **UUID** (в App DB предпочтительно UUID v7, как в остальных доменах).
|
||||
|
||||
---
|
||||
|
||||
## 9. API
|
||||
|
||||
Общие конвенции — arch-02.
|
||||
|
||||
### 9.1. Клиентские (JWT)
|
||||
|
||||
| Метод и путь | Назначение |
|
||||
|---|---|
|
||||
| `POST /api/v1/auth/bootstrap` | Find-or-create пользователя + согласия |
|
||||
| `POST /api/v1/consents` | Повторная фиксация версий согласий |
|
||||
| `POST /api/v1/analytics/session-start` | Новая `UxSession` |
|
||||
| `GET /api/v1/me` | Readonly блочный профиль |
|
||||
| `GET /api/v1/me/documents` | Список документов (MVP может быть пустым) |
|
||||
| `GET /api/v1/documents/{id}` | Metadata документа (owner only) |
|
||||
| `GET /api/v1/documents/{id}/download-url` | Presigned GET + audit |
|
||||
|
||||
Auth у Keycloak: публичные OIDC endpoints через `/auth/*` (не часть `api-backend`).
|
||||
|
||||
### 9.2. Public (без JWT)
|
||||
|
||||
| Метод и путь | Назначение |
|
||||
|---|---|
|
||||
| `GET /api/v1/public/settings` (и связанные public) | Флаги auth, URL/версии согласий, OTP UI-параметры по `is_public` |
|
||||
|
||||
### 9.3. Internal (смежные)
|
||||
|
||||
| Метод и путь | Кто → кто | Назначение |
|
||||
|---|---|---|
|
||||
| `GET /internal/settings/v1/otp` | Keycloak SPI → api-backend | Продуктовые OTP limits |
|
||||
| `POST /internal/sms/v1/send` | Keycloak → sms-service | Заказ SMS OTP (real mode) |
|
||||
|
||||
### 9.4. Ошибки (домен пользователя)
|
||||
|
||||
| Код | HTTP | Когда |
|
||||
|---|---|---|
|
||||
| `phone_claim_missing` | 400 | Нет канонического телефона в JWT при bootstrap |
|
||||
| `validation_error` | 400 | Невалидные версии/тело согласий или device |
|
||||
| `unauthorized` | 401 | Нет/невалиден JWT |
|
||||
| `consents_required` | 403 | Обязательные согласия не приняты |
|
||||
| `resource_state_conflict` | 409 | Protected endpoint до bootstrap («bootstrap required»); **открытый вопрос TBD-1** по унификации с `404` для consents |
|
||||
| `profile_not_found` | 404 | Профиль не найден (по контракту envelope) |
|
||||
| `rate_limit_exceeded` | 429 | Превышен лимит |
|
||||
|
||||
Дифференцированные тексты ошибок OTP на UI (неверный код / истёк / лимит send / лимит verify) — backlog п.10; контракт Keycloak/frontend уточняется отдельно, в этом ТЗ фиксируется требование продукта.
|
||||
|
||||
### 9.5. Rate limiting
|
||||
|
||||
| Зона | Identity |
|
||||
|---|---|
|
||||
| Auth edge (`nginx`) | IP |
|
||||
| bootstrap / consents / session-start | user + IP |
|
||||
| OTP product limits | phone (Keycloak counters) |
|
||||
|
||||
---
|
||||
|
||||
## 10. Модель данных (схема `han_app`)
|
||||
|
||||
Общие правила — module-01 §9.1 / arch-05: UUID PK, `timestamptz` UTC, common fields, soft-delete `A`/`D`, FK `ON DELETE RESTRICT`.
|
||||
|
||||
### 10.1. `user_identities`
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `id` | uuid PK | `user_id` |
|
||||
| `keycloak_sub` | varchar(255) NOT NULL UNIQUE | JWT `sub` |
|
||||
| `phone_number` | varchar(32) NOT NULL | E.164 из JWT |
|
||||
| `last_login_at` | timestamptz NOT NULL | Обновляется на bootstrap |
|
||||
| common fields | обязательны | |
|
||||
|
||||
Индексы: unique `keycloak_sub`; index на `phone_number` для CRM map. **Телефон в App DB не unique:** identity master — Keycloak; временный конфликт при merge/миграции допустим на уровне данных, но продуктово один phone = один active `sub`.
|
||||
|
||||
### 10.2. `user_consents`
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `id` | uuid PK | |
|
||||
| `user_id` | uuid FK | |
|
||||
| `ux_session_id` | uuid NULL | Если сессия уже есть |
|
||||
| `consent_type` | varchar | `personal_data` \| `user_agreement` \| `marketing` |
|
||||
| `document_version` | varchar | Версия из `app_settings` |
|
||||
| `accepted` | boolean | |
|
||||
| `accepted_at` | timestamptz | |
|
||||
| `client_ip` | inet | |
|
||||
| `user_agent_hash` | varchar | |
|
||||
| device snapshot | jsonb / поля | По module-01 (`device_json` и т.п.) |
|
||||
| common fields | обязательны | |
|
||||
|
||||
Unique `(user_id, consent_type, document_version)`. Записи immutable.
|
||||
|
||||
### 10.3. `client_profiles`
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `id` | uuid PK | |
|
||||
| `user_id` | uuid UNIQUE FK | 1:1 с identity |
|
||||
| `bitrix_contact_id` | varchar/nullable | После успешного map |
|
||||
| `full_name` | varchar NULL | |
|
||||
| `citizenship` | varchar NULL | |
|
||||
| `russian_phone` | varchar NULL | Seed из auth-телефона |
|
||||
| `foreign_phone` | varchar NULL | |
|
||||
| `email` | varchar NULL | |
|
||||
| `source_updated_at` | timestamptz NULL | Метка источника sync |
|
||||
| common fields | обязательны | |
|
||||
|
||||
Partial unique на `bitrix_contact_id` среди active. PII не попадает в логи и generic audit payload.
|
||||
|
||||
### 10.4. `ux_sessions`
|
||||
|
||||
`id` = `ux_session_id`; `user_id`; `start_reason`; `platform`; `app_version`; `device_id`; `started_at`; common fields.
|
||||
|
||||
### 10.5. Триггеры sync
|
||||
|
||||
| Событие | `task_type` |
|
||||
|---|---|
|
||||
| Insert active `UserIdentity` / `ClientProfile` без mapping | `contact.map_or_create` |
|
||||
| Изменение tracked profile / auth-phone полей | `contact.update` |
|
||||
|
||||
Подавление эха: GUC `han.sync_suppress` при записи из `bitrix-sync`. Ошибка CRM не откатывает bootstrap и чат.
|
||||
|
||||
---
|
||||
|
||||
## 11. Фоновые и смежные процессы
|
||||
|
||||
| Процесс | Владелец | Связь с пользователем |
|
||||
|---|---|---|
|
||||
| OTP challenge / counters / expiry | Keycloak SPI | До появления App user |
|
||||
| SMS order / delivery journal | `sms-service` | Только доставка кода |
|
||||
| `contact.map_or_create` / `contact.update` | `bitrix-sync` | После bootstrap / изменения профиля |
|
||||
| Token refresh / logout | Frontend + Keycloak | Не трогает App DB identity |
|
||||
| Retention UX-сессий (если введён) | ops / module | Не удаляет `UserIdentity` |
|
||||
|
||||
---
|
||||
|
||||
## 12. Audit и observability
|
||||
|
||||
| `event_type` | Actor | Когда |
|
||||
|---|---|---|
|
||||
| `auth.bootstrap` | user | Успешный bootstrap |
|
||||
| `consent.recorded` | user | Запись согласий (bootstrap или `/consents`) |
|
||||
| `session_start` | user | Новая UX-сессия |
|
||||
| `document.download_url_issued` | user | Скачивание документа профиля |
|
||||
| OTP security events | Keycloak | Send/verify attempts (schema `keycloak`, phone HMAC/masked) |
|
||||
|
||||
В audit **нет:** полного phone/email/name в свободном тексте логов общего контура, OTP raw code, tokens, presigned URL.
|
||||
|
||||
Метрики (минимум): число bootstrap/сутки, доля `consents_required`, доля `phone_claim_missing`, latency map Contact, доля пользователей без `bitrix_contact_id` спустя N минут после входа.
|
||||
|
||||
---
|
||||
|
||||
## 13. Хранение данных и PII
|
||||
|
||||
- `UserIdentity`, `ClientProfile`, `UserConsent` — прикладные строки, soft-delete, физическое удаление запрещено (arch-05).
|
||||
- Auth-мастер PII телефона — Keycloak; App DB держит кэш для FK/CRM/UI.
|
||||
- Согласия хранятся бессрочно как юридически значимый журнал (immutable rows).
|
||||
- Гостевые локальные согласия на устройстве до OTP **не** являются серверным журналом и при сбое до bootstrap могут быть потеряны — клиент проходит согласия снова.
|
||||
- Right-to-erasure / удаление аккаунта клиентом — вне scope MVP; потребует отдельной политики по IdP + App DB + CRM.
|
||||
|
||||
---
|
||||
|
||||
## 14. Смежные сервисы
|
||||
|
||||
| Компонент | Ответственность в домене User |
|
||||
|---|---|
|
||||
| **Frontend** | Гость/ЛК, согласия UI, OTP UX, tokens, refresh, bootstrap/session-start, профиль readonly, отложенное сообщение после входа |
|
||||
| **Keycloak** | IdP, OTP, phone uniqueness, tokens, sessions |
|
||||
| **api-backend** | Bootstrap, consents, me/profile, JWT validation, ownership, settings bridge OTP |
|
||||
| **sms-service** | Durable order SMS (real mode) |
|
||||
| **bitrix-sync** | Map/update Contact; **не** создаёт UserIdentity |
|
||||
| **nginx** | `/auth/*`, edge rate limit auth |
|
||||
| **App DB** | Таблицы §10, триггеры sync |
|
||||
|
||||
Порядок работ (если дорабатывать домен): (1) Keycloak OTP + phone claims → (2) bootstrap + consents + identity/profile → (3) session-start → (4) me/profile UI → (5) CRM map → (6) documents post-MVP.
|
||||
|
||||
---
|
||||
|
||||
## 15. Влияние на arch-документы
|
||||
|
||||
| Документ | Статус относительно этой постановки |
|
||||
|---|---|
|
||||
| `arch-00-glossary.md` | Термины `UserIdentity`, `ClientProfile`, `UserConsent`, `UxSession`, `guest_session_id`, `keycloak_sub` уже заданы |
|
||||
| `arch-01-system-architecture.md` | Потоки гостя, OTP, возврата, профиля — канон сценариев |
|
||||
| `arch-02-api-contracts.md` | Контракты bootstrap / consents / session-start / me |
|
||||
| `arch-04-settings-and-content.md` | `auth.*`, `otp.phone.*`, `consent.*`, `ux.session.*` |
|
||||
| `module-01-api-backend.md` | Таблицы и алгоритмы bootstrap |
|
||||
| `module-08-keycloak.md` | OTP-only phone flow |
|
||||
| `module-07-bitrix-sync.md` | Обработка `contact.*` задач |
|
||||
|
||||
Этот документ **не заменяет** module-спеки; он собирает бизнес-смысл сущности «Пользователь» для аналитики и смежных фич (уведомления, чат, документы).
|
||||
|
||||
---
|
||||
|
||||
## 16. Критерии приёмки
|
||||
|
||||
1. Гость видит только public API; write без JWT недоступен; `guest_session_id` не открывает API.
|
||||
2. Вход только по телефону + OTP; пароль/email/social отсутствуют.
|
||||
3. После OTP `bootstrap` создаёт/находит `UserIdentity`, минимальный `ClientProfile`, пишет согласия; телефон берётся из JWT, не из body.
|
||||
4. Повторный bootstrap идемпотентен; `last_login_at` обновляется.
|
||||
5. Без обязательных согласий — `403 consents_required`; без phone claim — `400 phone_claim_missing`.
|
||||
6. Protected endpoint до bootstrap — безопасный отказ (`409` до закрытия TBD-1).
|
||||
7. Возврат с валидным refresh — без OTP; провал refresh — гостевой UI.
|
||||
8. `session-start` только с JWT; в гостевом режиме не вызывается; idle/cold/first_launch создают новую UX-сессию.
|
||||
9. `GET /me` отдаёт блочный readonly профиль; PATCH/PUT нет.
|
||||
10. Ошибка CRM sync не ломает вход; Contact мапится асинхронно.
|
||||
11. Один active phone ↔ один active `sub` на стороне Keycloak; merge не происходит молча при login.
|
||||
12. Отложенное сообщение / популярный вопрос после auth уходит штатно; ошибка доставки не маскируется под ошибку входа.
|
||||
13. Выход очищает tokens и возвращает в гостевой UI, не удаляя `UserIdentity`.
|
||||
14. PII не светится в обычных логах/audit payload; OTP code не логируется.
|
||||
|
||||
---
|
||||
|
||||
## 17. Журнал решений и открытых вопросов (ненормативно)
|
||||
|
||||
### 17.1. Принятые решения
|
||||
|
||||
| # | Решение | Раздел / источник |
|
||||
|---|---|---|
|
||||
| D1 | Два режима: гость и авторизованный; гостевые данные в App DB не пишутся | §2, arch-01 |
|
||||
| D2 | Слои IdP / UserIdentity / ClientProfile / Bitrix Contact разделены; master auth — Keycloak | §5.1 |
|
||||
| D3 | OTP-only phone; password disabled | §3, module-08 |
|
||||
| D4 | Согласия продуктовые в API; Keycloak их не заменяет; серверная запись только после JWT | §5.3 |
|
||||
| D5 | Телефон только из JWT claims | §5.4, arch-02 |
|
||||
| D6 | Профиль readonly и блочный; документы — отдельный блок, доставка post-MVP | §5.5 |
|
||||
| D7 | Bootstrap атомарный + идемпотентный; sync через триггеры БД | §7.3 |
|
||||
| D8 | UX-сессия ≠ auth; только для авторизованных; хранение id в памяти | §6.5, arch-00 |
|
||||
| D9 | CRM не блокирует авторизацию | §6.2 |
|
||||
| D10 | Один verified phone = один active `sub` | module-08 |
|
||||
|
||||
### 17.2. Открытые вопросы
|
||||
|
||||
| # | Вопрос | Предложение | Влияние |
|
||||
|---|---|---|---|
|
||||
| Q1 | Единый код для protected endpoint до bootstrap: `409` vs `404` (TBD-1 module-01) | Оставить `409 resource_state_conflict` | OpenAPI, клиентский UX |
|
||||
| Q2 | Дифференцированные ошибки OTP на UI (backlog п.10) | Зафиксировать словарь кодов Keycloak → frontend texts | module-08 + frontend |
|
||||
| Q3 | История устройств входа (backlog п.19) | Отдельная сущность/таблица, не смешивать с `UxSession` | Новая постановка |
|
||||
| Q4 | Debounce/backoff SMS после интеграции провайдера (backlog п.11) | Надстройка над `otp.phone.min_seconds_between_attempts` | Keycloak SPI |
|
||||
| Q5 | Тестовый пользователь с фиксированным SMS (backlog п.16) | Операционный allow-list / mock per-phone, не дырка в prod limits | ops + module-08 |
|
||||
| Q6 | Продуктовый self-service смены телефона | Позже: re-auth + OTP нового номера + invalidate sessions | Keycloak + bootstrap |
|
||||
| Q7 | Клиентское удаление аккаунта / right-to-erasure | Вне MVP; отдельная юридическая и техническая постановка | IdP + App + CRM |
|
||||
| Q8 | Расширение блоков профиля сверх «Личные данные» / «Документы» (как в Figma-моках) | Только после продуктового решения; Figma не канон | UI + `client_profiles` / новые таблицы |
|
||||
|
||||
---
|
||||
|
||||
## 18. Связь с уведомлениями
|
||||
|
||||
| Аспект | Гость | Авторизованный пользователь |
|
||||
|---|---|---|
|
||||
| Контур уведомлений | G (`guest_notifications`) | P (`notifications.user_id`) |
|
||||
| Бейдж непрочитанных | нет | да |
|
||||
| Центр уведомлений | auth-gate | список |
|
||||
| Перенос G → P при логине | **запрещён** | — |
|
||||
|
||||
Домен User задаёт, **кто** видит контур P; домен Notification задаёт **что** показывается. Владелец персональных записей — всегда `user_identities.id`.
|
||||
Reference in New Issue
Block a user