diff --git a/website/.env b/website/.env index ff5808a..c2b97c5 100644 --- a/website/.env +++ b/website/.env @@ -1 +1,2 @@ -REACT_APP_TITLE=Deighton AR System \ No newline at end of file +REACT_APP_TITLE=Deighton AR System +REACT_APP_SUPPORT_EMAIL=support@kss.us.com diff --git a/website/src/API.js b/website/src/API.js index 9f87df7..32b8435 100644 --- a/website/src/API.js +++ b/website/src/API.js @@ -1,5 +1,6 @@ import EventEmitter from 'eventemitter3' import io from 'socket.io-client' +import autobind from 'autobind-decorator' const authTokenName = 'AuthToken' @@ -28,6 +29,7 @@ class APIError extends Error { } } +@autobind class API extends EventEmitter { constructor() { super() diff --git a/website/src/App.js b/website/src/App.js index dc5638f..b647e0f 100644 --- a/website/src/App.js +++ b/website/src/App.js @@ -1,5 +1,5 @@ import React, { Component, Fragment } from 'react' -import { Login, Logout, ResetPassword, ForgotPassword, ConfirmEmail, ProtectedRoute } from './Auth' +import { Login, Logout, ResetPassword, ForgotPassword, ConfirmEmail, ProtectedRoute, DefaultRoute } from './Auth' import { Home } from './Home' import { Profile } from './Profile' import { Users } from './Users' @@ -92,16 +92,17 @@ export class App extends Component { - - - - - - ()} /> - - - - ()} /> + + + + + + + ()} /> + + + ()} /> + diff --git a/website/src/Auth/ConfirmEmail.js b/website/src/Auth/ConfirmEmail.js index 0182b67..ad583d4 100644 --- a/website/src/Auth/ConfirmEmail.js +++ b/website/src/Auth/ConfirmEmail.js @@ -2,6 +2,7 @@ import React from 'react' import { api } from 'src/API' import PropTypes from 'prop-types' import { MessageModal, WaitModal } from '../Modal' +import { Logout } from '.' import autobind from 'autobind-decorator' export class ConfirmEmail extends React.Component { @@ -17,7 +18,8 @@ export class ConfirmEmail extends React.Component { } componentDidMount(props) { - let emailToken = new URLSearchParams(decodeURIComponent(window.location.search)).get('email-token') + const emailToken = new URLSearchParams(decodeURIComponent(window.location.search)).get('email-token') + this.setState({ waitModal: { message: 'Validating Email...' } }) if (emailToken) { api.confirmEmail(emailToken).then((response) => { @@ -29,21 +31,17 @@ export class ConfirmEmail extends React.Component { this.props.history.replace('/login') } }).catch((err) => { - console.error(err) - const supportEmail = 'support@kingstonsoftware.solutions' // TODO: From configuration - const message = err.message.includes('The token was not found') - ? 'This email address may have already been confirmed.' - : `Please contact ${supportEmail} to request a new user invitation` this.setState({ waitModal: null, messageModal: { - title: 'Error Verifying Email...', - message: `We couldn't complete that request. ${message}` + icon: 'hand', + message: `Please contact ${process.env.REACT_APP_SUPPORT_EMAIL} to request another confirmation email.`, + detail: err.message } }) }) } else { - this.props.history.replace('/login') + this.props.history.replace('/') } } @@ -54,14 +52,23 @@ export class ConfirmEmail extends React.Component { } render() { + const { messageModal, waitModal } = this.state + + if (api.loggedInUser) { + return + } + return (
- + -
) diff --git a/website/src/Auth/DefaultRoute.js b/website/src/Auth/DefaultRoute.js new file mode 100644 index 0000000..4612f70 --- /dev/null +++ b/website/src/Auth/DefaultRoute.js @@ -0,0 +1,47 @@ +import React, { Fragment, Component } from 'react' +import { api } from 'src/API' +import { Route, Redirect } from 'react-router-dom' +import { Column } from 'ui' +import autobind from 'autobind-decorator' + +export class DefaultRoute extends Component { + @autobind + updateComponent() { + this.forceUpdate() + } + + componentDidMount() { + api.addListener('login', this.updateComponent) + } + + componentWillUnmount() { + api.removeListener('login', this.updateComponent) + } + + render() { + const user = api.loggedInUser + let redirect = null + + if (user) { + if (!user.pending) { + redirect = + } + } else { + redirect = + } + + return ( + { + return ( + + + {redirect} + + ) + }} + /> + ) + } +} diff --git a/website/src/Auth/ForgotPassword.js b/website/src/Auth/ForgotPassword.js index 6cd2e5b..1bd57fc 100644 --- a/website/src/Auth/ForgotPassword.js +++ b/website/src/Auth/ForgotPassword.js @@ -5,6 +5,7 @@ import { Image, Text, Column, Row, BoundInput, BoundButton, Box } from 'ui' import { MessageModal, WaitModal } from '../Modal' import { api } from 'src/API' import { FormBinder } from 'react-form-binder' +import { Logout } from '.' import headerLogo from 'images/deighton.png' import { sizeInfo, colorInfo } from 'ui/style' import autobind from 'autobind-decorator' @@ -65,6 +66,10 @@ export class ForgotPassword extends Component { render() { const { binder, waitModal, messageModal } = this.state + if (api.loggedInUser) { + return + } + return ( diff --git a/website/src/Auth/Logout.js b/website/src/Auth/Logout.js index 9b19c7c..d805b40 100644 --- a/website/src/Auth/Logout.js +++ b/website/src/Auth/Logout.js @@ -1,21 +1,36 @@ -import React from 'react' +import React, { Component, Fragment } from 'react' import PropTypes from 'prop-types' import { api } from 'src/API' +import { Column } from 'ui' -export class Logout extends React.Component { +export class Logout extends Component { static propTypes = { history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), + redirect: PropTypes.string, } componentDidMount(event) { - api.logout().then(() => { - if (this.props.history) { - this.props.history.replace('/login') + const { history, redirect } = this.props + const cb = () => { + if (history && redirect) { + try { + history.replace(redirect) + } catch (error) { + history.replace('/login') + } + } else { + window.location.replace('/login') } - }) + } + + api.logout().then(cb, cb) } render() { - return null + return ( + + + + ) } } diff --git a/website/src/Auth/ProtectedRoute.js b/website/src/Auth/ProtectedRoute.js index 663de1d..5c1f4b0 100644 --- a/website/src/Auth/ProtectedRoute.js +++ b/website/src/Auth/ProtectedRoute.js @@ -6,14 +6,7 @@ import autobind from 'autobind-decorator' export class ProtectedRoute extends React.Component { static propTypes = { - location: PropTypes.shape({ - pathname: PropTypes.string, - search: PropTypes.string, - }), - } - - static defaultProps = { - roles: ['administrator'] + location: PropTypes.shape({ pathname: PropTypes.string, search: PropTypes.string }), } @autobind @@ -30,7 +23,7 @@ export class ProtectedRoute extends React.Component { } render(props) { - let user = api.loggedInUser + const user = api.loggedInUser if (user) { if (user.pending) { @@ -40,8 +33,8 @@ export class ProtectedRoute extends React.Component { } else if (user.administrator) { return } + } else { + return } - - return } } diff --git a/website/src/Auth/ResetPassword.js b/website/src/Auth/ResetPassword.js index b8878ca..b8873e0 100644 --- a/website/src/Auth/ResetPassword.js +++ b/website/src/Auth/ResetPassword.js @@ -1,6 +1,7 @@ import React, { Component, Fragment } from 'react' import PropTypes from 'prop-types' import { Box, Text, Image, Column, Row, BoundInput, BoundButton } from 'ui' +import { Logout } from '.' import { MessageModal, WaitModal } from '../Modal' import { api } from 'src/API' import { FormBinder } from 'react-form-binder' @@ -32,7 +33,39 @@ export class ResetPassword extends Component { this.state = { binder: new FormBinder({}, ResetPassword.bindings), messageModal: null, - waitModal: null + waitModal: null, + tokenConfirmed: false, + } + } + + componentDidMount(props) { + if (this.state.tokenConfirmed) { + return + } + + const passwordToken = new URLSearchParams(decodeURIComponent(window.location.search)).get('password-token') + + this.setState({ waitModal: { message: 'Confirming password reset...' } }) + if (passwordToken) { + api.confirmResetPassword(passwordToken).then((response) => { + this.setState({ waitModal: null }) + if (response && response.valid) { + this.setState({ tokenConfirmed: true }) + } else { + this.props.history.replace('/') + } + }).catch((err) => { + this.setState({ + waitModal: null, + messageModal: { + icon: 'hand', + message: `We were unable to confirm you requested a password reset. Please request another reset email.`, + detail: err.message + } + }) + }) + } else { + this.props.history.replace('/') } } @@ -54,8 +87,9 @@ export class ResetPassword extends Component { waitModal: null, messageModal: { icon: 'hand', - title: 'There was a problem changing your password', + message: 'There was a problem changing your password. Please request another reset email.', detail: err.message, + noRetry: true, } }) }) @@ -63,12 +97,20 @@ export class ResetPassword extends Component { @autobind handleMessageModalDismiss() { - this.setState({ messageModal: null }) + if (this.state.messageModal.noRetry) { + this.props.history.replace('/login') + } else { + this.setState({ messageModal: null }) + } } render() { const { messageModal, waitModal, binder } = this.state + if (api.loggedInUser) { + return + } + return ( @@ -132,8 +174,8 @@ export class ResetPassword extends Component { +
Change Password diff --git a/website/src/Profile/Profile.js b/website/src/Profile/Profile.js index b4e5f72..a4f24cc 100644 --- a/website/src/Profile/Profile.js +++ b/website/src/Profile/Profile.js @@ -1,10 +1,12 @@ -import React from 'react' +import React, { Fragment, Component } from 'react' import { ProfileForm } from './ProfileForm' import { api } from 'src/API' import { WaitModal, MessageModal, ChangePasswordModal, ChangeEmailModal } from '../Modal' +import { Column, Row } from 'ui' +import { sizeInfo } from 'ui/style' import autobind from 'autobind-decorator' -export class Profile extends React.Component { +export class Profile extends Component { constructor(props) { super(props) @@ -129,26 +131,37 @@ export class Profile extends React.Component { render() { return ( -
- + + + + + + + + + + + + + - + - + - - - -
+ +
+ + ) } } diff --git a/website/src/Profile/ProfileForm.js b/website/src/Profile/ProfileForm.js index 8b39616..350bdc4 100644 --- a/website/src/Profile/ProfileForm.js +++ b/website/src/Profile/ProfileForm.js @@ -1,6 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' -import { Column, Button, BoundInput, BoundButton } from 'ui' +import { Column, Row, Box, Button, BoundInput, BoundButton } from 'ui' +import { sizeInfo, colorInfo } from 'ui/style' import { regExpPattern } from 'regexp-pattern' import { FormBinder } from 'react-form-binder' import autobind from 'autobind-decorator' @@ -90,32 +91,39 @@ export class ProfileForm extends React.Component { } render() { + const { binder } = this.state + return ( - - - - - - - - - - - -