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
|
são gerados, nunca editados à mão (consistente com "fechamento
|
||||||
imutável", secao 137)
|
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
|
## 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 { PlatformUsersController } from "./platform-users.controller";
|
||||||
import { PlatformAuditController } from "./platform-audit.controller";
|
import { PlatformAuditController } from "./platform-audit.controller";
|
||||||
import { PlatformHealthController } from "./platform-health.controller";
|
import { PlatformHealthController } from "./platform-health.controller";
|
||||||
|
import { PlatformRolesController } from "./platform-roles.controller";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [PlatformOverviewController, PlatformUsersController, PlatformAuditController, PlatformHealthController],
|
controllers: [
|
||||||
|
PlatformOverviewController,
|
||||||
|
PlatformUsersController,
|
||||||
|
PlatformAuditController,
|
||||||
|
PlatformHealthController,
|
||||||
|
PlatformRolesController,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class PlatformModule {}
|
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",
|
href: "/platform/sistema/usuarios",
|
||||||
description: "Todos os usuários da plataforma, cross-tenant",
|
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",
|
label: "Auditoria",
|
||||||
href: "/platform/sistema/auditoria",
|
href: "/platform/sistema/auditoria",
|
||||||
|
|||||||
Reference in New Issue
Block a user