mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-01-25 02:08:19 +00:00
Compare commits
8 Commits
fix/reg_tr
...
fix/588
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d26912694d | ||
|
|
c9fcdb08eb | ||
|
|
c33eb46ce0 | ||
|
|
f003c158dc | ||
|
|
b1ddaf230d | ||
|
|
0260b1ec8b | ||
|
|
1c1f97f045 | ||
|
|
e6c5a18c87 |
4
.env
4
.env
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
86
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user