Files
jambonz-webapp/src/components/forms/SipTrunkForm.js
T

679 lines
22 KiB
JavaScript

import React, { useState, useEffect, useContext, useRef } from 'react';
import { useHistory } from 'react-router-dom';
import axios from 'axios';
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
import Form from '../elements/Form';
import Input from '../elements/Input';
import Label from '../elements/Label';
import Checkbox from '../elements/Checkbox';
import InputGroup from '../elements/InputGroup';
import FormError from '../blocks/FormError';
import Button from '../elements/Button';
import TrashButton from '../elements/TrashButton';
import Loader from '../blocks/Loader';
import sortSipGateways from '../../helpers/sortSipGateways';
const SipTrunkForm = props => {
const history = useHistory();
const dispatch = useContext(NotificationDispatchContext);
// Refs
const refName = useRef(null);
const refIp = useRef([]);
const refPort = useRef([]);
const refInbound = useRef([]);
const refOutbound = useRef([]);
const refTrash = useRef([]);
const refAdd = useRef(null);
// Form inputs
const [ name, setName ] = useState('');
const [ nameInvalid, setNameInvalid ] = useState(false);
const [ description, setDescription ] = useState('');
const [ sipGateways, setSipGateways ] = useState([
{
sip_gateway_sid: '',
ip: '',
port: 5060,
inbound: true,
outbound: true,
invalidIp: false,
invalidPort: false,
invalidInbound: false,
invalidOutbound: false,
}
]);
const [ sipTrunks, setSipTrunks ] = useState([]);
const [ sipTrunkSid, setSipTrunkSid ] = useState('');
const [ showLoader, setShowLoader ] = useState(true);
const [ errorMessage, setErrorMessage ] = useState('');
useEffect(() => {
const getAPIData = async () => {
if (!localStorage.getItem('token')) {
history.push('/');
dispatch({
type: 'ADD',
level: 'error',
message: 'You must log in to view that page.',
});
return;
}
try {
const sipTrunksPromise = axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/VoipCarriers`,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
const sipGatewaysPromise = axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/SipGateways`,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
const promiseAllValues = await Promise.all([
sipTrunksPromise,
sipGatewaysPromise,
]);
const allSipTrunks = promiseAllValues[0].data;
const allSipGateways = promiseAllValues[1].data;
setSipTrunks(allSipTrunks);
if (props.type === 'setup' && allSipTrunks.length > 1) {
history.push('/internal/sip-trunks');
dispatch({
type: 'ADD',
level: 'error',
message: 'That page is only accessible during setup.',
});
}
if (props.type === 'edit' || props.type === 'setup') {
const currentSipTrunk = props.type === 'edit'
? allSipTrunks.filter(s => s.voip_carrier_sid === props.voip_carrier_sid)
: allSipTrunks;
if (props.type === 'edit' && !currentSipTrunk.length) {
history.push('/internal/sip-trunks');
dispatch({
type: 'ADD',
level: 'error',
message: 'That SIP trunk does not exist.',
});
return;
}
const currentSipGateways = allSipGateways.filter(s => {
return s.voip_carrier_sid === currentSipTrunk[0].voip_carrier_sid;
});
sortSipGateways(currentSipGateways);
if (currentSipTrunk.length) {
setName(currentSipTrunk[0].name);
setDescription(currentSipTrunk[0].description);
setSipGateways(currentSipGateways.map(s => ({
sip_gateway_sid: s.sip_gateway_sid,
ip: s.ipv4,
port: s.port,
inbound: s.inbound === 1,
outbound: s.outbound === 1,
invalidIp: false,
invalidPort: false,
invalidInbound: false,
invalidOutbound: false,
})));
setSipTrunkSid(currentSipTrunk[0].voip_carrier_sid);
}
}
setShowLoader(false);
} catch (err) {
if (err.response && err.response.status === 401) {
localStorage.removeItem('token');
sessionStorage.clear();
history.push('/');
dispatch({
type: 'ADD',
level: 'error',
message: 'Your session has expired. Please log in and try again.',
});
} else {
setErrorMessage('Something went wrong, please try again.');
dispatch({
type: 'ADD',
level: 'error',
message: (err.response && err.response.data && err.response.data.msg) || 'Unable to get accounts',
});
console.log(err.response || err);
}
setShowLoader(false);
}
};
getAPIData();
// eslint-disable-next-line
}, []);
const addSipGateway = () => {
const newSipGateways = [
...sipGateways,
{
sip_gateway_sid: '',
ip: '',
port: 5060,
inbound: true,
outbound: true,
invalidIp: false,
invalidPort: false,
invalidInbound: false,
invalidOutbound: false,
}
];
setSipGateways(newSipGateways);
};
const removeSipGateway = index => {
const newSipGateways = sipGateways.filter((s,i) => i !== index);
setSipGateways(newSipGateways);
setErrorMessage('');
};
const updateSipGateways = (e, i, key) => {
const newSipGateways = [...sipGateways];
const newValue =
key === 'invalidIp' ||
key === 'invalidPort' ||
key === 'invalidInbound' ||
key === 'invalidOutbound'
? true
: (key === 'inbound') || (key === 'outbound')
? e.target.checked
: e.target.value;
newSipGateways[i][key] = newValue;
setSipGateways(newSipGateways);
};
const resetInvalidFields = () => {
setNameInvalid(false);
const newSipGateways = [...sipGateways];
newSipGateways.forEach((s, i) => {
newSipGateways[i].invalidIp = false;
newSipGateways[i].invalidPort = false;
newSipGateways[i].invalidInbound = false;
newSipGateways[i].invalidOutbound = false;
});
setSipGateways(newSipGateways);
};
const handleSumit = async e => {
let isMounted = true;
try {
setShowLoader(true);
e.preventDefault();
setErrorMessage('');
resetInvalidFields();
let errorMessages = [];
let focusHasBeenSet = false;
if (!name) {
errorMessages.push('Please enter a name for this SIP Trunk.');
setNameInvalid(true);
if (!focusHasBeenSet) {
refName.current.focus();
focusHasBeenSet = true;
}
}
if (!sipGateways.length) {
errorMessages.push('You must provide at least one SIP Gateway.');
if (!focusHasBeenSet) {
refAdd.current.focus();
focusHasBeenSet = true;
}
}
const regIp = /^((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])$/;
const regFqdn = /^([a-zA-Z][^.]*)(\.[^.]+){2,}$/;
const regFqdnTopLevel = /^([a-zA-Z][^.]*)(\.[^.]+)$/;
const regPort = /^[0-9]+$/;
sipGateways.forEach(async (gateway, i) => {
//-----------------------------------------------------------------------------
// IP validation
//-----------------------------------------------------------------------------
const type = regIp.test(gateway.ip)
? 'ip'
: regFqdn.test(gateway.ip)
? 'fqdn'
: regFqdnTopLevel.test(gateway.ip)
? 'fqdn-top-level'
: 'invalid';
if (!gateway.ip) {
errorMessages.push('The IP Address cannot be blank. Please provide an IP address or delete the row.');
updateSipGateways(null, i, 'invalidIp');
if (!focusHasBeenSet) {
refIp.current[i].focus();
focusHasBeenSet = true;
}
}
else if (type === 'fqdn-top-level') {
errorMessages.push('When using an FQDN, you must use a subdomain (e.g. sip.example.com).');
updateSipGateways(null, i, 'invalidIp');
if (!focusHasBeenSet) {
refIp.current[i].focus();
focusHasBeenSet = true;
}
}
else if (type === 'invalid') {
errorMessages.push('Please provide a valid IP address or fully qualified domain name.');
updateSipGateways(null, i, 'invalidIp');
if (!focusHasBeenSet) {
refIp.current[i].focus();
focusHasBeenSet = true;
}
}
//-----------------------------------------------------------------------------
// Port validation
//-----------------------------------------------------------------------------
if (
gateway.port && (
!(regPort.test(gateway.port))
|| (parseInt(gateway.port) < 0)
|| (parseInt(gateway.port) > 65535)
)
) {
errorMessages.push('Please provide a valid port number between 0 and 65535');
updateSipGateways(null, i, 'invalidPort');
if (!focusHasBeenSet) {
refPort.current[i].focus();
focusHasBeenSet = true;
}
}
//-----------------------------------------------------------------------------
// inbound/outbound validation
//-----------------------------------------------------------------------------
if (type === 'fqdn' && (!gateway.outbound || gateway.inbound)) {
errorMessages.push('A fully qualified domain name may only be used for outbound calls.');
updateSipGateways(null, i, 'invalidIp');
if (gateway.inbound) updateSipGateways(null, i, 'invalidInbound');
if (!gateway.outbound) updateSipGateways(null, i, 'invalidOutbound');
if (!focusHasBeenSet) {
if (gateway.inbound) {
refInbound.current[i].focus();
} else {
refOutbound.current[i].focus();
}
focusHasBeenSet = true;
}
}
else if (!gateway.inbound && !gateway.outbound) {
errorMessages.push('Each SIP Gateway must accept inbound calls, outbound calls, or both.');
updateSipGateways(null, i, 'invalidInbound');
updateSipGateways(null, i, 'invalidOutbound');
if (!focusHasBeenSet) {
refInbound.current[i].focus();
focusHasBeenSet = true;
}
}
//-----------------------------------------------------------------------------
// duplicates validation
//-----------------------------------------------------------------------------
sipGateways.forEach((otherGateway, j) => {
if (i >= j) return;
if (!gateway.ip) return;
if (type === 'invalid') return;
if (gateway.ip === otherGateway.ip && gateway.port === otherGateway.port) {
errorMessages.push('Each SIP gateway must have a unique IP address.');
updateSipGateways(null, i, 'invalidIp');
updateSipGateways(null, i, 'invalidPort');
updateSipGateways(null, j, 'invalidIp');
updateSipGateways(null, j, 'invalidPort');
if (!focusHasBeenSet) {
refTrash.current[j].focus();
focusHasBeenSet = true;
}
}
});
});
// remove duplicate error messages
for (let i = 0; i < errorMessages.length; i++) {
for (let j = 0; j < errorMessages.length; j++) {
if (i >= j) continue;
if (errorMessages[i] === errorMessages[j]) {
errorMessages.splice(j, 1);
j = j - 1;
}
}
}
if (errorMessages.length > 1) {
setErrorMessage(errorMessages);
return;
} else if (errorMessages.length === 1) {
setErrorMessage(errorMessages[0]);
return;
}
//=============================================================================
// Submit
//=============================================================================
const creatingNewTrunk = props.type === 'add' || (props.type === 'setup' && !sipTrunks.length);
const method = creatingNewTrunk
? 'post'
: 'put';
const url = creatingNewTrunk
? '/VoipCarriers'
: `/VoipCarriers/${sipTrunkSid}`;
// Create or update SIP Trunk / VoIP Carrier
const voipCarrier = await axios({
method,
baseURL: process.env.REACT_APP_API_BASE_URL,
url,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
data: {
name,
description,
},
});
const voip_carrier_sid = voipCarrier.data.sid;
// get updated gateway info from API in order to delete ones that user has removed from UI
let sipGatewaysFromAPI;
if (!creatingNewTrunk) {
const results = await axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/SipGateways',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
sipGatewaysFromAPI = results.data.filter(s => s.voip_carrier_sid === sipTrunkSid);
}
//-----------------------------------------------------------------------------
// Create or Update SIP Gateways
//-----------------------------------------------------------------------------
// Keeping track of created SIP gateways in case one throws an error, then all
// of the ones created before that (as well as the sip trunk) have to be deleted.
let completedSipGateways = [];
try {
for (const s of sipGateways) {
const creatingNewGateway = creatingNewTrunk || s.sip_gateway_sid === '';
const method = creatingNewGateway
? 'post'
: 'put';
const url = creatingNewGateway
? '/SipGateways'
: `/SipGateways/${s.sip_gateway_sid}`;
const data = {
ipv4: s.ip,
port: s.port,
inbound: s.inbound,
outbound: s.outbound,
};
if (creatingNewGateway) {
data.voip_carrier_sid = voip_carrier_sid || sipTrunkSid;
}
const result = await axios({
method,
baseURL: process.env.REACT_APP_API_BASE_URL,
url,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
data,
});
if (creatingNewGateway) {
completedSipGateways.push(result.data.sid);
}
};
} catch (err) {
if (completedSipGateways.length) {
for (const sid of completedSipGateways) {
await axios({
method: 'delete',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/SipGateways/${sid}`,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
}
}
if (voip_carrier_sid) {
await axios({
method: 'delete',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/VoipCarriers/${voip_carrier_sid}`,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
}
throw err;
}
// delete removed gateways (after add/update in case add/update caused errors)
if (!creatingNewTrunk) {
for (const remote of sipGatewaysFromAPI) {
const match = sipGateways.filter(local => local.sip_gateway_sid === remote.sip_gateway_sid);
if (!match.length) {
await axios({
method: 'delete',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/SipGateways/${remote.sip_gateway_sid}`,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
}
}
}
if (props.type === 'setup') {
isMounted = false;
history.push('/setup-complete');
} else {
isMounted = false;
history.push('/internal/sip-trunks');
const dispatchMessage = props.type === 'add'
? 'SIP trunk created successfully'
: 'SIP trunk updated successfully';
dispatch({
type: 'ADD',
level: 'success',
message: dispatchMessage
});
}
} catch(err) {
if (err.response && err.response.status === 401) {
localStorage.removeItem('token');
sessionStorage.clear();
isMounted = false;
history.push('/');
dispatch({
type: 'ADD',
level: 'error',
message: 'Your session has expired. Please log in and try again.',
});
} else {
setErrorMessage((err.response && err.response.data && err.response.data.msg) || 'Something went wrong, please try again.');
console.log(err.response || err);
}
} finally {
if (isMounted) {
setShowLoader(false);
}
}
};
return (
showLoader
? <Loader height={props.type === 'setup' ? '424px' : '376px'}/>
: <Form
large
onSubmit={handleSumit}
>
<Label htmlFor="name">Name</Label>
<Input
large={props.type === 'setup'}
name="name"
id="name"
value={name}
onChange={e => setName(e.target.value)}
placeholder="SIP trunk provider name"
invalid={nameInvalid}
autoFocus
ref={refName}
/>
<Label htmlFor="description">Description</Label>
<Input
large={props.type === 'setup'}
name="description"
id="description"
value={description}
onChange={e => setDescription(e.target.value)}
placeholder="Optional"
/>
<hr style={{ margin: '0.5rem -2rem' }} />
<div
style={{ whiteSpace: 'nowrap' }}
>SIP Gateways</div>
{
sipGateways.length
? <div>{/* for CSS grid layout */}</div>
: null
}
{sipGateways.map((g, i) => (
<React.Fragment key={i}>
<Label htmlFor={`sipGatewaysIp[${i}]`}>IP Address</Label>
<InputGroup>
<Input
large={props.type === 'setup'}
name={`sipGatewaysIp[${i}]`}
id={`sipGatewaysIp[${i}]`}
value={sipGateways[i].ip}
onChange={e => updateSipGateways(e, i, 'ip')}
placeholder={'1.2.3.4'}
invalid={sipGateways[i].invalidIp}
ref={ref => refIp.current[i] = ref}
/>
<Label
middle
htmlFor={`sipGatewaysPort[${i}]`}
>
Port
</Label>
<Input
large={props.type === 'setup'}
width="5rem"
name={`sipGatewaysPort[${i}]`}
id={`sipGatewaysPort[${i}]`}
value={sipGateways[i].port}
onChange={e => updateSipGateways(e, i, 'port')}
placeholder="5060"
invalid={sipGateways[i].invalidPort}
ref={ref => refPort.current[i] = ref}
/>
<Checkbox
large={props.type === 'setup'}
id={`inbound[${i}]`}
label="Inbound"
tooltip="Sends us calls"
checked={sipGateways[i].inbound}
onChange={e => updateSipGateways(e, i, 'inbound')}
invalid={sipGateways[i].invalidInbound}
ref={ref => refInbound.current[i] = ref}
/>
<Checkbox
large={props.type === 'setup'}
id={`outbound[${i}]`}
label="Outbound"
tooltip="Accepts calls from us"
checked={sipGateways[i].outbound}
onChange={e => updateSipGateways(e, i, 'outbound')}
invalid={sipGateways[i].invalidOutbound}
ref={ref => refOutbound.current[i] = ref}
/>
<TrashButton
onClick={() => removeSipGateway(i)}
ref={ref => refTrash.current[i] = ref}
/>
</InputGroup>
</React.Fragment>
))}
<Button
square
type="button"
onClick={addSipGateway}
ref={refAdd}
>
+
</Button>
{errorMessage && (
<FormError grid message={errorMessage} />
)}
<InputGroup flexEnd spaced>
{props.type === 'edit' && (
<Button
grid
gray
type="button"
onClick={() => {
history.push('/internal/sip-trunks');
dispatch({
type: 'ADD',
level: 'info',
message: 'Changes canceled',
});
}}
>
Cancel
</Button>
)}
<Button
large={props.type === 'setup'}
grid
fullWidth={props.type === 'setup' || props.type === 'add'}
>
{props.type === 'setup'
? 'Save and Continue'
: props.type === 'add'
? 'Add SIP Trunk'
: 'Save'
}
</Button>
</InputGroup>
</Form>
);
};
export default SipTrunkForm;