KI-AGENT: Easybell SIP-Trunk integrieren

This commit is contained in:
2026-05-21 17:54:58 +02:00
parent 63bf57e720
commit d26fe6dcef
3 changed files with 182 additions and 71 deletions

View File

@@ -64,11 +64,34 @@ const testAccounts = () => [
}, },
] ]
const trunkProviders = {
telekom: {
key: "telekom",
label: "Telekom",
defaultRegistrar: "tel.t-online.de",
aorContactIncludesUser: false,
},
easybell: {
key: "easybell",
label: "Easybell",
defaultRegistrar: "voip.easybell.de",
aorContactIncludesUser: true,
},
} as const
type TrunkProviderKey = keyof typeof trunkProviders
const normalizeTrunkProvider = (provider: string | null | undefined): TrunkProviderKey =>
provider === "easybell" ? "easybell" : "telekom"
const trunkProviderConfig = (provider: string | null | undefined) =>
trunkProviders[normalizeTrunkProvider(provider)]
const sanitizeTrunk = (trunk: any) => ({ const sanitizeTrunk = (trunk: any) => ({
id: trunk?.id || null, id: trunk?.id || null,
provider: trunk?.provider || "telekom", provider: normalizeTrunkProvider(trunk?.provider),
enabled: Boolean(trunk?.enabled), enabled: Boolean(trunk?.enabled),
registrar: trunk?.registrar || "tel.t-online.de", registrar: trunk?.registrar || trunkProviderConfig(trunk?.provider).defaultRegistrar,
sipUser: trunk?.sipUser || "", sipUser: trunk?.sipUser || "",
authUser: trunk?.authUser || "", authUser: trunk?.authUser || "",
passwordConfigured: Boolean(trunk?.password), passwordConfigured: Boolean(trunk?.password),
@@ -146,45 +169,51 @@ const durationSeconds = (startedAt?: Date | null, endedAt?: Date | null) => {
const asteriskValue = (value: string | null | undefined) => const asteriskValue = (value: string | null | undefined) =>
String(value || "").replace(/[\r\n]/g, "").trim() String(value || "").replace(/[\r\n]/g, "").trim()
const renderTelekomPjsipConfig = (trunk: any) => { const renderProviderPjsipConfig = (trunk: any) => {
const provider = trunkProviderConfig(trunk?.provider)
if (!trunk?.enabled) { if (!trunk?.enabled) {
return [ return [
"; Von FEDEO generiert.", "; Von FEDEO generiert.",
"; Telekom-Trunk ist deaktiviert.", `; ${provider.label}-Trunk ist deaktiviert.`,
"", "",
].join("\n") ].join("\n")
} }
const registrar = asteriskValue(trunk.registrar) || "tel.t-online.de" const providerKey = provider.key
const registrar = asteriskValue(trunk.registrar) || provider.defaultRegistrar
const sipUser = asteriskValue(trunk.sipUser) const sipUser = asteriskValue(trunk.sipUser)
const authUser = asteriskValue(trunk.authUser) || sipUser const authUser = asteriskValue(trunk.authUser) || sipUser
const password = asteriskValue(trunk.password) const password = asteriskValue(trunk.password)
const callerId = asteriskValue(trunk.callerId) || sipUser const callerId = asteriskValue(trunk.callerId) || sipUser
const externalMediaAddress = asteriskValue(trunk.externalMediaAddress || trunk.externalSignalingAddress) const externalMediaAddress = asteriskValue(trunk.externalMediaAddress || trunk.externalSignalingAddress)
const aorContact = provider.aorContactIncludesUser && sipUser
? `sip:${sipUser}@${registrar}`
: `sip:${registrar}`
return [ return [
"; Von FEDEO generiert. Änderungen im Container können überschrieben werden.", "; Von FEDEO generiert. Änderungen im Container können überschrieben werden.",
"[telekom-auth]", `[${providerKey}-auth]`,
"type=auth", "type=auth",
"auth_type=userpass", "auth_type=userpass",
`username=${authUser}`, `username=${authUser}`,
`password=${password}`, `password=${password}`,
"", "",
"[telekom-aor]", `[${providerKey}-aor]`,
"type=aor", "type=aor",
`contact=sip:${registrar}`, `contact=${aorContact}`,
"", "",
"[telekom]", `[${providerKey}]`,
"type=endpoint", "type=endpoint",
"transport=transport-udp", "transport=transport-udp",
"context=from-telekom", `context=from-${providerKey}`,
"disallow=all", "disallow=all",
"allow=alaw,ulaw", "allow=alaw,ulaw",
"aors=telekom-aor", `aors=${providerKey}-aor`,
"outbound_auth=telekom-auth", `outbound_auth=${providerKey}-auth`,
`from_user=${sipUser}`, `from_user=${sipUser}`,
`from_domain=${registrar}`, `from_domain=${registrar}`,
`callerid=Telekom <${callerId}>`, `callerid=${provider.label} <${callerId}>`,
...(externalMediaAddress ? [`media_address=${externalMediaAddress}`] : []), ...(externalMediaAddress ? [`media_address=${externalMediaAddress}`] : []),
"direct_media=no", "direct_media=no",
"force_rport=yes", "force_rport=yes",
@@ -192,15 +221,15 @@ const renderTelekomPjsipConfig = (trunk: any) => {
"rtp_symmetric=yes", "rtp_symmetric=yes",
"timers=no", "timers=no",
"", "",
"[telekom-identify]", `[${providerKey}-identify]`,
"type=identify", "type=identify",
"endpoint=telekom", `endpoint=${providerKey}`,
`match=${registrar}`, `match=${registrar}`,
"", "",
"[telekom-registration]", `[${providerKey}-registration]`,
"type=registration", "type=registration",
"transport=transport-udp", "transport=transport-udp",
"outbound_auth=telekom-auth", `outbound_auth=${providerKey}-auth`,
`server_uri=sip:${registrar}`, `server_uri=sip:${registrar}`,
`client_uri=sip:${sipUser}@${registrar}`, `client_uri=sip:${sipUser}@${registrar}`,
`contact_user=${sipUser}`, `contact_user=${sipUser}`,
@@ -208,20 +237,23 @@ const renderTelekomPjsipConfig = (trunk: any) => {
"forbidden_retry_interval=300", "forbidden_retry_interval=300",
"expiration=480", "expiration=480",
"line=yes", "line=yes",
"endpoint=telekom", `endpoint=${providerKey}`,
"", "",
].join("\n") ].join("\n")
} }
const renderTelekomExtensionsConfig = (trunk: any) => { const renderProviderExtensionsConfig = (trunk: any) => {
const provider = trunkProviderConfig(trunk?.provider)
if (!trunk?.enabled) { if (!trunk?.enabled) {
return [ return [
"; Von FEDEO generiert.", "; Von FEDEO generiert.",
"; Telekom-Routing ist deaktiviert.", `; ${provider.label}-Routing ist deaktiviert.`,
"", "",
].join("\n") ].join("\n")
} }
const providerKey = provider.key
const inboundExtension = asteriskValue(trunk.inboundExtension) || "1001" const inboundExtension = asteriskValue(trunk.inboundExtension) || "1001"
const outboundPrefix = asteriskValue(trunk.outboundPrefix) || "0" const outboundPrefix = asteriskValue(trunk.outboundPrefix) || "0"
const escapedPrefix = outboundPrefix.replace(/[^0-9*#+]/g, "") const escapedPrefix = outboundPrefix.replace(/[^0-9*#+]/g, "")
@@ -231,23 +263,23 @@ const renderTelekomExtensionsConfig = (trunk: any) => {
"; Von FEDEO generiert. Änderungen im Container können überschrieben werden.", "; Von FEDEO generiert. Änderungen im Container können überschrieben werden.",
"[fedeo-local]", "[fedeo-local]",
escapedPrefix escapedPrefix
? `exten => _${escapedPrefix}X.,1,NoOp(FEDEO ausgehend über Telekom: $` + "{EXTEN})" ? `exten => _${escapedPrefix}X.,1,NoOp(FEDEO ausgehend über ${provider.label}: $` + "{EXTEN})"
: "exten => _X.,1,NoOp(FEDEO ausgehend über Telekom: ${EXTEN})", : `exten => _X.,1,NoOp(FEDEO ausgehend über ${provider.label}: $` + "{EXTEN})",
` same => n,Set(CALLERID(num)=${callerId})`, ` same => n,Set(CALLERID(num)=${callerId})`,
" same => n,Dial(PJSIP/${EXTEN}@telekom,60)", ` same => n,Dial(PJSIP/$` + `{EXTEN}@${providerKey},60)`,
" same => n,Hangup()", " same => n,Hangup()",
"", "",
"exten => _+X.,1,NoOp(FEDEO ausgehend über Telekom: ${EXTEN})", `exten => _+X.,1,NoOp(FEDEO ausgehend über ${provider.label}: $` + "{EXTEN})",
` same => n,Set(CALLERID(num)=${callerId})`, ` same => n,Set(CALLERID(num)=${callerId})`,
" same => n,Dial(PJSIP/${EXTEN}@telekom,60)", ` same => n,Dial(PJSIP/$` + `{EXTEN}@${providerKey},60)`,
" same => n,Hangup()", " same => n,Hangup()",
"", "",
"[from-telekom]", `[from-${providerKey}]`,
"exten => s,1,NoOp(FEDEO eingehend über Telekom)", `exten => s,1,NoOp(FEDEO eingehend über ${provider.label})`,
` same => n,Dial(PJSIP/${inboundExtension},30)`, ` same => n,Dial(PJSIP/${inboundExtension},30)`,
" same => n,Hangup()", " same => n,Hangup()",
"", "",
"exten => _X!,1,NoOp(FEDEO eingehend über Telekom: ${EXTEN})", `exten => _X!,1,NoOp(FEDEO eingehend über ${provider.label}: $` + "{EXTEN})",
` same => n,Dial(PJSIP/${inboundExtension},30)`, ` same => n,Dial(PJSIP/${inboundExtension},30)`,
" same => n,Hangup()", " same => n,Hangup()",
"", "",
@@ -290,11 +322,11 @@ const writeAsteriskTrunkConfig = async (trunk: any) => {
const files = [ const files = [
{ {
name: "pjsip.telekom.conf", name: "pjsip.telekom.conf",
content: renderTelekomPjsipConfig(trunk), content: renderProviderPjsipConfig(trunk),
}, },
{ {
name: "extensions.telekom.conf", name: "extensions.telekom.conf",
content: renderTelekomExtensionsConfig(trunk), content: renderProviderExtensionsConfig(trunk),
}, },
{ {
name: "pjsip.transport.conf", name: "pjsip.transport.conf",
@@ -351,11 +383,12 @@ const runAsteriskAmiCommand = async (command: string, timeoutMs = 5000) => {
}) })
} }
const runAsteriskReload = async () => { const runAsteriskReload = async (trunk: any) => {
const provider = trunkProviderConfig(trunk?.provider)
const commands = [ const commands = [
"module reload res_pjsip.so", "module reload res_pjsip.so",
"dialplan reload", "dialplan reload",
"pjsip send register telekom-registration", `pjsip send register ${provider.key}-registration`,
] ]
const results = [] const results = []
@@ -369,14 +402,17 @@ const runAsteriskReload = async () => {
} }
} }
const readAsteriskTrunkStatus = async () => { const readAsteriskTrunkStatus = async (trunk?: any) => {
const provider = trunkProviderConfig(trunk?.provider)
const registrationName = `${provider.key}-registration`
const registrations = await runAsteriskAmiCommand("pjsip show registrations") const registrations = await runAsteriskAmiCommand("pjsip show registrations")
const raw = registrations.raw || "" const raw = registrations.raw || ""
return { return {
reachable: registrations.ok, reachable: registrations.ok,
registered: /telekom-registration[\s\S]*Registered/i.test(raw), provider: provider.key,
hasRegistration: raw.includes("telekom-registration"), registered: new RegExp(`${registrationName}[\\s\\S]*Registered`, "i").test(raw),
hasRegistration: raw.includes(registrationName),
registrations: raw, registrations: raw,
} }
} }
@@ -397,19 +433,34 @@ export default async function telephonyRoutes(server: FastifyInstance) {
return trunk || null return trunk || null
} }
const loadActiveTenantTrunk = async (tenantId: number | null) => {
if (!tenantId) return null
const trunks = await server.db
.select()
.from(telephonyTrunks)
.where(eq(telephonyTrunks.tenantId, tenantId))
return trunks.find((trunk) => trunk.enabled)
|| trunks.find((trunk) => trunk.provider === "easybell")
|| trunks.find((trunk) => trunk.provider === "telekom")
|| trunks[0]
|| null
}
const externalTelephonyConfig = async (tenantId: number | null) => { const externalTelephonyConfig = async (tenantId: number | null) => {
const trunk = await loadTenantTrunk(tenantId) const trunk = await loadActiveTenantTrunk(tenantId)
if (trunk) { if (trunk) {
return { return {
enabled: trunk.enabled, enabled: trunk.enabled,
provider: trunk.provider, provider: trunk.provider,
inboundExtension: trunk.inboundExtension, inboundExtension: trunk.inboundExtension,
outboundPrefix: trunk.outboundPrefix, outboundPrefix: trunk.outboundPrefix,
registrar: trunk.registrar, registrar: trunk.registrar,
externalSignalingAddress: trunk.externalSignalingAddress, externalSignalingAddress: trunk.externalSignalingAddress,
externalMediaAddress: trunk.externalMediaAddress, externalMediaAddress: trunk.externalMediaAddress,
localNetworks: trunk.localNetworks, localNetworks: trunk.localNetworks,
sipUserConfigured: Boolean(trunk.sipUser), sipUserConfigured: Boolean(trunk.sipUser),
authUserConfigured: Boolean(trunk.authUser), authUserConfigured: Boolean(trunk.authUser),
passwordConfigured: Boolean(trunk.password), passwordConfigured: Boolean(trunk.password),
callerIdConfigured: Boolean(trunk.callerId), callerIdConfigured: Boolean(trunk.callerId),
@@ -483,23 +534,26 @@ export default async function telephonyRoutes(server: FastifyInstance) {
server.get("/telephony/trunk-config", async (req) => { server.get("/telephony/trunk-config", async (req) => {
const tenantId = requireTenant(req.user.tenant_id) const tenantId = requireTenant(req.user.tenant_id)
const trunk = await loadTenantTrunk(tenantId) const provider = normalizeTrunkProvider((req.query as { provider?: string })?.provider)
const trunk = await loadTenantTrunk(tenantId, provider)
return sanitizeTrunk(trunk) return sanitizeTrunk(trunk || { provider })
}) })
server.put("/telephony/trunk-config", async (req, reply) => { server.put("/telephony/trunk-config", async (req, reply) => {
const tenantId = requireTenant(req.user.tenant_id) const tenantId = requireTenant(req.user.tenant_id)
const body = (req.body || {}) as any const body = (req.body || {}) as any
const existing = await loadTenantTrunk(tenantId) const provider = normalizeTrunkProvider(bodyString(body, "provider"))
const providerConfig = trunkProviderConfig(provider)
const existing = await loadTenantTrunk(tenantId, provider)
const password = bodyString(body, "password") const password = bodyString(body, "password")
const clearPassword = body?.clearPassword === true const clearPassword = body?.clearPassword === true
const now = new Date() const now = new Date()
const values = { const values = {
tenantId, tenantId,
provider: "telekom", provider,
enabled: Boolean(body.enabled), enabled: Boolean(body.enabled),
registrar: bodyString(body, "registrar") || "tel.t-online.de", registrar: bodyString(body, "registrar") || providerConfig.defaultRegistrar,
sipUser: bodyString(body, "sipUser"), sipUser: bodyString(body, "sipUser"),
authUser: bodyString(body, "authUser"), authUser: bodyString(body, "authUser"),
callerId: bodyString(body, "callerId"), callerId: bodyString(body, "callerId"),
@@ -525,10 +579,21 @@ export default async function telephonyRoutes(server: FastifyInstance) {
.set(values) .set(values)
.where(and( .where(and(
eq(telephonyTrunks.tenantId, tenantId), eq(telephonyTrunks.tenantId, tenantId),
eq(telephonyTrunks.provider, "telekom") eq(telephonyTrunks.provider, provider)
)) ))
.returning() .returning()
if (values.enabled) {
await server.db
.update(telephonyTrunks)
.set({ enabled: false, updatedAt: now, updatedBy: req.user.user_id })
.where(and(
eq(telephonyTrunks.tenantId, tenantId),
eq(telephonyTrunks.enabled, true),
eq(telephonyTrunks.provider, provider === "easybell" ? "telekom" : "easybell")
))
}
return sanitizeTrunk(updated) return sanitizeTrunk(updated)
} }
@@ -541,12 +606,23 @@ export default async function telephonyRoutes(server: FastifyInstance) {
}) })
.returning() .returning()
if (values.enabled) {
await server.db
.update(telephonyTrunks)
.set({ enabled: false, updatedAt: now, updatedBy: req.user.user_id })
.where(and(
eq(telephonyTrunks.tenantId, tenantId),
eq(telephonyTrunks.enabled, true),
eq(telephonyTrunks.provider, provider === "easybell" ? "telekom" : "easybell")
))
}
return sanitizeTrunk(created) return sanitizeTrunk(created)
}) })
server.post("/telephony/trunk-config/apply", async (req, reply) => { server.post("/telephony/trunk-config/apply", async (req, reply) => {
const tenantId = requireTenant(req.user.tenant_id) const tenantId = requireTenant(req.user.tenant_id)
const trunk = await loadTenantTrunk(tenantId) const trunk = await loadActiveTenantTrunk(tenantId)
if (!trunk) { if (!trunk) {
return reply.code(404).send({ error: "Telefonie-Trunk ist noch nicht konfiguriert." }) return reply.code(404).send({ error: "Telefonie-Trunk ist noch nicht konfiguriert." })
@@ -564,8 +640,8 @@ export default async function telephonyRoutes(server: FastifyInstance) {
let warning: string | null = null let warning: string | null = null
try { try {
reload = await runAsteriskReload() reload = await runAsteriskReload(trunk)
status = await readAsteriskTrunkStatus() status = await readAsteriskTrunkStatus(trunk)
} catch (error: any) { } catch (error: any) {
warning = error?.message || "Asterisk konnte nicht neu geladen werden." warning = error?.message || "Asterisk konnte nicht neu geladen werden."
} }
@@ -581,10 +657,11 @@ export default async function telephonyRoutes(server: FastifyInstance) {
}) })
server.get("/telephony/trunk-status", async (req, reply) => { server.get("/telephony/trunk-status", async (req, reply) => {
requireTenant(req.user.tenant_id) const tenantId = requireTenant(req.user.tenant_id)
const trunk = await loadActiveTenantTrunk(tenantId)
try { try {
return await readAsteriskTrunkStatus() return await readAsteriskTrunkStatus(trunk)
} catch (error: any) { } catch (error: any) {
return reply.code(200).send({ return reply.code(200).send({
reachable: false, reachable: false,

View File

@@ -52,7 +52,7 @@ const applyTrunk = async () => {
trunkStatus.value = res?.status || trunkStatus.value trunkStatus.value = res?.status || trunkStatus.value
toast.add({ toast.add({
title: res?.warning ? "Trunk-Konfiguration geschrieben" : "Trunk angewendet", title: res?.warning ? "Trunk-Konfiguration geschrieben" : "Trunk angewendet",
description: res?.warning || (res?.status?.registered ? "Telekom-Registration ist aktiv." : "Asterisk wurde neu geladen."), description: res?.warning || (res?.status?.registered ? "Trunk-Registration ist aktiv." : "Asterisk wurde neu geladen."),
color: res?.warning ? "orange" : "success" color: res?.warning ? "orange" : "success"
}) })
} catch (error) { } catch (error) {
@@ -115,7 +115,7 @@ onMounted(async () => {
Externe Telefonie Externe Telefonie
</h2> </h2>
<p class="mt-1 text-sm text-gray-500"> <p class="mt-1 text-sm text-gray-500">
Telekom-Anbindung über den lokalen Asterisk-Trunk. SIP-Trunk-Anbindung über den lokalen Asterisk.
</p> </p>
</div> </div>
<UBadge :color="config?.external?.enabled ? 'success' : 'neutral'" variant="soft"> <UBadge :color="config?.external?.enabled ? 'success' : 'neutral'" variant="soft">
@@ -197,7 +197,7 @@ onMounted(async () => {
<UAlert <UAlert
:color="trunkStatus?.registered ? 'success' : (trunkStatus?.reachable ? 'warning' : 'neutral')" :color="trunkStatus?.registered ? 'success' : (trunkStatus?.reachable ? 'warning' : 'neutral')"
:icon="trunkStatus?.registered ? 'i-heroicons-check-circle' : (trunkStatus?.reachable ? 'i-heroicons-exclamation-triangle' : 'i-heroicons-signal-slash')" :icon="trunkStatus?.registered ? 'i-heroicons-check-circle' : (trunkStatus?.reachable ? 'i-heroicons-exclamation-triangle' : 'i-heroicons-signal-slash')"
:title="trunkStatus?.registered ? 'Telekom-Trunk registriert' : (trunkStatus?.hasRegistration ? 'Telekom-Trunk nicht registriert' : 'Keine Telekom-Registration aktiv')" :title="trunkStatus?.registered ? 'SIP-Trunk registriert' : (trunkStatus?.hasRegistration ? 'SIP-Trunk nicht registriert' : 'Keine Trunk-Registration aktiv')"
:description="trunkStatus?.message || (trunkStatus?.reachable ? 'Asterisk-AMI ist erreichbar.' : 'Asterisk-AMI ist noch nicht erreichbar.')" :description="trunkStatus?.message || (trunkStatus?.reachable ? 'Asterisk-AMI ist erreichbar.' : 'Asterisk-AMI ist noch nicht erreichbar.')"
/> />
<div class="flex flex-wrap gap-2 lg:justify-end"> <div class="flex flex-wrap gap-2 lg:justify-end">

View File

@@ -135,9 +135,26 @@ const mcpTokenForm = reactive({
const telephonyTrunkLoading = ref(false) const telephonyTrunkLoading = ref(false)
const telephonyTrunkSaving = ref(false) const telephonyTrunkSaving = ref(false)
const telephonyTrunkApplying = ref(false) const telephonyTrunkApplying = ref(false)
const telephonyProviderOptions = [
{ label: "Easybell", value: "easybell" },
{ label: "Telekom", value: "telekom" }
]
const telephonyProviderDefaults = {
easybell: {
registrar: "voip.easybell.de",
title: "Easybell SIP-Trunk",
description: "Nutze SIP-Benutzername und SIP-Passwort aus dem Easybell-Kundenportal. Der Registrar ist für SIP-Trunks in der Regel voip.easybell.de."
},
telekom: {
registrar: "tel.t-online.de",
title: "Telekom Zugangsdaten",
description: "Die SIP-ID ist meistens deine Rufnummer mit Vorwahl ohne Leerzeichen und Sonderzeichen. Falls dein Anschluss die klassischen Zugangsdaten nutzt, kannst du den Auth-User aus Anschlusskennung, Zugangsnummer, #, Mitbenutzernummer und @t-online.de bilden."
}
}
const telephonyTrunkForm = reactive({ const telephonyTrunkForm = reactive({
provider: "easybell",
enabled: false, enabled: false,
registrar: "tel.t-online.de", registrar: "voip.easybell.de",
sipUser: "", sipUser: "",
authUser: "", authUser: "",
password: "", password: "",
@@ -150,6 +167,7 @@ const telephonyTrunkForm = reactive({
externalMediaAddress: "", externalMediaAddress: "",
localNetworks: "172.16.0.0/12,192.168.0.0/16,10.0.0.0/8" localNetworks: "172.16.0.0/12,192.168.0.0/16,10.0.0.0/8"
}) })
const activeTelephonyProvider = computed(() => telephonyProviderDefaults[telephonyTrunkForm.provider] || telephonyProviderDefaults.easybell)
const setupPage = async () => { const setupPage = async () => {
itemInfo.value = auth.activeTenantData itemInfo.value = auth.activeTenantData
@@ -215,9 +233,10 @@ const loadTelephonyTrunk = async () => {
telephonyTrunkLoading.value = true telephonyTrunkLoading.value = true
try { try {
const res = await useNuxtApp().$api("/api/telephony/trunk-config") const res = await useNuxtApp().$api(`/api/telephony/trunk-config?provider=${telephonyTrunkForm.provider}`)
telephonyTrunkForm.provider = res?.provider || telephonyTrunkForm.provider || "easybell"
telephonyTrunkForm.enabled = Boolean(res?.enabled) telephonyTrunkForm.enabled = Boolean(res?.enabled)
telephonyTrunkForm.registrar = res?.registrar || "tel.t-online.de" telephonyTrunkForm.registrar = res?.registrar || activeTelephonyProvider.value.registrar
telephonyTrunkForm.sipUser = res?.sipUser || "" telephonyTrunkForm.sipUser = res?.sipUser || ""
telephonyTrunkForm.authUser = res?.authUser || "" telephonyTrunkForm.authUser = res?.authUser || ""
telephonyTrunkForm.password = "" telephonyTrunkForm.password = ""
@@ -239,7 +258,7 @@ const loadTelephonyTrunk = async () => {
const saveTelephonyTrunk = async () => { const saveTelephonyTrunk = async () => {
if (telephonyTrunkForm.enabled && (!telephonyTrunkForm.sipUser?.trim() || (!telephonyTrunkForm.password?.trim() && !telephonyTrunkForm.passwordConfigured))) { if (telephonyTrunkForm.enabled && (!telephonyTrunkForm.sipUser?.trim() || (!telephonyTrunkForm.password?.trim() && !telephonyTrunkForm.passwordConfigured))) {
toast.add({ toast.add({
title: "Telekom-Zugang unvollständig", title: "Trunk-Zugang unvollständig",
description: "Bitte gib mindestens SIP-ID und Kennwort an.", description: "Bitte gib mindestens SIP-ID und Kennwort an.",
color: "orange" color: "orange"
}) })
@@ -252,6 +271,7 @@ const saveTelephonyTrunk = async () => {
const res = await useNuxtApp().$api("/api/telephony/trunk-config", { const res = await useNuxtApp().$api("/api/telephony/trunk-config", {
method: "PUT", method: "PUT",
body: { body: {
provider: telephonyTrunkForm.provider,
enabled: telephonyTrunkForm.enabled, enabled: telephonyTrunkForm.enabled,
registrar: telephonyTrunkForm.registrar, registrar: telephonyTrunkForm.registrar,
sipUser: telephonyTrunkForm.sipUser, sipUser: telephonyTrunkForm.sipUser,
@@ -292,7 +312,7 @@ const applyTelephonyTrunk = async () => {
toast.add({ toast.add({
title: res?.warning ? "Trunk-Konfiguration geschrieben" : "Telefonie-Trunk angewendet", title: res?.warning ? "Trunk-Konfiguration geschrieben" : "Telefonie-Trunk angewendet",
description: res?.warning || (res?.status?.registered ? "Telekom-Registration ist aktiv." : "Asterisk wurde neu geladen."), description: res?.warning || (res?.status?.registered ? "Trunk-Registration ist aktiv." : "Asterisk wurde neu geladen."),
color: res?.warning ? "orange" : "success" color: res?.warning ? "orange" : "success"
}) })
} catch (error) { } catch (error) {
@@ -363,6 +383,14 @@ onMounted(() => {
loadMcpTokens() loadMcpTokens()
loadTelephonyTrunk() loadTelephonyTrunk()
}) })
watch(() => telephonyTrunkForm.provider, async (provider) => {
const defaults = telephonyProviderDefaults[provider] || telephonyProviderDefaults.easybell
if (!telephonyTrunkForm.registrar || ["tel.t-online.de", "voip.easybell.de"].includes(telephonyTrunkForm.registrar)) {
telephonyTrunkForm.registrar = defaults.registrar
}
await loadTelephonyTrunk()
})
</script> </script>
<template> <template>
@@ -476,7 +504,7 @@ onMounted(() => {
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between"> <div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div> <div>
<h3 class="text-base font-semibold text-highlighted">Telefonie-Trunk</h3> <h3 class="text-base font-semibold text-highlighted">Telefonie-Trunk</h3>
<p class="text-sm text-muted">Konfiguriere den Telekom-Anschluss für externe Anrufe über Asterisk.</p> <p class="text-sm text-muted">Konfiguriere den SIP-Trunk für externe Anrufe über Asterisk.</p>
</div> </div>
<UBadge :color="telephonyTrunkForm.enabled ? 'success' : 'neutral'" variant="soft"> <UBadge :color="telephonyTrunkForm.enabled ? 'success' : 'neutral'" variant="soft">
{{ telephonyTrunkForm.enabled ? "Aktiv" : "Inaktiv" }} {{ telephonyTrunkForm.enabled ? "Aktiv" : "Inaktiv" }}
@@ -484,28 +512,34 @@ onMounted(() => {
</div> </div>
<UAlert <UAlert
title="Telekom Zugangsdaten" :title="activeTelephonyProvider.title"
color="neutral" color="neutral"
variant="outline" variant="outline"
> >
<template #description> <template #description>
<p class="text-sm"> <p class="text-sm">
Die SIP-ID ist meistens deine Rufnummer mit Vorwahl ohne Leerzeichen und Sonderzeichen. {{ activeTelephonyProvider.description }}
Falls dein Anschluss die klassischen Zugangsdaten nutzt, kannst du den Auth-User aus
Anschlusskennung, Zugangsnummer, #, Mitbenutzernummer und @t-online.de bilden.
</p> </p>
</template> </template>
</UAlert> </UAlert>
<div class="grid gap-4 md:grid-cols-2"> <div class="grid gap-4 md:grid-cols-2">
<UFormField label="Provider">
<USelectMenu
v-model="telephonyTrunkForm.provider"
:items="telephonyProviderOptions"
label-key="label"
value-key="value"
/>
</UFormField>
<UFormField label="Trunk aktivieren"> <UFormField label="Trunk aktivieren">
<USwitch v-model="telephonyTrunkForm.enabled" /> <USwitch v-model="telephonyTrunkForm.enabled" />
</UFormField> </UFormField>
<UFormField label="Registrar"> <UFormField label="Registrar">
<UInput v-model="telephonyTrunkForm.registrar" placeholder="tel.t-online.de" /> <UInput v-model="telephonyTrunkForm.registrar" :placeholder="activeTelephonyProvider.registrar" />
</UFormField> </UFormField>
<UFormField label="SIP-ID / Rufnummer"> <UFormField label="SIP-ID / Rufnummer">
<UInput v-model="telephonyTrunkForm.sipUser" placeholder="0301234567" /> <UInput v-model="telephonyTrunkForm.sipUser" placeholder="SIP-Benutzername" />
</UFormField> </UFormField>
<UFormField label="Auth-User"> <UFormField label="Auth-User">
<UInput v-model="telephonyTrunkForm.authUser" placeholder="Optional" /> <UInput v-model="telephonyTrunkForm.authUser" placeholder="Optional" />
@@ -514,7 +548,7 @@ onMounted(() => {
<UInput <UInput
v-model="telephonyTrunkForm.password" v-model="telephonyTrunkForm.password"
type="password" type="password"
:placeholder="telephonyTrunkForm.passwordConfigured ? 'Bereits gespeichert' : 'Persönliches Kennwort'" :placeholder="telephonyTrunkForm.passwordConfigured ? 'Bereits gespeichert' : 'SIP-Kennwort'"
/> />
</UFormField> </UFormField>
<UFormField label="Gespeichertes Kennwort löschen"> <UFormField label="Gespeichertes Kennwort löschen">
@@ -524,7 +558,7 @@ onMounted(() => {
/> />
</UFormField> </UFormField>
<UFormField label="Absendernummer"> <UFormField label="Absendernummer">
<UInput v-model="telephonyTrunkForm.callerId" placeholder="Optional, z. B. 0301234567" /> <UInput v-model="telephonyTrunkForm.callerId" placeholder="Optional, z. B. 49301234567" />
</UFormField> </UFormField>
<UFormField label="Eingehende Nebenstelle"> <UFormField label="Eingehende Nebenstelle">
<UInput v-model="telephonyTrunkForm.inboundExtension" placeholder="1001" /> <UInput v-model="telephonyTrunkForm.inboundExtension" placeholder="1001" />