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

View File

@@ -30,6 +30,33 @@ export async function userHasPermission(
);
}
/**
* Todas as permission keys que `userId` tem no contexto de `tenantId`
* (ou só as de escopo PLATFORM, se `tenantId` for omitido) — usado pelo
* frontend pra filtrar o menu (agente.md secao 169: "itens sem permissão
* não aparecem"), nunca pra decidir autorização de escrita (isso continua
* sendo `userHasPermission`/`PermissionGuard` no backend, a única fonte
* confiável — o menu é só UX, esconder um item não substitui o check).
*/
export async function getUserPermissionKeys(userId: string, tenantId?: string): Promise<string[]> {
const prisma = getPrismaClient();
const where = tenantId
? { userId, OR: [{ tenantId }, { tenantId: null }] }
: { userId, tenantId: null };
const userRoles = await prisma.userRole.findMany({
where,
include: { role: { include: { rolePermissions: { include: { permission: true } } } } },
});
const keys = new Set<string>();
for (const userRole of userRoles) {
for (const rp of userRole.role.rolePermissions) keys.add(rp.permission.key);
}
return Array.from(keys);
}
/** Atalho: usuário tem QUALQUER role com scope PLATFORM (ex.: platform_super_admin). */
export async function isPlatformUser(userId: string): Promise<boolean> {
const prisma = getPrismaClient();