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 area | public |
| Feature group | contacts |
| Статус | approved |
| Документ | docs/06-services/identity/api/investor/request-phone-change.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Bearer JWT) |
| Auth policy / Permissions | authenticated; permission identity.security.manage-self |
| Scope (RBAC) | self |
| Record-level | только собственная запись (UserId из sub) |
| Tenant isolation | не применяется (ADR-0052) |
| Audit | security-sensitive: запрос смены логина логируется (оба номера маскируются) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/investors/me/phone/change-request |
| Success status | 200 |
| Idempotency header | не применяется (повтор гасит предыдущий Pending — INV-S1 Superseded) |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса (body RequestPhoneChangeForm)
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | identity claims (sub) | Guid | Да | валидный JWT | 401 без токена |
newPhoneNumber | body | string | Да | min 1, max 32; нормализация E.164; уникальность (INV-1) | PII, маскирование |
IpAddress / UserAgent | middleware | string? | Нет | ≤ 45 / ≤ 512 | rate-limit / анти-фрод |
Модель ответа RequestPhoneChangeCommandResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
phoneChangeConfirmationRequestId | string (Guid) | Да | SignInConfirmationRequest.Id | — | Идентификатор запроса на смену телефона (confirm дополнительно сверяет newPhoneNumber). |
realConfirmationCodeDispatched | bool | Да | verifier | — | false — код не отправлялся реально (тестовый контур). |
Алгоритм
- Контекст и доступ. Операцию выполняет аутентифицированный пользователь над собственной записью
(
UserIdиз токена); из middleware известныIpAddress/UserAgent. - Проверка входа и лимиты.
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. - Проверки состояния. Получить пользователя; он
Active— иначеIDENTITY_USER_BLOCKED; контакты управляются самим пользователем (ContactsManagedBy == SelfService, INV-4) — иначеIDENTITY_CONTACTS_MANAGED_EXTERNALLY. - Правила и маппинг. Сгенерировать код (CSPRNG); создать запрос подтверждения (INV-S8: при
PhoneChangeобязателенNewPhoneNumber): цельPhoneChange,UserId,PhoneNumber = новый номер(куда отправлен код),NewPhoneNumber = новый номер, хэш кода (HMAC-SHA256),ExpiresAt = now + 5 мин,IpAddress,UserAgent; фиксируется доменное событиеConfirmationCodeRequestedEvent(→ СМС-verifier, отправка наNewPhoneNumber). - Что меняется. В одном согласованном изменении: прежний активный (
Pending) запрос той же цели гасится вSuperseded(INV-S1) и создаётся новый запрос. - События. Интеграционных нет; доменное
ConfirmationCodeRequestedEvent→ СМС-verifier. - Результат.
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, WhenPOST .../phone/change-request, Then созданPending-запросPhoneChangeсNewPhoneNumber, код уходит на новый номер, ответ200{phoneChangeConfirmationRequestId, realConfirmationCodeDispatched}. - Given уже есть активный
Pending-запросPhoneChange, When повторный запрос, Then прежний →Superseded(INV-S1), создаётся новый, отправляется новый код. - Given
newPhoneNumberзанят (INV-1), When запрос, ThenIDENTITY_USER_PHONE_TAKEN(400). - Given
newPhoneNumberравен текущему, When запрос, ThenIDENTITY_PHONE_NOT_CHANGED(400). - Given
ContactsManagedBy = TalentSync(INV-4), When запрос, ThenIDENTITY_CONTACTS_MANAGED_EXTERNALLY(400). - Given невалидный формат/длина номера, When запрос, Then
ValidationError(400). - Given учётка
Blocked/Archived, When запрос, ThenIDENTITY_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-login | re-login |
ValidationError | 400 | Формат/длина номера | Список ошибок валидации | no retry |
IDENTITY_USER_PHONE_TAKEN | 400 | Новый номер занят (INV-1; паттерн DUPLICATE_PHONE) | «Номер уже используется» | no retry |
IDENTITY_PHONE_NOT_CHANGED | 400 | Новый номер равен текущему | «Номер не изменился» | no retry |
IDENTITY_CONTACTS_MANAGED_EXTERNALLY | 400 | INV-4 | «Контакты управляются внешней системой» | no retry |
IDENTITY_USER_BLOCKED | 400 | Status != Active | «Учётная запись заблокирована» | no retry |
IDENTITY_CONFIRMATION_RATE_LIMITED | 429 | INV-S4 | «Слишком много запросов кода» | retry ≥ 60 сек |
IDENTITY_SMS_DISPATCH_FAILED | 400 | Отказ verifier | «Не удалось отправить код» | retry |
Совместимость и наблюдаемость
- Logs:
UserId, маскированные текущий/новый номера; metrics:identity_phone_change_requests_total{result},identity_duplicate_phone_total; traces — OTel.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (3): GET /api/v1/profile/me — Получение своего профиля ЛК (getMyProfile), Identity API — карта областей и реестр investor-области, POST /api/investors/me/phone/change-confirm — Подтверждение смены телефона (confirmPhoneChange)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: user.md (INV-1/4,
PhoneNumber), user-session.md (ConfirmationPurpose = PhoneChange,NewPhoneNumber, INV-S8). - Продолжение: confirm-phone-change.md.
- Конфликт телефона Talent-менеджера и инвестора (merge R6) — talent-replica.md.