diff --git a/TODO.md b/TODO.md index d6d330d..bc12158 100644 --- a/TODO.md +++ b/TODO.md @@ -1334,6 +1334,25 @@ secao 134-139, 168) são gerados, nunca editados à mão (consistente com "fechamento imutável", secao 137) +## PHASE 36 — Platform: Sistema > Permissões (agente.md secao 142-145, +168) +- [x] `GET /platform/roles` (novo) — roles do sistema com as permissions + de cada uma + catálogo completo de permissions. `roles`/ + `permissions` não têm RLS (catálogo global, vem do seed). Só + leitura — RBAC é system-defined (`packages/auth/src/seed.ts`), sem + endpoint de criar/editar role customizada ainda. +- [x] Frontend: `/platform/sistema/permissoes` — um card por role (nome, + key, escopo Plataforma/Tenant, badges com cada permission) + tabela + do catálogo completo de permissions com descrição. +- [x] Testado ponta a ponta contra a API real: as 4 roles do sistema + (`platform_super_admin` 33 permissions, `tenant_admin` 30, + `supervisor` 17, `agent` 2) renderizadas corretamente com as + permissions certas. Smoke test de regressão nas 19 telas do tenant + + 9 telas platform, todas 200. +- [ ] "Sistema > Configurações" continua "em breve" — nunca teve escopo + definido na especificação além do nome, mesma pendência já + documentada na PHASE 34 + --- ## Riscos conhecidos diff --git a/apps/api/src/platform/platform-roles.controller.ts b/apps/api/src/platform/platform-roles.controller.ts new file mode 100644 index 0000000..5a305e5 --- /dev/null +++ b/apps/api/src/platform/platform-roles.controller.ts @@ -0,0 +1,46 @@ +import { Controller, ForbiddenException, Get, UseGuards } from "@nestjs/common"; +import { getPrismaClient } from "@b2bcall/database"; +import { isPlatformUser, type AccessTokenClaims } from "@b2bcall/auth"; +import { JwtAuthGuard } from "../common/guards/jwt-auth.guard"; +import { PermissionGuard } from "../common/guards/permission.guard"; +import { RequirePermission } from "../common/decorators/require-permission.decorator"; +import { CurrentUser } from "../common/decorators/current-user.decorator"; + +/** + * "Sistema > Permissões" (agente.md secao 142-145, 168) — só leitura por + * enquanto: RBAC é system-defined (roles/permissions vêm do seed, + * `packages/auth/src/seed.ts`), sem UI de criar role customizada ainda. + * `roles`/`permissions` não têm RLS (catálogo global do sistema). + */ +@UseGuards(JwtAuthGuard, PermissionGuard) +@Controller("platform/roles") +export class PlatformRolesController { + @RequirePermission("roles.manage") + @Get() + async list(@CurrentUser() user: AccessTokenClaims) { + if (!(await isPlatformUser(user.sub))) { + throw new ForbiddenException("So' um usuario com role de plataforma pode ver o catalogo de roles/permissoes"); + } + const prisma = getPrismaClient(); + + const [roles, permissions] = await Promise.all([ + prisma.role.findMany({ + include: { rolePermissions: { include: { permission: true } } }, + orderBy: { name: "asc" }, + }), + prisma.permission.findMany({ orderBy: { key: "asc" } }), + ]); + + return { + permissions, + roles: roles.map((r) => ({ + id: r.id, + key: r.key, + name: r.name, + scope: r.scope, + isSystem: r.isSystem, + permissionKeys: r.rolePermissions.map((rp) => rp.permission.key), + })), + }; + } +} diff --git a/apps/api/src/platform/platform.module.ts b/apps/api/src/platform/platform.module.ts index 7a438c1..b6a29dd 100644 --- a/apps/api/src/platform/platform.module.ts +++ b/apps/api/src/platform/platform.module.ts @@ -3,8 +3,15 @@ import { PlatformOverviewController } from "./platform-overview.controller"; import { PlatformUsersController } from "./platform-users.controller"; import { PlatformAuditController } from "./platform-audit.controller"; import { PlatformHealthController } from "./platform-health.controller"; +import { PlatformRolesController } from "./platform-roles.controller"; @Module({ - controllers: [PlatformOverviewController, PlatformUsersController, PlatformAuditController, PlatformHealthController], + controllers: [ + PlatformOverviewController, + PlatformUsersController, + PlatformAuditController, + PlatformHealthController, + PlatformRolesController, + ], }) export class PlatformModule {} diff --git a/apps/frontend/.impeccable/review/platform-permissoes-desktop.png b/apps/frontend/.impeccable/review/platform-permissoes-desktop.png new file mode 100644 index 0000000..d182e30 Binary files /dev/null and b/apps/frontend/.impeccable/review/platform-permissoes-desktop.png differ diff --git a/apps/frontend/src/app/platform/sistema/permissoes/page.tsx b/apps/frontend/src/app/platform/sistema/permissoes/page.tsx new file mode 100644 index 0000000..c60abb3 --- /dev/null +++ b/apps/frontend/src/app/platform/sistema/permissoes/page.tsx @@ -0,0 +1,24 @@ +import { requireSession } from "@/lib/session"; +import { apiFetch } from "@/lib/api"; +import { PermissoesView } from "./permissoes-view"; + +interface RoleWithPermissions { + id: string; + key: string; + name: string; + scope: "PLATFORM" | "TENANT"; + isSystem: boolean; + permissionKeys: string[]; +} + +interface Permission { + id: string; + key: string; + description: string | null; +} + +export default async function PermissoesPage() { + const session = await requireSession(); + const data = await apiFetch<{ roles: RoleWithPermissions[]; permissions: Permission[] }>("/platform/roles", session.accessToken); + return ; +} diff --git a/apps/frontend/src/app/platform/sistema/permissoes/permissoes-view.tsx b/apps/frontend/src/app/platform/sistema/permissoes/permissoes-view.tsx new file mode 100644 index 0000000..cecea09 --- /dev/null +++ b/apps/frontend/src/app/platform/sistema/permissoes/permissoes-view.tsx @@ -0,0 +1,80 @@ +import { ShieldCheck } from "lucide-react"; +import { Panel, PanelHeader } from "@/components/ui/panel"; +import { Pill } from "@/components/ui/pill"; +import { TBody, TD, TH, THead, TR, Table } from "@/components/ui/table"; + +interface RoleWithPermissions { + id: string; + key: string; + name: string; + scope: "PLATFORM" | "TENANT"; + isSystem: boolean; + permissionKeys: string[]; +} + +interface Permission { + id: string; + key: string; + description: string | null; +} + +export function PermissoesView({ roles, permissions }: { roles: RoleWithPermissions[]; permissions: Permission[] }) { + return ( +
+
+

Permissões

+

+ RBAC do sistema (agente.md secao 142-145) — roles e o catálogo de permissions são definidos no seed, sem + UI de criar role customizada ainda; esta tela é só leitura. +

+
+ +
+ {roles.map((role) => ( + +
+
+ +

{role.name}

+ {role.key} + {role.scope === "PLATFORM" ? "Plataforma" : "Tenant"} +
+ {role.permissionKeys.length} permission(ões) +
+
+ {role.permissionKeys.length === 0 ? ( + Nenhuma permission atribuída. + ) : ( + role.permissionKeys.map((key) => ( + + {key} + + )) + )} +
+
+ ))} +
+ + + + + + + + + + + + {permissions.map((p) => ( + + + + + ))} + +
ChaveDescrição
{p.key}{p.description ?? "—"}
+
+
+ ); +} diff --git a/apps/frontend/src/components/platform-shell/nav-data.ts b/apps/frontend/src/components/platform-shell/nav-data.ts index 7e8bade..54ac947 100644 --- a/apps/frontend/src/components/platform-shell/nav-data.ts +++ b/apps/frontend/src/components/platform-shell/nav-data.ts @@ -84,7 +84,11 @@ export const PLATFORM_NAV: NavSection[] = [ href: "/platform/sistema/usuarios", description: "Todos os usuários da plataforma, cross-tenant", }, - { label: "Permissões" }, + { + label: "Permissões", + href: "/platform/sistema/permissoes", + description: "Catálogo de roles e permissions do sistema", + }, { label: "Auditoria", href: "/platform/sistema/auditoria",