Compare commits

...

8 Commits

Author SHA1 Message Date
Sam Machin
d26912694d remove filter for active carriers from phone number config 2026-01-09 18:02:44 +00:00
Sam Machin
c9fcdb08eb Feature admin only numbers & carrier (#587)
* Hide create carrier/number controls if ADMIN_CARRIER env var is set

* hide add caririer button

* add to example .env

* hide delete phone number
2026-01-07 08:01:12 -05:00
Hoan Luu Huu
c33eb46ce0 soundhound speech credential support audio endpoint (#582)
* soubound speech credential support audio endpoint

* wip
2025-11-28 21:48:13 -05:00
Hoan Luu Huu
f003c158dc fix outbound call routing race condition show default lcr route set that user cannot delete (#577) 2025-11-18 07:52:28 -05:00
Sam Machin
b1ddaf230d require IP auth trunk to have either inbound or outbound carrier (#579)
also cleaned up wordig to be consistent `IP Trunk` not `Static IP Whitelist`
2025-11-10 10:29:45 -05:00
Hoan Luu Huu
0260b1ec8b Inbound and outbound sipgateway can be duplicated (#576) 2025-11-08 08:49:00 -05:00
Anton Voylenko
1c1f97f045 chore: bump node version (#575) 2025-11-04 19:33:41 -05:00
Hoan Luu Huu
e6c5a18c87 fixed reg trunk validation cannot move tab and focus to missing fields (#574)
* fixed reg trunk validation cannot move tab and focus to missing fields

* fixed reg trunk validation cannot move tab and focus to missing fields

* wip
2025-10-27 07:21:39 -04:00
13 changed files with 343 additions and 294 deletions

4
.env
View File

@@ -31,4 +31,6 @@
## AWS region for enabling Recent Call Feature server logs
#VITE_APP_AWS_REGION=us-west-2
## enable lazy loading for phone numbers (improves performance when managing large quantities)
# VITE_APP_ENABLE_PHONE_NUMBER_LAZY_LOAD=true
# VITE_APP_ENABLE_PHONE_NUMBER_LAZY_LOAD=true
# hides controlls to add Carrier and Phone number from non Admin/SP Users (also need to set flag on API server to block API calls)
#VITE_ADMIN_CARRIER=1

View File

@@ -1,4 +1,4 @@
FROM node:18.15-alpine3.16 as builder
FROM node:20-alpine AS builder
RUN apk update && apk add --no-cache python3 make g++
COPY . /opt/app
WORKDIR /opt/app/
@@ -6,7 +6,7 @@ RUN npm install
RUN npm run build
RUN npm prune
FROM node:18.14.1-alpine as webapp
FROM node:20-alpine AS webapp
RUN apk add curl
WORKDIR /opt/app
COPY . /opt/app

View File

@@ -18,7 +18,7 @@ DISABLE_CALL_RECORDING=${DISABLE_CALL_RECORDING:-false}
# Serialize window global to provide the API URL to static frontend dist
# This is declared and utilized in the web app: src/api/constants.ts
SCRIPT_TAG="<script>window.JAMBONZ = {API_BASE_URL: \"${API_BASE_URL}\",DISABLE_LCR: \"${DISABLE_LCR}\",DISABLE_JAEGER_TRACING: \"${DISABLE_JAEGER_TRACING}\",DISABLE_CUSTOM_SPEECH: \"${DISABLE_CUSTOM_SPEECH}\",ENABLE_FORGOT_PASSWORD: \"${ENABLE_FORGOT_PASSWORD}\",DISABLE_CALL_RECORDING: \"${DISABLE_CALL_RECORDING}\"};</script>"
SCRIPT_TAG="<script>window.JAMBONZ = {API_BASE_URL: \"${API_BASE_URL}\",DISABLE_LCR: \"${DISABLE_LCR}\",DISABLE_JAEGER_TRACING: \"${DISABLE_JAEGER_TRACING}\",DISABLE_CUSTOM_SPEECH: \"${DISABLE_CUSTOM_SPEECH}\",ENABLE_FORGOT_PASSWORD: \"${ENABLE_FORGOT_PASSWORD}\",DISABLE_CALL_RECORDING: \"${DISABLE_CALL_RECORDING}\",ADMIN_CARRIER: \"${ADMIN_CARRIER}\"};</script>"
sed -i -e "\@</head>@i\ $SCRIPT_TAG" ./dist/index.html
# Start the frontend web app static server

86
package-lock.json generated
View File

@@ -46,13 +46,13 @@
"nanoid": "^5.1.5",
"prettier": "^3.2.5",
"sass": "^1.89.2",
"serve": "^14.2.4",
"serve": "^14.2.5",
"ts-node": "^10.9.2",
"typescript": "^5.4.4",
"vite": "^6.0.1"
"vite": "^6.4.1"
},
"engines": {
"node": ">=14.18"
"node": ">=18"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -2196,19 +2196,6 @@
"integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==",
"dev": true
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"dev": true,
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/acorn": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
@@ -3265,6 +3252,7 @@
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-db": ">= 1.43.0 < 2"
},
@@ -3273,37 +3261,30 @@
}
},
"node_modules/compression": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
"integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
"dev": true,
"license": "MIT",
"dependencies": {
"accepts": "~1.3.5",
"bytes": "3.0.0",
"compressible": "~2.0.16",
"bytes": "3.1.2",
"compressible": "~2.0.18",
"debug": "2.6.9",
"on-headers": "~1.0.2",
"safe-buffer": "5.1.2",
"negotiator": "~0.6.4",
"on-headers": "~1.1.0",
"safe-buffer": "5.2.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/compression/node_modules/bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
"dev": true,
"engines": {
"node": ">= 0.8"
}
},
"node_modules/compression/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
@@ -3312,13 +3293,8 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true
},
"node_modules/compression/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
"dev": true,
"license": "MIT"
},
"node_modules/concat-map": {
"version": "0.0.1",
@@ -6911,10 +6887,11 @@
"dev": true
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -7071,10 +7048,11 @@
}
},
"node_modules/on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
@@ -8047,10 +8025,11 @@
}
},
"node_modules/serve": {
"version": "14.2.4",
"resolved": "https://registry.npmjs.org/serve/-/serve-14.2.4.tgz",
"integrity": "sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ==",
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/serve/-/serve-14.2.5.tgz",
"integrity": "sha512-Qn/qMkzCcMFVPb60E/hQy+iRLpiU8PamOfOSYoAHmmF+fFFmpPpqa6Oci2iWYpTdOUM3VF+TINud7CfbQnsZbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@zeit/schemas": "2.36.0",
"ajv": "8.12.0",
@@ -8059,7 +8038,7 @@
"chalk": "5.0.1",
"chalk-template": "0.4.0",
"clipboardy": "3.0.0",
"compression": "1.7.4",
"compression": "1.8.1",
"is-port-reachable": "4.0.0",
"serve-handler": "6.1.6",
"update-check": "1.5.4"
@@ -9104,10 +9083,11 @@
}
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",

View File

@@ -5,7 +5,7 @@
"license": "MIT",
"type": "module",
"engines": {
"node": ">=14.18"
"node": ">=18"
},
"contributors": [
{
@@ -77,10 +77,10 @@
"nanoid": "^5.1.5",
"prettier": "^3.2.5",
"sass": "^1.89.2",
"serve": "^14.2.4",
"serve": "^14.2.5",
"ts-node": "^10.9.2",
"typescript": "^5.4.4",
"vite": "^6.0.1"
"vite": "^6.4.1"
},
"lint-staged": {
"*.{ts,tsx}": "eslint --max-warnings=0",

View File

@@ -35,6 +35,7 @@ interface JambonzWindowObject {
DISABLE_ADDITIONAL_SPEECH_VENDORS: string;
AWS_REGION: string;
ENABLE_PHONE_NUMBER_LAZY_LOAD: string;
ADMIN_CARRIER: string;
}
declare global {
@@ -110,6 +111,8 @@ export const STRIPE_PUBLISHABLE_KEY: string =
window.JAMBONZ?.STRIPE_PUBLISHABLE_KEY ||
import.meta.env.VITE_APP_STRIPE_PUBLISHABLE_KEY;
export const ADMIN_CARRIER: string =
window.JAMBONZ?.ADMIN_CARRIER || import.meta.env.VITE_ADMIN_CARRIER || "0";
/** TCP Max Port */
export const TCP_MAX_PORT = 65535;

View File

@@ -450,6 +450,7 @@ export interface SpeechCredential {
resemble_tts_uri: null | string;
resemble_tts_use_tls: number;
api_uri: null | string;
houndify_server_uri: null | string;
}
export interface Alert {

View File

@@ -463,24 +463,18 @@ export const CarrierForm = ({
};
const getSipValidation = () => {
if (sipInboundGateways.length === 0 && sipOutboundGateways.length === 0) {
if (trunkType === "static_ip") {
setActiveTab("inbound");
return "Static IP Whitelist trunk type requires at least one inbound gateway.";
} else if (trunkType === "reg") {
setActiveTab("outbound");
return "Registration trunk type requires at least one outbound gateway.";
}
}
if (trunkType === "static_ip" && sipInboundGateways.length < 1) {
setActiveTab("inbound");
return "Static IP Whitelist trunk type requires at least one inbound gateway.";
if (
trunkType === "static_ip" &&
sipInboundGateways.length === 0 &&
sipOutboundGateways.length === 0
) {
setActiveTab("general");
return "IP Trunk type requires at least one inbound or outbound gateway.";
}
if (trunkType === "reg" && sipOutboundGateways.length < 1) {
setActiveTab("outbound");
return "Registration trunk type requires at least one outbound gateway.";
return "Registration Trunk type requires at least one outbound gateway.";
}
// Validate Auth Trunk credentials
@@ -577,26 +571,6 @@ export const CarrierForm = ({
return "Each SIP gateway must have a unique IP address.";
}
}
// Check for duplicates between inbound and outbound gateways
for (let i = 0; i < sipInboundGateways.length; i++) {
const inboundGateway = sipInboundGateways[i];
const dupeInOutbound = sipOutboundGateways.find((g) => {
return (
inboundGateway.ipv4 &&
g.ipv4 === inboundGateway.ipv4 &&
g.port === inboundGateway.port
);
});
if (dupeInOutbound) {
if (refSipInboundIp.current[i]) {
refSipInboundIp.current[i].focus();
}
setActiveTab("inbound");
return "Each SIP gateway must have a unique IP address.";
}
}
};
const getSmppValidation = () => {
@@ -721,9 +695,11 @@ export const CarrierForm = ({
if (sipGatewayValidation) {
if (
sipGatewayValidation ===
"Static IP Whitelist trunk type requires at least one inbound gateway."
"IP Trunk type requires at least one inbound or outbound gateway."
) {
setSipInboundMessage(sipGatewayValidation);
setSipOutboundMessage(sipGatewayValidation);
toastError(sipGatewayValidation);
} else if (
sipGatewayValidation ===
"Auth Trunk requires both username and password credentials."
@@ -731,7 +707,7 @@ export const CarrierForm = ({
setSipInboundMessage(sipGatewayValidation);
} else if (
sipGatewayValidation ===
"Registration trunk type requires at least one outbound gateway."
"Registration Trunk type requires at least one outbound gateway."
) {
setSipOutboundMessage(sipGatewayValidation);
} else {
@@ -906,26 +882,33 @@ export const CarrierForm = ({
const invalidField = e.target as unknown as HTMLInputElement;
const fieldName = invalidField.name || invalidField.id;
// Map field names to tabs
if (fieldName === "carrier_name") {
setActiveTab("general");
} else if (fieldName?.includes("inbound_auth_")) {
setActiveTab("inbound");
} else if (
fieldName?.includes("sip_username") ||
fieldName?.includes("sip_password") ||
fieldName?.includes("sip_realm") ||
fieldName?.includes("from_user") ||
fieldName?.includes("from_domain")
) {
setActiveTab("outbound"); // Changed from "registration" to "outbound"
} else if (fieldName?.includes("sip_")) {
setActiveTab("sip");
// Simple mapping: which tab should this field be on?
let targetTab = "general";
if (fieldName?.includes("inbound_auth_")) {
targetTab = "inbound";
} else if (fieldName?.includes("sip_") || fieldName?.includes("from_")) {
targetTab = "outbound";
} else if (fieldName?.includes("smpp_")) {
setActiveTab("smpp");
targetTab = "smpp";
}
// Allow the default browser validation message to show
// If we're not on the right tab, switch to it
if (activeTab !== targetTab) {
e.preventDefault(); // Stop the "not focusable" error
setActiveTab(targetTab);
setTimeout(() => {
const field =
document.getElementById(fieldName) ||
document.querySelector(`[name="${fieldName}"]`);
if (field && field instanceof HTMLInputElement) {
field.focus();
field.scrollIntoView({ behavior: "smooth", block: "center" });
field.reportValidity();
}
}, 100);
}
};
return (
@@ -1401,122 +1384,164 @@ export const CarrierForm = ({
</MXS>
</fieldset>
<fieldset>
<Checkzone
hidden
name="sip_credentials"
label="Authentication"
initialCheck={initialRegister}
handleChecked={(e) => {
if (!e.target.checked) {
setSipUser("");
setSipPass("");
setSipRegister(false);
}
}}
>
<MS>Does your carrier require authentication?</MS>
<label htmlFor="sip_username">
Auth username{" "}
{sipPass || sipRegister || trunkType === "reg" ? (
<span>*</span>
) : (
""
)}
</label>
<input
id="sip_username"
name="sip_username"
type="text"
value={sipUser}
placeholder="SIP username for authenticating outbound calls"
required={
sipRegister || sipPass.length > 0 || trunkType === "reg"
}
onChange={(e) => {
setSipUser(e.target.value);
}}
/>
<label htmlFor="sip_password">
Password
{sipUser || sipRegister || trunkType === "reg" ? (
<span>*</span>
) : (
""
)}
</label>
<Passwd
id="sip_password"
name="sip_password"
value={sipPass}
placeholder="SIP password for authenticating outbound calls"
required={
sipRegister || sipUser.length > 0 || trunkType === "reg"
}
onChange={(e) => {
setSipPass(e.target.value);
}}
/>
</Checkzone>
{/* Authentication Fields - shared component */}
{(() => {
const authFields = (
<>
<label htmlFor="sip_username">
Auth username{" "}
{(sipPass || sipRegister || trunkType === "reg") && (
<span>*</span>
)}
</label>
<input
id="sip_username"
name="sip_username"
type="text"
value={sipUser}
placeholder="SIP username for authenticating outbound calls"
required={
trunkType === "reg" || sipRegister || sipPass.length > 0
}
onChange={(e) => setSipUser(e.target.value)}
/>
<label htmlFor="sip_password">
Password{" "}
{(sipUser || sipRegister || trunkType === "reg") && (
<span>*</span>
)}
</label>
<Passwd
id="sip_password"
name="sip_password"
value={sipPass}
placeholder="SIP password for authenticating outbound calls"
required={
trunkType === "reg" || sipRegister || sipUser.length > 0
}
onChange={(e) => setSipPass(e.target.value)}
/>
</>
);
if (trunkType === "reg") {
return (
<div>
<div className="label">Authentication</div>
<MS>
Registration trunk requires authentication credentials.
</MS>
{authFields}
</div>
);
} else {
return (
<Checkzone
key={`sip_credentials_${trunkType}`}
hidden
name="sip_credentials"
label="Authentication"
initialCheck={initialRegister}
handleChecked={(e) => {
if (!e.target.checked) {
setSipUser("");
setSipPass("");
setSipRegister(false);
}
}}
>
<MS>Does your carrier require authentication?</MS>
{authFields}
</Checkzone>
);
}
})()}
</fieldset>
<fieldset>
<Checkzone
hidden
name="sip_register"
label="Require SIP Register"
initialCheck={initialSipRegister}
handleChecked={(e) => {
setSipRegister(e.target.checked);
if (!e.target.checked) {
setSipRealm("");
setFromUser("");
setFromDomain("");
setRegPublicIpInContact(false);
}
}}
>
<MS>Carrier requires SIP Register.</MS>
<label htmlFor="sip_realm">
SIP realm
{sipRegister || trunkType === "reg" ? <span>*</span> : ""}
</label>
<input
id="sip_realm"
name="sip_realm"
type="text"
value={sipRealm}
placeholder="SIP realm for registration"
required={sipRegister || trunkType === "reg"}
onChange={(e) => setSipRealm(e.target.value)}
/>
<label htmlFor="from_user">Username</label>
<input
id="from_user"
name="from_user"
type="text"
value={fromUser}
placeholder="Optional: specify user part of SIP From header"
onChange={(e) => setFromUser(e.target.value)}
/>
<label htmlFor="from_domain">SIP from domain</label>
<input
id="from_domain"
name="from_domain"
type="text"
value={fromDomain}
placeholder="Optional: specify host part of SIP From header"
onChange={(e) => setFromDomain(e.target.value)}
/>
<label htmlFor="reg_public_ip_in_contact_2" className="chk">
<input
id="reg_public_ip_in_contact_2"
name="reg_public_ip_in_contact_2"
type="checkbox"
checked={regPublicIpInContact}
onChange={(e) => setRegPublicIpInContact(e.target.checked)}
/>
<div>Use public IP in contact</div>
</label>
</Checkzone>
{/* SIP Registration Fields - shared component */}
{(() => {
const sipRegFields = (
<>
<label htmlFor="sip_realm">
SIP realm{" "}
{(sipRegister || trunkType === "reg") && <span>*</span>}
</label>
<input
id="sip_realm"
name="sip_realm"
type="text"
value={sipRealm}
placeholder="SIP realm for registration"
required={trunkType === "reg" || sipRegister}
onChange={(e) => setSipRealm(e.target.value)}
/>
<label htmlFor="from_user">Username</label>
<input
id="from_user"
name="from_user"
type="text"
value={fromUser}
placeholder="Optional: specify user part of SIP From header"
onChange={(e) => setFromUser(e.target.value)}
/>
<label htmlFor="from_domain">SIP from domain</label>
<input
id="from_domain"
name="from_domain"
type="text"
value={fromDomain}
placeholder="Optional: specify host part of SIP From header"
onChange={(e) => setFromDomain(e.target.value)}
/>
<label htmlFor="reg_public_ip_in_contact" className="chk">
<input
id="reg_public_ip_in_contact"
name="reg_public_ip_in_contact"
type="checkbox"
checked={regPublicIpInContact}
onChange={(e) =>
setRegPublicIpInContact(e.target.checked)
}
/>
<div>Use public IP in contact</div>
</label>
</>
);
if (trunkType === "reg") {
return (
<div>
<div className="label">SIP Registration</div>
<MS>
Registration trunk requires SIP registration settings.
</MS>
{sipRegFields}
</div>
);
} else {
return (
<Checkzone
key={`sip_register_${trunkType}`}
hidden
name="sip_register"
label="Require SIP Register"
initialCheck={initialSipRegister}
handleChecked={(e) => {
setSipRegister(e.target.checked);
if (!e.target.checked) {
setSipRealm("");
setFromUser("");
setFromDomain("");
setRegPublicIpInContact(false);
}
}}
>
<MS>Carrier requires SIP Register.</MS>
{sipRegFields}
</Checkzone>
);
}
})()}
</fieldset>
<fieldset>
<Checkzone

View File

@@ -31,6 +31,9 @@ import {
ENABLE_HOSTED_SYSTEM,
PER_PAGE_SELECTION,
USER_ACCOUNT,
ADMIN_CARRIER,
USER_ADMIN,
USER_SP,
} from "src/api/constants";
import { DeleteCarrier } from "./delete";
@@ -202,13 +205,16 @@ export const Carriers = () => {
</M>
)}
</div>
<Link to={`${ROUTE_INTERNAL_CARRIERS}/add`} title="Add a Carrier">
{" "}
<Icon>
<Icons.Plus />
</Icon>
</Link>
{((ADMIN_CARRIER === "1" &&
(user?.scope === USER_ADMIN || user?.scope === USER_SP)) ||
ADMIN_CARRIER === "0") && (
<Link to={`${ROUTE_INTERNAL_CARRIERS}/add`} title="Add a Carrier">
{" "}
<Icon>
<Icons.Plus />
</Icon>
</Link>
)}
</section>
<section className="filters filters--multi">
<SearchFilter
@@ -325,11 +331,15 @@ export const Carriers = () => {
)}
</div>
</Section>
<Section clean>
<Button small as={Link} to={`${ROUTE_INTERNAL_CARRIERS}/add`}>
Add carrier
</Button>
</Section>
{((ADMIN_CARRIER === "1" &&
(user?.scope === USER_ADMIN || user?.scope === USER_SP)) ||
ADMIN_CARRIER === "0") && (
<Section clean>
<Button small as={Link} to={`${ROUTE_INTERNAL_CARRIERS}/add`}>
Add carrier
</Button>
</Section>
)}
<footer>
<ButtonGroup>
<MS>

View File

@@ -67,9 +67,6 @@ export const LcrForm = ({ lcrDataMap, lcrRouteDataMap }: LcrFormProps) => {
const [accountSid, setAccountSid] = useState("");
const [isActive, setIsActive] = useState(true);
const [lcrRoutes, setLcrRoutes] = useState<LcrRoute[]>([LCR_ROUTE_TEMPLATE]);
const [previousLcrRoutes, setPreviousLcrRoutes] = useState<LcrRoute[]>([
LCR_ROUTE_TEMPLATE,
]);
const [previouseLcr, setPreviousLcr] = useState<Lcr | null>();
const [accounts] = useServiceProviderData<Account[]>("Accounts");
const [lcrForDelete, setLcrForDelete] = useState<Lcr | null>();
@@ -127,38 +124,35 @@ export const LcrForm = ({ lcrDataMap, lcrRouteDataMap }: LcrFormProps) => {
}, [lcrDataMap?.data, previouseLcr]);
useMemo(() => {
let default_lcr_route_sid = "";
if (
lcrRouteDataMap &&
lcrRouteDataMap.data &&
lcrRouteDataMap.data !== previousLcrRoutes
) {
setPreviousLcrRoutes(lcrRouteDataMap.data);
// Find default carrier
lcrRouteDataMap.data.forEach((lr) => {
lr.lcr_carrier_set_entries?.forEach((entry) => {
if (
entry.lcr_carrier_set_entry_sid ===
lcrDataMap?.data?.default_carrier_set_entry_sid
) {
// Only process when both lcrDataMap and lcrRouteDataMap are available
if (lcrRouteDataMap && lcrRouteDataMap.data && lcrDataMap?.data) {
const defaultCarrierSetEntrySid =
lcrDataMap.data.default_carrier_set_entry_sid;
// Find and store default route information
lcrRouteDataMap.data.forEach((route) => {
route.lcr_carrier_set_entries?.forEach((entry) => {
if (entry.lcr_carrier_set_entry_sid === defaultCarrierSetEntrySid) {
setDefaultLcrCarrier(entry.voip_carrier_sid || defaultCarrier);
setDefaultLcrCarrierSetEntrySid(
entry.lcr_carrier_set_entry_sid || null,
);
default_lcr_route_sid = entry.lcr_route_sid || "";
setDefaultLcrRoute(lr);
setDefaultLcrRoute(route);
}
});
});
}
if (lcrRouteDataMap && lcrRouteDataMap.data)
setLcrRoutes(
lcrRouteDataMap.data.filter(
(route) => route.lcr_route_sid !== default_lcr_route_sid,
),
);
}, [lcrRouteDataMap?.data]);
// Filter out routes that contain the default carrier set entry
const filteredRoutes = lcrRouteDataMap.data.filter((route) => {
return !route.lcr_carrier_set_entries?.some(
(entry) =>
entry.lcr_carrier_set_entry_sid === defaultCarrierSetEntrySid,
);
});
setLcrRoutes(filteredRoutes);
}
}, [lcrRouteDataMap?.data, lcrDataMap?.data]);
const addLcrRoutes = () => {
const newLcrRoute = LCR_ROUTE_TEMPLATE;

View File

@@ -147,8 +147,8 @@ export const PhoneNumberForm = ({ phoneNumber }: PhoneNumberFormProps) => {
voipCarriers?.filter(
(carrier) =>
!accountSid ||
(carrier.is_active &&
(!carrier.account_sid || carrier.account_sid === accountSid)),
!carrier.account_sid ||
carrier.account_sid === accountSid,
),
);
}

View File

@@ -28,7 +28,13 @@ import { hasLength, hasValue, formatPhoneNumber } from "src/utils";
import { DeletePhoneNumber } from "./delete";
import type { Account, PhoneNumber, Carrier, Application } from "src/api/types";
import { PER_PAGE_SELECTION, USER_ACCOUNT } from "src/api/constants";
import {
PER_PAGE_SELECTION,
USER_ACCOUNT,
USER_ADMIN,
ADMIN_CARRIER,
USER_SP,
} from "src/api/constants";
import { ScopedAccess } from "src/components/scoped-access";
import { Scope } from "src/store/types";
import { getAccountFilter, setLocation } from "src/store/localStore";
@@ -185,16 +191,20 @@ export const PhoneNumbers = () => {
<>
<section className="mast">
<H1 className="h2">Phone numbers</H1>
{hasLength(accounts) && hasLength(carriers) && (
<Link
to={`${ROUTE_INTERNAL_PHONE_NUMBERS}/add`}
title="Add a phone number"
>
<Icon>
<Icons.Plus />
</Icon>
</Link>
)}
{hasLength(accounts) &&
hasLength(carriers) &&
((ADMIN_CARRIER === "1" &&
(user?.scope === USER_ADMIN || user?.scope === USER_SP)) ||
ADMIN_CARRIER === "0") && (
<Link
to={`${ROUTE_INTERNAL_PHONE_NUMBERS}/add`}
title="Add a phone number"
>
<Icon>
<Icons.Plus />
</Icon>
</Link>
)}
</section>
<section className="filters filters--multi">
<SearchFilter
@@ -368,14 +378,19 @@ export const PhoneNumbers = () => {
>
<Icons.Edit3 />
</Link>
<button
type="button"
title="Delete phone number"
onClick={() => setPhoneNumber(phoneNumber)}
className="btnty"
>
<Icons.Trash />
</button>
{((ADMIN_CARRIER === "1" &&
(user?.scope === USER_ADMIN ||
user?.scope === USER_SP)) ||
ADMIN_CARRIER === "0") && (
<button
type="button"
title="Delete phone number"
onClick={() => setPhoneNumber(phoneNumber)}
className="btnty"
>
<Icons.Trash />
</button>
)}
</div>
</div>
);
@@ -404,13 +419,17 @@ export const PhoneNumbers = () => {
)}
</div>
</Section>
<Section clean>
{hasLength(accounts) && hasLength(carriers) && (
<Button small as={Link} to={`${ROUTE_INTERNAL_PHONE_NUMBERS}/add`}>
Add phone number
</Button>
)}
</Section>
{((ADMIN_CARRIER === "1" &&
(user?.scope === USER_ADMIN || user?.scope === USER_SP)) ||
ADMIN_CARRIER === "0") && (
<Section clean>
{hasLength(accounts) && hasLength(carriers) && (
<Button small as={Link} to={`${ROUTE_INTERNAL_PHONE_NUMBERS}/add`}>
Add phone number
</Button>
)}
</Section>
)}
<footer>
<ButtonGroup>
<MS>

View File

@@ -225,6 +225,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
useState(false);
const [resembleTtsUseTls, setResembleTtsUseTls] = useState(false);
const [tmpResembleTtsUseTls, setTmpResembleTtsUseTls] = useState(false);
const [houndifyServerUri, setHoundifyServerUri] = useState("");
const handleFile = (file: File) => {
const handleError = () => {
setGoogleServiceKey(null);
@@ -462,6 +463,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
client_id: clientId || null,
client_key: clientKey || null,
user_id: userId || null,
houndify_server_uri: houndifyServerUri || null,
}),
...(vendor === VENDOR_COBALT && {
cobalt_server_uri: cobaltServerUri || null,
@@ -878,6 +880,10 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
setUserId(credential.data.user_id);
}
if (credential?.data?.houndify_server_uri) {
setHoundifyServerUri(credential.data.houndify_server_uri);
}
if (credential?.data?.voice_engine) {
setTtsModelId(credential.data.voice_engine);
}
@@ -1484,6 +1490,15 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
onChange={(e) => setUserId(e.target.value)}
disabled={credential ? true : false}
/>
<label htmlFor="houndify_server_uri">Audio Endpoint</label>
<input
id="houndify_server_uri"
type="text"
name="houndify_server_uri"
placeholder="Audio Endpoint (optional)"
value={houndifyServerUri}
onChange={(e) => setHoundifyServerUri(e.target.value)}
/>
</fieldset>
)}
{vendor === VENDOR_NUANCE && (