GET /api/v1/companies/my/users — Список пользователей компании
Постраничный список учёток своей компании с фильтрами по статусу/роли и поиском по ФИО/телефону/email
(поисковая проекция UserSearchDocument).
Основа экрана «Команда» ЛК агентства/застройщика для управления менеджерами и агентами.
Назначение
Руководитель (или пользователь с правом просмотра команды) видит всех пользователей организации:
имя, контакты, статус, назначенные роли. Owning-контекст — identity (дом полей —
UserEntity, RoleDefinition). Feature group — companies.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | identity |
| API area | public |
| Feature group | companies |
| Статус | draft |
| Документ | docs/06-services/identity/api/company-user/list-company-users.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (пользователь компании) |
| Auth policy / Permissions | identity.users.view (дефолт: agency-owner, developer-manager, pin-manager, care-operator) |
| Scope (RBAC) | организация: Active RoleAssignment со scope Platform или Organization(CompanyId) |
| Record-level | не применяется — выборка целиком ограничена CompanyId |
| Tenant isolation | TenantId = CompanyId из claims; выборка ограничена company_id = TenantId (ADR-0052) |
| Audit | none (list без sensitive-раскрытия; телефоны отдаются в маскированном виде) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /api/v1/companies/my/users |
| Success status | 200 |
| Idempotency header | не применяется |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | identity claims | Guid | Да | Required | — |
CompanyId | identity claims (tenant_id) | Guid | Да | Required | Tenant isolation |
Page | query | int | Нет | >= 1, default 1 | — |
PageSize | query | int | Нет | 1..100, default 20 | — |
Status | query | UserStatus? | Нет | enum Invited/Active/Blocked/Archived; неизвестное значение → ValidationError | — |
RoleId | query | Guid? | Нет | роль этой компании или системная; фильтр по Active RoleAssignment | — |
SearchTerm | query | string? | Нет | 2..64 симв.; поиск по ФИО/телефону/email через поисковую проекцию UserSearchDocument | не логируется как есть (может содержать PII) |
Sort | query | string? | Нет | name / createdAt / lastSignInAt (+- префикс = desc), default name | — |
Модель ответа GetCompanyUsersQueryResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Items | IReadOnlyList<CompanyUserListItemModel> | Да | проекция | — | Страница пользователей. |
Page | int | Да | echo запроса | — | Текущая страница. |
PageSize | int | Да | echo запроса | — | Размер страницы. |
TotalCount | long | Да | Count по фильтру | — | Всего записей под фильтром. |
CompanyUserListItemModel
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | UserEntity.Id | — | Id пользователя. |
Name | FullNameModel (FirstName, LastName, MiddleName?) | Да | owned VO FullName | — | ФИО. |
Type | string (enum UserType) | Да | UserEntity.Type | — | Agent / DeveloperManager / PartnerManager / PinEmployee. |
Status | string (enum UserStatus) | Да | UserEntity.Status | — | Статус учётки. |
PhoneNumber | string | Да | UserEntity.PhoneNumber | маска +7***1234; полный номер — только при identity.users.manage | Телефон (PII). |
Email | string? | Нет | UserEntity.Email | маска d***@mail; полный — при identity.users.manage | Email (PII). |
ContactsManagedBy | string (enum) | Да | UserEntity.ContactsManagedBy | — | SelfService / TalentSync (фронт скрывает кнопки редактирования). |
Roles | IReadOnlyList<UserRoleBriefModel> | Да | join RoleAssignment (Active) × RoleDefinition | — | Активные роли в scope этой организации. |
LastSignInAt | DateTimeOffset? | Нет | UserEntity.LastSignInAt | — | Последний вход. |
CreatedAt | DateTimeOffset | Да | UserEntity.CreatedAt | — | Дата создания. |
UserRoleBriefModel
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
AssignmentId | Guid | Да | RoleAssignment.Id | — | Id назначения (для отзыва). |
RoleId | Guid | Да | RoleAssignment.RoleId | — | Id роли. |
RoleCode | string | Да | RoleDefinition.Code | — | Машинный код. |
RoleName | string | Да | RoleDefinition.Name | — | Название. |
ScopeType | string (enum ResourceScopeType) | Да | ResourceScope.Type | — | Organization / ProjectGroup / Project. |
ScopeProjectGroupId | Guid? | Нет | ResourceScope.ProjectGroupId | — | Группа проектов (chessboard). |
ScopeProjectId | Guid? | Нет | ResourceScope.ProjectId | — | Проект (chessboard). |
ExpiresAt | DateTimeOffset? | Нет | RoleAssignment.ExpiresAt | — | Срок действия. |
Алгоритм
- Контекст и доступ. Операцию выполняет пользователь компании; tenant — его компания (
CompanyId); требуется правоidentity.users.viewв scopeOrganization(CompanyId). Отсутствие claimtenant_id(инвестор) →IDENTITY_COMPANY_NOT_FOUND. - Проверка входа. Синтаксис по таблице входных данных (paging, enum,
SearchTerm); иначе —ValidationError. Если задан фильтрRoleId— роль должна существовать и быть этой компании либо системной; иначе —IDENTITY_ROLE_NOT_FOUND. Существование компании отдельно не проверяется — claim выдан по живой компании, пустой список валиден. - Что читаем.
- Без
SearchTerm: страница пользователей компании (CompanyId, не удалённые) с опциональными фильтрами поStatusиRoleId(по активным назначениям), сортировкойSortи пагинацией; секрет-поля (хэши пароля/пин-кода) не выбираются. - С
SearchTerm: релевантный поиск по ФИО/телефону/email через поисковую проекциюUserSearchDocument, сужённый кCompanyIdдо запроса (multi-match с fuzziness) — возвращает ранжированныеUserIdи общее число; затем эти пользователи гидрируются из основной БД (той же проекцией без секрет-полей) с сохранением порядка; фильтрRoleIdприменяется к загруженной странице; сортировка — по релевантности (Sortигнорируется). - Для страницы дозагружаются активные назначения пользователей в scope компании и роли этих назначений
(для
Code/Name).
- Без
- Маппинг. Поле-в-поле по таблице item-модели;
Roles— группировка назначений по пользователю с названием роли. Маскирование: если у актора нетidentity.users.manage,PhoneNumber→+7***1234,Email→d***@mail(полные значения — только при наличии этого права). - Результат. Страница пользователей с общим числом записей под фильтром (
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 |
|---|---|---|---|---|
ValidationError | 400 | Некорректные paging/enum/SearchTerm | Список ошибок полей | no retry |
IDENTITY_ROLE_NOT_FOUND | 400 | Фильтр RoleId указывает на несуществующую/чужую роль | «Роль не найдена» | no retry |
IDENTITY_COMPANY_NOT_FOUND | 404 | Claim tenant_id отсутствует (инвестор) | «Компания не найдена» | no retry |
FORBIDDEN | 403 | Нет 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; dashboardidentity-overview; runbook — не применяется.
Acceptance Criteria
- Given пользователь с
identity.users.view, WhenGET /companies/my/usersбез фильтров, Then200; страница пользователей своей компании (не удалённых),TotalCount= число под фильтром; у каждого — активные роли в scope организации. - Given
Status = Active, WhenGET, Then в ответе толькоActive-учётки; неизвестное значениеStatus→ValidationError(400). - Given
RoleId— роль этой компании, WhenGET, Then в ответе только пользователи с активным назначением этой роли; несуществующая/чужая роль →IDENTITY_ROLE_NOT_FOUND(400). - Given
SearchTermсовпадает с частью ФИО сотрудника, WhenGET, Then сотрудник в выдаче, порядок — по релевантности (Sortигнорируется); только что созданная учётка может появиться в поиске с задержкой свежести проекции. - Given актор без
identity.users.manage, WhenGET, ThenPhoneNumberзамаскирован (+7***1234),Email—d***@mail; сidentity.users.manage— полные значения. - Given
Page = 2,PageSize = 20, WhenGET, Then возвращается вторая страница,Page/PageSizeэхо-ответом;PageSize > 100→ValidationError(400). - Given у актора нет claim
tenant_id, WhenGET, ThenIDENTITY_COMPANY_NOT_FOUND(404). - Given актор без
identity.users.view, WhenGET, ThenFORBIDDEN(403). - Given в компании нет пользователей под фильтром, When
GET, Then200сItems = [],TotalCount = 0(не ошибка).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (2): Identity Company-User API — реестр методов и общие правила области, POST /api/v1/companies/my/users/{userId}/block — Блокировка пользователя компании
- Поиск (1): UserSearchDocument
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности:
UserEntity,RoleDefinition. - Поиск:
UserSearchDocument(ADR-0059). - Мутации команды: Приглашение пользователя в компанию, Блокировка пользователя компании, Назначение роли пользователю в scope.