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 <dan@nimblea.pe>
Co-authored-by: Dave Horton <daveh@beachdognet.com>
This commit is contained in:
Brandon Lee Kitajchuk
2021-12-06 14:58:42 -08:00
committed by GitHub
parent bfc7cc971c
commit 2db5f26dbf
5 changed files with 307 additions and 21 deletions
+1 -1
View File
@@ -1 +1 @@
REACT_APP_API_BASE_URL=http://[ip]:[port]/v1
REACT_APP_API_BASE_URL=http://127.0.0.1:3002/v1
+1 -12
View File
@@ -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;
+16
View File
@@ -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;
+287 -7
View File
@@ -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
? <Loader
@@ -596,7 +745,7 @@ const AccountForm = props => {
<Select
large={props.type === 'setup'}
name="method"
id="method"
id="regMethod"
value={regMethod}
onChange={e => setRegMethod(e.target.value)}
>
@@ -665,7 +814,7 @@ const AccountForm = props => {
<Select
large={props.type === 'setup'}
name="method"
id="method"
id="queueMethod"
value={queueMethod}
onChange={e => setQueueMethod(e.target.value)}
>
@@ -711,6 +860,137 @@ const AccountForm = props => {
</Button>
)}
{ process.env.REACT_APP_ENABLE_SUBSPACE ? (
<>
<Label htmlFor="subspaceId">Subspace</Label>
<InputGroup>
<Input
large={props.type === 'setup'}
name="subspaceId"
id="subspaceId"
value={subspaceId}
onChange={e => setSubspaceId(e.target.value)}
placeholder="Client Id for Subspace"
ref={refSubspaceId}
style={{ margin: '0 4px' }}
/>
<PasswordInput
large={props.type === 'setup'}
allowShowPassword
name="subspaceSecret"
id="subspaceSecret"
password={subspaceSecret}
setPassword={setSubspaceSecret}
setErrorMessage={setErrorMessage}
placeholder="Client Secret for Subspace"
ref={refSubspaceSecret}
style={{ margin: '0 4px' }}
/>
<StyledInputGroup>
<TableMenu
disabled={!hasSubspace}
sid="subspace"
open={menuOpen === "subspace"}
handleMenuOpen={handleSubspaceMenuOpen}
menuItems={subspaceSipTeleportId ? [subspaceMenuItems[1]] : [subspaceMenuItems[0]]}
/>
</StyledInputGroup>
</InputGroup>
{subspaceSipTeleportId ? (
<div style={{ gridColumn: 2, textAlign: 'left' }}>
<div>Subspace is now enabled. To send your traffic through Subspace:</div>
{subspaceSipTeleportEntryPoints.map(entrypoint => (
<div key={entrypoint.transport_type}>
<Span>send {entrypoint.transport_type.split('_').join(' and ')} traffic to&nbsp;</Span>
<CopyableText text={entrypoint.address} textType="Address" />
</div>
))}
</div>
) : null}
{showSubspaceModal && (
<Modal
title={subspaceEnable ? 'Have Subspace send SIP to:' : 'Are you sure you want to delete your Subspace SIP Teleport?'}
loader={generatingSubspace}
hideButtons={generatingSubspace}
maskClosable={!generatingSubspace}
actionText={subspaceEnable ? 'Save' : 'Disable'}
content={
<ModalContainer>
{subspaceEnable ? (
<>
{sipRealm && (
<Radio
noLeftMargin
name="subspaceSipRealm"
id="sipRealmAccount"
label={sipRealm}
checked={subspaceSipRealm === sipRealm}
onChange={() => {
setSubspaceSipRealm(sipRealm);
setSubspaceSipRealmOtherValue('');
}}
/>
)}
{sbcs.map((sbc) => {
return (
<Radio
key={sbc.ipv4}
noLeftMargin
name="subspaceSipRealm"
id={sbc.sbc_address_sid}
label={`${sbc.ipv4}:${sbc.port}`}
checked={subspaceSipRealm === `${sbc.ipv4}:${sbc.port}`}
onChange={() => {
setSubspaceSipRealm(`${sbc.ipv4}:${sbc.port}`);
setSubspaceSipRealmOtherValue('');
}}
/>
);
})}
<Radio
noLeftMargin
name="subspaceSipRealm"
id="sipRealmOther"
label="Other"
checked={subspaceSipRealm === 'other'}
onChange={() => {
setSubspaceSipRealm('other');
setTimeout(() => refSubspaceOtherSip.current.focus(), 0);
}}
/>
{subspaceSipRealm === 'other' && (
<Input
ref={refSubspaceOtherSip}
name="subspaceSipRealm"
id="sipRealmOtherValue"
value={subspaceSipRealmOtherValue}
onChange={e => setSubspaceSipRealmOtherValue(e.target.value)}
placeholder="IP address or DNS name"
style={{ marginTop: '8px' }}
/>
)}
</>
) : null}
</ModalContainer>
}
handleCancel={() => {
setShowSubspaceModal(false);
resetSubspaceState();
}}
handleSubmit={() => {
if (subspaceEnable) {
handleSubspaceEnable();
} else {
handleSubspaceDisable();
}
}}
/>
)}
</>
) : null }
{errorMessage && (
<FormError grid message={errorMessage} />
)}
@@ -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}