Achado real, detalhado pelo usuário testando o PABX de verdade: TenantsController
.create() gravava telephonyDomain="b2bcall.local" fixo pra TODO tenant novo —
b2bcall-fs-config decide qual tenant é dono de um REGISTER só pelo domínio
(Tenant.findFirst({telephonyDomain})), então com todo tenant no mesmo domínio o
isolamento de PABX (ramais, call groups, filas, IVR) não tinha como funcionar de
verdade. Investigado direto no container antes de mudar qualquer coisa: o sofia
profile "internal" (vanilla) já vem com <domain name="all" alias="true".../> — o
FreeSWITCH sempre aceitou domínio dinâmico por REGISTER, o bug era só a aplicação.
Nenhuma mudança de infra foi necessária.
Tenant.telephonyDomain agora é obrigatório e @unique (migration com backfill:
tenants existentes ganharam {code}.b2bcall.net, e os Extension.domain já criados
foram atualizados junto). Tela de criação de tenant sugere {code}.b2bcall.net ao
digitar o código, editável.
Extension.callGroup (novo) — ramais no mesmo grupo podem capturar a chamada um do
outro (*8), fora do grupo não. Vira a variable call-group no directory XML; a regra
de dialplan do *8 em si fica pra configurar em Telefonia > Dialplan (editor já
existe). Editável na criação e depois (PATCH /extensions/:id, novo).
POST /extensions/:id/reveal-password (novo) — achado real: "show once" puro não
funciona no dia a dia (reconfigurar um telefone/softphone precisa da senha de novo;
forçar reset toda vez derruba outro aparelho já configurado). sipPasswordEnc sempre
foi criptografia reversível, nunca hash — só não estava exposto. Auditado
(EXTENSION_PASSWORD_REVEALED) por ser sensível mesmo sem escrita.
apps/freeswitch-config reconstruído e reiniciado. Testado ponta a ponta contra o
container REAL via docker exec: senha revelada bate com a gerada na criação,
call-group aparece no XML, e o mesmo número de ramal em domínios diferentes nunca
se confunde (isolamento cross-tenant confirmado de verdade).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
70 lines
2.0 KiB
TypeScript
70 lines
2.0 KiB
TypeScript
import { IsEmail, IsIn, IsOptional, IsString, IsUUID, Matches, MaxLength } from "class-validator";
|
|
|
|
const TENANT_STATUSES = ["TRIAL", "ACTIVE", "SUSPENDED", "PAST_DUE", "CANCELLED"];
|
|
|
|
export class CreateTenantDto {
|
|
@IsString()
|
|
@MaxLength(40)
|
|
@Matches(/^[a-z0-9-]+$/, { message: "code deve ser minusculo, so letras/numeros/hifen (vira tambem o slug)" })
|
|
code!: string;
|
|
|
|
@IsString()
|
|
@MaxLength(200)
|
|
legalName!: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(200)
|
|
tradeName?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(40)
|
|
taxId?: string;
|
|
|
|
@IsUUID()
|
|
planId!: string;
|
|
|
|
/** Domínio SIP deste tenant (secao 178, docs/EXTENSIONS.md) — achado
|
|
* real reportado pelo usuário: antes disto todo tenant nascia com o
|
|
* mesmo domínio fixo ("b2bcall.local"), quebrando isolamento de PABX
|
|
* (call groups, filas, IVR não conseguem distinguir um tenant do
|
|
* outro). A tela sugere `{code}.b2bcall.net`, mas quem cria pode
|
|
* digitar outro — só precisa ser único (constraint no banco) e ter
|
|
* cara de domínio (não precisa resolver de verdade por DNS: o
|
|
* telefone/softphone aponta pro IP do servidor via `proxy=`, este
|
|
* campo só serve pra identificar o tenant no REGISTER). */
|
|
@IsString()
|
|
@MaxLength(120)
|
|
@Matches(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/, {
|
|
message: "telephonyDomain precisa ter formato de domínio (ex.: minhaempresa.b2bcall.net)",
|
|
})
|
|
telephonyDomain!: string;
|
|
|
|
/** Cria o primeiro usuário (tenant_admin) na mesma transação — sem essa
|
|
* conta o tenant fica inacessível (secao 141: "Tenant Admin é criado
|
|
* junto com o tenant"). Senha gerada e devolvida uma única vez, mesmo
|
|
* padrão de `SecretReveal` já usado pra senha SIP de ramal. */
|
|
@IsEmail()
|
|
adminEmail!: string;
|
|
|
|
@IsString()
|
|
@MaxLength(120)
|
|
adminName!: string;
|
|
}
|
|
|
|
export class UpdateTenantDto {
|
|
@IsOptional()
|
|
@IsIn(TENANT_STATUSES)
|
|
status?: string;
|
|
|
|
@IsOptional()
|
|
@IsUUID()
|
|
planId?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(200)
|
|
tradeName?: string;
|
|
}
|