# Бизнес-постановка: Пользователь (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 без CRM-идентификаторов | UI-поля — последнее успешно синхронизированное значение (входящий поток MVP — Bitrix24); auth-телефон инициирует sync, но master телефона — Keycloak | | **Bitrix Contact** | CRM Bitrix24 | Карточка клиента в CRM | Bitrix24 для CRM-полей; связь `user_id ↔ b24_id` хранится только в `bitrix_sync.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 | Последний успешный sync (MVP: Bitrix → App) | `client_profiles` | | `foreign_phone` | Не синхронизируется в первом релизе | `client_profiles` | | Связь с Contact | `bitrix-sync` | Только `bitrix_sync.entity_external_mapping`; в App DB не кэшируется | | 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` | Аналитическая сессия | | `b24_id` | Внутренний идентификатор Contact; используется только `bitrix-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 | | `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 | обязательны | | CRM Contact ID и mapping в `client_profiles` отсутствуют. 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` / `contact.deactivate` | `bitrix-sync` | После bootstrap / изменения телефона / деактивации | | `contact.rebind` | `bitrix-sync` | Audited административное исправление ошибочного mapping | | 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 и доля пользователей без active mapping спустя N минут считаются `bitrix-sync` по собственной схеме. --- ## 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`.