Appearance
Contrato do lookup de usuários do admin — GET /admin/users/lookup
Base URL
/api/v1. Erros emapplication/problem+json. Trilho admin global: admin-scoping-guide §5. Seletor de pessoas das telas de negócio: user-directory-contract.
1. Para que serve — e quando usar o outro
Preencher o seletor de pessoas do admin da plataforma: o campo de referência (type: 'reference') que guarda o id e mostra o nome, no trilho de administração. O caso que motivou a rota é "adicionar usuários ao tenant" (aba Usuários do detalhe do tenant), que procura quem ainda não está naquele tenant, em toda a plataforma.
O diretório não responde isso: por definição ele não lista quem está fora do escopo, e o admin da plataforma só enxerga nele o próprio tenant (ou o grupo). E a listagem GET /admin/users é a tela de administração — carrega vínculos, papéis, permissões e app ids de cada linha — cara demais para um autocomplete e sem como esconder no servidor quem já está no tenant.
GET /users/directory | GET /admin/users/lookup | |
|---|---|---|
| Pergunta | "quem está no meu escopo?" | "quem existe na plataforma?" |
| Onde usar | seletores das telas de negócio | seletores do admin (trilho global) |
| Público | qualquer membro | admin da plataforma |
| Permissão | read:user-directory (ou read:user-email, read:users) | admin:system ou manage:all-users |
| Item | { id, fullName } (+ email a pedido) | { id, name, description, isActive? } |
| Busca casa e-mail? | não | sim |
| Escopo | tenant do token ou grupo | plataforma |
Regra prática: se a tela está no trilho /admin/... e o operador precisa achar gente de qualquer tenant, use o lookup. Em qualquer outra tela use o diretório. Não existe versão do lookup no trilho tenant.
2. Requisição
GET /api/v1/admin/users/lookup?search=ana&page=1&pageSize=20&excludeTenantId=7d2e…| Parâmetro | Default | Regras |
|---|---|---|
search | ausente | casa nome, sobrenome, o nome como exibido ou o e-mail, sem distinguir maiúsculas nem acentos (joao acha João). Máx. 128 caracteres. Ausente ⇒ lista desde o começo |
page | 1 | 1-based |
pageSize | 20 | máximo 50 — acima disso é 400 |
excludeTenantId | ausente | tira da busca quem tem vínculo com esse tenant (§3.1). Não se aplica à hidratação |
ids | ausente | hidratação: resolve estes ids em vez de buscar. Repetido ou separado por vírgula; duplicados ignorados; vazio = ausente. Máximo 50 ids distintos — acima disso, ou com um id malformado, 400. Ver §4 |
Não existe sortBy, sortDirection, includeDeleted nem isActive. A ordem é por nome; na busca só aparecem contas ativas e não excluídas.
3. Resposta — busca
Envelope PagedResult<T> de sempre (sem o bloco scope: a rota não é scope-capable):
jsonc
{
"items": [
{ "id": "3f1c…", "name": "Ana Silva", "description": "ana.silva@acme.com" },
{ "id": "9a72…", "name": "", "description": "sem.nome@acme.com" }
],
"totalCount": 42, "pageNumber": 1, "pageSize": 20,
"totalPages": 3, "hasNext": true, "hasPrevious": false
}nameé o nome completo (firstName+lastName). Vem""quando o cadastro não tem nome (login federado sem claims de perfil é o caminho comum): renderize o seu texto de fallback. O backend não devolve placeholder que não saberia localizar.descriptioné o e-mail — o que separa dois homônimos. O admin já vê o endereço na listagem.isActivenão vem na busca (ausente ⇒ ativo): a busca só devolve contas ativas.- Uma consulta leve por chamada — sem vínculos, papéis, permissões nem app ids.
3.1 excludeTenantId — "adicionar ao tenant"
GET /api/v1/admin/users/lookup?search=ana&excludeTenantId={tenantId}Tira da busca todo mundo que já tem vínculo com esse tenant: vínculo ativo e também vínculo desativado (ou fora do período). É exatamente o conjunto que POST /tenants/{id}/users recusaria como "já vinculado", então o seletor não oferece uma escolha que o submit vai rejeitar. Quem teve o vínculo removido volta a aparecer — pode ser vinculado de novo. Com isso o front não precisa mais esconder os já vinculados no cliente.
4. Hidratação — ?ids=
Quando a tela já tem o id (o selecionado fora da página carregada, um formulário em edição, um chip), a pergunta é "qual o nome deste id?":
GET /api/v1/admin/users/lookup?ids=3f1c…,9a72…,c0de…jsonc
{
"items": [
{ "id": "3f1c…", "name": "Ana Silva", "description": "ana.silva@acme.com" },
{ "id": "9a72…", "name": "Bruno Costa", "description": "bruno@acme.com", "isActive": false }
],
"totalCount": 2, "pageNumber": 1, "pageSize": 3,
"totalPages": 1, "hasNext": false, "hasPrevious": false
}- Uma página só, sem COUNT. Todo id encontrado volta na página 1;
pageepageSizenão se aplicam e opageSizeda resposta é a quantidade de ids distintos pedidos. Mais de 50 ids distintos: quebre em lotes de 50. - Conta desativada vem com
isActive: false— mostre o nome, não ofereça de novo. O campo não vem nos demais itens (ausente ⇒ ativo). - Excluído ou inexistente não vem — status 200, sem 404. Id ausente da resposta = "Registro indisponível".
ids+search= AND (a interseção). Nenhum dos dois é ignorado em silêncio.excludeTenantIdNÃO se aplica. O selecionado precisa continuar mostrando o nome mesmo que já tenha sido vinculado ao tenant; pode mandar o parâmetro junto, ele é ignorado na hidratação.
ts
// referência de usuários no admin: busca paginada + hidratação em lote
const search = (q: string, page = 1) =>
api.get("/admin/users/lookup", { params: { search: q, page, pageSize: 20, excludeTenantId } });
const hydrate = async (ids: string[]) => {
const chunks = chunk([...new Set(ids)], 50);
const pages = await Promise.all(chunks.map(c =>
api.get("/admin/users/lookup", { params: { ids: c.join(",") } })));
return new Map(pages.flatMap(p => p.items).map(u => [u.id, u]));
// map.get(id) === undefined ⇒ "Registro indisponível"
// map.get(id)?.isActive === false ⇒ mostra o nome, não oferece de novo
};5. Permissão
A mesma da listagem do admin de usuários: admin:system ou manage:all-users. Como no resto do trilho, o front decide pela sessão: capabilities.users.default === "global" ⟺ o trilho /admin/users (e este lookup) responde 200. Um admin de tenant (read:users) recebe 403 aqui — o seletor dele é o /users/directory.
6. Erros
| Situação | HTTP | code |
|---|---|---|
| sem token | 401 | — |
sem admin:system nem manage:all-users | 403 | — |
pageSize > 50 ou < 1, page < 1, search > 128 | 400 | validação |
mais de 50 ids distintos; ids com valor que não é um id | 400 | validação |
excludeTenantId que não é um id | 400 | binding |
Ramifique sempre pelo code; title e detail são localizados (en, pt-BR, es-ES). Faça debounce de ~300 ms na digitação e cancele a requisição anterior.