Files
jambonz-webapp/src/containers/internal/views/carriers/form.tsx
Dave Horton ea2713a021 update deps (#422)
* update deps

* prettier

* wip
2024-04-07 18:42:42 -04:00

1463 lines
50 KiB
TypeScript

import React, { useEffect, useRef, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Button, ButtonGroup, Icon, MS, MXS, Tab, Tabs } from "@jambonz/ui-kit";
import {
deleteSipGateway,
deleteSmppGateway,
postCarrier,
postSipGateway,
postSmppGateway,
putCarrier,
putSipGateway,
putSmppGateway,
useApiData,
useServiceProviderData,
postPredefinedCarrierTemplate,
postPredefinedCarrierTemplateAccount,
} from "src/api";
import {
DEFAULT_SIP_GATEWAY,
DEFAULT_SMPP_GATEWAY,
FQDN,
FQDN_TOP_LEVEL,
INVALID,
IP,
NETMASK_OPTIONS,
SIP_GATEWAY_PROTOCOL_OPTIONS,
TCP_MAX_PORT,
TECH_PREFIX_MINLENGTH,
USER_ACCOUNT,
} from "src/api/constants";
import { Icons, Section } from "src/components";
import {
Checkzone,
Message,
Passwd,
Selector,
AccountSelect,
ApplicationSelect,
} from "src/components/forms";
import { MSG_REQUIRED_FIELDS } from "src/constants";
import { ROUTE_INTERNAL_CARRIERS } from "src/router/routes";
import { toastError, toastSuccess, useSelectState } from "src/store";
import {
checkSelectOptions,
getIpValidationType,
isUserAccountScope,
hasLength,
isValidPort,
disableDefaultTrunkRouting,
hasValue,
isNotBlank,
} from "src/utils";
import type {
Account,
UseApiDataMap,
Carrier,
SipGateway,
SmppGateway,
PredefinedCarrier,
Sbc,
Smpp,
Application,
} from "src/api/types";
import { setAccountFilter, setLocation } from "src/store/localStore";
import { RegisterStatus } from "./register-status";
type CarrierFormProps = {
carrier?: UseApiDataMap<Carrier>;
carrierSipGateways?: UseApiDataMap<SipGateway[]>;
carrierSmppGateways?: UseApiDataMap<SmppGateway[]>;
};
export const CarrierForm = ({
carrier,
carrierSipGateways,
carrierSmppGateways,
}: CarrierFormProps) => {
const navigate = useNavigate();
const user = useSelectState("user");
const currentServiceProvider = useSelectState("currentServiceProvider");
const refSipIp = useRef<HTMLInputElement[]>([]);
const refSipPort = useRef<HTMLInputElement[]>([]);
const refSmppIp = useRef<HTMLInputElement[]>([]);
const refSmppPort = useRef<HTMLInputElement[]>([]);
const [sbcs] = useApiData<Sbc[]>("Sbcs");
const [smpps] = useApiData<Smpp[]>("Smpps");
const [applications] = useServiceProviderData<Application[]>("Applications");
const [accounts] = useServiceProviderData<Account[]>("Accounts");
const [predefinedCarriers] =
useApiData<PredefinedCarrier[]>("PredefinedCarriers");
const [activeTab, setActiveTab] = useState("");
const [predefinedName, setPredefinedName] = useState("");
const [carrierName, setCarrierName] = useState("");
const [isActive, setIsActive] = useState(true);
const [e164, setE164] = useState(false);
const [applicationSid, setApplicationSid] = useState("");
const [accountSid, setAccountSid] = useState("");
const [sipRegister, setSipRegister] = useState(false);
const [sipUser, setSipUser] = useState("");
const [sipPass, setSipPass] = useState("");
const [sipRealm, setSipRealm] = useState("");
const [initialRegister, setInitialRegister] = useState(false);
const [fromUser, setFromUser] = useState("");
const [fromDomain, setFromDomain] = useState("");
const [regPublicIpInContact, setRegPublicIpInContact] = useState(false);
const [prefix, setPrefix] = useState("");
const [initialPrefix, setInitialPrefix] = useState(false);
const [diversion, setDiversion] = useState("");
const [initialDiversion, setInitialDiversion] = useState(false);
const [smppSystemId, setSmppSystemId] = useState("");
const [smppPass, setSmppPass] = useState("");
const [smppInboundSystemId, setSmppInboundSystemId] = useState("");
const [smppInboundPass, setSmppInboundPass] = useState("");
const [sipGateways, setSipGateways] = useState<SipGateway[]>([
DEFAULT_SIP_GATEWAY,
]);
const [smppGateways, setSmppGateways] = useState<SmppGateway[]>([
{
...DEFAULT_SMPP_GATEWAY,
inbound: 0,
},
{
...DEFAULT_SMPP_GATEWAY,
outbound: 0,
},
]);
const [sipMessage, setSipMessage] = useState("");
const [smppInboundMessage, setSmppInboundMessage] = useState("");
const [smppOutboundMessage, setSmppOutboundMessage] = useState("");
const setCarrierStates = (obj: Carrier) => {
if (obj) {
setIsActive(obj.is_active);
if (obj.name) {
setCarrierName(obj.name);
}
if (obj.e164_leading_plus) {
setE164(obj.e164_leading_plus);
}
if (obj.application_sid) {
setApplicationSid(obj.application_sid);
}
if (obj.account_sid) {
setAccountSid(obj.account_sid);
}
if (obj.requires_register) {
setSipRegister(obj.requires_register);
}
if (obj.register_username) {
setSipUser(obj.register_username);
}
if (obj.register_password) {
setSipPass(obj.register_password);
}
if (obj.register_sip_realm) {
setSipRealm(obj.register_sip_realm);
}
if (obj.register_from_user) {
setFromUser(obj.register_from_user);
}
if (obj.register_from_domain) {
setFromDomain(obj.register_from_domain);
}
if (obj.register_public_ip_in_contact) {
setRegPublicIpInContact(obj.register_public_ip_in_contact);
}
if (
obj.requires_register ||
obj.register_username ||
obj.register_password ||
obj.register_sip_realm ||
obj.register_from_user ||
obj.register_from_domain ||
obj.register_public_ip_in_contact
) {
setInitialRegister(true);
} else {
setInitialRegister(false);
}
if (obj.tech_prefix) {
setPrefix(obj.tech_prefix);
setInitialPrefix(true);
} else {
setInitialPrefix(false);
}
if (obj.diversion) {
setDiversion(obj.diversion);
setInitialDiversion(true);
} else {
setInitialDiversion(false);
}
if (obj.smpp_system_id) {
setSmppSystemId(obj.smpp_system_id);
}
if (obj.smpp_password) {
setSmppPass(obj.smpp_password);
}
if (obj.smpp_inbound_system_id) {
setSmppInboundSystemId(obj.smpp_inbound_system_id);
}
if (obj.smpp_inbound_password) {
setSmppInboundPass(obj.smpp_inbound_password);
}
}
};
const addSipGateway = () => {
setSipGateways((curr) => [...curr, DEFAULT_SIP_GATEWAY]);
};
const addSmppGateway = (obj: Partial<SmppGateway>) => {
setSmppGateways((curr) => [
...curr,
{
...DEFAULT_SMPP_GATEWAY /** { inbound: 1, outbound: 1 } */,
...obj /** pass the values: e.g. { outbound: 1, inbound: 0 } */,
},
]);
};
const updateSipGateways = (
index: number,
key: string,
value: (typeof sipGateways)[number][keyof SipGateway],
) => {
setSipGateways(
sipGateways.map((g, i) =>
i === index
? {
...g,
[key]: value,
// If Change to ipv4 and port is null, change port to 5060
...(key === "ipv4" &&
value &&
typeof value === "string" &&
getIpValidationType(value) === IP &&
g.port === null && { port: 5060 }),
}
: g,
),
);
};
const updateSmppGateways = (
index: number,
key: string,
value: (typeof smppGateways)[number][keyof SmppGateway],
) => {
setSmppGateways(
smppGateways.map((g, i) => (i === index ? { ...g, [key]: value } : g)),
);
};
const handleSipGatewayPutPost = (voip_carrier_sid: string) => {
Promise.all(
sipGateways.map(({ sip_gateway_sid, ...g }: SipGateway) =>
sip_gateway_sid
? putSipGateway(sip_gateway_sid, g)
: postSipGateway({ ...g, voip_carrier_sid }),
),
).then(() => {
if (carrierSipGateways) {
carrierSipGateways.refetch();
}
});
};
const handleSmppGatewayPutPost = (voip_carrier_sid: string) => {
Promise.all(
smppGateways
/** Ensure the empty UI fields don't actually save in the background... */
.filter((g) => g.ipv4.trim() !== "" && isValidPort(g.port))
.map(({ smpp_gateway_sid, ...g }: SmppGateway) => {
smpp_gateway_sid
? putSmppGateway(smpp_gateway_sid, g)
: postSmppGateway({ ...g, voip_carrier_sid });
}),
).then(() => {
if (carrierSmppGateways) {
carrierSmppGateways.refetch();
}
});
};
const handleSipGatewayDelete = (g?: SipGateway) => {
if (g && g.sip_gateway_sid) {
deleteSipGateway(g.sip_gateway_sid).then(() =>
toastSuccess("SIP gateway successfully deleted"),
);
}
};
const handleSmppGatewayDelete = (g?: SmppGateway) => {
if (g && g.smpp_gateway_sid) {
deleteSmppGateway(g.smpp_gateway_sid).then(() =>
toastSuccess(
`SMPP ${
g.outbound ? "outbound" : "inbound"
} gateway successfully deleted`,
),
);
}
};
const hasEmptySmppGateways = (type: keyof SmppGateway) => {
const filtered = smppGateways.filter((g) => g[type]);
return (
hasLength(filtered) &&
filtered.reduce((acc, g) => {
return acc + g.ipv4.trim();
}, "") === ""
);
};
const getSipValidation = () => {
if (!hasLength(sipGateways)) {
return "You must provide at least one SIP Gateway.";
}
for (let i = 0; i < sipGateways.length; i++) {
const gateway = sipGateways[i];
const type = getIpValidationType(gateway.ipv4);
/** DH: unclear why we had this restriction, removing for now
if (type === FQDN_TOP_LEVEL) {
refSipIp.current[i].focus();
return "When using an FQDN, you must use a subdomain (e.g. sip.example.com).";
} else if (type === FQDN && (!gateway.outbound || gateway.inbound)) {
*/
if (type === FQDN && (!gateway.outbound || gateway.inbound)) {
refSipIp.current[i].focus();
return "A fully qualified domain name may only be used for outbound calls.";
} else if (type === INVALID) {
refSipIp.current[i].focus();
return "Please provide a valid IP address or fully qualified domain name.";
}
/** Duplicates validation */
const dupeSipGateway = sipGateways.find((g) => {
return (
g !== gateway &&
gateway.ipv4 &&
g.ipv4 === gateway.ipv4 &&
g.port === gateway.port
);
});
if (dupeSipGateway) {
refSipIp.current[i].focus();
return "Each SIP gateway must have a unique IP address.";
}
}
};
const getSmppValidation = () => {
for (let i = 0; i < smppGateways.length; i++) {
const gateway = smppGateways[i];
const gatewayType = gateway.inbound ? "inbound" : "outbound";
const type = getIpValidationType(gateway.ipv4);
if (type === FQDN_TOP_LEVEL) {
refSmppIp.current[i].focus();
return {
msg: "When using an FQDN, you must use a subdomain (e.g. sip.example.com).",
type: gatewayType,
};
} else if (type === FQDN && (!gateway.outbound || gateway.inbound)) {
refSmppIp.current[i].focus();
return {
msg: "A fully qualified domain name may only be used for outbound calls.",
type: gatewayType,
};
} else if (type === INVALID && gateway.ipv4.trim() !== "") {
refSmppIp.current[i].focus();
return {
msg: `Please provide a valid ${gatewayType} IP address or fully qualified domain name.`,
type: gatewayType,
};
}
/** Duplicates validation */
const dupeSmppGateway = smppGateways.find((g) => {
return (
g !== gateway &&
gateway.ipv4 &&
g[gatewayType] === gateway[gatewayType] &&
g.ipv4 === gateway.ipv4 &&
g.port === gateway.port
);
});
if (dupeSmppGateway) {
refSmppIp.current[i].focus();
return {
msg: `Each ${gatewayType} SMPP gateway must have a unique IP address.`,
type: gatewayType,
};
}
}
};
const shouldValidateSmpp = () => {
return (
smppSystemId ||
smppPass ||
smppInboundPass ||
!hasEmptySmppGateways("outbound") ||
!hasEmptySmppGateways("inbound")
);
};
const handleActiveTab = () => {
/** When to switch to `sip` tab */
const emptySipIp = sipGateways.find((g) => g.ipv4.trim() === "");
const invalidSipPort = sipGateways.find(
(g) => hasValue(g.port) && !isValidPort(g.port),
);
const sipGatewayValidation = getSipValidation();
/** Empty SIP gateway */
/** Invalid SIP port number */
/** Outbound auth conditionals */
if (
emptySipIp ||
invalidSipPort ||
sipGatewayValidation ||
(sipUser && !sipPass) ||
(sipPass && !sipUser) ||
(sipRegister && (!sipRealm || !sipPass || !sipUser)) ||
(prefix && prefix.length < TECH_PREFIX_MINLENGTH)
) {
setActiveTab("sip");
return; /** Important so browser contstraints work properly */
}
/** When to switch to the `smpp` tab */
const invalidSmppPort = smppGateways
.filter((g) => g.outbound)
.find((g) => !isValidPort(g.port));
const smppGatewayValidation = shouldValidateSmpp() && getSmppValidation();
/** Outbound user/pass filled out but no gateways */
/** Inbound gateways but no inbound pass */
/** Invalid SMPP port number */
if (
invalidSmppPort ||
smppGatewayValidation ||
(smppSystemId && smppPass && hasEmptySmppGateways("outbound")) ||
(!smppInboundPass && !hasEmptySmppGateways("inbound"))
) {
setActiveTab("smpp");
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (isUserAccountScope(accountSid, user)) {
toastError("You do not have permissions to make changes to this Carrier");
return;
}
setSipMessage("");
setSmppInboundMessage("");
setSmppOutboundMessage("");
const sipGatewayValidation = getSipValidation();
if (sipGatewayValidation) {
setSipMessage(sipGatewayValidation);
return;
}
/** Conditions to validate SMPP gateway fields... */
if (shouldValidateSmpp()) {
const smppGatewayValidation = getSmppValidation();
if (smppGatewayValidation) {
if (smppGatewayValidation.type === "outbound") {
setSmppOutboundMessage(smppGatewayValidation.msg);
} else {
setSmppInboundMessage(smppGatewayValidation.msg);
}
return;
}
}
if (currentServiceProvider) {
const carrierPayload: Partial<Carrier> = {
name: carrierName.trim(),
e164_leading_plus: e164,
application_sid: applicationSid || null,
service_provider_sid: currentServiceProvider.service_provider_sid,
account_sid: accountSid || null,
requires_register: sipRegister,
register_username: sipUser.trim() || null,
register_password: sipPass.trim() || null,
register_sip_realm: sipRealm.trim() || null,
register_from_user: sipRegister && fromUser ? fromUser.trim() : null,
register_from_domain:
sipRegister && fromDomain ? fromDomain.trim() : null,
register_public_ip_in_contact: sipRegister && regPublicIpInContact,
tech_prefix: prefix.trim() || null,
diversion: diversion.trim() || null,
is_active: isActive,
smpp_system_id: smppSystemId.trim() || null,
smpp_password: smppPass.trim() || null,
smpp_inbound_system_id: smppInboundSystemId.trim() || null,
smpp_inbound_password: smppInboundPass.trim() || null,
};
if (carrier && carrier.data) {
putCarrier(
currentServiceProvider.service_provider_sid,
carrier.data.voip_carrier_sid,
carrierPayload,
)
.then(() => {
if (carrier.data?.voip_carrier_sid) {
handleSipGatewayPutPost(carrier.data.voip_carrier_sid);
handleSmppGatewayPutPost(carrier.data.voip_carrier_sid);
}
toastSuccess("Carrier updated successfully");
carrier.refetch();
navigate(
`${ROUTE_INTERNAL_CARRIERS}/${carrier.data?.voip_carrier_sid}/edit`,
);
})
.catch((error) => {
toastError(error.msg);
});
} else {
postCarrier(currentServiceProvider.service_provider_sid, {
...carrierPayload,
service_provider_sid: currentServiceProvider.service_provider_sid,
})
.then(({ json }) => {
handleSipGatewayPutPost(json.sid);
handleSmppGatewayPutPost(json.sid);
toastSuccess("Carrier created successfully");
navigate(ROUTE_INTERNAL_CARRIERS);
setAccountFilter(accountSid);
})
.catch((error) => {
toastError(error.msg);
});
}
}
};
useEffect(() => {
setLocation();
if (predefinedName && hasLength(predefinedCarriers)) {
const predefinedCarrierSid = predefinedCarriers.find(
(a) => a.name === predefinedName,
)?.predefined_carrier_sid;
if (currentServiceProvider && predefinedCarrierSid) {
const postPredefinedCarrier =
user?.scope === USER_ACCOUNT
? postPredefinedCarrierTemplateAccount(
accountSid,
predefinedCarrierSid,
)
: postPredefinedCarrierTemplate(
currentServiceProvider.service_provider_sid,
predefinedCarrierSid,
);
postPredefinedCarrier
.then(({ json }) => {
navigate(`${ROUTE_INTERNAL_CARRIERS}/${json.sid}/edit`);
})
.catch((error) => {
toastError(error.msg);
});
}
}
}, [predefinedName]);
useEffect(() => {
if (carrier && carrier.data) {
setCarrierStates(carrier.data);
}
}, [carrier]);
/** This fixes a re-rendering glitch when we used useEffect that was annoying but not breaking */
/** https://beta.reactjs.org/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes */
const [prevSipGateways, setPrevSipGateways] = useState<SipGateway[]>();
const [prevSmppGateways, setPrevSmppGateways] = useState<SmppGateway[]>();
if (
carrierSipGateways &&
hasLength(carrierSipGateways.data) &&
carrierSipGateways.data !== prevSipGateways
) {
setPrevSipGateways(carrierSipGateways.data); /** Deadly important */
setSipGateways(carrierSipGateways.data);
}
if (
carrierSmppGateways &&
hasLength(carrierSmppGateways.data) &&
carrierSmppGateways.data !== prevSmppGateways
) {
const inbound = carrierSmppGateways.data.filter((g) => g.inbound);
const outbound = carrierSmppGateways.data.filter((g) => g.outbound);
setPrevSmppGateways(carrierSmppGateways.data); /** Deadly important */
setSmppGateways(carrierSmppGateways.data);
if (inbound.length <= 0) {
addSmppGateway({ inbound: 1, outbound: 0 });
}
if (outbound.length <= 0) {
addSmppGateway({ outbound: 1, inbound: 0 });
}
}
return (
<Section slim>
<form
className={`form form--internal ${
!carrier?.data && carrier?.refetch ? "form--blur" : ""
}`}
onSubmit={handleSubmit}
>
<fieldset>
<MS>{MSG_REQUIRED_FIELDS}</MS>
</fieldset>
{carrier &&
carrier.data &&
Boolean(carrier.data.requires_register) &&
carrier.data.register_status && (
<fieldset>
<div className="m med">Register status</div>
<RegisterStatus carrier={carrier.data} />
</fieldset>
)}
<fieldset>
<div className="multi">
<div className="inp">
<label htmlFor="carrier_name">
Carrier name<span>*</span>
</label>
<input
id="carrier_name"
name="carrier_name"
type="text"
required
placeholder="Carrier name"
value={carrierName}
onChange={(e) => setCarrierName(e.target.value)}
/>
</div>
{!carrier && (
<div className="sel sel--preset">
<label htmlFor="predefined_select">
Select a predefined carrier
</label>
<Selector
id="predefined_select"
name="predefined_select"
value={predefinedName}
options={[
{
name: "None",
value: "",
},
].concat(
predefinedCarriers
? predefinedCarriers.map(
(carrier: PredefinedCarrier) => ({
name: carrier.name,
value: carrier.name,
}),
)
: [],
)}
onChange={(e) => setPredefinedName(e.target.value)}
/>
</div>
)}
</div>
<label htmlFor="is_active" className="chk">
<input
id="is_active"
name="is_active"
type="checkbox"
checked={isActive}
onChange={(e) => setIsActive(e.target.checked)}
/>
<div>Active</div>
</label>
</fieldset>
<Tabs active={[activeTab, setActiveTab]}>
<Tab id="sip" label="Voice">
<fieldset>
<details>
<summary>
Have your carriers whitelist our SIP signaling IPs
</summary>
{hasLength(sbcs) &&
sbcs.map((sbc) => {
return (
<MS key={sbc.sbc_address_sid}>
{sbc.ipv4}:{sbc.port}
</MS>
);
})}
</details>
</fieldset>
<fieldset>
<label htmlFor="e164" className="chk">
<input
id="e164"
name="e164"
type="checkbox"
checked={e164}
onChange={(e) => setE164(e.target.checked)}
/>
<div>E.164 syntax</div>
</label>
<MXS>
<em>Prepend a leading + on origination attempts.</em>
</MXS>
<AccountSelect
accounts={
user?.scope === USER_ACCOUNT
? accounts?.filter(
(acct) => user.account_sid === acct.account_sid,
)
: accounts
}
account={[accountSid, setAccountSid]}
label="Used by"
required={false}
defaultOption={checkSelectOptions(user, carrier?.data)}
disabled={
user?.scope !== USER_ACCOUNT
? false
: user.account_sid !== accountSid
? true
: false
}
/>
{user &&
disableDefaultTrunkRouting(user?.scope) &&
accountSid &&
hasLength(applications) && (
<>
<ApplicationSelect
label="Default Application"
defaultOption="None"
application={[applicationSid, setApplicationSid]}
applications={applications.filter(
(application) => application.account_sid === accountSid,
)}
/>
</>
)}
</fieldset>
<fieldset>
<Checkzone
hidden
name="sip_credentials"
label="Outbound authentication"
initialCheck={initialRegister}
handleChecked={(e) => {
if (!e.target.checked) {
setSipUser("");
setSipPass("");
setSipRealm("");
setSipRegister(false);
setFromUser("");
setFromDomain("");
setRegPublicIpInContact(false);
}
}}
>
<MS>
Does your carrier require authentication on outbound calls?
</MS>
<label htmlFor="sip_username">
Auth username {sipPass || sipRegister ? <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}
onChange={(e) => {
setSipUser(e.target.value);
}}
/>
<label htmlFor="sip_password">
Password
{sipUser || sipRegister ? <span>*</span> : ""}
</label>
<Passwd
id="sip_password"
name="sip_password"
value={sipPass}
placeholder="SIP password for authenticating outbound calls"
required={sipRegister || sipUser.length > 0}
onChange={(e) => {
setSipPass(e.target.value);
}}
/>
<label htmlFor="sip_register" className="chk">
<input
id="sip_register"
name="sip_register"
type="checkbox"
checked={sipRegister}
onChange={(e) => setSipRegister(e.target.checked)}
/>
<div>Require SIP Register</div>
</label>
{sipRegister && (
<>
<MS>
Carrier requires SIP Register before sending outbound
calls.
</MS>
<label htmlFor="sip_realm">
SIP realm{sipRegister ? <span>*</span> : ""}
</label>
<input
id="sip_realm"
name="sip_realm"
type="text"
value={sipRealm}
placeholder="SIP realm for registration"
required={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>
</>
)}
</Checkzone>
</fieldset>
<fieldset>
<Checkzone
hidden
name="tech_prefix_check"
label="Tech prefix"
initialCheck={initialPrefix}
handleChecked={(e) => {
if (!e.target.checked) {
setPrefix("");
}
}}
>
<MS>
Does your carrier require a tech prefix on outbound calls?
</MS>
<input
id="tech_prefix"
name="tech_prefix"
type="text"
value={prefix}
minLength={TECH_PREFIX_MINLENGTH}
placeholder="Tech prefix"
onChange={(e) => {
setPrefix(e.target.value);
}}
/>
</Checkzone>
</fieldset>
<fieldset>
<Checkzone
hidden
name="diversion_check"
label="SIP Diversion Header"
initialCheck={initialDiversion}
handleChecked={(e) => {
if (!e.target.checked) {
setDiversion("");
}
}}
>
<MS>
Does your carrier support the SIP Diversion header for
authenticating the calling number?
</MS>
<input
id="diversion"
name="diversion"
type="text"
value={diversion}
placeholder="Phone number or SIP URI"
onChange={(e) => {
setDiversion(e.target.value);
}}
/>
</Checkzone>
</fieldset>
<fieldset>
<label htmlFor="sip_gateways">
SIP gateways<span>*</span>
</label>
<MXS>
<em>At least one SIP gateway is required.</em>
</MXS>
<label htmlFor="sip_gateways">
Network address / Port / Netmask
</label>
{sipMessage && <Message message={sipMessage} />}
{hasLength(sipGateways) &&
sipGateways.map((g, i) => (
<div
key={`sip_gateway_${i}`}
className="gateway gateway--sip"
>
<div>
<div>
<input
id={`sip_ip_${i}`}
name={`sip_ip_${i}`}
type="text"
placeholder="1.2.3.4 / sip.my.com"
required
value={g.ipv4}
onChange={(e) => {
updateSipGateways(i, "ipv4", e.target.value);
}}
ref={(ref: HTMLInputElement) =>
(refSipIp.current[i] = ref)
}
/>
</div>
<div>
<input
id={`sip_port_${i}`}
name={`sip_port_${i}`}
type="number"
min="0"
max={TCP_MAX_PORT}
placeholder={
g.protocol === "tls" || g.protocol === "tls/srtp"
? ""
: DEFAULT_SIP_GATEWAY.port?.toString()
}
value={g.port === null ? "" : g.port}
onChange={(e) => {
updateSipGateways(
i,
"port",
g.outbound > 0 &&
!isNotBlank(e.target.value) &&
getIpValidationType(g.ipv4) !== IP
? null
: Number(e.target.value),
);
}}
ref={(ref: HTMLInputElement) =>
(refSipPort.current[i] = ref)
}
/>
</div>
{g.outbound ? (
<div>
<Selector
id={`sip_protocol_${i}`}
name={`sip_protocol${i}`}
value={g.protocol}
options={SIP_GATEWAY_PROTOCOL_OPTIONS}
onChange={(e) => {
updateSipGateways(i, "protocol", e.target.value);
}}
/>
</div>
) : (
<div>
<Selector
id={`sip_netmask_${i}`}
name={`sip_netmask${i}`}
value={g.netmask}
options={NETMASK_OPTIONS}
onChange={(e) => {
updateSipGateways(i, "netmask", e.target.value);
}}
/>
</div>
)}
</div>
<div>
<div>
<label
htmlFor={`sip__gw_is_active_${i}`}
className="chk"
>
<input
id={`sip__gw_is_active_${i}`}
name={`sip__gw_is_active_${i}`}
type="checkbox"
checked={g.is_active ? true : false}
onChange={(e) => {
updateSipGateways(
i,
"is_active",
e.target.checked ? 1 : 0,
);
}}
/>
<div>Active</div>
</label>
</div>
<div>
<label htmlFor={`sip_inbound_${i}`} className="chk">
<input
id={`sip_inbound_${i}`}
name={`sip_inbound_${i}`}
type="checkbox"
checked={g.inbound ? true : false}
required={!g.outbound}
onChange={(e) => {
updateSipGateways(
i,
"inbound",
e.target.checked ? 1 : 0,
);
}}
/>
<div>Inbound</div>
</label>
</div>
<div>
<label htmlFor={`sip_outbound_${i}`} className="chk">
<input
id={`sip_outbound_${i}`}
name={`sip_outbound_${i}`}
type="checkbox"
checked={g.outbound ? true : false}
required={!g.inbound}
onChange={(e) => {
updateSipGateways(
i,
"outbound",
e.target.checked,
);
}}
/>
<div>Outbound</div>
</label>
</div>
{g.outbound > 0 && g.protocol === "tls/srtp" && (
<div>
<label
htmlFor={`sip_pad_crypto_${i}`}
className="chk"
>
<input
id={`sip_pad_crypto_${i}`}
name={`sip_pad_crypto_${i}`}
type="checkbox"
checked={g.pad_crypto ? true : false}
onChange={(e) => {
updateSipGateways(
i,
"pad_crypto",
e.target.checked,
);
}}
/>
<div>Pad crypto</div>
</label>
</div>
)}
</div>
<button
className="btnty"
title="Delete SIP Gateway"
type="button"
onClick={() => {
setSipMessage("");
if (sipGateways.length === 1) {
setSipMessage(
"You must provide at least one SIP Gateway.",
);
} else {
handleSipGatewayDelete(
sipGateways.find((g2, i2) => i2 === i),
);
setSipGateways(
sipGateways.filter((g2, i2) => i2 !== i),
);
}
}}
>
<Icon>
<Icons.Trash2 />
</Icon>
</button>
</div>
))}
<ButtonGroup left>
<button
className="btnty"
type="button"
title="Add SIP Gateway"
onClick={() => {
setSipMessage("");
addSipGateway();
}}
>
<Icon subStyle="teal">
<Icons.Plus />
</Icon>
</button>
</ButtonGroup>
</fieldset>
</Tab>
<Tab id="smpp" label="SMS">
<fieldset>
<details>
<summary>
Have your carriers whitelist our SMPP signaling IPs
</summary>
{hasLength(smpps) &&
smpps.map((smpp) => {
return (
<MS key={smpp.smpp_address_sid}>
{smpp.ipv4}:{smpp.port}
{smpp.use_tls && " (TLS)"}
</MS>
);
})}
</details>
</fieldset>
<fieldset>
<label htmlFor="outbound_smpp">Outbound SMPP</label>
<label htmlFor="outbound_id">System ID</label>
<input
id="outbound_id"
name="outbound_id"
type="text"
value={smppSystemId}
placeholder="SMPP system ID to authenticate with"
onChange={(e) => {
setSmppSystemId(e.target.value);
}}
/>
<label htmlFor="outbound_pass">Password</label>
<Passwd
id="outbound_pass"
name="outbound_pass"
value={smppPass}
placeholder="SMPP password to authenticate with"
onChange={(e) => {
setSmppPass(e.target.value);
}}
/>
<label htmlFor="outbound_smpp">
Carrier SMPP gateways
<span>{smppSystemId || smppPass ? "*" : ""}</span>
</label>
<MXS>
<em>
At least one outbound gateway is required when using system ID
or password above.
</em>
</MXS>
<label htmlFor="outbound_smpp">IP or DNS / Port</label>
{smppOutboundMessage && <Message message={smppOutboundMessage} />}
{hasLength(smppGateways.filter((g) => g.outbound)) &&
smppGateways.map((g, i) => {
return g.outbound ? (
<div key={`smpp_gateway_outbound_${i}`} className="gateway">
<div>
<div>
<input
id={`ip_${i}`}
name={`ip_${i}`}
type="text"
placeholder="1.2.3.4 / smpp.my.com"
required={smppSystemId || smppPass ? true : false}
value={g.ipv4}
onChange={(e) =>
updateSmppGateways(i, "ipv4", e.target.value)
}
ref={(ref: HTMLInputElement) =>
(refSmppIp.current[i] = ref)
}
/>
</div>
<div>
<input
id={`port_${i}`}
name={`port_${i}`}
type="number"
min="0"
max={TCP_MAX_PORT}
placeholder={DEFAULT_SMPP_GATEWAY.port.toString()}
value={g.port}
onChange={(e) =>
updateSmppGateways(
i,
"port",
Number(e.target.value),
)
}
ref={(ref: HTMLInputElement) =>
(refSmppPort.current[i] = ref)
}
/>
</div>
<div>
<label htmlFor={`use_tls_${i}`} className="chk">
<input
id={`use_tls_${i}`}
name={`use_tls_${i}`}
type="checkbox"
checked={g.use_tls}
onChange={(e) =>
updateSmppGateways(
i,
"use_tls",
e.target.checked,
)
}
/>
<div>Use&nbsp;TLS</div>
</label>
</div>
</div>
<button
title="Delete Outbound SMPP Gateway"
type="button"
className="btnty"
onClick={() => {
setSmppOutboundMessage("");
if (
hasLength(smpps) &&
smppGateways.filter((g) => g.outbound).length <=
1 &&
(smppSystemId || smppPass)
) {
setSmppOutboundMessage(
"You must provide at least one Outbound Gateway.",
);
} else {
handleSmppGatewayDelete(
smppGateways.find((g2, i2) => i2 === i),
);
setSmppGateways(
smppGateways.filter((g2, i2) => i2 !== i),
);
}
}}
>
<Icon>
<Icons.Trash2 />
</Icon>
</button>
</div>
) : null;
})}
<ButtonGroup left>
<button
className="btnty"
type="button"
onClick={() => addSmppGateway({ inbound: 0, outbound: 1 })}
title="Add Outbound SMPP Gateway"
>
<Icon subStyle="teal">
<Icons.Plus />
</Icon>
</button>
</ButtonGroup>
</fieldset>
<fieldset>
<label htmlFor="inbound_smpp">Inbound SMPP</label>
<label htmlFor="inbound_id">System ID</label>
<input
id="inbound_id"
name="inbound_id"
type="text"
value={smppInboundSystemId}
placeholder="SMPP system ID to authenticate with"
onChange={(e) => {
setSmppInboundSystemId(e.target.value);
}}
/>
<label htmlFor="inbound_pass">
Password
<span>{!hasEmptySmppGateways("inbound") ? "*" : ""}</span>
</label>
<MXS>
<em>
Password is required if whitelisting carrier IP address(es)
below.
</em>
</MXS>
<Passwd
id="inbound_pass"
name="inbound_pass"
value={smppInboundPass}
placeholder="SMPP password for authenticating inbound messages"
required={!hasEmptySmppGateways("inbound")}
onChange={(e) => {
setSmppInboundPass(e.target.value);
}}
/>
<label htmlFor="inbound_smpp">
Carrier IP address(es) to whitelist
</label>
<MXS>
<em>
Fully qualified domain names (e.g. sip.example.com) may only
be used for outbound calls above.
</em>
</MXS>
<label htmlFor="inbound_smpp">IP Adress / Netmask</label>
{smppInboundMessage && <Message message={smppInboundMessage} />}
{hasLength(smppGateways.filter((g) => g.inbound)) &&
smppGateways.map((g, i) => {
return g.inbound ? (
<div key={`smpp_gateway_inbound_${i}`} className="gateway">
<div>
<div>
<input
id={`smpp_ip_${i}`}
name={`smpp_ip_${i}`}
type="text"
placeholder="1.2.3.4"
pattern="((25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9])"
value={g.ipv4}
onChange={(e) =>
updateSmppGateways(i, "ipv4", e.target.value)
}
ref={(ref: HTMLInputElement) =>
(refSmppIp.current[i] = ref)
}
/>
</div>
<div>
<Selector
id={`smpp_netmask_${i}`}
name={`smpp_netmask_${i}`}
options={NETMASK_OPTIONS}
value={g.netmask}
onChange={(e) =>
updateSmppGateways(i, "netmask", e.target.value)
}
/>
</div>
</div>
<button
className="btnty"
title="Delete Inbound SMPP Gateway"
type="button"
onClick={() => {
handleSmppGatewayDelete(
smppGateways.find((g2, i2) => i2 === i),
);
setSmppGateways(
smppGateways.filter((g2, i2) => i2 !== i),
);
}}
>
<Icon>
<Icons.Trash2 />
</Icon>
</button>
</div>
) : null;
})}
<ButtonGroup left>
<button
className="btnty"
type="button"
onClick={() => addSmppGateway({ outbound: 0, inbound: 1 })}
title="Add Inbound SMPP Gateway"
>
<Icon subStyle="teal">
<Icons.Plus />
</Icon>
</button>
</ButtonGroup>
</fieldset>
</Tab>
</Tabs>
<fieldset>
<ButtonGroup left>
<Button
small
subStyle="grey"
as={Link}
to={ROUTE_INTERNAL_CARRIERS}
>
Cancel
</Button>
<Button type="submit" small onClick={handleActiveTab}>
Save
</Button>
</ButtonGroup>
</fieldset>
</form>
</Section>
);
};
export default CarrierForm;