Event Contract: pin.identity.user-signed-in (UserSignedInKafkaEvent)
Версионируемый контракт интеграционного события «пользователь вошёл». Consumer-документы ссылаются сюда, не переописывая payload.
Назначение
Фиксирует каждый успешный вход (по СМС-коду / паролем / пин-кодом) и даёт downstream-контекстам факт
входа без синхронного запроса: user-activity связывает anonymous_id-сессию с UserId и ведёт
витрину сессий/устройств, notifications шлёт алерт о входе с нового устройства. Интеграционного
события на каждый запрос подтверждения нет — только вход (правило user-session.md).
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.identity.user-signed-in |
| Topic | pin.identity.sign-ins.v1 |
| Partitions / key | 6 (1–10k одновременно активных, ADR-0056) / key = UserId |
| ContractVersion | 1 |
| Producer | identity: подтверждение входа (СМС/пароль/пин); ротация refresh-токена входом не считается и события не публикует |
| Статус | draft |
Событие публикуется атомарно с созданием RefreshSession и обновлением UserEntity.LastSignInAt:
до фиксации входа событие не отправляется, после — доставляется гарантированно (at-least-once).
CreatedAt события = OccurredAt входа.
Payload UserSignedInKafkaEvent
Дом полей — entity: RefreshSession (раздел «Доменные и
интеграционные события»: UserId, SessionId, SignInMethod, Device.Platform, OccurredAt).
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
UserId | Guid | Да | RefreshSession.UserId | Immutable | Вошедший пользователь (= Kafka key). |
SessionId | Guid | Да | RefreshSession.Id | Immutable | Созданная refresh-сессия (claim sid access-JWT); связка для витрины сессий user-activity. |
SignInMethod | string (enum SignInMethod) | Да | RefreshSession.SignInMethod | Additive only (новые значения) | Sms / Password / PinCode (значение Refresh в этом событии не встречается — ротация входом не считается). |
DevicePlatform | string? | Нет | RefreshSession.Device.Platform | Additive only | web / ios / android; null — не определена. |
DeviceDisplayName | string? | Нет | RefreshSession.Device.DisplayName | Additive only | Читаемое имя устройства («Chrome, Windows», ≤ 128) — для текста алерта о новом устройстве. |
IsFirstSignIn | bool | Да | признак первого входа: до входа у учётки не было LastSignInAt | Additive only | Первый вход учётки (перевод Invited → Active, welcome-сценарии). |
OccurredAt | DateTimeOffset | Да | RefreshSession.IssuedAt | Immutable | Момент входа (дублирует CreatedAt события для удобства потребителя). |
Вложенных моделей нет.
Полный слепок устройства (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-сообщение при IsFirstSignIn | MessageId | < 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, OccurredAt | Internal | include |
DevicePlatform, DeviceDisplayName | Internal (обобщённые, не сырой UA) | include |
IpAddress, UserAgent (сырые) | PII | exclude из события; живут только в RefreshSession/SessionDevice c маскированием в логах |
Событие НЕ содержит токенов (access/refresh), кодов подтверждения, телефона.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Правила реферальной привязки (attach-rules) — дом доменных правил, Entity / Aggregate: NotificationEntity
- API (1): POST /api/admin/v1/users/{userId}/sessions/revoke — Отзыв сессий пользователя (admin)
- Use Cases (4): UC-ACT-001. Трекинг сессии гостя и пользователя, UC-ACT-005. Слияние гостевой и пользовательской активности при входе, UC-REF-004. Авторизация со ссылкой в окне lazy-регистрации → связь + лид (правило R2), UC-REF-005. Авторизация: «Имя заполнено И регистрация ≥ 4 ч» → только лид, без связи (правило R3)
- События (2): Event Contract: pin.identity.user-upserted (UserUpsertedKafkaEvent), Event Contract: pin.user-activity.session-linked (SessionLinkedKafkaEvent)
- Консюмеры (4): Consumer: Identity Events → Notifications (notifications-identity-events), Consumer: Identity Sign-Ins (UserActivitySignedInConsumer), Consumer: ReferralUserSignedInConsumer (pin.identity.sign-ins.v1), Consumers контекста favorites: решение «consumed-событий нет»
- index.md (3): Identity Service (Pin.Identity), Referral Service, user-activity Service
- registries (1): Реестр событий PIN
Связанные документы
- Сущности: 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.