Compare commits

..

7 Commits

Author SHA1 Message Date
Brandon Lee Kitajchuk
8538d40696 Add account sid selector to speech form (#24)
* Add account sid selector to speech form

* Add account selector to speech service list view

* Add account selector to carrier list view

* Remove client-side check for existing speech services
2021-09-01 12:47:32 -04:00
Brandon Lee Kitajchuk
eda1fa0dc4 Chore/carrier apps accounts (#23)
* Scope accounts and applications to current service provider for Phone Numbers

* Scope accounts to current service provider when adding or editing an Application

* Implement account and application logic for add or edit Carrier form

* Implement delete action for service providers
2021-08-28 09:20:49 -04:00
Brandon Lee Kitajchuk
0174315a68 Only show accounts for current service provider when adding a new application (#21) 2021-08-26 12:25:26 -04:00
Brandon Lee Kitajchuk
b86bf0c403 User service provider context when adding an account (#20) 2021-08-26 12:21:32 -04:00
Brandon Lee Kitajchuk
3fc1c800ac Add queue event webhook to accounts list (#19) 2021-08-25 19:30:52 -04:00
Brandon Lee Kitajchuk
ee4483288d Adding pcap file download button to RecentCalls view (#17) 2021-08-01 21:02:01 -04:00
Dave Horton
d3f1dbf332 LICENSE 2021-07-21 12:37:41 -04:00
12 changed files with 763 additions and 194 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Drachtio Communications Services, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -46,6 +46,38 @@ const StyledInput = styled.input`
&:active:not([disabled]):after {
background: #A40D40;
}
&::file-selector-button {
content: '${props => props.validFile ? 'Choose a Different File' : 'Choose File'}';
position: absolute;
display: flex;
justify-content: center;
align-items: center;
height: 2.25rem;
top: 0;
left: 0;
padding: 0 1rem;
border-radius: 0.25rem;
background: #D91C5C;
color: #FFF;
font-weight: 500;
cursor: pointer;
outline: 0;
border: 0;
}
&:focus::file-selector-button {
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12),
inset 0 0 0 0.25rem #890934;
}
&:hover:not([disabled])::file-selector-button {
background: #BD164E;
}
&:active:not([disabled])::file-selector-button {
background: #A40D40;
}
`;
const FileUpload = (props, ref) => {

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect, useContext, useRef } from 'react';
import { useHistory } from 'react-router-dom';
import axios from 'axios';
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
import Form from '../elements/Form';
import Input from '../elements/Input';
import Label from '../elements/Label';
@@ -49,40 +50,52 @@ const AccountForm = props => {
let history = useHistory();
const dispatch = useContext(NotificationDispatchContext);
const jwt = localStorage.getItem("token");
const currentServiceProvider = useContext(ServiceProviderValueContext);
// Refs
const refName = useRef(null);
const refSipRealm = useRef(null);
const refRegWebhook = useRef(null);
const refUser = useRef(null);
const refPassword = useRef(null);
const refRegUser = useRef(null);
const refRegPassword = useRef(null);
const refQueueWebhook = useRef(null);
const refQueueUser = useRef(null);
const refQueuePassword = useRef(null);
// Form inputs
const [ name, setName ] = useState('');
const [ sipRealm, setSipRealm ] = useState('');
const [ name, setName ] = useState('');
const [ sipRealm, setSipRealm ] = useState('');
const [ deviceCallingApplication, setDeviceCallingApplication ] = useState('');
const [ regWebhook, setRegWebhook ] = useState('');
const [ method, setMethod ] = useState('POST');
const [ user, setUser ] = useState('' || '');
const [ password, setPassword ] = useState('' || '');
const [ regWebhook, setRegWebhook ] = useState('');
const [ regMethod, setRegMethod ] = useState('POST');
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('' || '');
// Invalid form inputs
const [ invalidName, setInvalidName ] = useState(false);
const [ invalidSipRealm, setInvalidSipRealm ] = useState(false);
const [ invalidRegWebhook, setInvalidRegWebhook ] = useState(false);
const [ invalidUser, setInvalidUser ] = useState(false);
const [ invalidPassword, setInvalidPassword ] = useState(false);
const [ invalidName, setInvalidName ] = useState(false);
const [ invalidSipRealm, setInvalidSipRealm ] = useState(false);
const [ invalidRegWebhook, setInvalidRegWebhook ] = useState(false);
const [ invalidRegUser, setInvalidRegUser ] = useState(false);
const [ invalidRegPassword, setInvalidRegPassword ] = useState(false);
const [ invalidQueueWebhook, setInvalidQueueWebhook ] = useState(false);
const [ invalidQueueUser, setInvalidQueueUser ] = useState(false);
const [ invalidQueuePassword, setInvalidQueuePassword ] = useState(false);
const [ showLoader, setShowLoader ] = useState(true);
const [ errorMessage, setErrorMessage ] = useState('');
const [ showAuth, setShowAuth ] = useState(false);
const toggleAuth = () => setShowAuth(!showAuth);
const [ showRegAuth, setShowRegAuth ] = useState(false);
const [ showQueueAuth, setShowQueueAuth ] = useState(false);
const toggleRegAuth = () => setShowRegAuth(!showRegAuth);
const toggleQueueAuth = () => setShowQueueAuth(!showQueueAuth);
const [ accounts, setAccounts ] = useState([]);
const [ accountSid, setAccountSid ] = useState('');
const [ serviceProviderSid, setServiceProviderSid ] = useState('');
const [ accountApplications, setAccountApplications ] = useState([]);
const [ menuOpen, setMenuOpen ] = useState(null);
@@ -187,18 +200,6 @@ const AccountForm = props => {
promiseList.push(applicationsPromise);
}
if (props.type === 'add') {
const serviceProvidersPromise = axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/ServiceProviders',
headers: {
Authorization: `Bearer ${jwt}`,
},
});
promiseList.push(serviceProvidersPromise);
}
const promiseAllValues = await Promise.all(promiseList);
const accountsData = (promiseAllValues[0] && promiseAllValues[0].data) || [];
@@ -212,11 +213,6 @@ const AccountForm = props => {
setAccountApplications(accountApplicationsData);
}
if (props.type === 'add') {
const serviceProviders = (promiseAllValues[1] && promiseAllValues[1].data) || '';
setServiceProviderSid(serviceProviders[0].service_provider_sid);
}
if (props.type === 'setup' && accountsData.length > 1) {
history.push('/internal/accounts');
dispatch({
@@ -250,16 +246,27 @@ const AccountForm = props => {
setSipRealm(acc.sip_realm || '');
setDeviceCallingApplication(acc.device_calling_application_sid || '');
setRegWebhook((acc.registration_hook && acc.registration_hook.url ) || '');
setMethod((acc.registration_hook && acc.registration_hook.method ) || 'post');
setUser((acc.registration_hook && acc.registration_hook.username) || '');
setPassword((acc.registration_hook && acc.registration_hook.password) || '');
setRegMethod((acc.registration_hook && acc.registration_hook.method ) || 'post');
setRegUser((acc.registration_hook && acc.registration_hook.username) || '');
setRegPassword((acc.registration_hook && acc.registration_hook.password) || '');
setQueueWebhook((acc.queue_event_hook && acc.queue_event_hook.url ) || '');
setQueueMethod((acc.queue_event_hook && acc.queue_event_hook.method ) || 'post');
setQueueUser((acc.queue_event_hook && acc.queue_event_hook.username) || '');
setQueuePassword((acc.queue_event_hook && acc.queue_event_hook.password) || '');
setWebhookSecret(acc.webhook_secret || '');
if (
(acc.registration_hook && acc.registration_hook.username) ||
(acc.registration_hook && acc.registration_hook.password)
) {
setShowAuth(true);
setShowRegAuth(true);
}
if (
(acc.queue_event_hook && acc.queue_event_hook.username) ||
(acc.queue_event_hook && acc.queue_event_hook.password)
) {
setShowQueueAuth(true);
}
}
setShowLoader(false);
@@ -298,8 +305,11 @@ const AccountForm = props => {
setInvalidName(false);
setInvalidSipRealm(false);
setInvalidRegWebhook(false);
setInvalidUser(false);
setInvalidPassword(false);
setInvalidRegUser(false);
setInvalidRegPassword(false);
setInvalidQueueWebhook(false);
setInvalidQueueUser(false);
setInvalidQueuePassword(false);
let errorMessages = [];
let focusHasBeenSet = false;
@@ -342,15 +352,29 @@ const AccountForm = props => {
});
if ((user && !password) || (!user && password)) {
errorMessages.push('Username and password must be either both filled out or both empty.');
setInvalidUser(true);
setInvalidPassword(true);
if ((regUser && !regPassword) || (!regUser && regPassword)) {
errorMessages.push('Registration webhook username and password must be either both filled out or both empty.');
setInvalidRegUser(true);
setInvalidRegPassword(true);
if (!focusHasBeenSet) {
if (!user) {
refUser.current.focus();
if (!regUser) {
refRegUser.current.focus();
} else {
refPassword.current.focus();
refRegPassword.current.focus();
}
focusHasBeenSet = true;
}
}
if ((queueUser && !queuePassword) || (!queueUser && queuePassword)) {
errorMessages.push('Queue event webhook username and password must be either both filled out or both empty.');
setInvalidQueueUser(true);
setInvalidQueuePassword(true);
if (!focusHasBeenSet) {
if (!queueUser) {
refQueueUser.current.focus();
} else {
refQueuePassword.current.focus();
}
focusHasBeenSet = true;
}
@@ -369,15 +393,21 @@ const AccountForm = props => {
sip_realm: sipRealm.trim() || null,
registration_hook: {
url: regWebhook.trim(),
method: method,
username: user.trim() || null,
password: password || null,
method: regMethod,
username: regUser.trim() || null,
password: regPassword || null,
},
queue_event_hook: {
url: queueWebhook.trim(),
method: queueMethod,
username: queueUser.trim() || null,
password: queuePassword || null,
},
webhook_secret: webhookSecret || null,
};
if (props.type === 'add') {
axiosData.service_provider_sid = serviceProviderSid;
axiosData.service_provider_sid = currentServiceProvider;
}
if (props.type === 'edit') {
@@ -567,26 +597,26 @@ const AccountForm = props => {
large={props.type === 'setup'}
name="method"
id="method"
value={method}
onChange={e => setMethod(e.target.value)}
value={regMethod}
onChange={e => setRegMethod(e.target.value)}
>
<option value="POST">POST</option>
<option value="GET">GET</option>
</Select>
</InputGroup>
{showAuth ? (
{showRegAuth ? (
<InputGroup>
<Label indented htmlFor="user">User</Label>
<Input
large={props.type === 'setup'}
name="user"
id="user"
value={user || ''}
onChange={e => setUser(e.target.value)}
value={regUser || ''}
onChange={e => setRegUser(e.target.value)}
placeholder="Optional"
invalid={invalidUser}
ref={refUser}
invalid={invalidRegUser}
ref={refRegUser}
/>
<Label htmlFor="password" middle>Password</Label>
<PasswordInput
@@ -594,12 +624,12 @@ const AccountForm = props => {
allowShowPassword
name="password"
id="password"
password={password}
setPassword={setPassword}
password={regPassword}
setPassword={setRegPassword}
setErrorMessage={setErrorMessage}
placeholder="Optional"
invalid={invalidPassword}
ref={refPassword}
invalid={invalidRegPassword}
ref={refRegPassword}
/>
</InputGroup>
) : (
@@ -607,7 +637,75 @@ const AccountForm = props => {
text
formLink
type="button"
onClick={toggleAuth}
onClick={toggleRegAuth}
>
Use HTTP Basic Authentication
</Button>
)}
<Label htmlFor="queueWebhook">Queue Event Webhook</Label>
<InputGroup>
<Input
large={props.type === 'setup'}
name="queueWebhook"
id="queueWebhook"
value={queueWebhook}
onChange={e => setQueueWebhook(e.target.value)}
placeholder="URL to notify when a member joins or leaves a queue"
invalid={invalidQueueWebhook}
ref={refQueueWebhook}
/>
<Label
middle
htmlFor="method"
>
Method
</Label>
<Select
large={props.type === 'setup'}
name="method"
id="method"
value={queueMethod}
onChange={e => setQueueMethod(e.target.value)}
>
<option value="POST">POST</option>
</Select>
</InputGroup>
{showQueueAuth ? (
<InputGroup>
<Label indented htmlFor="user">User</Label>
<Input
large={props.type === 'setup'}
name="user"
id="user"
value={queueUser || ''}
onChange={e => setQueueUser(e.target.value)}
placeholder="Optional"
invalid={invalidQueueUser}
ref={refQueueUser}
/>
<Label htmlFor="password" middle>Password</Label>
<PasswordInput
large={props.type === 'setup'}
allowShowPassword
name="password"
id="password"
password={queuePassword}
setPassword={setQueuePassword}
setErrorMessage={setErrorMessage}
placeholder="Optional"
invalid={invalidQueuePassword}
ref={refQueuePassword}
/>
</InputGroup>
) : (
<Button
text
formLink
type="button"
onClick={toggleQueueAuth}
>
Use HTTP Basic Authentication
</Button>

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect, useContext, useRef } from 'react';
import { useHistory } from 'react-router-dom';
import axios from 'axios';
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
import Form from '../elements/Form';
import Input from '../elements/Input';
import Label from '../elements/Label';
@@ -20,6 +21,7 @@ import CopyableText from '../elements/CopyableText';
const ApplicationForm = props => {
let history = useHistory();
const dispatch = useContext(NotificationDispatchContext);
const currentServiceProvider = useContext(ServiceProviderValueContext);
// Refs
const refName = useRef(null);
@@ -118,7 +120,7 @@ const ApplicationForm = props => {
applicationsPromise,
]);
const accounts = promiseAllValues[0].data;
const accounts = promiseAllValues[0].data.filter(a => a.service_provider_sid === currentServiceProvider);
const applications = promiseAllValues[1].data;
setAccounts(accounts);
@@ -493,7 +495,7 @@ const ApplicationForm = props => {
-- Choose the account this application will be associated with --
</option>
)}
{accounts.map(a => (
{accounts.filter(a => a.service_provider_sid === currentServiceProvider).map(a => (
<option
key={a.account_sid}
value={a.account_sid}

View File

@@ -5,6 +5,7 @@ import styled from "styled-components/macro";
import { Menu, Dropdown } from "antd";
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
import Form from '../elements/Form';
import Input from '../elements/Input';
import PasswordInput from '../elements/PasswordInput';
@@ -18,7 +19,6 @@ import Loader from '../blocks/Loader';
import sortSipGateways from '../../helpers/sortSipGateways';
import Select from '../elements/Select';
import handleErrors from "../../helpers/handleErrors";
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext'
const StyledForm = styled(Form)`
@media (max-width: 978.98px) {
@@ -185,6 +185,8 @@ const CarrierForm = (props) => {
]);
const [ applicationValues, setApplicationValues ] = useState([]);
const [ accounts, setAccounts ] = useState([]);
const [ accountSid, setAccountSid ] = useState('');
const [ carrierSid, setCarrierSid ] = useState('');
const [ showLoader, setShowLoader ] = useState(true);
@@ -220,7 +222,16 @@ const CarrierForm = (props) => {
Authorization: `Bearer ${jwt}`,
},
});
const accountsPromise = axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/Accounts',
headers: {
Authorization: `Bearer ${jwt}`,
},
});
promises.push(applicationPromise);
promises.push(accountsPromise);
if (type === 'edit') {
const carrierPromise = axios({
@@ -248,11 +259,12 @@ const CarrierForm = (props) => {
const promiseResponses = await Promise.all(promises);
setApplicationValues(promiseResponses[0].data);
setAccounts(promiseResponses[1].data);
if (type === 'edit') {
const carrier = promiseResponses[1].data;
const allSipGateways = promiseResponses[2].data;
const carrier = promiseResponses[2].data;
const allSipGateways = promiseResponses[3].data;
if (!carrier) {
isMounted = false;
@@ -274,6 +286,7 @@ const CarrierForm = (props) => {
setName(carrier.name || '');
setE164(carrier.e164_leading_plus === 1);
setApplication(carrier.application_sid || '');
setAccountSid(carrier.account_sid || '');
setAuthenticate(carrier.register_username ? true : false);
setRegister(carrier.requires_register === 1);
setUsername(carrier.register_username || '');
@@ -639,6 +652,7 @@ const CarrierForm = (props) => {
name: name.trim() || null,
e164_leading_plus: e164 ? 1 : 0,
application_sid: application || null,
account_sid: accountSid || null,
requires_register: register ? 1 : 0,
register_username: username ? username.trim() : null,
register_password: password ? password : null,
@@ -751,7 +765,11 @@ const CarrierForm = (props) => {
}
isMounted = false;
history.push('/internal/carriers');
if (accountSid) {
history.push(`/internal/carriers?account_sid=${accountSid}`);
} else {
history.push('/internal/carriers');
}
const dispatchMessage = type === 'add'
? 'Carrier created successfully'
: 'Carrier updated successfully';
@@ -894,29 +912,65 @@ const CarrierForm = (props) => {
onChange={e => setE164(e.target.checked)}
/>
<Label htmlFor="application">Application</Label>
<Label htmlFor="account">Used by</Label>
<Select
name="application"
id="application"
value={application}
onChange={e => setApplication(e.target.value)}
name="account"
id="account"
value={accountSid}
onChange={(e) => {
setAccountSid(e.target.value);
setApplication('');
}}
>
<option value="">
{type === 'add'
? '-- OPTIONAL: Application to invoke on calls arriving from this carrier --'
: '-- NONE --'
}
All accounts
</option>
{applicationValues.map(a => (
{accounts.filter(a => a.service_provider_sid === currentServiceProvider).map(a => (
<option
key={a.application_sid}
value={a.application_sid}
key={a.account_sid}
value={a.account_sid}
>
{a.name}
</option>
))}
</Select>
{accountSid && (
<>
<Label htmlFor="application">Default Application</Label>
<Select
name="application"
id="application"
value={application}
onChange={e => setApplication(e.target.value)}
>
<option value="">
{type === 'add'
? '-- OPTIONAL: Application to invoke on calls arriving from this carrier --'
: '-- NONE --'
}
</option>
{applicationValues.filter((a) => {
// Map an application to a service provider through it's account_sid
const acct = accounts.find(ac => a.account_sid === ac.account_sid);
if (accountSid) {
return a.account_sid === accountSid;
}
return acct.service_provider_sid === currentServiceProvider;
}).map(a => (
<option
key={a.application_sid}
value={a.application_sid}
>
{a.name}
</option>
))}
</Select>
</>
)}
<hr style={{ margin: '0.5rem -2rem' }} />
{

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect, useContext, useRef } from 'react';
import { useHistory } from 'react-router-dom';
import axios from 'axios';
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
import Form from '../elements/Form';
import Input from '../elements/Input';
import Label from '../elements/Label';
@@ -16,6 +17,7 @@ const PhoneNumberForm = props => {
let history = useHistory();
const dispatch = useContext(NotificationDispatchContext);
const currentServiceProvider = useContext(ServiceProviderValueContext);
// Refs
const refPhoneNumber = useRef(null);
@@ -358,7 +360,10 @@ const PhoneNumberForm = props => {
name="account"
id="account"
value={account}
onChange={e => setAccount(e.target.value)}
onChange={(e) => {
setAccount(e.target.value);
setApplication('');
}}
invalid={invalidAccount}
ref={refAccount}
>
@@ -368,7 +373,7 @@ const PhoneNumberForm = props => {
) && (
<option value="">-- Choose the account that this phone number should be associated with --</option>
)}
{accountValues.map(a => (
{accountValues.filter(a => a.service_provider_sid === currentServiceProvider).map(a => (
<option
key={a.account_sid}
value={a.account_sid}
@@ -391,7 +396,16 @@ const PhoneNumberForm = props => {
: '-- NONE --'
}
</option>
{applicationValues.map(a => (
{applicationValues.filter((a) => {
// Map an application to a service provider through it's account_sid
const acct = accountValues.find(ac => a.account_sid === ac.account_sid);
if (account) {
return a.account_sid === account;
}
return acct.service_provider_sid === currentServiceProvider;
}).map(a => (
<option
key={a.application_sid}
value={a.application_sid}

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect, useContext, useRef } from 'react';
import { useHistory } from 'react-router-dom';
import axios from 'axios';
import styled from 'styled-components';
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
import { ShowMsTeamsDispatchContext } from '../../contexts/ShowMsTeamsContext';
import Form from '../elements/Form';
@@ -11,9 +12,23 @@ import InputGroup from '../elements/InputGroup';
import FormError from '../blocks/FormError';
import Button from '../elements/Button';
import Loader from '../blocks/Loader';
import Modal from '../blocks/Modal';
import { ServiceProviderValueContext } from '../../contexts/ServiceProviderContext';
import handleErrors from "../../helpers/handleErrors";
const Td = styled.td`
padding: 0.5rem 0;
&:first-child {
font-weight: 500;
padding-right: 1.5rem;
vertical-align: top;
}
& ul {
margin: 0;
padding-left: 1.25rem;
}
`;
const SettingsForm = () => {
const history = useHistory();
const dispatch = useContext(NotificationDispatchContext);
@@ -40,8 +55,9 @@ const SettingsForm = () => {
const [ showLoader, setShowLoader ] = useState(true);
const [ errorMessage, setErrorMessage ] = useState('');
const [ serviceProviderSid, setServiceProviderSid ] = useState('');
const [ serviceProviders, setServiceProviders ] = useState([]);
const [ confirmDelete, setConfirmDelete ] = useState(false);
useEffect(() => {
const getSettingsData = async () => {
@@ -59,14 +75,16 @@ const SettingsForm = () => {
const serviceProvidersResponse = await axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/ServiceProviders/${currentServiceProvider}`,
url: `/ServiceProviders`,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
const sp = serviceProvidersResponse.data;
const sps = serviceProvidersResponse.data;
const sp = sps.find(s => s.service_provider_sid === currentServiceProvider);
setServiceProviders(sps);
setServiceProviderName(sp.name || '');
setServiceProviderSid(sp.service_provider_sid || '');
setEnableMsTeams(sp.ms_teams_fqdn ? true : false);
@@ -96,6 +114,32 @@ const SettingsForm = () => {
setEnableMsTeams(e.target.checked);
};
const handleDelete = () => {
setErrorMessage('');
axios({
method: 'delete',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/ServiceProviders/${serviceProviderSid}`,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
})
.then(() => {
setConfirmDelete(false);
setErrorMessage('');
history.push('/internal/accounts');
dispatch({
type: 'ADD',
level: 'success',
message: 'Service Provider Deleted'
});
})
.catch((error) => {
setErrorMessage(error.response.data.msg);
});
};
const handleSubmit = async (e) => {
let isMounted = true;
try {
@@ -210,69 +254,114 @@ const SettingsForm = () => {
return (
showLoader
? <Loader height="365px" />
: <Form
large
wideLabel
onSubmit={handleSubmit}
>
<Label htmlFor="serviceProviderName">Service Provider Name</Label>
<Input
name="serviceProviderName"
id="serviceProviderName"
value={serviceProviderName}
onChange={e => setServiceProviderName(e.target.value)}
invalid={invalidServiceProviderName}
ref={refServiceProviderName}
/>
<div>{/* needed for CSS grid layout */}</div>
<Checkbox
noLeftMargin
id="enableMsTeams"
label="Enable Microsoft Teams Direct Routing"
checked={enableMsTeams}
onChange={toggleMsTeams}
invalid={invalidEnableMsTeams}
ref={refEnableMsTeams}
/>
: (
<>
<Form
large
wideLabel
onSubmit={handleSubmit}
>
<Label htmlFor="serviceProviderName">Service Provider Name</Label>
<Input
name="serviceProviderName"
id="serviceProviderName"
value={serviceProviderName}
onChange={e => setServiceProviderName(e.target.value)}
invalid={invalidServiceProviderName}
ref={refServiceProviderName}
/>
<div>{/* needed for CSS grid layout */}</div>
<Checkbox
noLeftMargin
id="enableMsTeams"
label="Enable Microsoft Teams Direct Routing"
checked={enableMsTeams}
onChange={toggleMsTeams}
invalid={invalidEnableMsTeams}
ref={refEnableMsTeams}
/>
<Label htmlFor="sbcDomainName">SBC Domain Name</Label>
<Input
name="sbcDomainName"
id="sbcDomainName"
value={sbcDomainName}
onChange={e => setSbcDomainName(e.target.value)}
placeholder="Fully qualified domain name used for Microsoft Teams"
invalid={invalidSbcDomainName}
autoFocus={enableMsTeams}
ref={refSbcDomainName}
disabled={!enableMsTeams}
title={(!enableMsTeams && "You must enable Microsoft Teams Direct Routing in order to provide an SBC Domain Name") || ""}
/>
<Label htmlFor="sbcDomainName">SBC Domain Name</Label>
<Input
name="sbcDomainName"
id="sbcDomainName"
value={sbcDomainName}
onChange={e => setSbcDomainName(e.target.value)}
placeholder="Fully qualified domain name used for Microsoft Teams"
invalid={invalidSbcDomainName}
autoFocus={enableMsTeams}
ref={refSbcDomainName}
disabled={!enableMsTeams}
title={(!enableMsTeams && "You must enable Microsoft Teams Direct Routing in order to provide an SBC Domain Name") || ""}
/>
{errorMessage && (
<FormError grid message={errorMessage} />
)}
{errorMessage && !confirmDelete && (
<FormError grid message={errorMessage} />
)}
<InputGroup flexEnd spaced>
<Button
grid
gray
type="button"
onClick={() => {
history.push('/internal/accounts');
dispatch({
type: 'ADD',
level: 'info',
message: 'Changes canceled',
});
<InputGroup flexEnd spaced>
<Button
grid
gray
type="button"
onClick={() => {
history.push('/internal/accounts');
dispatch({
type: 'ADD',
level: 'info',
message: 'Changes canceled',
});
}}
>
Cancel
</Button>
{serviceProviders.length > 1 && (
<Button
grid
gray
type="button"
onClick={() => setConfirmDelete(true)}
>
Delete
</Button>
)}
<Button grid>Save</Button>
</InputGroup>
</Form>
{confirmDelete && serviceProviders.length > 1 && (
<Modal
title="Are you sure you want to delete the Service Provider?"
loader={false}
content={
<div>
<table>
<tbody>
<tr>
<Td>Service Provider Name:</Td>
<Td>{serviceProviderName}</Td>
</tr>
<tr>
<Td>SBC Domain Name:</Td>
<Td>{sbcDomainName || '[none]'}</Td>
</tr>
</tbody>
</table>
{errorMessage && (
<FormError message={errorMessage} />
)}
</div>
}
handleCancel={() => {
setConfirmDelete(false);
setErrorMessage('');
}}
>
Cancel
</Button>
<Button grid>Save</Button>
</InputGroup>
</Form>
handleSubmit={handleDelete}
actionText="Delete"
/>
)}
</>
)
);
};

View File

@@ -8,6 +8,7 @@ import handleErrors from '../../helpers/handleErrors';
import Form from '../elements/Form';
import Input from '../elements/Input';
import Label from '../elements/Label';
import Select from '../elements/Select';
import InputGroup from '../elements/InputGroup';
import PasswordInput from '../elements/PasswordInput';
import Radio from '../elements/Radio';
@@ -79,6 +80,8 @@ const SpeechServicesAddEdit = (props) => {
const [ secretAccessKey, setSecretAccessKey ] = useState('');
const [ useForTts, setUseForTts ] = useState(false);
const [ useForStt, setUseForStt ] = useState(false);
const [ accounts, setAccounts ] = useState([]);
const [ accountSid, setAccountSid ] = useState('');
// Invalid form inputs
const [ invalidVendorGoogle, setInvalidVendorGoogle ] = useState(false);
@@ -100,6 +103,17 @@ const SpeechServicesAddEdit = (props) => {
const getAPIData = async () => {
let isMounted = true;
try {
const accountsResponse = await axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: '/Accounts',
headers: {
Authorization: `Bearer ${jwt}`,
},
});
setAccounts(accountsResponse.data);
if (type === 'edit') {
const speechCredential = await axios({
method: 'get',
@@ -119,6 +133,7 @@ const SpeechServicesAddEdit = (props) => {
} catch (err) {
}
setAccountSid( speechCredential.data.account_sid || '');
setVendor( speechCredential.data.vendor || undefined);
setServiceKey( serviceKeyJson || '');
setDisplayedServiceKey( displayedServiceKeyJson || '');
@@ -238,33 +253,6 @@ const SpeechServicesAddEdit = (props) => {
return;
}
// Check if user already has a speech service with the selected vendor
const speechServices = await axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials`,
headers: {
Authorization: `Bearer ${jwt}`,
},
});
if (type === 'add' && speechServices.data.some(speech => speech.vendor === vendor)) {
setErrorMessage('You can only have one speech credential per vendor.');
setShowLoader(false);
if (vendor === 'google') {
setInvalidVendorGoogle(true);
if (!focusHasBeenSet) {
refVendorGoogle.current.focus();
}
} else if (vendor === 'aws') {
setInvalidVendorAws(true);
if (!focusHasBeenSet) {
refVendorAws.current.focus();
}
}
return;
}
//===============================================
// Submit
//===============================================
@@ -290,6 +278,8 @@ const SpeechServicesAddEdit = (props) => {
secret_access_key: vendor === 'aws' ? secretAccessKey : null,
use_for_tts: useForTts,
use_for_stt: useForStt,
service_provider_sid: accountSid ? null : currentServiceProvider,
account_sid: accountSid || null,
}
});
@@ -383,7 +373,11 @@ const SpeechServicesAddEdit = (props) => {
// If successful, go to speech services
//===============================================
isMounted = false;
history.push('/internal/speech-services');
if (accountSid) {
history.push(`/internal/speech-services?account_sid=${accountSid}`);
} else {
history.push('/internal/speech-services');
}
const dispatchMessage = type === 'add'
? 'Speech service created successfully'
: 'Speech service updated successfully';
@@ -452,6 +446,26 @@ const SpeechServicesAddEdit = (props) => {
/>
</InputGroup>
<Label htmlFor="account">Used by</Label>
<Select
name="account"
id="account"
value={accountSid}
onChange={e => setAccountSid(e.target.value)}
>
<option value="">
All accounts
</option>
{accounts.filter(a => a.service_provider_sid === currentServiceProvider).map(a => (
<option
key={a.account_sid}
value={a.account_sid}
>
{a.name}
</option>
))}
</Select>
{vendor === 'google' ? (
<>
<Label htmlFor="serviceKey">Service Key</Label>

View File

@@ -41,10 +41,8 @@ const AccountsList = () => {
sid: a.account_sid,
name: a.name,
sip_realm: a.sip_realm,
url: a.registration_hook && a.registration_hook.url,
method: a.registration_hook && a.registration_hook.method,
username: a.registration_hook && a.registration_hook.username,
password: a.registration_hook && a.registration_hook.password,
url_reg: a.registration_hook && a.registration_hook.url,
url_queue: a.queue_event_hook && a.queue_event_hook.url,
}));
return(simplifiedAccounts);
} catch (err) {
@@ -75,7 +73,7 @@ const AccountsList = () => {
const items = [
{ name: 'Name:' , content: account.name || '[none]' },
{ name: 'SIP Realm:' , content: account.sip_realm || '[none]' },
{ name: 'Registration Webhook:' , content: account.url || '[none]' },
{ name: 'Registration Webhook:' , content: account.url_reg || '[none]' },
];
return items;
};
@@ -221,7 +219,8 @@ const AccountsList = () => {
{ header: 'Name', key: 'name' },
{ header: 'AccountSid', key: 'sid' },
{ header: 'SIP Realm', key: 'sip_realm' },
{ header: 'Registration Webhook', key: 'url' },
{ header: 'Registration Webhook', key: 'url_reg' },
{ header: 'Queue Event Webhook', key: 'url_queue' },
]}
formatContentToDelete={formatAccountToDelete}
deleteContent={deleteAccount}

View File

@@ -1,27 +1,97 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useEffect, useContext, useCallback } from 'react';
import React, { useEffect, useContext, useState } from 'react';
import axios from 'axios';
import { useHistory } from 'react-router-dom';
import { useHistory, useLocation } from 'react-router-dom';
import styled from 'styled-components/macro';
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
import InternalTemplate from '../../templates/InternalTemplate';
import TableContent from '../../blocks/TableContent.js';
import Sbcs from '../../blocks/Sbcs';
import sortSipGateways from '../../../helpers/sortSipGateways';
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
import InputGroup from '../../../components/elements/InputGroup';
import Select from '../../../components/elements/Select';
import handleErrors from '../../../helpers/handleErrors';
const FilterLabel = styled.span`
color: #231f20;
text-align: right;
font-size: 14px;
margin-left: 1rem;
margin-right: 0.5rem;
`;
const StyledInputGroup = styled(InputGroup)`
padding: 1rem 1rem 0;
@media (max-width: 767.98px) {
display: grid;
grid-template-columns: auto 1fr auto 1fr;
grid-row-gap: 1rem;
}
@media (max-width: 575.98px) {
display: grid;
grid-template-columns: auto 1fr;
grid-row-gap: 1rem;
}
`;
const AccountSelect = styled(Select)`
min-width: 150px;
`;
const CarriersList = () => {
let history = useHistory();
const dispatch = useContext(NotificationDispatchContext);
const currentServiceProvider = useContext(ServiceProviderValueContext);
const location = useLocation();
const locationAccountSid = new URLSearchParams(location.search).get('account_sid');
const [accountSid, setAccountSid] = useState('');
const [accountList, setAccountList] = useState([]);
useEffect(() => {
document.title = `Carriers | Jambonz | Open Source CPAAS`;
}, []);
//=============================================================================
// Get accounts
//=============================================================================
useEffect(() => {
if (currentServiceProvider) {
const getAccounts = async () => {
try {
const accountResponse = await axios({
method: "get",
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
if (locationAccountSid) {
setAccountSid(locationAccountSid);
}
setAccountList((accountResponse.data || []).sort((a, b) => a.name.localeCompare(b.name)));
} catch (err) {
handleErrors({ err, history, dispatch });
}
};
getAccounts();
} else {
setAccountList([]);
}
}, [currentServiceProvider]);
//=============================================================================
// Get sip trunks
//=============================================================================
const getCarriers = useCallback(async () => {
const getCarriers = async () => {
try {
if (!localStorage.getItem('token')) {
history.push('/');
@@ -33,6 +103,7 @@ const CarriersList = () => {
return;
}
if(!currentServiceProvider) return [];
if (!accountList.length) return [];
// Get all SIP trunks
const trunkResults = await axios({
method: 'get',
@@ -43,9 +114,13 @@ const CarriersList = () => {
},
});
const trunkResultsFiltered = accountSid ?
trunkResults.data.filter(t => t.account_sid === accountSid) :
trunkResults.data.filter(t => t.account_sid === null);
// Add appropriate gateways to each trunk
const trunkMap = {};
for (const t of trunkResults.data) {
for (const t of trunkResultsFiltered) {
const gws = await axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
@@ -57,7 +132,7 @@ const CarriersList = () => {
trunkMap[t.voip_carrier_sid] = gws.data;
}
const trunksWithGateways = trunkResults.data.map(t => {
const trunksWithGateways = trunkResultsFiltered.map(t => {
const gateways = trunkMap[t.voip_carrier_sid] || [];
sortSipGateways(gateways);
return {
@@ -98,7 +173,7 @@ const CarriersList = () => {
console.log(err.response || err);
}
}
}, [currentServiceProvider, history, dispatch]);
};
//=============================================================================
// Delete sip trunk
@@ -173,6 +248,22 @@ const CarriersList = () => {
addButtonLink="/internal/carriers/add"
subtitle={<Sbcs />}
>
<StyledInputGroup flexEnd space>
<FilterLabel htmlFor="account">Used By:</FilterLabel>
<AccountSelect
name="account"
id="account"
value={accountSid}
onChange={e => setAccountSid(e.target.value)}
>
<option value="">
All accounts
</option>
{accountList.map((acc) => (
<option key={acc.account_sid} value={acc.account_sid}>{acc.name}</option>
))}
</AccountSelect>
</StyledInputGroup>
<TableContent
name="Carrier"
urlParam="carriers"

View File

@@ -58,6 +58,72 @@ const AccountSelect = styled(Select)`
min-width: 150px;
`;
const StyledPcapLink = styled.a`
display: flex;
justify-content: center;
align-items: center;
position: relative;
outline: 0;
height: 36px;
padding: 10px 26px 8px;
border-radius: 0.25rem;
background: #D91C5C;
color: #FFF;
text-decoration: none;
&:hover {
color: #FFF;
background: #BD164E;
}
`;
const PcapButton = ({call_data, account_sid, jwt_token}) => {
const [pcap, setPcap] = useState(null);
useEffect(() => {
axios({
method: "get",
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/Accounts/${account_sid}/RecentCalls/${call_data.sip_callid}`,
headers: {
Authorization: `Bearer ${jwt_token}`,
},
}).then((result_1) => {
if (result_1.status === 200 && result_1.data.total > 0) {
axios({
method: "get",
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/Accounts/${account_sid}/RecentCalls/${call_data.sip_callid}/pcap`,
headers: {
Authorization: `Bearer ${jwt_token}`,
},
responseType: "blob",
}).then((result_2) => {
setPcap({
dataUrl: URL.createObjectURL(result_2.data),
fileName: `callid-${call_data.sip_callid}.pcap`,
});
});
}
});
}, [call_data, account_sid, jwt_token, setPcap]);
if (pcap) {
return (
<Label>
<StyledPcapLink
href={pcap.dataUrl}
download={pcap.fileName}
>
Download pcap
</StyledPcapLink>
</Label>
);
}
return null;
};
const RecentCallsIndex = () => {
let history = useHistory();
const dispatch = useContext(NotificationDispatchContext);
@@ -253,6 +319,7 @@ const RecentCallsIndex = () => {
</React.Fragment>
);
})}
<PcapButton call_data={data} account_sid={account} jwt_token={jwt} />
</ExpandedSection>
);
};

View File

@@ -1,24 +1,93 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useContext, useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import React, { useContext, useState, useEffect } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import axios from 'axios';
import styled from 'styled-components/macro';
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
import handleErrors from '../../../helpers/handleErrors';
import InternalTemplate from '../../templates/InternalTemplate';
import TableContent from '../../../components/blocks/TableContent';
import { ServiceProviderValueContext } from '../../../contexts/ServiceProviderContext';
import Sbcs from '../../blocks/Sbcs';
import InputGroup from '../../../components/elements/InputGroup';
import Select from '../../../components/elements/Select';
const FilterLabel = styled.span`
color: #231f20;
text-align: right;
font-size: 14px;
margin-left: 1rem;
margin-right: 0.5rem;
`;
const StyledInputGroup = styled(InputGroup)`
padding: 1rem 1rem 0;
@media (max-width: 767.98px) {
display: grid;
grid-template-columns: auto 1fr auto 1fr;
grid-row-gap: 1rem;
}
@media (max-width: 575.98px) {
display: grid;
grid-template-columns: auto 1fr;
grid-row-gap: 1rem;
}
`;
const AccountSelect = styled(Select)`
min-width: 150px;
`;
const SpeechServicesList = () => {
let history = useHistory();
const dispatch = useContext(NotificationDispatchContext);
const currentServiceProvider = useContext(ServiceProviderValueContext);
const jwt = localStorage.getItem('token');
const location = useLocation();
const locationAccountSid = new URLSearchParams(location.search).get('account_sid');
const [accountSid, setAccountSid] = useState('');
const [accountList, setAccountList] = useState([]);
//=============================================================================
// Get accounts
//=============================================================================
useEffect(() => {
if (currentServiceProvider) {
const getAccounts = async () => {
try {
const accountResponse = await axios({
method: "get",
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/ServiceProviders/${currentServiceProvider}/Accounts`,
headers: {
Authorization: `Bearer ${jwt}`,
},
});
setAccountList((accountResponse.data || []).sort((a, b) => a.name.localeCompare(b.name)));
if (locationAccountSid) {
setAccountSid(locationAccountSid);
}
} catch (err) {
handleErrors({ err, history, dispatch });
}
};
getAccounts();
} else {
setAccountList([]);
}
}, [currentServiceProvider]);
//=============================================================================
// Get speech services
//=============================================================================
const getSpeechServices = useCallback(async () => {
const jwt = localStorage.getItem('token');
const getSpeechServices = async () => {
try {
if (!jwt) {
history.push('/');
@@ -31,10 +100,14 @@ const SpeechServicesList = () => {
}
if(!currentServiceProvider) return [];
const speechApiUrl = accountSid ?
`/Accounts/${accountSid}/SpeechCredentials` :
`/ServiceProviders/${currentServiceProvider}/SpeechCredentials`;
const speechServices = await axios({
method: 'get',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/ServiceProviders/${currentServiceProvider}/SpeechCredentials`,
url: speechApiUrl,
headers: {
Authorization: `Bearer ${jwt}`,
},
@@ -137,7 +210,7 @@ const SpeechServicesList = () => {
} catch (err) {
handleErrors({ err, history, dispatch, fallbackMessage: 'Unable to get speech services' });
}
}, [currentServiceProvider]);
};
//=============================================================================
// Delete speech service
@@ -149,8 +222,7 @@ const SpeechServicesList = () => {
{ name: 'Last Used', content: s.last_used || 'Never' },
];
};
const deleteSpeechService = useCallback(async speechServiceToDelete => {
const jwt = localStorage.getItem('token');
const deleteSpeechService = async speechServiceToDelete => {
try {
if (!jwt) {
history.push('/');
@@ -187,7 +259,7 @@ const SpeechServicesList = () => {
return ((err.response && err.response.data && err.response.data.msg) || 'Unable to delete speech service');
}
}
}, [currentServiceProvider]);
};
//=============================================================================
// Render
@@ -200,6 +272,22 @@ const SpeechServicesList = () => {
addButtonText="Add Speech Service"
addButtonLink="/internal/speech-services/add"
>
<StyledInputGroup flexEnd space>
<FilterLabel htmlFor="account">Used By:</FilterLabel>
<AccountSelect
name="account"
id="account"
value={accountSid}
onChange={e => setAccountSid(e.target.value)}
>
<option value="">
All accounts
</option>
{accountList.map((acc) => (
<option key={acc.account_sid} value={acc.account_sid}>{acc.name}</option>
))}
</AccountSelect>
</StyledInputGroup>
<TableContent
normalTable
name="speech service"