diff --git a/src/App.js b/src/App.js
index b2fcdaf..282ac3a 100644
--- a/src/App.js
+++ b/src/App.js
@@ -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() {
+
+
diff --git a/src/components/forms/SettingsForm.js b/src/components/forms/SettingsForm.js
new file mode 100644
index 0000000..4df49f0
--- /dev/null
+++ b/src/components/forms/SettingsForm.js
@@ -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
+ ?
+ :
+ );
+};
+
+export default SettingsForm;
diff --git a/src/components/pages/internal/Settings.js b/src/components/pages/internal/Settings.js
new file mode 100644
index 0000000..756767f
--- /dev/null
+++ b/src/components/pages/internal/Settings.js
@@ -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 (
+
+
+
+ );
+};
+
+export default Settings;
diff --git a/src/components/templates/InternalTemplate.js b/src/components/templates/InternalTemplate.js
index b52562b..da6d5bc 100644
--- a/src/components/templates/InternalTemplate.js
+++ b/src/components/templates/InternalTemplate.js
@@ -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 => {
} />
} />
} />
+ } />
{props.breadcrumbs && (
diff --git a/src/images/SettingsIcon.svg b/src/images/SettingsIcon.svg
new file mode 100644
index 0000000..c564236
--- /dev/null
+++ b/src/images/SettingsIcon.svg
@@ -0,0 +1,3 @@
+