Files
B2BCall-dialer/packages/auth/src/seed.ts
Matheus ed4ae4421e feat(telefonia): rotas de entrada por DID — fundação real pro IVR
Pedido do usuário: "pode iniciar a montar o IVR e as rotas de entrada".
Investigando antes de escrever qualquer XML de IVR, achei que NENHUMA
chamada de entrada por tronco tinha como funcionar hoje, IVR ou não:
nenhuma carregava `b2bcall_tenant_id` (só REGISTER de ramal e discagem de
saída setam essa variable), e mesmo corrigindo isso, o profile "external"
apontava pro contexto "public" vanilla — um arquivo ESTÁTICO, que sempre
ganha de uma consulta ao mod_xml_curl, então nunca seria dinâmico
enquanto se chamasse "public".

Perguntei ao usuário a granularidade certa (por DID ou por tronco) antes
de desenhar o schema — escolheu por DID, mais flexível (um tronco pode
carregar vários números com destinos diferentes). `InboundRoute` nova
(RLS real, FORCE ROW LEVEL SECURITY): `didNumber` único GLOBAL entre
tenants (mesma exceção já aceita em Tenant.telephonyDomain) — é a ÚNICA
forma de descobrir de qual tenant é uma chamada de entrada ANTES de
identificar o tenant. Resolvido por fan-out sobre tenants ativos, nunca
uma query sem contexto de RLS.

Dockerfile repontou o profile external pra context="inbound" (sem
arquivo estático, cai no mod_xml_curl de verdade). O XML gerado pra esse
contexto injeta b2bcall_tenant_id + domain_name (achado real: sem setar
domain_name explicitamente, o bridge da "Discagem interna" resolvia pro
domínio GLOBAL default, nunca pro do tenant) e transfere pro dialplan
real do tenant — reaproveita 100% do que já existe, inclusive pickup de
grupo (PHASE 53).

CRUD completo (InboundRoutesController, permissions novas no seed) + tela
"Telefonia > Rotas de Entrada" no frontend.

Testado com uma chamada REAL: um softphone registrado como ramal normal,
outro discando direto pro profile external (porta 5080, sem registrar —
exatamente como um provedor de tronco manda) um DID cadastrado. `show
channels` confirma: tenant certo, contexto certo, domínio certo no
bridge, codec PCMU negociado nos dois lados, ramal tocou e atendeu de
verdade. Detalhes completos, inclusive uma tentativa de teste que falhou
por limitação do canal `loopback` (não um bug), em docs/INBOUND_ROUTES.md.

O IVR em si (menu com play_and_get_digits) fica pra próxima fase — esta
é a fundação sem a qual nada de chamada de entrada funcionava.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
2026-08-30 16:45:18 -03:00

176 lines
6.8 KiB
TypeScript

/**
* Seed de RBAC (agente.md secoes 142, 145, 199, 200): catálogo de
* permissions, roles de sistema, e o Platform Super Admin inicial.
*
* Idempotente — seguro rodar de novo (upsert por chave única). Roda com:
* pnpm --filter @b2bcall/auth run seed
*/
import { randomBytes } from "node:crypto";
import { writeFileSync, chmodSync } from "node:fs";
import { resolve } from "node:path";
import { getPrismaClient } from "@b2bcall/database";
import { hashPassword } from "./password";
// Catálogo completo de permissions (agente.md secao 145).
const PERMISSIONS: Array<{ key: string; description: string }> = [
{ key: "tenants.view", description: "Ver tenants (plataforma)" },
{ key: "tenants.manage", description: "Criar/editar/suspender tenants" },
{ key: "billing.view", description: "Ver consumo e faturas" },
{ key: "billing.manage", description: "Gerenciar fechamentos de billing" },
{ key: "pricing.manage", description: "Gerenciar planos e price books" },
{ key: "dashboard.view", description: "Ver dashboards" },
{ key: "extensions.view", description: "Ver ramais" },
{ key: "extensions.manage", description: "Criar/editar ramais" },
{ key: "trunks.view", description: "Ver troncos" },
{ key: "trunks.manage", description: "Criar/editar troncos" },
{ key: "inbound_routes.view", description: "Ver rotas de entrada" },
{ key: "inbound_routes.manage", description: "Criar/editar rotas de entrada" },
{ key: "agents.view", description: "Ver agentes" },
{ key: "agents.manage", description: "Criar/editar agentes" },
{ key: "queues.view", description: "Ver filas" },
{ key: "queues.manage", description: "Criar/editar filas" },
{ key: "campaigns.view", description: "Ver campanhas" },
{ key: "campaigns.create", description: "Criar campanhas" },
{ key: "campaigns.update", description: "Editar campanhas" },
{ key: "campaigns.start", description: "Iniciar campanhas" },
{ key: "campaigns.pause", description: "Pausar campanhas" },
{ key: "campaigns.stop", description: "Parar campanhas" },
{ key: "monitoring.view", description: "Ver monitoramento em tempo real" },
{ key: "reports.view", description: "Ver relatórios" },
{ key: "reports.export", description: "Exportar relatórios" },
{ key: "recordings.view", description: "Ver gravações" },
{ key: "recordings.download", description: "Baixar gravações" },
{ key: "ai.view", description: "Ver análises de IA" },
{ key: "ai.manage", description: "Configurar providers/prompts de IA" },
{ key: "ai.analyze", description: "Disparar análise de IA manualmente" },
{ key: "freeswitch.view", description: "Ver estado do FreeSWITCH" },
{ key: "freeswitch.configure", description: "Configurar FreeSWITCH" },
{ key: "users.manage", description: "Gerenciar usuários" },
{ key: "roles.manage", description: "Gerenciar roles/permissões" },
{ key: "audit.view", description: "Ver audit log" },
];
// Mapeamento inicial role -> permissions. Ponto de partida razoável;
// revisar quando existir uma UI de administração de RBAC.
const ROLE_PERMISSIONS: Record<string, string[]> = {
platform_super_admin: PERMISSIONS.map((p) => p.key), // tudo
tenant_admin: PERMISSIONS.map((p) => p.key).filter(
(key) => !["tenants.view", "tenants.manage", "pricing.manage"].includes(key),
),
supervisor: [
"dashboard.view",
"extensions.view",
"trunks.view",
"inbound_routes.view",
"agents.view",
"agents.manage",
"queues.view",
"campaigns.view",
"campaigns.update",
"campaigns.start",
"campaigns.pause",
"campaigns.stop",
"monitoring.view",
"reports.view",
"reports.export",
"recordings.view",
"recordings.download",
"ai.view",
],
agent: ["dashboard.view", "campaigns.view"],
};
const SYSTEM_ROLES: Array<{ key: string; name: string; scope: "PLATFORM" | "TENANT" }> = [
{ key: "platform_super_admin", name: "Platform Super Admin", scope: "PLATFORM" },
{ key: "tenant_admin", name: "Tenant Admin", scope: "TENANT" },
{ key: "supervisor", name: "Supervisor", scope: "TENANT" },
{ key: "agent", name: "Agente", scope: "TENANT" },
];
const PLATFORM_ADMIN_EMAIL = "admin@b2bcall.local";
async function main() {
const prisma = getPrismaClient();
for (const permission of PERMISSIONS) {
await prisma.permission.upsert({
where: { key: permission.key },
update: { description: permission.description },
create: permission,
});
}
console.log(`Permissions: ${PERMISSIONS.length} sincronizadas.`);
for (const role of SYSTEM_ROLES) {
const created = await prisma.role.upsert({
where: { key: role.key },
update: { name: role.name, scope: role.scope, isSystem: true },
create: { ...role, isSystem: true },
});
const permissionKeys = ROLE_PERMISSIONS[role.key] ?? [];
const permissions = await prisma.permission.findMany({
where: { key: { in: permissionKeys } },
});
await prisma.rolePermission.deleteMany({ where: { roleId: created.id } });
if (permissions.length > 0) {
await prisma.rolePermission.createMany({
data: permissions.map((p) => ({ roleId: created.id, permissionId: p.id })),
skipDuplicates: true,
});
}
console.log(`Role '${role.key}': ${permissions.length} permissions.`);
}
const existingPlatformAdmin = await prisma.userRole.findFirst({
where: { tenantId: null, role: { key: "platform_super_admin" } },
});
if (existingPlatformAdmin) {
console.log("Platform Super Admin já existe, pulando criação.");
} else {
const platformSuperAdminRole = await prisma.role.findUniqueOrThrow({
where: { key: "platform_super_admin" },
});
const initialPassword = randomBytes(18).toString("base64url");
const passwordHash = await hashPassword(initialPassword);
const admin = await prisma.user.upsert({
where: { email: PLATFORM_ADMIN_EMAIL },
update: {},
create: {
email: PLATFORM_ADMIN_EMAIL,
passwordHash,
name: "Platform Super Admin",
mustChangePassword: true,
},
});
await prisma.userRole.create({
data: { userId: admin.id, roleId: platformSuperAdminRole.id, tenantId: null },
});
const firstLoginPath = resolve(__dirname, "../../../FIRST_LOGIN.txt");
writeFileSync(
firstLoginPath,
`B2BCall — Platform Super Admin (gerado em ${new Date().toISOString()})\n` +
`Email: ${PLATFORM_ADMIN_EMAIL}\n` +
`Senha: ${initialPassword}\n\n` +
`Troca de senha OBRIGATÓRIA no primeiro login. Apague este arquivo depois de guardar a senha em um local seguro.\n`,
);
chmodSync(firstLoginPath, 0o600);
console.log(`Platform Super Admin criado: ${PLATFORM_ADMIN_EMAIL}`);
console.log(`Senha salva em ${firstLoginPath} (permissao 600) — nao sera exibida no terminal.`);
}
await prisma.$disconnect();
}
main().catch(async (err) => {
console.error(err);
process.exit(1);
});