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

Event Contract: pin.identity.user-signed-in (UserSignedInKafkaEvent)

Версионируемый контракт интеграционного события «пользователь вошёл». Consumer-документы ссылаются сюда, не переописывая payload.

Назначение

Фиксирует каждый успешный вход (по СМС-коду / паролем / пин-кодом) и даёт downstream-контекстам факт входа без синхронного запроса: user-activity связывает anonymous_id-сессию с UserId и ведёт витрину сессий/устройств, notifications шлёт алерт о входе с нового устройства. Интеграционного события на каждый запрос подтверждения нет — только вход (правило user-session.md).

Metadata

ПолеЗначение
Event typepin.identity.user-signed-in
Topicpin.identity.sign-ins.v1
Partitions / key6 (1–10k одновременно активных, ADR-0056) / key = UserId
ContractVersion1
Produceridentity: подтверждение входа (СМС/пароль/пин); ротация refresh-токена входом не считается и события не публикует
Статусdraft
Атомарность публикации

Событие публикуется атомарно с созданием RefreshSession и обновлением UserEntity.LastSignInAt: до фиксации входа событие не отправляется, после — доставляется гарантированно (at-least-once). CreatedAt события = OccurredAt входа.

Payload UserSignedInKafkaEvent

Дом полей — entity: RefreshSession (раздел «Доменные и интеграционные события»: UserId, SessionId, SignInMethod, Device.Platform, OccurredAt).

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
UserIdGuidДаRefreshSession.UserIdImmutableВошедший пользователь (= Kafka key).
SessionIdGuidДаRefreshSession.IdImmutableСозданная refresh-сессия (claim sid access-JWT); связка для витрины сессий user-activity.
SignInMethodstring (enum SignInMethod)ДаRefreshSession.SignInMethodAdditive only (новые значения)Sms / Password / PinCode (значение Refresh в этом событии не встречается — ротация входом не считается).
DevicePlatformstring?НетRefreshSession.Device.PlatformAdditive onlyweb / ios / android; null — не определена.
DeviceDisplayNamestring?НетRefreshSession.Device.DisplayNameAdditive onlyЧитаемое имя устройства («Chrome, Windows», ≤ 128) — для текста алерта о новом устройстве.
IsFirstSignInboolДапризнак первого входа: до входа у учётки не было LastSignInAtAdditive onlyПервый вход учётки (перевод Invited → Active, welcome-сценарии).
OccurredAtDateTimeOffsetДаRefreshSession.IssuedAtImmutableМомент входа (дублирует CreatedAt события для удобства потребителя).

Вложенных моделей нет.

PII-минимизация

Полный слепок устройства (UserAgent, IpAddress) в payload не входит; витрина сессий user-activity получает их отдельным internal-контрактом интроспекции при отображении списка сессий (source of truth — RefreshSession).

Когда публикуется

УсловиеПереход состоянияПубликация
Подтверждение входа по СМС-коду (ConfirmationPurpose = SignIn подтверждён кодом)SignInConfirmationRequest: Pending → Confirmed; RefreshSession: [*] → Active; UserEntity: Invited → Active (первый вход)атомарно со сменой состояния (запрос + сессия + учётка)
Вход паролем (при включённой двухфакторной аутентификации — после СМС-подтверждения)RefreshSession: [*] → Activeта же
Вход пин-кодомта жета же

Потребители (consumers)

Сверено с user-session.md и service-boundaries.md.

Consumer-контекстЭффектИдемпотентностьЗадержка (SLA)
user-activityСвязывание активной anonymous_id-сессии с UserId (события до входа приписываются пользователю); запись в витрину сессий/устройствMessageId (повтор — no-op: связка уже создана)< 1 мин
notificationsАлерт «вход с нового устройства» (сравнение DevicePlatform+DeviceDisplayName с известными сессиями пользователя); welcome-сообщение при IsFirstSignInMessageId< 1 мин

Порядок и согласованность

  • Факт, не снапшот. Событие append-only; потребитель дедуплицирует по MessageId и не нуждается в Version-guard (порядок входов per-user обеспечен ключом UserId, но факты независимы).
  • Разные топики с user-upserted. Для нового пользователя (регистрация в UC-IDN-001) вход публикуется в pin.identity.sign-ins.v1, а реплика — в pin.identity.users.v1 (разные топики, межтопиковый порядок не гарантирован): user-activity/notifications могут получить факт входа раньше реплики пользователя. Потребитель терпит отсутствие реплики (буфер/first-seen); флаг IsFirstSignIn позволяет отработать welcome-сценарий без обращения в identity.
  • Ротация не порождает событие. Обновление access-JWT ротацией refresh-токена входом не считается и в этот топик не пишет — иначе алерт «новое устройство» ложно срабатывал бы на каждый refresh.

Версионирование и совместимость

  • ContractVersion = 1; несоответствие версии события — warn-and-proceed.
  • Additive-only без bump; breaking — ContractVersion = 2 + топик pin.identity.sign-ins.v2 + dual-publish + sunset.
  • Unknown enum SignInMethod — fallback: обрабатывать как обычный вход, warning.
  • Событие — факт (append-only, не снапшот): потребители не нуждаются в version-guard, только в дедупликации по MessageId.

Безопасность

ПолеКласс данныхПравило
UserId, SessionId, SignInMethod, IsFirstSignIn, OccurredAtInternalinclude
DevicePlatform, DeviceDisplayNameInternal (обобщённые, не сырой UA)include
IpAddress, UserAgent (сырые)PIIexclude из события; живут только в RefreshSession/SessionDevice c маскированием в логах
Исключённые данные

Событие НЕ содержит токенов (access/refresh), кодов подтверждения, телефона.

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

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

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

  • Сущности: user-session.md (RefreshSession, SessionDevice, INV-S6/S7), user.md (LastSignInAt, Invited → Active).
  • Витрина сессий — docs/06-services/user-activity/ (ADR-0054); source of truth отзыва — revoke-user-sessions и self-service revoke.