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

POST /api/investors/me/phone/change-request — Запрос смены телефона (requestPhoneChange)

Назначение

Первый шаг смены номера телефона: проверить уникальность нового номера (INV-1, паттерн DUPLICATE_PHONE) и отправить СМС-код на новый номер (подтверждение владения новым номером). Вызывается аутентифицированным пользователем. Смену применит confirm-phone-change.md. Телефон — основной логин СМС-входа, поэтому операция максимально security-sensitive. Для ContactsManagedBy = TalentSync запрещена (INV-4).

Metadata

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

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

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

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/investors/me/phone/change-request
Success status200
Idempotency headerне применяется (повтор гасит предыдущий Pending — INV-S1 Superseded)
Correlationсквозной trace через traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДавалидный JWT401 без токена
newPhoneNumberbodystringДаmin 1, max 32; нормализация E.164; уникальность (INV-1)PII, маскирование
IpAddress / UserAgentmiddlewarestring?Нет≤ 45 / ≤ 512rate-limit / анти-фрод

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

ПолеТипОбязательностьИсточникMaskingОписание
phoneChangeConfirmationRequestIdstring (Guid)ДаSignInConfirmationRequest.IdИдентификатор запроса на смену телефона (confirm дополнительно сверяет newPhoneNumber).
realConfirmationCodeDispatchedboolДаverifierfalse — код не отправлялся реально (тестовый контур).

Алгоритм

  1. Контекст и доступ. Операцию выполняет аутентифицированный пользователь над собственной записью (UserId из токена); из middleware известны IpAddress/UserAgent.
  2. Проверка входа и лимиты. newPhoneNumber непустой ≤ 32, валидный E.164 (нормализация) — иначе ValidationError. Rate-limit (INV-S4, по новому номеру + IP, код уходит на новый) — превышение ⇒ IDENTITY_CONFIRMATION_RATE_LIMITED (429). Уникальность (INV-1, паттерн DUPLICATE_PHONE): номер занят ⇒ IDENTITY_USER_PHONE_TAKEN; номер равен текущему ⇒ IDENTITY_PHONE_NOT_CHANGED.
  3. Проверки состояния. Получить пользователя; он Active — иначе IDENTITY_USER_BLOCKED; контакты управляются самим пользователем (ContactsManagedBy == SelfService, INV-4) — иначе IDENTITY_CONTACTS_MANAGED_EXTERNALLY.
  4. Правила и маппинг. Сгенерировать код (CSPRNG); создать запрос подтверждения (INV-S8: при PhoneChange обязателен NewPhoneNumber): цель PhoneChange, UserId, PhoneNumber = новый номер (куда отправлен код), NewPhoneNumber = новый номер, хэш кода (HMAC-SHA256), ExpiresAt = now + 5 мин, IpAddress, UserAgent; фиксируется доменное событие ConfirmationCodeRequestedEvent (→ СМС-verifier, отправка на NewPhoneNumber).
  5. Что меняется. В одном согласованном изменении: прежний активный (Pending) запрос той же цели гасится в Superseded (INV-S1) и создаётся новый запрос.
  6. События. Интеграционных нет; доменное ConfirmationCodeRequestedEvent → СМС-verifier.
  7. Результат. phoneChangeConfirmationRequestId, realConfirmationCodeDispatched; HTTP 200.

Диаграмма

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

ТипДетали
Интеграционные событияnone
Внешние вызовыСМС-verifier: код на новый номер (timeout 5 с, 1 retry)
Проекции / поискrate-limit-счётчик
УведомленияСМС с кодом на новый номер

Предусловия и постусловия

  • Пред: пользователь аутентифицирован и Active; контактами управляет сам (ContactsManagedBy = SelfService, INV-4); newPhoneNumber валиден по E.164, не равен текущему и на момент проверки не занят (INV-1, паттерн DUPLICATE_PHONE).
  • Пост (успех): создан Pending-запрос цели PhoneChange с NewPhoneNumber (INV-S8, TTL 5 мин), PhoneNumber = новый (куда ушёл код); прежний Pending той же цели погашен в Superseded (INV-S1); СМС-код отправлен на новый номер. User.PhoneNumber не меняется — смену (и, по INV-S7, отзыв прочих сессий) применит только confirm-phone-change.md.
Окно гонки на уникальность номера

Проверка занятости номера здесь «мягкая» (по чтению); жёсткая уникальность (ux_users_phone_number) гарантируется при применении — при гонке двух смен на один номер подтвердит только первый, второй на confirm получит IDENTITY_USER_PHONE_TAKEN.

Acceptance Criteria

  • Given Active-инвестор с SelfService-контактами и свободным валидным newPhoneNumber, When POST .../phone/change-request, Then создан Pending-запрос PhoneChange с NewPhoneNumber, код уходит на новый номер, ответ 200 {phoneChangeConfirmationRequestId, realConfirmationCodeDispatched}.
  • Given уже есть активный Pending-запрос PhoneChange, When повторный запрос, Then прежний → Superseded (INV-S1), создаётся новый, отправляется новый код.
  • Given newPhoneNumber занят (INV-1), When запрос, Then IDENTITY_USER_PHONE_TAKEN (400).
  • Given newPhoneNumber равен текущему, When запрос, Then IDENTITY_PHONE_NOT_CHANGED (400).
  • Given ContactsManagedBy = TalentSync (INV-4), When запрос, Then IDENTITY_CONTACTS_MANAGED_EXTERNALLY (400).
  • Given невалидный формат/длина номера, When запрос, Then ValidationError (400).
  • Given учётка Blocked/Archived, When запрос, Then IDENTITY_USER_BLOCKED (400).
  • Given превышен лимит (INV-S4, по новому номеру + IP), When запрос, Then IDENTITY_CONFIRMATION_RATE_LIMITED (429).
  • Given verifier отказал, When запрос, Then IDENTITY_SMS_DISPATCH_FAILED (400).

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/просрочен JWTклиент выполняет re-loginre-login
ValidationError400Формат/длина номераСписок ошибок валидацииno retry
IDENTITY_USER_PHONE_TAKEN400Новый номер занят (INV-1; паттерн DUPLICATE_PHONE)«Номер уже используется»no retry
IDENTITY_PHONE_NOT_CHANGED400Новый номер равен текущему«Номер не изменился»no retry
IDENTITY_CONTACTS_MANAGED_EXTERNALLY400INV-4«Контакты управляются внешней системой»no retry
IDENTITY_USER_BLOCKED400Status != Active«Учётная запись заблокирована»no retry
IDENTITY_CONFIRMATION_RATE_LIMITED429INV-S4«Слишком много запросов кода»retry ≥ 60 сек
IDENTITY_SMS_DISPATCH_FAILED400Отказ verifier«Не удалось отправить код»retry

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

  • Logs: UserId, маскированные текущий/новый номера; metrics: identity_phone_change_requests_total{result}, identity_duplicate_phone_total; traces — OTel.

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

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

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