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

POST /api/investors/me/email/change-confirm — Подтверждение смены email (confirmEmailChange)

Назначение

Второй шаг смены email: проверить код из письма (request-email-change.md) и применить новый адрес (Email = NewEmail, EmailConfirmedAt = now). Вызывается аутентифицированным пользователем. Новый email становится виден при следующем чтении профиля (get-current-investor.md).

Metadata

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

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

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

HTTP-контракт

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

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

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

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

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

Алгоритм

  1. Контекст и доступ. Операцию выполняет аутентифицированный пользователь над собственной записью (UserId из токена).
  2. Проверка входа. code непустой, ≤ 16 символов — иначе ValidationError.
  3. Проверки состояния. Получить пользователя; он Active — иначе IDENTITY_USER_BLOCKED; контакты управляются самим пользователем (ContactsManagedBy == SelfService, INV-4) — иначе IDENTITY_CONTACTS_MANAGED_EXTERNALLY. Найти его активный (Pending) запрос с целью EmailChange — нет запроса ⇒ IDENTITY_CONFIRMATION_INVALID; запрос не истёк (INV-S3); в запросе задан NewEmail (INV-S8).
  4. Правила и переходы. Подтверждение кода: счётчик попыток увеличивается до сравнения (INV-S2, 5-я неверная ⇒ Exhausted); сравнение с хэшем в постоянном времени (INV-S5); при успехе запрос Confirmed, ConfirmedAt = now. Повторная проверка уникальности email (INV-3, гонка между request и confirm): адрес занят другим пользователем ⇒ IDENTITY_USER_EMAIL_TAKEN. Применение: Email = NewEmail, EmailConfirmedAt = now; фиксируется доменное событие UserContactChangedEvent (вид Email, старое/новое — маскированные).
  5. Что меняется. В одном согласованном изменении обновляются запрос и пользователь; уникальность email окончательно гарантируется ограничением уникальности в хранилище (нарушение → IDENTITY_USER_EMAIL_TAKEN).
  6. События. Интеграционных нет (email не входит в снапшот pin.identity.users.v1); доменное UserContactChangedEvent — аудит.
  7. Результат. investorId; HTTP 200.

Диаграмма

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

ТипДетали
Интеграционные событияnone
Внешние вызовыnone
Проекции / поискnone
Уведомленияуведомление на старый email «адрес изменён» через сервис notifications

Ошибки и failure modes

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

Acceptance Criteria

  • Given активный self-service пользователь и непросроченный Pending-запрос EmailChange с заданным NewEmail, When confirm с верным code и свободным NewEmail, Then запрос → Confirmed, Email = NewEmail, EmailConfirmedAt = now, поднимается UserContactChangedEvent (Email, маскированные значения), ответ 200; новый email виден при следующем чтении профиля.
  • Given между request и confirm тот же адрес занял другой пользователь (INV-3), When confirm с верным code, Then IDENTITY_USER_EMAIL_TAKEN (400) — требуется новый request с другим адресом.
  • Given code неверный, When confirm, Then счётчик попыток растёт; на 5-й неверной — запрос → Exhausted и IDENTITY_CONFIRMATION_EXHAUSTED; до этого — IDENTITY_CONFIRMATION_INVALID (400).
  • Given запрос истёк (INV-S3) / нет Pending-запроса EmailChange, When confirm, Then IDENTITY_CONFIRMATION_EXPIRED / IDENTITY_CONFIRMATION_INVALID (400).
  • Given контакты управляются извне (ContactsManagedBy != SelfService, INV-4), When confirm, Then IDENTITY_CONTACTS_MANAGED_EXTERNALLY (400); Given Status != Active, Then IDENTITY_USER_BLOCKED (400).

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

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

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

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

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