feat(platform): Sistema > Usuários/Auditoria, Infraestrutura > Saúde

Três endpoints novos, todos platform-only: GET /platform/users (cross-
tenant, users não tem RLS) + PATCH .../status (desabilitar tem efeito
real — login() já checava status ACTIVE desde a PHASE 04); GET
/platform/audit-log (últimos 200 eventos, audit_logs também sem RLS,
linha imutável); GET /platform/health (Postgres/Redis + FreeSWITCH via
conexão ESL avulsa, sem manter estado).

Achado de arquitetura documentado explicitamente na própria tela: o check
de FreeSWITCH sempre falha neste ambiente porque apps/api roda no host e
a porta 8021 é deliberadamente não publicada (decisão da PHASE 01/05) —
não é um bug, é a rede isolada do jeito certo.

Frontend: /platform/sistema/usuarios, /auditoria, /platform/
infraestrutura/saude. Testado ponta a ponta contra dados reais (3
usuários da plataforma, audit log com eventos reais desta sessão,
inclusive uma referência órfã tratada corretamente). Smoke test nas 19
telas anteriores, todas 200.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
2026-08-29 20:04:05 -03:00
parent 2c1269a83a
commit a23e68b011
17 changed files with 654 additions and 3 deletions

View File

@@ -0,0 +1,56 @@
import { Controller, ForbiddenException, Get, Query, 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 > Auditoria" (agente.md secao 150-151, 168) — `audit_logs` não
* tem RLS (linha imutável de auditoria, precisa sobreviver mesmo que o
* tenant seja apagado — decisão do schema desde a PHASE 04), então uma
* consulta cross-tenant direta é segura aqui; só platform admin acessa
* este endpoint (um tenant admin vê o próprio audit trail por outro
* caminho, se/quando existir).
*/
@UseGuards(JwtAuthGuard, PermissionGuard)
@Controller("platform/audit-log")
export class PlatformAuditController {
@RequirePermission("audit.view")
@Get()
async list(
@CurrentUser() user: AccessTokenClaims,
@Query("action") action?: string,
@Query("tenantId") tenantId?: string,
): Promise<Record<string, unknown>[]> {
if (!(await isPlatformUser(user.sub))) {
throw new ForbiddenException("So' um usuario com role de plataforma pode ver o audit log da plataforma");
}
const prisma = getPrismaClient();
const entries = await prisma.auditLog.findMany({
where: {
...(action ? { action: { contains: action, mode: "insensitive" } } : {}),
...(tenantId ? { tenantId } : {}),
},
orderBy: { createdAt: "desc" },
take: 200,
});
const userIds = Array.from(new Set(entries.map((e) => e.userId).filter((id): id is string => id != null)));
const tenantIds = Array.from(new Set(entries.map((e) => e.tenantId).filter((id): id is string => id != null)));
const [users, tenants] = await Promise.all([
userIds.length ? prisma.user.findMany({ where: { id: { in: userIds } }, select: { id: true, email: true } }) : [],
tenantIds.length ? prisma.tenant.findMany({ where: { id: { in: tenantIds } }, select: { id: true, legalName: true } }) : [],
]);
const userEmail = new Map(users.map((u) => [u.id, u.email]));
const tenantName = new Map(tenants.map((t) => [t.id, t.legalName]));
return entries.map((e) => ({
...e,
userEmail: e.userId ? (userEmail.get(e.userId) ?? e.userId) : null,
tenantName: e.tenantId ? (tenantName.get(e.tenantId) ?? e.tenantId) : null,
}));
}
}

View File

@@ -0,0 +1,65 @@
import { Controller, ForbiddenException, Get, UseGuards } from "@nestjs/common";
import { getPrismaClient } from "@b2bcall/database";
import { isPlatformUser, type AccessTokenClaims } from "@b2bcall/auth";
import { FreeSwitchTelephonyProvider } from "@b2bcall/telephony";
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";
import { getRedisClient } from "../common/redis";
type CheckStatus = "ok" | "fail";
async function timed<T>(fn: () => Promise<T>): Promise<{ status: CheckStatus; latencyMs: number }> {
const start = Date.now();
try {
await fn();
return { status: "ok", latencyMs: Date.now() - start };
} catch {
return { status: "fail", latencyMs: Date.now() - start };
}
}
/**
* "Infraestrutura > Saúde" (agente.md secao 168, 187). `apps/api` não
* mantém uma conexão ESL permanente (isso é trabalho de fs-events/fs-
* config/predictive-dialer, secao 21) — pra não adicionar esse estado só
* pra um health check de tela de admin, abre uma conexão ESL avulsa,
* espera conectar, e derruba. Aceitável aqui: endpoint de baixo tráfego,
* chamado por um humano olhando a tela, não um hot path.
*/
@UseGuards(JwtAuthGuard, PermissionGuard)
@Controller("platform/health")
export class PlatformHealthController {
@RequirePermission("freeswitch.view")
@Get()
async check(@CurrentUser() user: AccessTokenClaims) {
if (!(await isPlatformUser(user.sub))) {
throw new ForbiddenException("So' um usuario com role de plataforma pode ver a saude da infraestrutura");
}
const [postgres, redis, freeswitch] = await Promise.all([
timed(() => getPrismaClient().$queryRaw`SELECT 1`),
timed(async () => {
await getRedisClient().ping();
}),
timed(async () => {
const host = process.env.ESL_HOST;
const port = Number(process.env.ESL_PORT ?? 8021);
const password = process.env.ESL_PASSWORD;
if (!host || !password) throw new Error("ESL nao configurado");
const provider = new FreeSwitchTelephonyProvider({ host, port, password });
try {
provider.connect();
const connected = await provider.waitUntilConnected(2500);
if (!connected) throw new Error("timeout conectando no ESL");
} finally {
await provider.disconnect();
}
}),
]);
return { postgres, redis, freeswitch, checkedAt: new Date().toISOString() };
}
}

View File

@@ -0,0 +1,72 @@
import { Body, Controller, ForbiddenException, Get, NotFoundException, Param, Patch, UseGuards } from "@nestjs/common";
import { IsIn } from "class-validator";
import { getPrismaClient } from "@b2bcall/database";
import { recordAuditEvent, 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";
class UpdateUserStatusDto {
@IsIn(["ACTIVE", "DISABLED"])
status!: "ACTIVE" | "DISABLED";
}
/**
* "Sistema > Usuários" (agente.md secao 168) — visão cross-tenant, só
* platform admin. `users` não tem RLS (identidade global, secao 148),
* então lida direto com `getPrismaClient()`; as memberships de cada
* usuário (que tenants ele pertence) É que têm RLS — resolvidas uma por
* uma via `withTenantContext`, mesmo padrão já usado em
* `TenantsController.list` pro `memberCount`.
*/
@UseGuards(JwtAuthGuard, PermissionGuard)
@Controller("platform/users")
export class PlatformUsersController {
@RequirePermission("users.manage")
@Get()
async list(@CurrentUser() user: AccessTokenClaims) {
if (!(await isPlatformUser(user.sub))) {
throw new ForbiddenException("So' um usuario com role de plataforma pode listar usuarios da plataforma");
}
const prisma = getPrismaClient();
const [users, platformAdmins] = await Promise.all([
prisma.user.findMany({
where: { deletedAt: null },
select: { id: true, email: true, name: true, status: true, mustChangePassword: true, createdAt: true },
orderBy: { createdAt: "desc" },
}),
prisma.userRole.findMany({ where: { tenantId: null, role: { key: "platform_super_admin" } }, select: { userId: true } }),
]);
const platformAdminIds = new Set(platformAdmins.map((r) => r.userId));
return users.map((u) => ({ ...u, isPlatformUser: platformAdminIds.has(u.id) }));
}
@RequirePermission("users.manage")
@Patch(":id/status")
async updateStatus(@CurrentUser() user: AccessTokenClaims, @Param("id") id: string, @Body() dto: UpdateUserStatusDto) {
if (!(await isPlatformUser(user.sub))) {
throw new ForbiddenException("So' um usuario com role de plataforma pode desabilitar usuarios");
}
const prisma = getPrismaClient();
const existing = await prisma.user.findFirst({ where: { id, deletedAt: null } });
if (!existing) throw new NotFoundException();
const updated = await prisma.user.update({ where: { id }, data: { status: dto.status } });
await recordAuditEvent(prisma, {
action: "USER_STATUS_UPDATE",
tenantId: null,
userId: user.sub,
entityType: "user",
entityId: id,
after: { status: dto.status },
});
return { id: updated.id, status: updated.status };
}
}

View File

@@ -1,7 +1,10 @@
import { Module } from "@nestjs/common";
import { PlatformOverviewController } from "./platform-overview.controller";
import { PlatformUsersController } from "./platform-users.controller";
import { PlatformAuditController } from "./platform-audit.controller";
import { PlatformHealthController } from "./platform-health.controller";
@Module({
controllers: [PlatformOverviewController],
controllers: [PlatformOverviewController, PlatformUsersController, PlatformAuditController, PlatformHealthController],
})
export class PlatformModule {}