From 2db5f26dbfd9a01847bc83870cba99446c8ee1f9 Mon Sep 17 00:00:00 2001 From: Brandon Lee Kitajchuk Date: Mon, 6 Dec 2021 14:58:42 -0800 Subject: [PATCH] Subspace (#35) * pushing up what ive got from laptop * beginnings of a UI for setting up subspace on a jambonz account * enable the env flag and move content to right place * changes to support subspace (thanks to nimbleape) * fix column names * Implement SIP realm selection for Subspace API calls * Hook up Subspace disable method * Finish up Subspace API handling Co-authored-by: Dan Jenkins Co-authored-by: Dave Horton --- .env | 2 +- src/components/elements/CopyableText.js | 13 +- src/components/elements/Span.js | 16 + src/components/forms/AccountForm.js | 294 +++++++++++++++++- src/components/pages/internal/AccountsList.js | 3 +- 5 files changed, 307 insertions(+), 21 deletions(-) create mode 100644 src/components/elements/Span.js diff --git a/.env b/.env index 597cb3a..5afc927 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -REACT_APP_API_BASE_URL=http://[ip]:[port]/v1 +REACT_APP_API_BASE_URL=http://127.0.0.1:3002/v1 diff --git a/src/components/elements/CopyableText.js b/src/components/elements/CopyableText.js index 2334899..4afb3b3 100644 --- a/src/components/elements/CopyableText.js +++ b/src/components/elements/CopyableText.js @@ -2,19 +2,8 @@ import React, { useContext } from 'react'; import styled from 'styled-components/macro'; import { NotificationDispatchContext } from '../../contexts/NotificationContext'; import Button from './Button'; +import Span from './Span'; -const Span = styled.span` - text-align: left; - ${props => props.hasBorder ? ` - height: 2.25rem; - display: flex; - align-items: center; - width: 100%; - padding: 0 1rem; - border: 1px solid #B6B6B6; - border-radius: 0.125rem; - ` : ''} -`; const StyledButton = styled(Button)` margin-left: 1rem; diff --git a/src/components/elements/Span.js b/src/components/elements/Span.js new file mode 100644 index 0000000..d88e249 --- /dev/null +++ b/src/components/elements/Span.js @@ -0,0 +1,16 @@ +import styled from 'styled-components/macro'; + +const Span = styled.span` + text-align: left; + ${props => props.hasBorder ? ` + height: 2.25rem; + display: flex; + align-items: center; + width: 100%; + padding: 0 1rem; + border: 1px solid #B6B6B6; + border-radius: 0.125rem; + ` : ''} +`; + +export default Span; \ No newline at end of file diff --git a/src/components/forms/AccountForm.js b/src/components/forms/AccountForm.js index ebb110d..4b613f5 100644 --- a/src/components/forms/AccountForm.js +++ b/src/components/forms/AccountForm.js @@ -6,6 +6,7 @@ import { ServiceProviderValueContext } from '../../contexts/ServiceProviderConte import Form from '../elements/Form'; import Input from '../elements/Input'; import Label from '../elements/Label'; +import Radio from '../elements/Radio'; import Select from '../elements/Select'; import InputGroup from '../elements/InputGroup'; import PasswordInput from '../elements/PasswordInput'; @@ -17,6 +18,7 @@ import Button from '../elements/Button'; import Link from '../elements/Link'; import Tooltip from '../elements/Tooltip'; import CopyableText from '../elements/CopyableText'; +import Span from '../elements/Span'; import handleErrors from "../../helpers/handleErrors"; import styled from 'styled-components/macro'; @@ -61,6 +63,9 @@ const AccountForm = props => { const refQueueWebhook = useRef(null); const refQueueUser = useRef(null); const refQueuePassword = useRef(null); + const refSubspaceId = useRef(null); + const refSubspaceSecret = useRef(null); + const refSubspaceOtherSip = useRef(null); // Form inputs const [ name, setName ] = useState(''); @@ -68,13 +73,24 @@ const AccountForm = props => { const [ deviceCallingApplication, setDeviceCallingApplication ] = useState(''); const [ regWebhook, setRegWebhook ] = useState(''); const [ regMethod, setRegMethod ] = useState('POST'); - const [ regUser, setRegUser ] = useState('' || ''); - const [ regPassword, setRegPassword ] = useState('' || ''); + const [ regUser, setRegUser ] = useState(''); + const [ regPassword, setRegPassword ] = useState(''); const [ webhookSecret, setWebhookSecret ] = useState(''); const [ queueWebhook, setQueueWebhook ] = useState(''); const [ queueMethod, setQueueMethod ] = useState('POST'); - const [ queueUser, setQueueUser ] = useState('' || ''); - const [ queuePassword, setQueuePassword ] = useState('' || ''); + const [ queueUser, setQueueUser ] = useState(''); + const [ queuePassword, setQueuePassword ] = useState(''); + const [ hasSubspace, setHasSubspace ] = useState(false); + const [ subspaceId, setSubspaceId ] = useState(''); + const [ subspaceSecret, setSubspaceSecret ] = useState(''); + const [ subspaceSipTeleportId, setSubspaceSipTeleportId ] = useState(''); + const [ subspaceSipTeleportEntryPoints, setSubspaceSipTeleportEntryPoints ] = useState([]); + const [ showSubspaceModal, setShowSubspaceModal ] = useState(false); + const [ generatingSubspace, setGeneratingSubspace ] = useState(false); + const [ subspaceSipRealm, setSubspaceSipRealm ] = useState(''); + const [ sbcs, setSbcs ] = useState([]); + const [ subspaceSipRealmOtherValue, setSubspaceSipRealmOtherValue ] = useState(''); + const [ subspaceEnable, setSubspaceEnable ] = useState(false); // Invalid form inputs const [ invalidName, setInvalidName ] = useState(false); @@ -85,6 +101,10 @@ const AccountForm = props => { const [ invalidQueueWebhook, setInvalidQueueWebhook ] = useState(false); const [ invalidQueueUser, setInvalidQueueUser ] = useState(false); const [ invalidQueuePassword, setInvalidQueuePassword ] = useState(false); + // eslint-disable-next-line no-unused-vars + const [ invalidSubspaceId, setInvalidSubspaceId ] = useState(false); + // eslint-disable-next-line no-unused-vars + const [ invalidSubspaceClient, setInvalidSubspaceClient ] = useState(false); const [ showLoader, setShowLoader ] = useState(true); const [ errorMessage, setErrorMessage ] = useState(''); @@ -110,6 +130,14 @@ const AccountForm = props => { } }; + const handleSubspaceMenuOpen = sid => { + if (menuOpen === sid) { + setMenuOpen(null); + } else { + setMenuOpen(sid); + } + }; + const copyWebhookSecret = async e => { e.preventDefault(); setMenuOpen(null); @@ -164,6 +192,96 @@ const AccountForm = props => { } }; + const toggleSubspaceTeleport = (enable, e) => { + e.preventDefault(); + setMenuOpen(null); + setSubspaceEnable(enable); + setShowSubspaceModal(true); + }; + + const resetSubspaceState = () => { + setMenuOpen(null); + setShowSubspaceModal(false); + setSubspaceSipRealmOtherValue(''); + setGeneratingSubspace(false); + setSubspaceEnable(false); + setSubspaceSipRealm(''); + }; + + const handleSubspaceEnable = async () => { + try { + setGeneratingSubspace(true); + + const destination = subspaceSipRealm === 'other' + ? subspaceSipRealmOtherValue + : subspaceSipRealm; + const response = await axios({ + method: 'post', + baseURL: process.env.REACT_APP_API_BASE_URL, + url: `/Accounts/${accountSid}/SubspaceTeleport`, + headers: { + Authorization: `Bearer ${jwt}`, + }, + data: { destination }, + }); + + if (response.status === 200) { + setSubspaceSipTeleportId(response.data.subspace_sip_teleport_id || ''); + setSubspaceSipTeleportEntryPoints(response.data.subspace_sip_teleport_destinations || []); + + dispatch({ + type: 'ADD', + level: 'success', + message: 'Successfully enabled subspace teleport.', + }); + } + + resetSubspaceState(); + } catch (err) { + resetSubspaceState(); + if (err.response.status === 500 && err.response.data.msg === 'Too Many Requests') { + dispatch({ + type: 'ADD', + level: 'error', + message: 'You have already created the maximum number of SIP Teleports allowed for your Subspace account.', + }); + } else { + handleErrors({ err, history, dispatch }); + } + } + }; + + const handleSubspaceDisable = async () => { + try { + setGeneratingSubspace(true); + + const response = await axios({ + method: 'delete', + baseURL: process.env.REACT_APP_API_BASE_URL, + url: `/Accounts/${accountSid}/SubspaceTeleport`, + headers: { + Authorization: `Bearer ${jwt}`, + }, + }); + + if (response.status === 204) { + setSubspaceSipTeleportId(''); + setSubspaceSipTeleportEntryPoints([]); + + dispatch({ + type: 'ADD', + level: 'success', + message: `Successfully disabled subspace teleport.`, + }); + } + + resetSubspaceState(); + } catch (err) { + resetSubspaceState(); + handleErrors({ err, history, dispatch }); + } + }; + useEffect(() => { const getAccounts = async () => { try { @@ -200,6 +318,16 @@ const AccountForm = props => { promiseList.push(applicationsPromise); } + const sbcsPromise = await axios({ + method: 'get', + baseURL: process.env.REACT_APP_API_BASE_URL, + url: '/Sbcs', + headers: { + Authorization: `Bearer ${jwt}`, + }, + }); + promiseList.push(sbcsPromise); + const promiseAllValues = await Promise.all(promiseList); const accountsData = (promiseAllValues[0] && promiseAllValues[0].data) || []; @@ -213,6 +341,8 @@ const AccountForm = props => { setAccountApplications(accountApplicationsData); } + setSbcs(promiseAllValues[2].data); + if (props.type === 'setup' && accountsData.length > 1) { history.push('/internal/accounts'); dispatch({ @@ -254,7 +384,11 @@ const AccountForm = props => { setQueueUser((acc.queue_event_hook && acc.queue_event_hook.username) || ''); setQueuePassword((acc.queue_event_hook && acc.queue_event_hook.password) || ''); setWebhookSecret(acc.webhook_secret || ''); - + setSubspaceId(acc.subspace_client_id || ''); + setSubspaceSecret(acc.subspace_client_secret || ''); + setSubspaceSipTeleportId(acc.subspace_sip_teleport_id || ''); + setSubspaceSipTeleportEntryPoints(acc.subspace_sip_teleport_destinations ? JSON.parse(acc.subspace_sip_teleport_destinations) : []); + setHasSubspace(acc.subspace_client_id ? true : false); if ( (acc.registration_hook && acc.registration_hook.username) || (acc.registration_hook && acc.registration_hook.password) @@ -404,6 +538,8 @@ const AccountForm = props => { password: queuePassword || null, }, webhook_secret: webhookSecret || null, + subspace_client_id: subspaceId || null, + subspace_client_secret: subspaceSecret || null, }; if (props.type === 'add') { @@ -480,6 +616,19 @@ const AccountForm = props => { }, ]; + const subspaceMenuItems = [ + { + type: 'button', + name: 'Enable', + action: toggleSubspaceTeleport.bind(toggleSubspaceTeleport, true), + }, + { + type: 'button', + name: 'Disable', + action: toggleSubspaceTeleport.bind(toggleSubspaceTeleport, false), + }, + ]; + return ( showLoader ? { setQueueMethod(e.target.value)} > @@ -711,6 +860,137 @@ const AccountForm = props => { )} + { process.env.REACT_APP_ENABLE_SUBSPACE ? ( + <> + + + setSubspaceId(e.target.value)} + placeholder="Client Id for Subspace" + ref={refSubspaceId} + style={{ margin: '0 4px' }} + /> + + + + + + + + {subspaceSipTeleportId ? ( +
+
Subspace is now enabled. To send your traffic through Subspace:
+ {subspaceSipTeleportEntryPoints.map(entrypoint => ( +
+ send {entrypoint.transport_type.split('_').join(' and ')} traffic to  + +
+ ))} +
+ ) : null} + {showSubspaceModal && ( + + {subspaceEnable ? ( + <> + {sipRealm && ( + { + setSubspaceSipRealm(sipRealm); + setSubspaceSipRealmOtherValue(''); + }} + /> + )} + {sbcs.map((sbc) => { + return ( + { + setSubspaceSipRealm(`${sbc.ipv4}:${sbc.port}`); + setSubspaceSipRealmOtherValue(''); + }} + /> + ); + })} + { + setSubspaceSipRealm('other'); + setTimeout(() => refSubspaceOtherSip.current.focus(), 0); + }} + /> + {subspaceSipRealm === 'other' && ( + setSubspaceSipRealmOtherValue(e.target.value)} + placeholder="IP address or DNS name" + style={{ marginTop: '8px' }} + /> + )} + + ) : null} + + } + handleCancel={() => { + setShowSubspaceModal(false); + resetSubspaceState(); + }} + handleSubmit={() => { + if (subspaceEnable) { + handleSubspaceEnable(); + } else { + handleSubspaceDisable(); + } + }} + /> + )} + + ) : null } + {errorMessage && ( )} diff --git a/src/components/pages/internal/AccountsList.js b/src/components/pages/internal/AccountsList.js index d12f3c1..6807cd0 100644 --- a/src/components/pages/internal/AccountsList.js +++ b/src/components/pages/internal/AccountsList.js @@ -43,6 +43,7 @@ const AccountsList = () => { sip_realm: a.sip_realm, url_reg: a.registration_hook && a.registration_hook.url, url_queue: a.queue_event_hook && a.queue_event_hook.url, + subspace_enabled: a.subspace_sip_teleport_id ? 'Enabled' : '' })); return(simplifiedAccounts); } catch (err) { @@ -220,7 +221,7 @@ const AccountsList = () => { { header: 'AccountSid', key: 'sid' }, { header: 'SIP Realm', key: 'sip_realm' }, { header: 'Registration Webhook', key: 'url_reg' }, - { header: 'Queue Event Webhook', key: 'url_queue' }, + { header: 'Queue Event Webhook', key: 'url_queue' } ]} formatContentToDelete={formatAccountToDelete} deleteContent={deleteAccount}