feat: "itens sem permissão não aparecem" — filtro de menu por permission (secao 169)

getUserPermissionKeys(userId, tenantId?) (novo, packages/auth) devolve todas as
permission keys do usuário no contexto atual, mesma query de userHasPermission só
que retornando o conjunto inteiro. GET /auth/me agora inclui permissionKeys — leitura
adicional só pra UX, nunca decisão de autorização (isso continua sendo o
PermissionGuard em cada endpoint).

NavLeaf/NavSection ganham campo opcional permission; todos os ~35 itens de menu
(tenant e platform) anotados com a permission key mínima que o endpoint GET
correspondente já exige. filterNavByPermissions() (novo, nav-types.ts) esconde o
item, ou a seção inteira se nenhum filho sobrar.

TenantSidebar/PlatformSidebar/*Topbar são client components que importam
TENANT_NAV/PLATFORM_NAV direto (não recebem como prop do server) porque os ícones
(LucideIcon, funções) não são serializáveis através da fronteira RSC — restrição já
documentada desde a PHASE 23. O filtro roda dentro desses wrappers, client-side,
recebendo só permissionKeys: string[] (serializável) como prop.

Testado ponta a ponta com um supervisor de teste de verdade (convidado, senha
trocada, logado via UI): menu perde Telefonia > Dialplan e a seção Administração
inteira (nenhum permission bate). Confirmado que a autorização real continua valendo
sem o item no menu: acesso direto a /app/administracao/usuarios continua batendo 403
no backend. Regressão: tenant_admin e platform_super_admin continuam vendo todo item
do próprio menu.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
2026-08-30 09:17:05 -03:00
parent 6f18b3a297
commit 798aa7b579
15 changed files with 183 additions and 32 deletions

35
TODO.md
View File

@@ -1700,6 +1700,41 @@ secao 96-103, 124, 169) + achado real de autorização em `/ai/models`
aplicado (0 chamadas — não existe tráfego real neste tenant ainda,
resultado honesto)
## PHASE 48 — "Itens sem permissão não aparecem" (agente.md secao 169)
- [x] `getUserPermissionKeys(userId, tenantId?)` (novo, `packages/auth`) —
todas as permission keys do usuário no contexto atual, mesma query
de `userHasPermission` só que devolvendo o conjunto inteiro em vez
de checar uma. `GET /auth/me` agora devolve `permissionKeys` (só
leitura adicional, não é decisão de autorização — isso continua
sendo o `PermissionGuard` em cada endpoint, secao 31/146: esconder
um item de menu nunca substitui o check no backend)
- [x] `NavLeaf`/`NavSection` ganham campo opcional `permission`; cada um
dos ~35 itens de menu (tenant e platform) anotado com a permission
key mínima que o endpoint GET correspondente já exige (ex.: Discador
usa `campaigns.view`, Administração usa `users.manage`,
Infraestrutura usa `freeswitch.view`). `filterNavByPermissions()`
(novo, `nav-types.ts`) esconde o item, ou a seção inteira se
nenhum filho sobrar
- [x] achado de arquitetura ao implementar (não um bug, uma restrição já
documentada): `TenantSidebar`/`PlatformSidebar`/`*Topbar` são
client components que importam `TENANT_NAV`/`PLATFORM_NAV`
DIRETO (em vez de receber como prop do server layout) porque os
ícones (`LucideIcon`, funções) não são serializáveis através da
fronteira RSC — bug real já documentado desde a PHASE 23. Por isso
o filtro roda dentro desses wrappers client-side, sobre o array já
no bundle, recebendo só `permissionKeys: string[]` (serializável)
como prop
- [x] Testado ponta a ponta com um supervisor de teste de verdade
(convidado, senha trocada, logado via UI): menu mostra só Discador/
Call Center/Telefonia (sem Dialplan, que exige `freeswitch.view` —
supervisor não tem)/Monitoramento/Gravações/IA/Relatórios — a seção
Administração inteira some (nenhum dos 3 filhos exige menos que
`users.manage`). Confirmado que a autorização de verdade continua
valendo mesmo sem o item no menu: acesso direto a
`/app/administracao/usuarios` continua batendo 403 no backend.
Regressão: tenant_admin e platform_super_admin continuam vendo
TODO item do próprio menu (sem perder nada)
---
## Riscos conhecidos