feat(platform): Sistema > Permissões (catálogo RBAC, só leitura)
GET /platform/roles novo — lista as 4 roles do sistema com as permissions de cada uma, mais o catálogo completo de permissions. roles/permissions não têm RLS (catálogo global do seed); só leitura, RBAC é system-defined, sem UI de criar role customizada ainda. Frontend: /platform/sistema/permissoes, um card por role + tabela do catálogo completo. Testado ponta a ponta: as 4 roles reais (platform_super_admin 33 permissions, tenant_admin 30, supervisor 17, agent 2) corretas. Smoke test nas 19 telas do tenant + 9 telas platform, todas 200. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
19
TODO.md
19
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
|
||||
|
||||
46
apps/api/src/platform/platform-roles.controller.ts
Normal file
46
apps/api/src/platform/platform-roles.controller.ts
Normal file
@@ -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),
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
BIN
apps/frontend/.impeccable/review/platform-permissoes-desktop.png
Normal file
BIN
apps/frontend/.impeccable/review/platform-permissoes-desktop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 171 KiB |
24
apps/frontend/src/app/platform/sistema/permissoes/page.tsx
Normal file
24
apps/frontend/src/app/platform/sistema/permissoes/page.tsx
Normal file
@@ -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 <PermissoesView roles={data.roles} permissions={data.permissions} />;
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold text-foreground">Permissões</h1>
|
||||
<p className="mt-1 max-w-2xl text-sm text-muted-foreground">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{roles.map((role) => (
|
||||
<Panel key={role.id}>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-border px-5 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldCheck className="h-4 w-4 text-muted-foreground" aria-hidden />
|
||||
<h2 className="text-sm font-semibold text-foreground">{role.name}</h2>
|
||||
<Pill>{role.key}</Pill>
|
||||
<Pill tone={role.scope === "PLATFORM" ? "accent" : "neutral"}>{role.scope === "PLATFORM" ? "Plataforma" : "Tenant"}</Pill>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">{role.permissionKeys.length} permission(ões)</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5 p-5">
|
||||
{role.permissionKeys.length === 0 ? (
|
||||
<span className="text-sm text-muted-foreground">Nenhuma permission atribuída.</span>
|
||||
) : (
|
||||
role.permissionKeys.map((key) => (
|
||||
<span key={key} className="rounded-full border border-border bg-muted px-2 py-0.5 font-mono text-xs text-muted-foreground">
|
||||
{key}
|
||||
</span>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Panel>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Panel>
|
||||
<PanelHeader title="Catálogo de permissions" description={`${permissions.length} permission(ões) no sistema`} />
|
||||
<Table>
|
||||
<THead>
|
||||
<TR>
|
||||
<TH>Chave</TH>
|
||||
<TH>Descrição</TH>
|
||||
</TR>
|
||||
</THead>
|
||||
<TBody>
|
||||
{permissions.map((p) => (
|
||||
<TR key={p.id}>
|
||||
<TD className="font-mono text-xs text-foreground">{p.key}</TD>
|
||||
<TD className="text-muted-foreground">{p.description ?? "—"}</TD>
|
||||
</TR>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
</Panel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user