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

GET /api/v1/companies/my/users — Список пользователей компании

Постраничный список учёток своей компании с фильтрами по статусу/роли и поиском по ФИО/телефону/email (поисковая проекция UserSearchDocument). Основа экрана «Команда» ЛК агентства/застройщика для управления менеджерами и агентами.

Назначение

Руководитель (или пользователь с правом просмотра команды) видит всех пользователей организации: имя, контакты, статус, назначенные роли. Owning-контекст — identity (дом полей — UserEntity, RoleDefinition). Feature group — companies.

Metadata

ПолеЗначение
Сервис/контекстidentity
API areapublic
Feature groupcompanies
Статусdraft
Документdocs/06-services/identity/api/company-user/list-company-users.md

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

ПроверкаЗначение
Actor typeUser (пользователь компании)
Auth policy / Permissionsidentity.users.view (дефолт: agency-owner, developer-manager, pin-manager, care-operator)
Scope (RBAC)организация: Active RoleAssignment со scope Platform или Organization(CompanyId)
Record-levelне применяется — выборка целиком ограничена CompanyId
Tenant isolationTenantId = CompanyId из claims; выборка ограничена company_id = TenantId (ADR-0052)
Auditnone (list без sensitive-раскрытия; телефоны отдаются в маскированном виде)

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/v1/companies/my/users
Success status200
Idempotency headerне применяется
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claimsGuidДаRequired
CompanyIdidentity claims (tenant_id)GuidДаRequiredTenant isolation
PagequeryintНет>= 1, default 1
PageSizequeryintНет1..100, default 20
StatusqueryUserStatus?Нетenum Invited/Active/Blocked/Archived; неизвестное значение → ValidationError
RoleIdqueryGuid?Нетроль этой компании или системная; фильтр по Active RoleAssignment
SearchTermquerystring?Нет2..64 симв.; поиск по ФИО/телефону/email через поисковую проекцию UserSearchDocumentне логируется как есть (может содержать PII)
Sortquerystring?Нетname / createdAt / lastSignInAt (+- префикс = desc), default name

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

ПолеТипОбязательностьИсточникMaskingОписание
ItemsIReadOnlyList<CompanyUserListItemModel>ДапроекцияСтраница пользователей.
PageintДаecho запросаТекущая страница.
PageSizeintДаecho запросаРазмер страницы.
TotalCountlongДаCount по фильтруВсего записей под фильтром.

CompanyUserListItemModel

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаUserEntity.IdId пользователя.
NameFullNameModel (FirstName, LastName, MiddleName?)Даowned VO FullNameФИО.
Typestring (enum UserType)ДаUserEntity.TypeAgent / DeveloperManager / PartnerManager / PinEmployee.
Statusstring (enum UserStatus)ДаUserEntity.StatusСтатус учётки.
PhoneNumberstringДаUserEntity.PhoneNumberмаска +7***1234; полный номер — только при identity.users.manageТелефон (PII).
Emailstring?НетUserEntity.Emailмаска d***@mail; полный — при identity.users.manageEmail (PII).
ContactsManagedBystring (enum)ДаUserEntity.ContactsManagedBySelfService / TalentSync (фронт скрывает кнопки редактирования).
RolesIReadOnlyList<UserRoleBriefModel>Даjoin RoleAssignment (Active) × RoleDefinitionАктивные роли в scope этой организации.
LastSignInAtDateTimeOffset?НетUserEntity.LastSignInAtПоследний вход.
CreatedAtDateTimeOffsetДаUserEntity.CreatedAtДата создания.

UserRoleBriefModel

ПолеТипОбязательностьИсточникMaskingОписание
AssignmentIdGuidДаRoleAssignment.IdId назначения (для отзыва).
RoleIdGuidДаRoleAssignment.RoleIdId роли.
RoleCodestringДаRoleDefinition.CodeМашинный код.
RoleNamestringДаRoleDefinition.NameНазвание.
ScopeTypestring (enum ResourceScopeType)ДаResourceScope.TypeOrganization / ProjectGroup / Project.
ScopeProjectGroupIdGuid?НетResourceScope.ProjectGroupIdГруппа проектов (chessboard).
ScopeProjectIdGuid?НетResourceScope.ProjectIdПроект (chessboard).
ExpiresAtDateTimeOffset?НетRoleAssignment.ExpiresAtСрок действия.

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь компании; tenant — его компания (CompanyId); требуется право identity.users.view в scope Organization(CompanyId). Отсутствие claim tenant_id (инвестор) → IDENTITY_COMPANY_NOT_FOUND.
  2. Проверка входа. Синтаксис по таблице входных данных (paging, enum, SearchTerm); иначе — ValidationError. Если задан фильтр RoleId — роль должна существовать и быть этой компании либо системной; иначе — IDENTITY_ROLE_NOT_FOUND. Существование компании отдельно не проверяется — claim выдан по живой компании, пустой список валиден.
  3. Что читаем.
    • Без SearchTerm: страница пользователей компании (CompanyId, не удалённые) с опциональными фильтрами по Status и RoleId (по активным назначениям), сортировкой Sort и пагинацией; секрет-поля (хэши пароля/пин-кода) не выбираются.
    • С SearchTerm: релевантный поиск по ФИО/телефону/email через поисковую проекцию UserSearchDocument, сужённый к CompanyId до запроса (multi-match с fuzziness) — возвращает ранжированные UserId и общее число; затем эти пользователи гидрируются из основной БД (той же проекцией без секрет-полей) с сохранением порядка; фильтр RoleId применяется к загруженной странице; сортировка — по релевантности (Sort игнорируется).
    • Для страницы дозагружаются активные назначения пользователей в scope компании и роли этих назначений (для Code/Name).
  4. Маппинг. Поле-в-поле по таблице item-модели; Roles — группировка назначений по пользователю с названием роли. Маскирование: если у актора нет identity.users.manage, PhoneNumber+7***1234, Emaild***@mail (полные значения — только при наличии этого права).
  5. Результат. Страница пользователей с общим числом записей под фильтром (200). Актуальность — read-your-writes из основной БД.

Диаграмма

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

ТипДетали
Проекции / поискпри заданном SearchTerm — чтение поисковой проекции UserSearchDocument; проекция не мутируется

Согласованность и свежесть

  • Без SearchTerm — выборка из основной БД, read-your-writes: только что приглашённый/изменённый пользователь виден сразу, счётчики и роли актуальны на момент запроса.
  • С SearchTerm — ранжирование по поисковой проекции UserSearchDocument, которую наполняют события pin.identity.users.v1 (окно свежести проекции — секунды). Гидрация найденных UserId и их роли берутся из основной БД — сами данные строки всегда актуальны, лишь попадание в результат зависит от свежести проекции.
Задержка поиска для новых учёток

Только что созданная учётка может краткий промежуток не находиться по имени/телефону в поиске, хотя уже видна в обычном (не-поисковом) списке — до момента, пока UserSearchDocument не подхватит соответствующее событие pin.identity.users.v1.

Деградация при недоступности поисковой витрины

Список без relevance-поиска либо ошибка витрины — поведение по README identity. Рекомендуемое поведение UI — предложить фильтры вместо строкового поиска.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Некорректные paging/enum/SearchTermСписок ошибок полейno retry
IDENTITY_ROLE_NOT_FOUND400Фильтр RoleId указывает на несуществующую/чужую роль«Роль не найдена»no retry
IDENTITY_COMPANY_NOT_FOUND404Claim tenant_id отсутствует (инвестор)«Компания не найдена»no retry
FORBIDDEN403Нет identity.users.view в покрывающем scope«Недостаточно прав»no retry

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

  • Новые optional поля item-модели — additive; смена masking-правила — security review.
  • Logs: CompanyId, фильтры (без SearchTerm-значения), количество; metrics: identity_company_users_list_total{result}, latency histogram; traces: OTel span; dashboard identity-overview; runbook — не применяется.

Acceptance Criteria

  • Given пользователь с identity.users.view, When GET /companies/my/users без фильтров, Then 200; страница пользователей своей компании (не удалённых), TotalCount = число под фильтром; у каждого — активные роли в scope организации.
  • Given Status = Active, When GET, Then в ответе только Active-учётки; неизвестное значение StatusValidationError (400).
  • Given RoleId — роль этой компании, When GET, Then в ответе только пользователи с активным назначением этой роли; несуществующая/чужая роль → IDENTITY_ROLE_NOT_FOUND (400).
  • Given SearchTerm совпадает с частью ФИО сотрудника, When GET, Then сотрудник в выдаче, порядок — по релевантности (Sort игнорируется); только что созданная учётка может появиться в поиске с задержкой свежести проекции.
  • Given актор без identity.users.manage, When GET, Then PhoneNumber замаскирован (+7***1234), Emaild***@mail; с identity.users.manage — полные значения.
  • Given Page = 2, PageSize = 20, When GET, Then возвращается вторая страница, Page/PageSize эхо-ответом; PageSize > 100ValidationError (400).
  • Given у актора нет claim tenant_id, When GET, Then IDENTITY_COMPANY_NOT_FOUND (404).
  • Given актор без identity.users.view, When GET, Then FORBIDDEN (403).
  • Given в компании нет пользователей под фильтром, When GET, Then 200 с Items = [], TotalCount = 0 (не ошибка).

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

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

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