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

POST /api/messenger/blacklist — Заблокировать пользователя

Добавить пользователя в чёрный список актора: запрет писать и звонить обеим сторонам, пока запись существует. Материализует агрегат BlacklistEntity (направленная запись Owner → Blocked); эффект для доменного сервиса CommunicationPolicyвзаимный deny (INV-BL-3). Владелец — контекст messenger, feature group blacklist. Блокировка приватна: заблокированному не раскрывается.

Назначение

Принять от авторизованного пользователя команду блокировки собеседника (с необязательной приватной причиной) и завести запись чёрного списка; идемпотентно при повторе (INV-BL-1).

Metadata

ПолеЗначение
Сервис/контекстmessenger
API areapublic
Feature groupblacklist
Статусdraft
Документdocs/06-services/messenger/api/blacklist/block-user.md

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

ПроверкаЗначение
Actor typeUser
Auth policy / Permissionsmessenger.blacklist.manage (JWT)
Scope (RBAC)платформа (person-level, вне tenant, ADR-0052)
Record-levelOwnerUserId = actor — владелец создаваемой записи (форсится сервером)
Tenant isolationЗапрос не tenanted (blacklist — вне tenant)
Auditcreate (CreatedByUserId = OwnerUserId, инвариант «создатель = владелец»); Reason в логи не пишется

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/messenger/blacklist
Success status201 (создано) / 200 (идемпотентный повтор существующей блокировки)
Idempotency headerне применяется (бизнес-идемпотентность по паре Owner,Blocked, шаг 4)
Correlationсквозной trace через traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
OwnerUserIdidentity claimsGuidДаRequired (= actor); из тела не принимаетсяRecord-level (own-only)
BlockedUserIdbodyGuidДа!= Guid.Empty; != OwnerUserId (INV-BL-1); существует в реплике (INV-BL-6)
Reasonbodystring?Нет0..256 (varchar(256))приватная заметка; в логи не пишется

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

ПолеТипОбязательностьИсточникMaskingОписание
BlacklistIdGuidДаBlacklistEntity.IdИдентификатор записи блокировки.
BlockedUserIdGuidДаBlacklistEntity.BlockedUserIdКого заблокировали.
CreatedAtDateTimeOffsetДаBlacklistEntity.CreatedAtМомент блокировки.
AlreadyBlockedboolДаcomputed (шаг 4)true — идемпотентный повтор (запись уже была).

Алгоритм

  1. Контекст и доступ. Операцию выполняет авторизованный пользователь (User); OwnerUserId = актор (из тела не принимается); требуется право messenger.blacklist.manage (иначе MESSENGER_BLACKLIST_FORBIDDEN).
  2. Проверка входа. BlockedUserId непустой и != OwnerUserId (иначе MESSENGER_BLACKLIST_SELF, INV-BL-1); Reason — не длиннее 256 символов, иначе ошибка валидации.
  3. Проверки предусловий. Блокируемый пользователь существует в реплике (INV-BL-6), иначе MESSENGER_BLACKLIST_USER_NOT_FOUND. Проверить, нет ли уже записи блокировки пары Owner → Blocked.
  4. Идемпотентность и создание. Если запись пары уже есть — идемпотентный возврат: существующая запись с AlreadyBlocked = true, без изменений. Иначе создать запись BlacklistEntity (Owner → Blocked, Reason, CreatedByUserId = OwnerUserId). Эффект: для пары CommunicationPolicy.CanMessage/CanCall становится deny (INV-BL-3); активный контакт пары (contact.md) при этом не удаляется (INV-BL-2).
  5. Реалтайм-уведомление. В той же согласованной операции только инициатору (Owner) уходит realtime- сигнал BlacklistChanged через SignalR-hub (UI прячет кнопки «написать»/«позвонить»). Заблокированному ничего не отправляется. Интеграционных Kafka-событий нет — блокировка приватна, кросс-сервисных потребителей нет.
  6. Согласованность. Запись блокировки и реалтайм-эффект фиксируются атомарно; гонку одновременных BlockUser закрывает уникальный индекс ux_blacklist_owner_blocked (конфликт вставки → MESSENGER_BLACKLIST_CONFLICT, при повторе — идемпотентная обработка как в шаге 4).
  7. Результат. BlockUserResult → HTTP 201 (создано) либо 200 при AlreadyBlocked = true.

Диаграмма

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

ТипДетали
Integration eventsnone (blacklist приватен; Kafka/outbox не используется)
External callsnone
Cache / projections / searchCommunicationPolicy для пары становится deny (вычисляется на чтении; кэша нет)
Notificationsrealtime BlacklistChanged в SignalR-hub — только Owner; заблокированному — ничего

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400пустой/некорректный BlockedUserId, Reason > 256«Проверьте данные блокировки»no retry
MESSENGER_BLACKLIST_SELF400BlockedUserId == OwnerUserId (INV-BL-1)«Нельзя заблокировать себя»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
MESSENGER_BLACKLIST_FORBIDDEN403нет permission messenger.blacklist.manage«Недостаточно прав»no retry
MESSENGER_BLACKLIST_USER_NOT_FOUND404BlockedUserId нет в реплике (INV-BL-6)«Пользователь не найден»no retry
MESSENGER_BLACKLIST_CONFLICT409гонка вставки (unique-нарушение)«Повторите попытку»retry (перечитка → идемпотентный ответ)

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

  • Новые optional поля тела/ответа — additive; удаление/переименование/смена типа — breaking (migration + rollout). Reason — приватная заметка, наружу заблокированному не отдаётся.
  • Logs: user blocked (OwnerUserId, BlockedUserId, AlreadyBlocked; Reason/ФИО не пишутся); metrics: messenger_blacklist_block_total{result}, latency p95; traces — сквозной trace (traceparent); dashboard pin-messenger.

Acceptance Criteria

  • Happy path. Given авторизованный Owner с messenger.blacklist.manage, существующий Blocked вне списка; When POST {blockedUserId, reason?}; Then 201, запись Owner → Blocked, CommunicationPolicy пары → deny обеим сторонам (INV-BL-3), realtime BlacklistChanged только Owner, AlreadyBlocked = false.
  • Идемпотентный повтор. Given запись пары уже есть; When повторный POST; Then 200, AlreadyBlocked = true, новой строки и события нет (INV-BL-1).
  • Нельзя себя. Given BlockedUserId == OwnerUserId; When POST; Then 400 MESSENGER_BLACKLIST_SELF (INV-BL-1).
  • Нет пользователя. Given BlockedUserId отсутствует в реплике; When POST; Then 404 MESSENGER_BLACKLIST_USER_NOT_FOUND (INV-BL-6).
  • Гонка вставки. Given два параллельных Block на пару; When оба POST; Then одна строка (ux_blacklist_owner_blocked), проигравший → 409 MESSENGER_BLACKLIST_CONFLICT → перечитка → идемпотентный ответ.
  • Контакт не рвётся. Given Accepted-контакт пары; When POST; Then контакт не удалён (INV-BL-2), но CanMessage/CanCalldeny.
  • Приватность/PII. На любом исходе заблокированному realtime не уходит; Reason/ФИО в логи не пишутся.

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

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

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