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 ? :
setName(e.target.value)} placeholder="SIP trunk provider name" invalid={nameInvalid} autoFocus ref={refName} /> setDescription(e.target.value)} placeholder="Optional" />
SIP Gateways
{ sipGateways.length ?
{/* for CSS grid layout */}
: null } {sipGateways.map((g, i) => ( updateSipGateways(e, i, 'ip')} placeholder={'1.2.3.4'} invalid={sipGateways[i].invalidIp} ref={ref => refIp.current[i] = ref} /> updateSipGateways(e, i, 'port')} placeholder="5060" invalid={sipGateways[i].invalidPort} ref={ref => refPort.current[i] = ref} /> updateSipGateways(e, i, 'inbound')} invalid={sipGateways[i].invalidInbound} ref={ref => refInbound.current[i] = ref} /> updateSipGateways(e, i, 'outbound')} invalid={sipGateways[i].invalidOutbound} ref={ref => refOutbound.current[i] = ref} /> removeSipGateway(i)} ref={ref => refTrash.current[i] = ref} /> ))} {errorMessage && ( )} {props.type === 'edit' && ( )} ); }; export default SipTrunkForm;