mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-07-24 04:52:05 +00:00
Implement settings page
This commit is contained in:
@@ -16,6 +16,7 @@ import AccountsAddEdit from './components/pages/internal/AccountsAddEdit';
|
||||
import ApplicationsAddEdit from './components/pages/internal/ApplicationsAddEdit';
|
||||
import SipTrunksAddEdit from './components/pages/internal/SipTrunksAddEdit';
|
||||
import PhoneNumbersAddEdit from './components/pages/internal/PhoneNumbersAddEdit';
|
||||
import Settings from './components/pages/internal/Settings';
|
||||
import InvalidRoute from './components/pages/InvalidRoute';
|
||||
|
||||
import Notification from './components/blocks/Notification';
|
||||
@@ -68,6 +69,8 @@ function App() {
|
||||
<PhoneNumbersAddEdit />
|
||||
</Route>
|
||||
|
||||
<Route exact path="/internal/settings"><Settings /></Route>
|
||||
|
||||
<Route><InvalidRoute /></Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
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 Select from '../elements/Select';
|
||||
import Checkbox from '../elements/Checkbox';
|
||||
import InputGroup from '../elements/InputGroup';
|
||||
import PasswordInput from '../elements/PasswordInput';
|
||||
import FormError from '../blocks/FormError';
|
||||
import Button from '../elements/Button';
|
||||
import Loader from '../blocks/Loader';
|
||||
|
||||
const SettingsForm = () => {
|
||||
const history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
|
||||
// Refs
|
||||
const refEnableMsTeams = useRef(null);
|
||||
const refSbcDomainName = useRef(null);
|
||||
const refSipDomain = useRef(null);
|
||||
const refRegWebhook = useRef(null);
|
||||
const refUser = useRef(null);
|
||||
const refPassword = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [ enableMsTeams, setEnableMsTeams ] = useState(false);
|
||||
const [ sbcDomainName, setSbcDomainName ] = useState('');
|
||||
const [ sipDomain, setSipDomain ] = useState('');
|
||||
const [ regWebhook, setRegWebhook ] = useState('');
|
||||
const [ method, setMethod ] = useState('POST');
|
||||
const [ user, setUser ] = useState('');
|
||||
const [ password, setPassword ] = useState('');
|
||||
|
||||
// For when user has data in sbcDomainName and then taps the checkbox to disable MsTeams
|
||||
const [ savedSbcDomainName, setSavedSbcDomainName ] = useState('');
|
||||
|
||||
// Invalid form inputs
|
||||
const [ invalidEnableMsTeams, setInvalidEnableMsTeams ] = useState(false);
|
||||
const [ invalidSbcDomainName, setInvalidSbcDomainName ] = useState(false);
|
||||
const [ invalidSipDomain, setInvalidSipDomain ] = useState(false);
|
||||
const [ invalidRegWebhook, setInvalidRegWebhook ] = useState(false);
|
||||
const [ invalidUser, setInvalidUser ] = useState(false);
|
||||
const [ invalidPassword, setInvalidPassword ] = useState(false);
|
||||
|
||||
const [ showLoader, setShowLoader ] = useState(true);
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
|
||||
const [ showAuth, setShowAuth ] = useState(false);
|
||||
const toggleAuth = () => setShowAuth(!showAuth);
|
||||
|
||||
const [ serviceProviderSid, setServiceProviderSid ] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const getSettingsData = async () => {
|
||||
try {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const serviceProvidersResponse = await axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/ServiceProviders',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const sp = serviceProvidersResponse.data[0];
|
||||
|
||||
setServiceProviderSid(sp.service_provider_sid || '');
|
||||
setEnableMsTeams(sp.ms_teams_fqdn ? true : false);
|
||||
setSbcDomainName(sp.ms_teams_fqdn || '');
|
||||
setSipDomain(sp.root_domain || '');
|
||||
setRegWebhook((sp.registration_hook && sp.registration_hook.url) || '');
|
||||
setMethod((sp.registration_hook && sp.registration_hook.method) || 'post');
|
||||
setUser((sp.registration_hook && sp.registration_hook.username) || '');
|
||||
setPassword((sp.registration_hook && sp.registration_hook.password) || '');
|
||||
|
||||
if (
|
||||
(sp.registration_hook && sp.registration_hook.username) ||
|
||||
(sp.registration_hook && sp.registration_hook.password)
|
||||
) {
|
||||
setShowAuth(true);
|
||||
}
|
||||
|
||||
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 {
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: (err.response && err.response.data && err.response.data.msg) || 'Something went wrong, please try again.',
|
||||
});
|
||||
console.log(err.response || err);
|
||||
}
|
||||
setShowLoader(false);
|
||||
}
|
||||
};
|
||||
getSettingsData();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const toggleMsTeams = (e) => {
|
||||
if (!e.target.checked && sbcDomainName) {
|
||||
setSavedSbcDomainName(sbcDomainName);
|
||||
setSbcDomainName('');
|
||||
}
|
||||
if (e.target.checked && savedSbcDomainName) {
|
||||
setSbcDomainName(savedSbcDomainName);
|
||||
setSavedSbcDomainName('');
|
||||
}
|
||||
setEnableMsTeams(e.target.checked);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
let isMounted = true;
|
||||
try {
|
||||
//=============================================================================
|
||||
// reset
|
||||
//=============================================================================
|
||||
setShowLoader(true);
|
||||
e.preventDefault();
|
||||
setErrorMessage('');
|
||||
setInvalidEnableMsTeams(false);
|
||||
setInvalidSbcDomainName(false);
|
||||
setInvalidSipDomain(false);
|
||||
setInvalidRegWebhook(false);
|
||||
setInvalidUser(false);
|
||||
setInvalidPassword(false);
|
||||
let errorMessages = [];
|
||||
let focusHasBeenSet = false;
|
||||
|
||||
//=============================================================================
|
||||
// data checks
|
||||
//=============================================================================
|
||||
if (enableMsTeams && !sbcDomainName) {
|
||||
errorMessages.push(
|
||||
'You must provide an SBC Domain Name in order to enable Microsoft Teams Direct Routing'
|
||||
);
|
||||
setInvalidSbcDomainName(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refSbcDomainName.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!enableMsTeams && sbcDomainName) {
|
||||
errorMessages.push(
|
||||
'You must check "Enable Microsoft Teams Direct Routing" to enable this feature, or remove the SBC Domain Name provided'
|
||||
);
|
||||
setInvalidEnableMsTeams(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refEnableMsTeams.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sipDomain && (regWebhook || user || password)) {
|
||||
errorMessages.push(
|
||||
'You must provide a SIP Domain in order to provide a Registration Webhook'
|
||||
);
|
||||
setInvalidSipDomain(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refSipDomain.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (sipDomain && !regWebhook) {
|
||||
errorMessages.push(
|
||||
'You must provide a Registration Webhook when providing a SIP Domain'
|
||||
);
|
||||
setInvalidRegWebhook(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refRegWebhook.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ((user && !password) || (!user && password)) {
|
||||
errorMessages.push('Username and password must be either both filled out or both empty.');
|
||||
setInvalidUser(true);
|
||||
setInvalidPassword(true);
|
||||
if (!focusHasBeenSet) {
|
||||
if (!user) {
|
||||
refUser.current.focus();
|
||||
} else {
|
||||
refPassword.current.focus();
|
||||
}
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
return;
|
||||
} else if (errorMessages.length === 1) {
|
||||
setErrorMessage(errorMessages[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// submit data
|
||||
//=============================================================================
|
||||
const data = {
|
||||
ms_teams_fqdn: sbcDomainName.trim() || null,
|
||||
root_domain: sipDomain.trim() || null,
|
||||
};
|
||||
|
||||
if (regWebhook) {
|
||||
data.registration_hook = {
|
||||
url: regWebhook.trim() || null,
|
||||
method,
|
||||
username: user.trim() || null,
|
||||
password: password || null,
|
||||
};
|
||||
}
|
||||
|
||||
await axios({
|
||||
method: 'put',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/ServiceProviders/${serviceProviderSid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
data,
|
||||
});
|
||||
|
||||
//=============================================================================
|
||||
// redirect
|
||||
//=============================================================================
|
||||
isMounted = false;
|
||||
history.push('/internal/accounts');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'success',
|
||||
message: 'Settings updated'
|
||||
});
|
||||
|
||||
} 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="365px" />
|
||||
: <Form
|
||||
large
|
||||
wideLabel
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<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") || ""}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<Label htmlFor="sipDomain">Fallback SIP Domain</Label>
|
||||
<Input
|
||||
name="sipDomain"
|
||||
id="sipDomain"
|
||||
value={sipDomain}
|
||||
onChange={e => setSipDomain(e.target.value)}
|
||||
placeholder="Domain name that accounts will use as a fallback"
|
||||
invalid={invalidSipDomain}
|
||||
autoFocus={!enableMsTeams}
|
||||
ref={refSipDomain}
|
||||
/>
|
||||
|
||||
<Label htmlFor="regWebhook">Registration Webhook</Label>
|
||||
<InputGroup>
|
||||
<Input
|
||||
name="regWebhook"
|
||||
id="regWebhook"
|
||||
value={regWebhook}
|
||||
onChange={e => setRegWebhook(e.target.value)}
|
||||
placeholder="URL for your web application that handles registrations"
|
||||
invalid={invalidRegWebhook}
|
||||
ref={refRegWebhook}
|
||||
disabled={!sipDomain && !regWebhook && !user && !password}
|
||||
title={(
|
||||
!sipDomain &&
|
||||
!regWebhook &&
|
||||
!user &&
|
||||
!password &&
|
||||
"You must provide a SIP Domain in order to enter a registration webhook"
|
||||
) || ""}
|
||||
/>
|
||||
|
||||
<Label
|
||||
middle
|
||||
htmlFor="method"
|
||||
>
|
||||
Method
|
||||
</Label>
|
||||
<Select
|
||||
name="method"
|
||||
id="method"
|
||||
value={method}
|
||||
onChange={e => setMethod(e.target.value)}
|
||||
disabled={!sipDomain && !regWebhook && !user && !password}
|
||||
title={(
|
||||
!sipDomain &&
|
||||
!regWebhook &&
|
||||
!user &&
|
||||
!password &&
|
||||
"You must provide a SIP Domain in order to enter a registration webhook"
|
||||
) || ""}
|
||||
>
|
||||
<option value="POST">POST</option>
|
||||
<option value="GET">GET</option>
|
||||
</Select>
|
||||
</InputGroup>
|
||||
|
||||
{showAuth ? (
|
||||
<InputGroup>
|
||||
<Label indented htmlFor="user">User</Label>
|
||||
<Input
|
||||
name="user"
|
||||
id="user"
|
||||
value={user || ''}
|
||||
onChange={e => setUser(e.target.value)}
|
||||
placeholder="Optional"
|
||||
invalid={invalidUser}
|
||||
ref={refUser}
|
||||
disabled={!sipDomain && !regWebhook && !user && !password}
|
||||
title={(
|
||||
!sipDomain &&
|
||||
!regWebhook &&
|
||||
!user &&
|
||||
!password &&
|
||||
"You must provide a SIP Domain in order to enter a registration webhook"
|
||||
) || ""}
|
||||
/>
|
||||
<Label htmlFor="password" middle>Password</Label>
|
||||
<PasswordInput
|
||||
allowShowPassword
|
||||
name="password"
|
||||
id="password"
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
setErrorMessage={setErrorMessage}
|
||||
placeholder="Optional"
|
||||
invalid={invalidPassword}
|
||||
ref={refPassword}
|
||||
disabled={!sipDomain && !regWebhook && !user && !password}
|
||||
title={(
|
||||
!sipDomain &&
|
||||
!regWebhook &&
|
||||
!user &&
|
||||
!password &&
|
||||
"You must provide a SIP Domain in order to enter a registration webhook"
|
||||
) || ""}
|
||||
/>
|
||||
</InputGroup>
|
||||
) : (
|
||||
<Button
|
||||
text
|
||||
formLink
|
||||
type="button"
|
||||
onClick={toggleAuth}
|
||||
>
|
||||
Use HTTP Basic Authentication
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{errorMessage && (
|
||||
<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',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button grid>Save</Button>
|
||||
</InputGroup>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsForm;
|
||||
@@ -0,0 +1,20 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
import SettingsForm from '../../forms/SettingsForm';
|
||||
|
||||
const Settings = () => {
|
||||
const pageTitle = 'Settings';
|
||||
useEffect(() => {
|
||||
document.title = `${pageTitle} | Jambonz | Open Source CPAAS`;
|
||||
});
|
||||
return (
|
||||
<InternalTemplate
|
||||
type="form"
|
||||
title={pageTitle}
|
||||
>
|
||||
<SettingsForm />
|
||||
</InternalTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
@@ -8,6 +8,7 @@ import { ReactComponent as AccountsIcon } from '../../images/AccountsIcon.svg';
|
||||
import { ReactComponent as ApplicationsIcon } from '../../images/ApplicationsIcon.svg';
|
||||
import { ReactComponent as SipTrunksIcon } from '../../images/SipTrunksIcon.svg';
|
||||
import { ReactComponent as PhoneNumbersIcon } from '../../images/PhoneNumbersIcon.svg';
|
||||
import { ReactComponent as SettingsIcon } from '../../images/SettingsIcon.svg';
|
||||
import AddButton from '../elements/AddButton';
|
||||
import Breadcrumbs from '../blocks/Breadcrumbs';
|
||||
|
||||
@@ -141,6 +142,7 @@ const InternalTemplate = props => {
|
||||
<MenuLink to="/internal/applications" name="Applications" icon={<ApplicationsIcon />} />
|
||||
<MenuLink to="/internal/sip-trunks" name="SIP Trunks" icon={<SipTrunksIcon />} />
|
||||
<MenuLink to="/internal/phone-numbers" name="Phone Numbers" icon={<PhoneNumbersIcon />} />
|
||||
<MenuLink to="/internal/settings" name="Settings" icon={<SettingsIcon />} />
|
||||
</SideMenu>
|
||||
<PageMain>
|
||||
{props.breadcrumbs && (
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="21" height="20" viewBox="0 0 21 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.204789 7.87962L2.45962 7.51441C2.83787 6.26593 3.49894 5.14037 4.36828 4.21229L3.557 2.07657C4.59273 1.16294 5.81005 0.450407 7.14789 2.97165e-05L8.59238 1.77115C9.20524 1.62883 9.84384 1.55359 10.5 1.55359C11.1561 1.55359 11.7947 1.62882 12.4075 1.77112L13.852 0C15.1899 0.450363 16.4072 1.16289 17.4429 2.07649L16.6316 4.21221C17.501 5.14031 18.1621 6.26591 18.5404 7.51443L20.7952 7.87964C20.9295 8.55009 21 9.24361 21 9.95359C21 10.6635 20.9295 11.3571 20.7952 12.0275L18.5404 12.3927C18.1622 13.6412 17.501 14.7668 16.6317 15.695L17.4429 17.8307C16.4072 18.7443 15.1899 19.4568 13.8521 19.9072L12.4076 18.1361C11.7947 18.2784 11.1561 18.3536 10.5 18.3536C9.84382 18.3536 9.20521 18.2784 8.59233 18.136L7.14786 19.9071C5.81003 19.4568 4.59271 18.7442 3.55699 17.8306L4.36827 15.6949C3.49892 14.7668 2.83784 13.6412 2.4596 12.3927L0.204778 12.0275C0.0704634 11.3571 0 10.6636 0 9.95359C0 9.2436 0.070467 8.55007 0.204789 7.87962ZM10.5 14.1536C12.8196 14.1536 14.7 12.2732 14.7 9.95359C14.7 7.634 12.8196 5.75359 10.5 5.75359C8.1804 5.75359 6.3 7.634 6.3 9.95359C6.3 12.2732 8.1804 14.1536 10.5 14.1536Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
Reference in New Issue
Block a user