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
57 lines
2.6 KiB
TypeScript
57 lines
2.6 KiB
TypeScript
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,
|
|
}));
|
|
}
|
|
}
|