Перейти к основному содержимому

POST /api/investors/me/phone/change-confirm — Подтверждение смены телефона (confirmPhoneChange)

Назначение

Второй шаг смены номера: проверить СМС-код, отправленный на новый номер (request-phone-change.md), и применить новый номер (PhoneNumber = NewPhoneNumber, PhoneNumberConfirmedAt = now). Вызывается аутентифицированным пользователем. Меняется основной логин СМС-входа — новое значение уходит снапшот-событием доверенным потребителям.

Metadata

ПолеЗначение
Сервис/контекстidentity
API areapublic
Feature groupcontacts
Статусapproved
Документdocs/06-services/identity/api/investor/confirm-phone-change.md

Актор и доступ

ПроверкаЗначение
Actor typeUser (Bearer JWT)
Auth policy / Permissionsauthenticated; permission identity.security.manage-self
Scope (RBAC)self
Record-levelPending-запрос ищется строго по UserId актора + сверка newPhoneNumber
Tenant isolationне применяется (ADR-0052)
Auditupdate логина: событие UserContactChangedEvent (оба номера маскируются в логах)

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/investors/me/phone/change-confirm
Success status200
Idempotency headerне применяется; одноразовость — статус запроса Confirmed + оптимистическая блокировка
Correlationсквозной trace через traceparent

Входные данные / параметры запроса (body ConfirmPhoneChangeForm)

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДавалидный JWT401 без токена
newPhoneNumberbodystringДаmin 1, max 32; E.164; должен совпасть с request.NewPhoneNumberPII, маскирование
codebodystringДаmin 1, max 16никогда не логируется

Модель ответа ConfirmPhoneChangeCommandResult

ПолеТипОбязательностьИсточникMaskingОписание
investorIdstring (Guid)ДаUserEntity.IdИдентификатор пользователя.

Алгоритм

  1. Контекст и доступ. Операцию выполняет аутентифицированный пользователь над собственной записью (UserId из токена).
  2. Проверка входа. newPhoneNumber непустой ≤ 32 (нормализация E.164), code непустой ≤ 16 — иначе ValidationError.
  3. Проверки состояния. Получить пользователя; он Active — иначе IDENTITY_USER_BLOCKED; контакты управляются самим пользователем (ContactsManagedBy == SelfService, INV-4) — иначе IDENTITY_CONTACTS_MANAGED_EXTERNALLY. Найти его активный (Pending) запрос с целью PhoneChange — нет запроса ⇒ IDENTITY_CONFIRMATION_INVALID; запрос не истёк (INV-S3); номер во входе совпадает с request.NewPhoneNumber — иначе IDENTITY_CONFIRMATION_INVALID (код от другого номера неприменим).
  4. Правила и переходы. Подтверждение кода: счётчик попыток увеличивается до сравнения (INV-S2, 5-я неверная ⇒ Exhausted); сравнение с хэшем в постоянном времени (INV-S5); при успехе запрос Confirmed, ConfirmedAt = now. Повторная проверка уникальности номера (INV-1, гонка): номер занят ⇒ IDENTITY_USER_PHONE_TAKEN (паттерн DUPLICATE_PHONE). Применение: PhoneNumber = NewPhoneNumber, PhoneNumberConfirmedAt = now; фиксируется доменное событие UserContactChangedEvent (вид PhoneNumber, старое/новое — маскированные). Отзыв всех активных сессий пользователя (смена логина, INV-S7).
  5. Что меняется. В одном согласованном изменении обновляются запрос и пользователь; неверные попытки сохраняются до возврата ошибки; гонка двойного confirm разрешается оптимистической блокировкой; уникальность номера окончательно гарантируется ограничением уникальности в хранилище (INV-1, нарушение → IDENTITY_USER_PHONE_TAKEN).
  6. События. UserUpsertedKafkaEventpin.identity.users.v1 (ключ = UserId; снапшот с новым PhoneNumber в E.164 — только доверенные потребители crm/referral/messenger, а также Name, Type, CompanyId, Status, Version). Доменное UserContactChangedEvent — аудит.
  7. Результат. investorId; следующий вход — уже по новому номеру; HTTP 200.

Диаграмма

Побочные эффекты

ТипДетали
Интеграционные событияUserUpsertedKafkaEvent / pin.identity.users.v1 / ключ = UserId (новый PhoneNumber для реплик crm/referral/messenger)
Внешние вызовыnone
Проекции / поискnone
УведомленияСМС на старый номер «номер изменён» (security-алерт) через сервис notifications
Окно eventual-consistency и отзыв сессий

Смена номера — смена логина: отзываются все активные сессии пользователя (INV-S7), включая текущую, поэтому клиент разлогинивается и следующий вход выполняет уже по новому номеру (в отличие от смены пароля, где текущая сессия сохраняется). Новый PhoneNumber в E.164 уходит снапшотом UserUpsertedKafkaEvent; доверенные потребители (crm/referral/messenger) обновят свои реплики через секунды — до этого их поиск/контакты по номеру возвращают прежнее значение, что для пользователя выражается лишь в кратком лаге отображения нового номера у контрагентов.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/просрочен JWTклиент выполняет re-loginre-login
ValidationError400Формат/длины полейСписок ошибок валидацииno retry
IDENTITY_CONFIRMATION_INVALID400Нет Pending-запроса / номер не совпал с запросом / неверный код«Неверный код подтверждения»ввести заново
IDENTITY_CONFIRMATION_EXPIRED400INV-S3«Код истёк»новый request-шаг
IDENTITY_CONFIRMATION_EXHAUSTED400INV-S2«Превышено число попыток»новый request-шаг
IDENTITY_USER_PHONE_TAKEN400Номер занят (гонка, INV-1; паттерн DUPLICATE_PHONE)«Номер уже используется»новый request с другим номером
IDENTITY_CONTACTS_MANAGED_EXTERNALLY400INV-4«Контакты управляются внешней системой»no retry
IDENTITY_USER_BLOCKED400Status != Active«Учётная запись заблокирована»no retry

Acceptance Criteria

  • Given активный self-service пользователь и непросроченный Pending-запрос PhoneChange, вход с newPhoneNumber == request.NewPhoneNumber и верным code, свободный номер, When confirm, Then запрос → Confirmed, PhoneNumber = NewPhoneNumber, PhoneNumberConfirmedAt = now, все сессии отзываются, уходит UserUpsertedKafkaEvent (снапшот нового номера) + доменное UserContactChangedEvent, ответ 200; следующий вход — по новому номеру.
  • Given newPhoneNumber во входе не совпадает с request.NewPhoneNumber, When confirm, Then IDENTITY_CONFIRMATION_INVALID (400) — код от другого номера неприменим.
  • Given между request и confirm номер занял другой пользователь (INV-1), When confirm с верным code, Then IDENTITY_USER_PHONE_TAKEN (400) — требуется новый request с другим номером.
  • Given code неверный, When confirm, Then счётчик попыток растёт; на 5-й неверной — запрос → Exhausted и IDENTITY_CONFIRMATION_EXHAUSTED; до этого — IDENTITY_CONFIRMATION_INVALID (400).
  • Given запрос истёк (INV-S3) / нет Pending-запроса PhoneChange, When confirm, Then IDENTITY_CONFIRMATION_EXPIRED / IDENTITY_CONFIRMATION_INVALID (400).
  • Given контакты управляются извне (INV-4), When confirm, Then IDENTITY_CONTACTS_MANAGED_EXTERNALLY (400); Given Status != Active, Then IDENTITY_USER_BLOCKED (400).

Совместимость и наблюдаемость

  • Logs: UserId, результат, маскированные номера; metrics: identity_phone_change_confirms_total{result}; traces — OTel.

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы