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 + ? + :
+
{/* needed for CSS grid layout */}
+ + + + 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") || ""} + /> + +
+ + + setSipDomain(e.target.value)} + placeholder="Domain name that accounts will use as a fallback" + invalid={invalidSipDomain} + autoFocus={!enableMsTeams} + ref={refSipDomain} + /> + + + + 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" + ) || ""} + /> + + + + + + {showAuth ? ( + + + 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" + ) || ""} + /> + + + + ) : ( + + )} + + {errorMessage && ( + + )} + + + + + + + + ); +}; + +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 @@ + + +