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 area | public |
| Feature group | contacts |
| Статус | approved |
| Документ | docs/06-services/identity/api/investor/request-email-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: запрос смены контакта логируется (маскированный email) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/investors/me/email/change-request |
| Success status | 200 |
| Idempotency header | не применяется (повтор гасит предыдущий Pending — INV-S1 Superseded) |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса (body RequestEmailChangeForm)
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | identity claims (sub) | Guid | Да | валидный JWT | 401 без токена |
newEmail | body | string | Да | min 1, max 256; формат email; нормализация lower-case; уникальность (INV-3) | PII: маскирование d***@mail |
IpAddress / UserAgent | middleware | string? | Нет | ≤ 45 / ≤ 512 | rate-limit / анти-фрод |
Модель ответа RequestEmailChangeCommandResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
emailSent | bool | Да | email-адаптер | — | Письмо с кодом поставлено в очередь отправки. |
realConfirmationCodeDispatched | bool | Да | verifier/адаптер | — | false — код не отправлялся реально (тестовый контур). |
Id запроса в ответ не входит — confirm ищет активный (Pending) запрос по (UserId, цель
EmailChange).
Алгоритм
- Контекст и доступ. Операцию выполняет аутентифицированный пользователь над собственной записью
(
UserIdиз токена); из middleware известныIpAddress/UserAgent. - Проверка входа и лимиты.
newEmailнепустой ≤ 256, формат email — иначеValidationError. Rate-limit (INV-S4, канал email) — превышение ⇒IDENTITY_CONFIRMATION_RATE_LIMITED(429). Уникальность (INV-3): адрес занят другим пользователем ⇒IDENTITY_USER_EMAIL_TAKEN; адрес равен текущему ⇒IDENTITY_EMAIL_NOT_CHANGED. - Проверки состояния. Получить пользователя; он
Active— иначеIDENTITY_USER_BLOCKED; контакты управляются самим пользователем (ContactsManagedBy == SelfService, INV-4) — иначеIDENTITY_CONTACTS_MANAGED_EXTERNALLY. - Правила и маппинг. Сгенерировать код (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). - Что меняется. В одном согласованном изменении: прежний активный (
Pending) запрос той же цели гасится вSuperseded(INV-S1) и создаётся новый запрос. - События. Интеграционных нет; доменное
ConfirmationCodeRequestedEvent→ email-адаптер (шаблон «код подтверждения email», timeout 5 с, 1 retry; результат →emailSent). - Результат.
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, WhenPOST .../email/change-request, Then созданPending-запросEmailChangeсNewEmail, письмо с кодом уходит на новый адрес, ответ200{emailSent, realConfirmationCodeDispatched}; id запроса в ответ не входит. - Given уже есть активный
Pending-запросEmailChange, When повторный запрос, Then прежний →Superseded(INV-S1), создаётся новый, отправляется новый код. - Given
newEmailзанят другим пользователем (INV-3), When запрос, ThenIDENTITY_USER_EMAIL_TAKEN(400). - Given
newEmailсовпадает с текущим, When запрос, ThenIDENTITY_EMAIL_NOT_CHANGED(400). - Given
ContactsManagedBy = TalentSync(менеджер застройщика, INV-4), When запрос, ThenIDENTITY_CONTACTS_MANAGED_EXTERNALLY(400). - Given невалидный формат/длина email, When запрос, Then
ValidationError(400). - Given учётка
Blocked/Archived, When запрос, ThenIDENTITY_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-login | re-login |
ValidationError | 400 | Невалидный формат/длина email | Список ошибок валидации | no retry |
IDENTITY_USER_EMAIL_TAKEN | 400 | Email занят другим пользователем (INV-3) | «Email уже используется» | no retry |
IDENTITY_EMAIL_NOT_CHANGED | 400 | newEmail совпадает с текущим | «Email не изменился» | no retry |
IDENTITY_CONTACTS_MANAGED_EXTERNALLY | 400 | ContactsManagedBy = TalentSync (INV-4) | «Контакты управляются внешней системой» | no retry |
IDENTITY_USER_BLOCKED | 400 | Status != Active | «Учётная запись заблокирована» | no retry |
IDENTITY_CONFIRMATION_RATE_LIMITED | 429 | INV-S4 | «Слишком много запросов кода» | retry ≥ 60 сек |
IDENTITY_EMAIL_DISPATCH_FAILED | 400 | Отказ email-адаптера | «Не удалось отправить письмо» | retry |
Совместимость и наблюдаемость
Отсутствие requestId в ответе — намеренная асимметрия контракта.
- Logs:
UserId, маскированный новый email; metrics:identity_email_change_requests_total{result}; traces — OTel.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (3): GET /api/v1/profile/me — Получение своего профиля ЛК (getMyProfile), Identity API — карта областей и реестр investor-области, POST /api/investors/me/email/change-confirm — Подтверждение смены email (confirmEmailChange)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: user.md (INV-3/4,
Email,EmailConfirmedAt), user-session.md (ConfirmationPurpose = EmailChange, INV-S8). - Продолжение: confirm-email-change.md.