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

POST /api/investors/me/email/change-request — Запрос смены email (requestEmailChange)

Назначение

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

Metadata

ПолеЗначение
Сервис/контекстidentity
API areapublic
Feature groupcontacts
Статусapproved
Документdocs/06-services/identity/api/investor/request-email-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: запрос смены контакта логируется (маскированный email)

HTTP-контракт

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДавалидный JWT401 без токена
newEmailbodystringДаmin 1, max 256; формат email; нормализация lower-case; уникальность (INV-3)PII: маскирование d***@mail
IpAddress / UserAgentmiddlewarestring?Нет≤ 45 / ≤ 512rate-limit / анти-фрод

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

ПолеТипОбязательностьИсточникMaskingОписание
emailSentboolДаemail-адаптерПисьмо с кодом поставлено в очередь отправки.
realConfirmationCodeDispatchedboolДаverifier/адаптерfalse — код не отправлялся реально (тестовый контур).
Id запроса не возвращается

Id запроса в ответ не входит — confirm ищет активный (Pending) запрос по (UserId, цель EmailChange).

Алгоритм

  1. Контекст и доступ. Операцию выполняет аутентифицированный пользователь над собственной записью (UserId из токена); из middleware известны IpAddress/UserAgent.
  2. Проверка входа и лимиты. newEmail непустой ≤ 256, формат email — иначе ValidationError. Rate-limit (INV-S4, канал email) — превышение ⇒ IDENTITY_CONFIRMATION_RATE_LIMITED (429). Уникальность (INV-3): адрес занят другим пользователем ⇒ IDENTITY_USER_EMAIL_TAKEN; адрес равен текущему ⇒ IDENTITY_EMAIL_NOT_CHANGED.
  3. Проверки состояния. Получить пользователя; он Active — иначе IDENTITY_USER_BLOCKED; контакты управляются самим пользователем (ContactsManagedBy == SelfService, INV-4) — иначе IDENTITY_CONTACTS_MANAGED_EXTERNALLY.
  4. Правила и маппинг. Сгенерировать код (CSPRNG); создать запрос подтверждения (INV-S8: при EmailChange обязателен NewEmail): цель EmailChange, UserId, PhoneNumber = текущий (для rate-limit, см. user-session.md), NewEmail = нормализованный email, хэш кода (HMAC-SHA256), ExpiresAt = now + 5 мин, IpAddress, UserAgent; фиксируется доменное событие ConfirmationCodeRequestedEvent (→ email-адаптер, отправка на NewEmail).
  5. Что меняется. В одном согласованном изменении: прежний активный (Pending) запрос той же цели гасится в Superseded (INV-S1) и создаётся новый запрос.
  6. События. Интеграционных нет; доменное ConfirmationCodeRequestedEvent → email-адаптер (шаблон «код подтверждения email», timeout 5 с, 1 retry; результат → emailSent).
  7. Результат. emailSent, realConfirmationCodeDispatched; код в ответ не попадает; HTTP 200.

Диаграмма

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

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

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

  • Пред: пользователь аутентифицирован и Active; контактами управляет сам (ContactsManagedBy = SelfService, INV-4); newEmail валиден, не равен текущему и на момент проверки не занят (INV-3).
  • Пост (успех): создан Pending-запрос цели EmailChange с NewEmail (INV-S8, TTL 5 мин); прежний Pending той же цели погашен в Superseded (INV-S1); письмо с кодом поставлено в очередь на новый адрес. User.Email не меняется — смену применит только confirm-email-change.md.
Окно уникальности

Проверка занятости адреса на этом шаге — «мягкая» (по чтению). Жёсткая уникальность гарантируется при применении (unique-ограничение по email): если два пользователя запросили один и тот же новый адрес почти одновременно, оба получат письма, но подтвердить успеет только первый — второй на confirm получит IDENTITY_USER_EMAIL_TAKEN.

Acceptance Criteria

  • Given Active-инвестор с SelfService-контактами и свободным валидным newEmail, When POST .../email/change-request, Then создан Pending-запрос EmailChange с NewEmail, письмо с кодом уходит на новый адрес, ответ 200 {emailSent, realConfirmationCodeDispatched}; id запроса в ответ не входит.
  • Given уже есть активный Pending-запрос EmailChange, When повторный запрос, Then прежний → Superseded (INV-S1), создаётся новый, отправляется новый код.
  • Given newEmail занят другим пользователем (INV-3), When запрос, Then IDENTITY_USER_EMAIL_TAKEN (400).
  • Given newEmail совпадает с текущим, When запрос, Then IDENTITY_EMAIL_NOT_CHANGED (400).
  • Given ContactsManagedBy = TalentSync (менеджер застройщика, INV-4), When запрос, Then IDENTITY_CONTACTS_MANAGED_EXTERNALLY (400).
  • Given невалидный формат/длина email, When запрос, Then ValidationError (400).
  • Given учётка Blocked/Archived, When запрос, Then IDENTITY_USER_BLOCKED (400).
  • Given превышен лимит (INV-S4, канал email), When запрос, Then IDENTITY_CONFIRMATION_RATE_LIMITED (429).
  • Given email-адаптер отказал, When запрос, Then IDENTITY_EMAIL_DISPATCH_FAILED (400).

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/просрочен JWTклиент выполняет re-loginre-login
ValidationError400Невалидный формат/длина emailСписок ошибок валидацииno retry
IDENTITY_USER_EMAIL_TAKEN400Email занят другим пользователем (INV-3)«Email уже используется»no retry
IDENTITY_EMAIL_NOT_CHANGED400newEmail совпадает с текущим«Email не изменился»no retry
IDENTITY_CONTACTS_MANAGED_EXTERNALLY400ContactsManagedBy = TalentSync (INV-4)«Контакты управляются внешней системой»no retry
IDENTITY_USER_BLOCKED400Status != Active«Учётная запись заблокирована»no retry
IDENTITY_CONFIRMATION_RATE_LIMITED429INV-S4«Слишком много запросов кода»retry ≥ 60 сек
IDENTITY_EMAIL_DISPATCH_FAILED400Отказ email-адаптера«Не удалось отправить письмо»retry

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

Асимметрия контракта

Отсутствие requestId в ответе — намеренная асимметрия контракта.

  • Logs: UserId, маскированный новый email; metrics: identity_email_change_requests_total{result}; traces — OTel.

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

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

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