- extensions table (tenant-scoped, RLS): number, sip_password_enc
(AES-256-GCM via packages/shared/src/crypto.ts), caller_id, context,
sofia_profile, codecs, max_registrations
- apps/api/src/extensions: CRUD (POST/GET/GET:id/DELETE), protected by a
new generic PermissionGuard (@RequirePermission decorator), tenant
resolved only from the JWT (never trusted from the client)
- SIP password is returned in plaintext only once, in the create response;
toPublicExtension() explicitly destructures the encrypted field out
(not a spread) so it can't leak by accident
- b2bcall-fs-config now resolves real directory data: Tenant.telephonyDomain
-> Extension.number, decrypts the password, builds proper directory XML
including a dial-string param (missing it caused originate to fail with
MANDATORY_IE_MISSING instead of the expected USER_NOT_REGISTERED)
- pinned FreeSWITCH's 357737{domain} to a stable value (b2bcall.local) via a
vars.xml patch in the Dockerfile -- it previously used the container's
dynamic IP, which could never match a stored telephony_domain
- added HTTP Basic auth between FreeSWITCH and fs-config
(gateway-credentials, timingSafeEqual comparison) now that the service
returns real secret data, closing the gap flagged as pending in the XML
Curl phase instead of leaving it open
- found and fixed: PermissionGuard's constructor-injected Reflector came
back undefined at runtime under tsx/esbuild (unreliable cross-file
decorator metadata emission) -- fixed with an explicit @Inject(Reflector);
worth watching for in future guards/services run via tsx
- verified end-to-end: create extension -> originate user/<ext> reports
USER_NOT_REGISTERED (found, not registered) -> delete -> back to
SUBSCRIBER_ABSENT (not found); password never reappears in any GET;
unauthenticated fs-config requests get 401
- docs/EXTENSIONS.md
38 lines
1.4 KiB
SQL
38 lines
1.4 KiB
SQL
-- CreateTable
|
|
CREATE TABLE "extensions" (
|
|
"id" UUID NOT NULL,
|
|
"tenant_id" UUID NOT NULL,
|
|
"number" TEXT NOT NULL,
|
|
"name" TEXT NOT NULL,
|
|
"domain" TEXT NOT NULL,
|
|
"sip_password_enc" TEXT NOT NULL,
|
|
"caller_id_name" TEXT,
|
|
"caller_id_number" TEXT,
|
|
"context" TEXT NOT NULL DEFAULT 'default',
|
|
"sofia_profile" TEXT NOT NULL DEFAULT 'internal',
|
|
"codecs" TEXT NOT NULL DEFAULT 'PCMU,PCMA,OPUS',
|
|
"max_registrations" INTEGER NOT NULL DEFAULT 1,
|
|
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
"deleted_at" TIMESTAMP(3),
|
|
|
|
CONSTRAINT "extensions_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "extensions_tenant_id_idx" ON "extensions"("tenant_id");
|
|
|
|
-- CreateIndex
|
|
CREATE UNIQUE INDEX "extensions_tenant_id_number_key" ON "extensions"("tenant_id", "number");
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "extensions" ADD CONSTRAINT "extensions_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
|
|
-- Tabela de negocio tenant-scoped: RLS obrigatorio (ver docs/TENANT_ISOLATION.md).
|
|
ALTER TABLE "extensions" ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE "extensions" FORCE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY "tenant_isolation" ON "extensions"
|
|
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);
|