feat(frontend): Platform > Infraestrutura > FreeSWITCH/SIP Profiles/Nodes
GET /platform/freeswitch/channels|profiles|nodes fazem introspecção ESL real
(show channels/show calls/sofia status/show registrations/status/show gateways),
reaproveitando os métodos do FreeSwitchTelephonyProvider já verificados manualmente
contra o FreeSWITCH real. Mesmo padrão de conexão avulsa do PlatformHealthController
(connect → comando → disconnect).
Nunca deixa a indisponibilidade do ESL virar 500/503 — devolve { ok: false, error }
com 200, mesma filosofia do health check. Necessário: nesta VM apps/api roda fora do
Docker e a porta 8021 é deliberadamente não publicada no host, então as 3 telas
sempre mostram essa explicação aqui (mesmo texto já usado em Infraestrutura >
Saúde), mesmo com o endpoint 100% funcional — confirmado indiretamente pelo
b2bcall-fs-events, que fala ESL de dentro da rede Docker e está com heartbeat ativo.
"Nodes" mostra explicitamente 1 node (container único, sem clustering) em vez de
fingir uma lista.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
75
apps/api/src/platform/platform-freeswitch.controller.ts
Normal file
75
apps/api/src/platform/platform-freeswitch.controller.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Controller, ForbiddenException, Get, UseGuards } from "@nestjs/common";
|
||||
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";
|
||||
|
||||
/**
|
||||
* Infraestrutura > FreeSWITCH/SIP Profiles/Nodes (agente.md secao 168-169)
|
||||
* — introspecção ESL real, mesmo padrão de conexão avulsa (connect →
|
||||
* comando → disconnect) já usado em `PlatformHealthController`. Igual ao
|
||||
* health check, nunca deixa a indisponibilidade do ESL virar 500/503 pro
|
||||
* cliente — devolve `{ ok: false, error }` (mesma filosofia do `timed()`
|
||||
* de lá), porque nesta VM o Event Socket (8021) é deliberadamente não
|
||||
* publicado no host (agente.md secao 184) e `apps/api` roda fora do
|
||||
* Docker: essas 3 telas SEMPRE vão mostrar essa mensagem aqui, mesmo
|
||||
* endpoint funcionando corretamente em produção (mesma network do
|
||||
* FreeSWITCH). "Nodes" é sempre 1 node nesta implantação (container
|
||||
* único, sem clustering) — honesto em vez de fingir uma lista.
|
||||
*/
|
||||
async function withEsl<T>(fn: (provider: FreeSwitchTelephonyProvider) => Promise<T>): Promise<{ ok: true; data: T } | { ok: false; error: string }> {
|
||||
const host = process.env.ESL_HOST;
|
||||
const port = Number(process.env.ESL_PORT ?? 8021);
|
||||
const password = process.env.ESL_PASSWORD;
|
||||
if (!host || !password) return { ok: false, error: "ESL nao configurado (ESL_HOST/ESL_PASSWORD ausentes)" };
|
||||
|
||||
const provider = new FreeSwitchTelephonyProvider({ host, port, password });
|
||||
try {
|
||||
provider.connect();
|
||||
const connected = await provider.waitUntilConnected(2500);
|
||||
if (!connected) return { ok: false, error: "Timeout conectando no ESL do FreeSWITCH" };
|
||||
return { ok: true, data: await fn(provider) };
|
||||
} catch (err) {
|
||||
return { ok: false, error: err instanceof Error ? err.message : "Erro desconhecido falando com o ESL" };
|
||||
} finally {
|
||||
await provider.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
function requirePlatform(user: AccessTokenClaims): Promise<boolean> {
|
||||
return isPlatformUser(user.sub);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PermissionGuard)
|
||||
@Controller("platform/freeswitch")
|
||||
export class PlatformFreeswitchController {
|
||||
@RequirePermission("freeswitch.view")
|
||||
@Get("channels")
|
||||
async channels(@CurrentUser() user: AccessTokenClaims) {
|
||||
if (!(await requirePlatform(user))) throw new ForbiddenException("So' um usuario com role de plataforma pode ver o FreeSWITCH");
|
||||
return withEsl(async (p) => ({ channels: await p.getChannels(), calls: await p.getCalls() }));
|
||||
}
|
||||
|
||||
@RequirePermission("freeswitch.view")
|
||||
@Get("profiles")
|
||||
async profiles(@CurrentUser() user: AccessTokenClaims) {
|
||||
if (!(await requirePlatform(user))) throw new ForbiddenException("So' um usuario com role de plataforma pode ver SIP profiles");
|
||||
return withEsl(async (p) => ({
|
||||
sofiaStatus: await p.runApi("sofia status"),
|
||||
registrations: await p.getRegistrations(),
|
||||
}));
|
||||
}
|
||||
|
||||
@RequirePermission("freeswitch.view")
|
||||
@Get("nodes")
|
||||
async nodes(@CurrentUser() user: AccessTokenClaims) {
|
||||
if (!(await requirePlatform(user))) throw new ForbiddenException("So' um usuario com role de plataforma pode ver os nodes");
|
||||
return withEsl(async (p) => ({
|
||||
nodeCount: 1,
|
||||
status: await p.runApi("status"),
|
||||
gateways: await p.getGateways(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { PlatformAuditController } from "./platform-audit.controller";
|
||||
import { PlatformHealthController } from "./platform-health.controller";
|
||||
import { PlatformRolesController } from "./platform-roles.controller";
|
||||
import { PlatformQuotasController } from "./platform-quotas.controller";
|
||||
import { PlatformFreeswitchController } from "./platform-freeswitch.controller";
|
||||
|
||||
@Module({
|
||||
controllers: [
|
||||
@@ -14,6 +15,7 @@ import { PlatformQuotasController } from "./platform-quotas.controller";
|
||||
PlatformHealthController,
|
||||
PlatformRolesController,
|
||||
PlatformQuotasController,
|
||||
PlatformFreeswitchController,
|
||||
],
|
||||
})
|
||||
export class PlatformModule {}
|
||||
|
||||
Reference in New Issue
Block a user