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

POST /api/investors/auth/sign-up-or-sign-in — Войти или зарегистрироваться (signUpOrSignIn)

Назначение

Основная точка входа формы входа: семантика AddOrGet — существующий инвестор получает шаги входа, новый регистрируется по телефону (+ опциональное имя) и получает СМС-код. Вызывается анонимным клиентом. Принимает referrerUrl (slug реферальной ссылки из cookie 30 дней) — фиксируется в учётке и уходит событием в контекст referral.

Metadata

ПолеЗначение
Сервис/контекстidentity
API areapublic
Feature groupauth
Статусapproved
Документdocs/06-services/identity/api/investor/sign-up-or-sign-in.md

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

ПроверкаЗначение
Actor typeAnonymous
Auth policy / Permissionsnone
Scope (RBAC)не применяется
Record-levelсвязка по PhoneNumber
Tenant isolationне применяется — создаётся/находится инвестор вне tenant (CompanyId = null, ADR-0052)
Auditcreate (регистрация) — actor-stamping CreatedByUserId = null (самостоятельная)

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/investors/auth/sign-up-or-sign-in
Success status200
Idempotency headerне применяется — операция идемпотентна по смыслу (повтор для существующего номера = вход)
Correlationсквозной trace через traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
phoneNumberbodystringДаmin 1, max 32; нормализация E.164PII: маскирование в логах
firstNamebodystring?Нетmax 128; используется только при регистрации нового инвестораPII
sendSmsCodebodybool?Нетtrue — отправить СМС-код даже при наличии пароля/пин-кода; для нового инвестора и при отсутствии пароля/пин-кода код отправляется в любом случае
referrerUrlbodystring?Нетmax 64; slug реферальной ссылки (из реф-cookie)не логируется вместе с номером (связка гость→реферал)
IpAddress / UserAgentmiddlewarestring?Нет≤ 45 / ≤ 512rate-limit / анти-фрод

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

ПолеТипОбязательностьИсточникMaskingОписание
outcomeSignUpOrSignInOutcomeДаcomputed: Registered — создан новый, SignedIn — найден существующийИтог AddOrGet.
nextStepSignInNextStepДаcomputed (как в request-sign-in-code.md; для нового всегда RequiresSmsCode)Следующий шаг входа.
nextActionAuthNextAction?Нетcomputed по флагам пользователя (FillName → FillPinCode → FillEmail → FillPassword, README.md)Подсказка UI, не блокирует.
signInConfirmationRequestIdstring (Guid)?НетSignInConfirmationRequest.Id (если код отправлялся)Для confirmSignIn.
realConfirmationCodeDispatchedbool?Нетverifierfalse — код реально не отправлялся (тестовый контур).

Алгоритм

  1. Контекст и доступ. Актор анонимный; из middleware известны IpAddress/UserAgent.
  2. Проверка входа и лимиты. phoneNumber непустой ≤ 32, firstName ≤ 128, referrerUrl ≤ 64 — иначе ValidationError; нормализация E.164. Rate-limit (INV-S4, по номеру/IP) при отправке кода — превышение ⇒ IDENTITY_CONFIRMATION_RATE_LIMITED (429).
  3. Проверки сущностей. Найти пользователя по нормализованному номеру (отсутствие → ветка регистрации). Для существующего: Status ∈ {Active, Invited} — иначе IDENTITY_USER_BLOCKED; блокировка входа снята (INV-8) — иначе IDENTITY_USER_LOCKED_OUT.
  4. Правила и маппинг.
    • Ветка Registered (пользователь не найден): создаётся инвестор — Type = Investor, CompanyId = null, Status = Active, Name = FullName(firstName ?? "", lastName: ""), PhoneNumber = нормализованный, RegisteredVia = SmsSignUp, ReferrerUrl = referrerUrl, ContactsManagedBy = SelfService, SyncSource = Pin (INV-1: уникальность номера; INV-2); фиксируется доменное событие UserRegisteredEvent; outcome = Registered, nextStep = RequiresSmsCode, отправка кода обязательна.
    • Ветка SignedIn (пользователь найден): outcome = SignedIn; nextStep по флагам (пароль → RequiresPassword, иначе пин-код → RequiresPinCode, иначе RequiresSmsCode); код отправляется если nextStep = RequiresSmsCode или sendSmsCode == true; firstName/referrerUrl существующего пользователя не меняются (INV-11 — имя дозаполняется только в confirmSignIn и только если пустое).
    • При отправке кода: сгенерировать код (CSPRNG); создать запрос подтверждения (цель SignIn, UserId, PhoneNumber = нормализованный, хэш кода, ExpiresAt = now + 5 мин, IpAddress, UserAgent) + доменное ConfirmationCodeRequestedEvent.
    • nextAction — первая применимая подсказка по данным пользователя (после создания — как правило FillName при пустом имени).
  5. Что меняется. В одном согласованном изменении: ветка Registered создаёт нового пользователя; при отправке кода прежний активный (Pending) запрос той же пары (номер, SignIn) гасится в Superseded (INV-S1) и создаётся новый запрос. Гонка одновременной регистрации одного номера отсекается ограничением уникальности номера → повторное выполнение по ветке SignedIn.
  6. События. Registered → UserUpsertedKafkaEventpin.identity.users.v1 (ключ = UserId; Id, Type = Investor, CompanyId = null, Status, Name, PhoneNumber, RegisteredVia = SmsSignUp, ReferrerUrl — по нему referral выполняет привязку реферала при регистрации, Version). SignedIn → интеграционных событий нет.
  7. Результат. outcome, nextStep, nextAction?, signInConfirmationRequestId?, realConfirmationCodeDispatched? (маппинг в таблице ответа); токен не выдаётся — вход завершает confirmSignIn/verifySignInPassword; HTTP 200.

Диаграмма

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

ТипДетали
Интеграционные событияUserUpsertedKafkaEvent / topic pin.identity.users.v1 / ключ = UserId — только в ветке Registered; потребители: referral (привязка по ReferrerUrl), profile, crm, messenger, user-activity, notifications
Внешние вызовыСМС-verifier (timeout 5 с, 1 retry)
Проекции / поискrate-limit-счётчики
УведомленияСМС с кодом

Кросс-сервисный контракт и окно согласованности

  • Предоставляет (только ветка Registered): UserUpsertedKafkaEvent в топик pin.identity.users.v1, ContractVersion = 1, ключ партиционирования = UserId (порядок событий по одному пользователю сохраняется). Снапшот: Id, Type = Investor, CompanyId = null, Status, Name, PhoneNumber (E.164 — только доверенным consumer-ам), RegisteredVia = SmsSignUp, ReferrerUrl, Version. Потребители: referral (привязка реферала по ReferrerUrl), profile (создание профиля), crm (лиды/ lookup), messenger (контакт), user-activity (связывание anonymous-сессии), notifications.
  • Ожидает от других: ничего синхронно, кроме внешнего СМС-verifier (синхронный адаптер, timeout 5 с, 1 retry). Реферальная привязка — забота контекста referral; identity лишь переносит ReferrerUrl.
  • Окно согласованности: учётка и её ReferrerUrl доступны в identity сразу после коммита; привязка реферала в referral, профиль, контакт и связывание anonymous-сессии происходят асинхронно (лаг — секунды) по мере обработки события consumer-ами. До этого пользователь уже вошёл (после confirmSignIn), но реферальная атрибуция/профиль могут быть ещё не видны. Ветка SignedIn интеграционных событий не порождает.

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

  • Пред: актор анонимный; phoneNumber нормализуется в E.164; поля формы в пределах длин; лимиты INV-S4 не превышены при отправке кода.
  • Пост (Registered): создан инвестор (Type = Investor, CompanyId = null, Status = Active, Name = FullName(firstName ?? "", ""), ReferrerUrl, ContactsManagedBy = SelfService, SyncSource = Pin); зафиксирован UserRegisteredEvent; опубликован UserUpsertedKafkaEvent; создан Pending-запрос SignIn; код отправлен. Токен не выдаётся.
  • Пост (SignedIn): новый пользователь не создаётся; firstName/referrerUrl существующего не меняются (INV-11); при отправке кода прежний Pending пары (номер, SignIn) → Superseded (INV-S1) и создан новый. Интеграционных событий нет.

Acceptance Criteria

  • Given незанятый номер, When signUpOrSignIn {phoneNumber, firstName?, referrerUrl?}, Then outcome = Registered, nextStep = RequiresSmsCode, создан инвестор с ReferrerUrl, опубликован UserUpsertedKafkaEvent (key = UserId), отправлен СМС-код; nextAction = FillName при пустом имени.
  • Given существующий номер с паролем, sendSmsCode не задан, When signUpOrSignIn, Then outcome = SignedIn, nextStep = RequiresPassword, СМС не отправляется, интеграционных событий нет, firstName/referrerUrl существующего не изменены.
  • Given существующий номер только с пин-кодом, When signUpOrSignIn, Then nextStep = RequiresPinCode, без СМС.
  • Given существующий номер с паролем и sendSmsCode = true, When signUpOrSignIn, Then код всё равно отправляется, возвращается signInConfirmationRequestId; прежний PendingSuperseded.
  • Given существующий номер без пароля и пин-кода, When signUpOrSignIn, Then nextStep = RequiresSmsCode, создан Pending-запрос SignIn, код отправлен.
  • Given существующая учётка Blocked/Archived, When signUpOrSignIn, Then IDENTITY_USER_BLOCKED (400).
  • Given LockoutUntil > now (INV-8), When signUpOrSignIn, Then IDENTITY_USER_LOCKED_OUT (400).
  • Given два параллельных запроса на один незанятый номер, When оба доходят до создания, Then уникальность номера пропускает одного (Registered), второй повторно выполняется по ветке SignedIn.
  • Given превышен лимит СМС (INV-S4), When отправка кода, Then IDENTITY_CONFIRMATION_RATE_LIMITED (429).
  • Given verifier отказал, When отправка кода, Then IDENTITY_SMS_DISPATCH_FAILED (400). Токен не выдаётся ни в одной ветке — вход завершает confirmSignIn/verifySignInPassword.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Формат/длины полей формыСписок ошибок валидацииno retry
IDENTITY_USER_BLOCKED400Существующий Status ∈ {Blocked, Archived}«Учётная запись заблокирована»no retry
IDENTITY_USER_LOCKED_OUT400LockoutUntil > now«Вход временно заблокирован»retry позже
IDENTITY_CONFIRMATION_RATE_LIMITED429INV-S4«Слишком много запросов кода»retry ≥ 60 сек
IDENTITY_SMS_DISPATCH_FAILED400Отказ verifier«Не удалось отправить код»retry

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

  • Неизвестное значение outcome/nextStep — клиент трактует как RequiresSmsCode (safe default). Новые optional поля — additive.
  • Logs: outcome, nextStep, маскированный номер, наличие referrerUrl (bool); metrics: identity_sign_up_or_sign_in_total{outcome}; traces — OTel.

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

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

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