KI-AGENT: Telefonie-Trunk in Firmeneinstellungen verschieben

This commit is contained in:
2026-05-21 16:19:56 +02:00
parent ee6c2d7420
commit f6fb607008
7 changed files with 408 additions and 15 deletions

View File

@@ -0,0 +1,50 @@
CREATE TABLE IF NOT EXISTS "telephony_trunks" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"tenant_id" bigint NOT NULL,
"provider" text DEFAULT 'telekom' NOT NULL,
"enabled" boolean DEFAULT false NOT NULL,
"registrar" text DEFAULT 'tel.t-online.de' NOT NULL,
"sip_user" text,
"auth_user" text,
"password" text,
"caller_id" text,
"inbound_extension" text DEFAULT '1001' NOT NULL,
"outbound_prefix" text DEFAULT '0' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone,
"created_by" uuid,
"updated_by" uuid
);
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'telephony_trunks_tenant_id_tenants_id_fk'
) THEN
ALTER TABLE "telephony_trunks"
ADD CONSTRAINT "telephony_trunks_tenant_id_tenants_id_fk"
FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id")
ON DELETE cascade ON UPDATE no action;
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'telephony_trunks_created_by_auth_users_id_fk'
) THEN
ALTER TABLE "telephony_trunks"
ADD CONSTRAINT "telephony_trunks_created_by_auth_users_id_fk"
FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id")
ON DELETE no action ON UPDATE no action;
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'telephony_trunks_updated_by_auth_users_id_fk'
) THEN
ALTER TABLE "telephony_trunks"
ADD CONSTRAINT "telephony_trunks_updated_by_auth_users_id_fk"
FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id")
ON DELETE no action ON UPDATE no action;
END IF;
END $$;
CREATE UNIQUE INDEX IF NOT EXISTS "telephony_trunks_tenant_provider_idx"
ON "telephony_trunks" USING btree ("tenant_id", "provider");

View File

@@ -316,6 +316,13 @@
"when": 1780160400000,
"tag": "0044_telephony_calls",
"breakpoints": true
},
{
"idx": 45,
"version": "7",
"when": 1780164000000,
"tag": "0045_telephony_trunks",
"breakpoints": true
}
]
}

View File

@@ -76,6 +76,7 @@ export * from "./tasks"
export * from "./teams"
export * from "./taxtypes"
export * from "./telephony_calls"
export * from "./telephony_trunks"
export * from "./tenants"
export * from "./texttemplates"
export * from "./units"

View File

@@ -0,0 +1,48 @@
import {
pgTable,
uuid,
bigint,
text,
timestamp,
boolean,
uniqueIndex,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const telephonyTrunks = pgTable(
"telephony_trunks",
{
id: uuid("id").primaryKey().defaultRandom(),
tenantId: bigint("tenant_id", { mode: "number" })
.notNull()
.references(() => tenants.id, { onDelete: "cascade" }),
provider: text("provider").notNull().default("telekom"),
enabled: boolean("enabled").notNull().default(false),
registrar: text("registrar").notNull().default("tel.t-online.de"),
sipUser: text("sip_user"),
authUser: text("auth_user"),
password: text("password"),
callerId: text("caller_id"),
inboundExtension: text("inbound_extension").notNull().default("1001"),
outboundPrefix: text("outbound_prefix").notNull().default("0"),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }),
createdBy: uuid("created_by").references(() => authUsers.id),
updatedBy: uuid("updated_by").references(() => authUsers.id),
},
(table) => ({
tenantProviderIdx: uniqueIndex("telephony_trunks_tenant_provider_idx")
.on(table.tenantId, table.provider),
})
)
export type TelephonyTrunk = typeof telephonyTrunks.$inferSelect
export type NewTelephonyTrunk = typeof telephonyTrunks.$inferInsert

View File

@@ -1,6 +1,6 @@
import { FastifyInstance } from "fastify"
import { and, desc, eq } from "drizzle-orm"
import { telephonyCalls } from "../../db/schema"
import { telephonyCalls, telephonyTrunks } from "../../db/schema"
const envFlag = (value: string | undefined, fallback: boolean) => {
if (value === undefined || value === "") return fallback
@@ -43,7 +43,20 @@ const testAccounts = () => [
},
]
const externalTelephonyConfig = () => {
const sanitizeTrunk = (trunk: any) => ({
id: trunk?.id || null,
provider: trunk?.provider || "telekom",
enabled: Boolean(trunk?.enabled),
registrar: trunk?.registrar || "tel.t-online.de",
sipUser: trunk?.sipUser || "",
authUser: trunk?.authUser || "",
passwordConfigured: Boolean(trunk?.password),
callerId: trunk?.callerId || "",
inboundExtension: trunk?.inboundExtension || "1001",
outboundPrefix: trunk?.outboundPrefix || "0",
})
const envExternalTelephonyConfig = () => {
const provider = process.env.TELEPHONY_EXTERNAL_PROVIDER || (
envFlag(process.env.TELEPHONY_TELEKOM_ENABLED, false) ? "telekom" : ""
)
@@ -107,16 +120,50 @@ const durationSeconds = (startedAt?: Date | null, endedAt?: Date | null) => {
}
export default async function telephonyRoutes(server: FastifyInstance) {
server.get("/telephony/config", async () => ({
enabled: telephonyEnabled(),
provider: "asterisk",
mode: "local-test",
sipDomain: sipDomain(),
sipWebSocketUrl: publicAsteriskWsUrl(),
echoExtension: process.env.TELEPHONY_ECHO_EXTENSION || "600",
testAccounts: testAccounts(),
external: externalTelephonyConfig(),
}))
const loadTenantTrunk = async (tenantId: number | null, provider = "telekom") => {
if (!tenantId) return null
const [trunk] = await server.db
.select()
.from(telephonyTrunks)
.where(and(
eq(telephonyTrunks.tenantId, tenantId),
eq(telephonyTrunks.provider, provider)
))
.limit(1)
return trunk || null
}
const externalTelephonyConfig = async (tenantId: number | null) => {
const trunk = await loadTenantTrunk(tenantId)
if (trunk) {
return {
enabled: trunk.enabled,
provider: trunk.provider,
inboundExtension: trunk.inboundExtension,
outboundPrefix: trunk.outboundPrefix,
registrar: trunk.registrar,
sipUserConfigured: Boolean(trunk.sipUser),
authUserConfigured: Boolean(trunk.authUser),
passwordConfigured: Boolean(trunk.password),
callerIdConfigured: Boolean(trunk.callerId),
}
}
return envExternalTelephonyConfig()
}
server.get("/telephony/config", async (req) => ({
enabled: telephonyEnabled(),
provider: "asterisk",
mode: "local-test",
sipDomain: sipDomain(),
sipWebSocketUrl: publicAsteriskWsUrl(),
echoExtension: process.env.TELEPHONY_ECHO_EXTENSION || "600",
testAccounts: testAccounts(),
external: await externalTelephonyConfig(req.user?.tenant_id || null),
}))
server.get("/telephony/status", async () => {
const enabled = telephonyEnabled()
@@ -169,6 +216,66 @@ export default async function telephonyRoutes(server: FastifyInstance) {
}
})
server.get("/telephony/trunk-config", async (req) => {
const tenantId = requireTenant(req.user.tenant_id)
const trunk = await loadTenantTrunk(tenantId)
return sanitizeTrunk(trunk)
})
server.put("/telephony/trunk-config", async (req, reply) => {
const tenantId = requireTenant(req.user.tenant_id)
const body = (req.body || {}) as any
const existing = await loadTenantTrunk(tenantId)
const password = bodyString(body, "password")
const clearPassword = body?.clearPassword === true
const now = new Date()
const values = {
tenantId,
provider: "telekom",
enabled: Boolean(body.enabled),
registrar: bodyString(body, "registrar") || "tel.t-online.de",
sipUser: bodyString(body, "sipUser"),
authUser: bodyString(body, "authUser"),
callerId: bodyString(body, "callerId"),
inboundExtension: bodyString(body, "inboundExtension") || "1001",
outboundPrefix: bodyString(body, "outboundPrefix") || "0",
password: clearPassword ? null : (password || existing?.password || null),
updatedAt: now,
updatedBy: req.user.user_id,
}
if (values.enabled && (!values.sipUser || !values.password)) {
return reply.code(400).send({
error: "SIP-ID und Kennwort sind erforderlich, wenn der Trunk aktiviert wird.",
})
}
if (existing) {
const [updated] = await server.db
.update(telephonyTrunks)
.set(values)
.where(and(
eq(telephonyTrunks.tenantId, tenantId),
eq(telephonyTrunks.provider, "telekom")
))
.returning()
return sanitizeTrunk(updated)
}
const [created] = await server.db
.insert(telephonyTrunks)
.values({
...values,
createdAt: now,
createdBy: req.user.user_id,
})
.returning()
return sanitizeTrunk(created)
})
server.get("/telephony/calls", async (req) => {
const tenantId = requireTenant(req.user.tenant_id)
const limit = Math.min(