KI-AGENT: Easybell SIP-Trunk integrieren
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
Reference in New Issue
Block a user