Initial commit
This commit is contained in:
70
website/src/Auth/ConfirmEmail.js
Normal file
70
website/src/Auth/ConfirmEmail.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import React from 'react'
|
||||
import { api } from '../helpers'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Container } from 'semantic-ui-react'
|
||||
import { MessageDialog, WaitDialog } from '../Dialog'
|
||||
import './ConfirmEmail.scss'
|
||||
|
||||
export class ConfirmEmail extends React.Component {
|
||||
static propTypes = {
|
||||
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
|
||||
}
|
||||
constructor() {
|
||||
super()
|
||||
this.state = {
|
||||
waitDialog: null,
|
||||
messageDialog: null
|
||||
}
|
||||
this.handleMessageDialogDismiss = this.handleMessageDialogDismiss.bind(this)
|
||||
}
|
||||
|
||||
componentDidMount(props) {
|
||||
let emailToken = new URLSearchParams(decodeURIComponent(window.location.search)).get('email-token')
|
||||
this.setState({ waitDialog: { message: 'Validating Email...' } })
|
||||
if (emailToken) {
|
||||
api.confirmEmail(emailToken).then((response) => {
|
||||
this.setState({ waitDialog: null })
|
||||
if (response && response.passwordToken) {
|
||||
// API will send a password reset token if this is the first time loggin on
|
||||
this.props.history.replace(`/reset-password?password-token=${response.passwordToken}`)
|
||||
} else {
|
||||
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({
|
||||
waitDialog: null,
|
||||
messageDialog: {
|
||||
title: 'Error Verifying Email...',
|
||||
message: `We couldn't complete that request. ${message}`
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
this.props.history.replace('/login')
|
||||
}
|
||||
}
|
||||
|
||||
handleMessageDialogDismiss() {
|
||||
this.setState({ messageDialog: null })
|
||||
this.props.history.replace('/login')
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Container className='email-confirm-container'>
|
||||
<WaitDialog active={!!this.state.waitDialog}
|
||||
message={this.state.waitDialog ? this.state.waitDialog.message : ''} />
|
||||
|
||||
<MessageDialog error open={!!this.state.messageDialog}
|
||||
title={this.state.messageDialog ? this.state.messageDialog.title : ''}
|
||||
message={this.state.messageDialog ? this.state.messageDialog.message : ''}
|
||||
onDismiss={this.handleMessageDialogDismiss} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
11
website/src/Auth/ConfirmEmail.scss
Normal file
11
website/src/Auth/ConfirmEmail.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
.ui.container.email-confirm-container {
|
||||
display: flex;
|
||||
height: 80vh;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ui.container.email-confirm-container button {
|
||||
margin-top: 1em;
|
||||
}
|
||||
93
website/src/Auth/ForgotPassword.js
Normal file
93
website/src/Auth/ForgotPassword.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { regExpPattern } from 'regexp-pattern'
|
||||
import { Container, Header, Form, Message } from 'semantic-ui-react'
|
||||
import './ForgotPassword.scss'
|
||||
import { MessageDialog, WaitDialog } from '../Dialog'
|
||||
import { Validator, ValidatedInput, ValidatedButton } from '../Validated'
|
||||
import { api } from '../helpers'
|
||||
|
||||
export class ForgotPassword extends React.Component {
|
||||
static propTypes = {
|
||||
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
|
||||
}
|
||||
|
||||
static validations = {
|
||||
email: {
|
||||
alwaysGet: true,
|
||||
isValid: (r, v) => (regExpPattern.email.test(v))
|
||||
},
|
||||
submit: {
|
||||
nonValue: true,
|
||||
isDisabled: (r) => (!r.anyModified || !r.allValid)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
validator: new Validator({}, ForgotPassword.validations),
|
||||
messageDialog: null,
|
||||
waitDialog: null
|
||||
}
|
||||
this.handleSubmit = this.handleSubmit.bind(this)
|
||||
this.handleMessageDialogDismiss = this.handleMessageDialogDismiss.bind(this)
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
const obj = this.state.validator.getValues()
|
||||
|
||||
this.setState({ waitDialog: { message: 'Requesting Reset Email' } })
|
||||
api.sendResetPassword(obj.email).then((res) => {
|
||||
const email = this.state.validator.getField('email').value
|
||||
this.setState({
|
||||
waitDialog: null,
|
||||
messageDialog: {
|
||||
error: false,
|
||||
title: 'Password Reset Requested',
|
||||
message: `An email will be sent to '${email}' with a reset link. Please click on it to finish resetting the password.`
|
||||
}
|
||||
})
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
validator: new Validator({}, ForgotPassword.validations), // Reset to avoid rapid retries
|
||||
waitDialog: null,
|
||||
messageDialog: {
|
||||
error: true,
|
||||
title: 'Password Reset Failed',
|
||||
message: `There was a problem requesting the password reset. ${error ? error.message : ''}`
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
handleMessageDialogDismiss() {
|
||||
this.props.history.replace('/')
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Container className='forgot-password-container'>
|
||||
<Header content='Forgotten Password' size='large' />
|
||||
<Form size='large' onSubmit={this.handleSubmit}>
|
||||
<ValidatedInput label='Email' name='email'
|
||||
placeholder='example@xyz.com' validator={this.state.validator}
|
||||
message='A valid email address' />
|
||||
<Message info content='The email address of an existing user to send the password reset link to.' />
|
||||
<ValidatedButton className='submit' name='submit' content='Submit'
|
||||
primary submit validator={this.state.validator}>Submit</ValidatedButton>
|
||||
</Form>
|
||||
|
||||
<WaitDialog active={!!this.state.waitDialog}
|
||||
message={this.state.waitDialog ? this.state.waitDialog.message : ''} />
|
||||
|
||||
<MessageDialog
|
||||
open={!!this.state.messageDialog}
|
||||
error={this.state.messageDialog ? this.state.messageDialog.error : true}
|
||||
title={this.state.messageDialog ? this.state.messageDialog.title : ''}
|
||||
message={this.state.messageDialog ? this.state.messageDialog.message : ''}
|
||||
onDismiss={this.handleMessageDialogDismiss} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
23
website/src/Auth/ForgotPassword.scss
Normal file
23
website/src/Auth/ForgotPassword.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
.ui.container.forgot-password-container {
|
||||
height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ui.container.forgot-password-container .header {
|
||||
border-bottom: 1px solid #d4d4d5;
|
||||
width: 40%;
|
||||
padding-bottom: 0.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.ui.container.forgot-password-container form {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.ui.container.forgot-password-container .message {
|
||||
margin: 2em 0;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
125
website/src/Auth/Login.js
Normal file
125
website/src/Auth/Login.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Container, Header, Form, Message } from 'semantic-ui-react'
|
||||
import './Login.scss'
|
||||
import { regExpPattern } from 'regexp-pattern'
|
||||
import { api } from '../helpers'
|
||||
import { Validator, ValidatedInput, ValidatedCheckbox, ValidatedButton } from '../Validated'
|
||||
import { WaitDialog, MessageDialog } from '../Dialog'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export class Login extends React.Component {
|
||||
static propTypes = {
|
||||
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
|
||||
}
|
||||
|
||||
static validations = {
|
||||
email: {
|
||||
alwaysGet: true,
|
||||
isValid: (r, v) => (regExpPattern.email.test(v))
|
||||
},
|
||||
password: {
|
||||
alwaysGet: true,
|
||||
isValid: (r, v) => (v !== '')
|
||||
},
|
||||
rememberMe: {
|
||||
alwaysGet: true,
|
||||
initValue: true
|
||||
},
|
||||
submit: {
|
||||
nonValue: true,
|
||||
isDisabled: (r) => (!r.anyModified || !r.allValid)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.handleSubmit = this.handleSubmit.bind(this)
|
||||
this.handleMessageDialogDismiss = this.handleMessageDialogDismiss.bind(this)
|
||||
this.state = {
|
||||
waitDialog: false,
|
||||
messageDialog: null,
|
||||
validator: new Validator({}, Login.validations)
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
if (this.state.messageDialog) {
|
||||
this.setState({ messageDialog: null })
|
||||
return
|
||||
} else if (this.state.waitDialog) {
|
||||
return
|
||||
}
|
||||
|
||||
let obj = this.state.validator.getValues()
|
||||
|
||||
if (obj) {
|
||||
this.setState({ waitDialog: true })
|
||||
api.login(obj.email, obj.password, obj.rememberMe).then((user) => {
|
||||
this.setState({ waitDialog: false })
|
||||
if (this.props.history) {
|
||||
const landing = user.role === 'broker' ? '/broker-dashboard' : 'dashboard'
|
||||
let url = new URLSearchParams(window.location.search).get('redirect') || landing
|
||||
try {
|
||||
this.props.history.replace(url)
|
||||
} catch (error) {
|
||||
this.props.history.replace('/')
|
||||
}
|
||||
}
|
||||
}).catch((error) => {
|
||||
const elems = document.getElementsByName('email')
|
||||
if (elems) {
|
||||
elems[0].focus()
|
||||
}
|
||||
this.setState({
|
||||
validator: new Validator({ email: this.state.validator.getField('email').value }, Login.validations),
|
||||
waitDialog: false,
|
||||
messageDialog: { title: 'Login Error...', message: `Unable to login. ${error.message}` }
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleMessageDialogDismiss() {
|
||||
this.setState({ messageDialog: null })
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Container id='login' className='password-reset-container'>
|
||||
<Header size='large'>Login Portal</Header>
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
{/* Add in 'Username' field pass */}
|
||||
<ValidatedInput label='Email' name='email'
|
||||
placeholder='example@xyz.com' validator={this.state.validator}
|
||||
message='Enter the email address associated with your account.' width={16} />
|
||||
<ValidatedInput password label='Password' name='password'
|
||||
validator={this.state.validator} message='Enter your password.' width={16} />
|
||||
<Form.Group widths='equal' className='login-options'>
|
||||
<Form.Field className='login-password'>
|
||||
<Link to='/forgot-password'>Forgot your password?</Link>
|
||||
</Form.Field>
|
||||
<ValidatedCheckbox label='Remember Me'
|
||||
name='rememberMe' onChange={this.handleChange} validator={this.state.validator}
|
||||
message='Should we keep you logged in on this computer?' className='login-checkbox' />
|
||||
</Form.Group>
|
||||
<ValidatedButton className='submit' name='submit' content='Submit'
|
||||
primary submit validator={this.state.validator} />
|
||||
<Message info>
|
||||
Please contact <a href='mailto:support@jamoki.com'>support@jamoki.com</a> to request login credentials.
|
||||
</Message>
|
||||
</Form>
|
||||
|
||||
<WaitDialog active={this.state.waitDialog} message='Logging In' />
|
||||
|
||||
<MessageDialog error open={!!this.state.messageDialog}
|
||||
title={this.state.messageDialog ? this.state.messageDialog.title : ''}
|
||||
message={this.state.messageDialog ? this.state.messageDialog.message : ''}
|
||||
onDismiss={this.handleMessageDialogDismiss} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
3
website/src/Auth/Login.scss
Normal file
3
website/src/Auth/Login.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
#login .login-options { margin: 1.5em 0 3em 0; }
|
||||
#login .login-password { text-align: left; }
|
||||
#login .login-checkbox { text-align: right; }
|
||||
20
website/src/Auth/Logout.js
Normal file
20
website/src/Auth/Logout.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { api } from '../helpers'
|
||||
|
||||
export class Logout extends React.Component {
|
||||
static propTypes = {
|
||||
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
|
||||
}
|
||||
componentDidMount(event) {
|
||||
api.logout().then(() => {
|
||||
if (this.props.history) {
|
||||
this.props.history.push('/')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return null
|
||||
}
|
||||
}
|
||||
53
website/src/Auth/ProtectedRoute.js
Normal file
53
website/src/Auth/ProtectedRoute.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from 'react'
|
||||
import { Route, Redirect } from 'react-router-dom'
|
||||
import { PropTypes } from 'prop-types'
|
||||
import { api } from '../helpers'
|
||||
|
||||
export class ProtectedRoute extends React.Component {
|
||||
static propTypes = {
|
||||
roles: PropTypes.array,
|
||||
location: PropTypes.shape({
|
||||
pathname: PropTypes.string,
|
||||
search: PropTypes.string
|
||||
})
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.updateComponent = this.updateComponent.bind(this)
|
||||
}
|
||||
|
||||
updateComponent() {
|
||||
this.forceUpdate()
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
api.addListener('login', this.updateComponent)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
api.removeListener('login', this.updateComponent)
|
||||
}
|
||||
|
||||
render(props) {
|
||||
let user = api.loggedInUser
|
||||
|
||||
if (user) {
|
||||
if (user.pending) {
|
||||
// The Api might be in the middle of fetching the user information
|
||||
return <div />
|
||||
}
|
||||
let roles = this.props.roles
|
||||
|
||||
if (!roles || roles.includes(user.role)) {
|
||||
return <Route {...this.props} />
|
||||
} else if (!!user.role && user.role === 'broker') {
|
||||
return <Redirect to='/broker-dashboard' />
|
||||
} else if (!!user.role && (user.role === 'employee' || 'administrator' || 'executive')) {
|
||||
return <Redirect to='/dashboard' />
|
||||
}
|
||||
} else {
|
||||
return <Redirect to={`/login?redirect=${this.props.location.pathname}${this.props.location.search}`} />
|
||||
}
|
||||
}
|
||||
}
|
||||
93
website/src/Auth/ResetPassword.js
Normal file
93
website/src/Auth/ResetPassword.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Container, Header, Form, Message } from 'semantic-ui-react'
|
||||
import './ResetPassword.scss'
|
||||
import { Validator, ValidatedInput, ValidatedButton } from '../Validated'
|
||||
import { MessageDialog, WaitDialog } from '../Dialog'
|
||||
import { api } from '../helpers'
|
||||
|
||||
export class ResetPassword extends React.Component {
|
||||
static propTypes = {
|
||||
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
|
||||
}
|
||||
|
||||
static validations = {
|
||||
newPassword: {
|
||||
alwaysGet: true,
|
||||
isValid: (r, v) => (v.length >= 6)
|
||||
},
|
||||
reenteredNewPassword: {
|
||||
isValid: (r, v) => (v !== '' && v === r.getField('newPassword').value)
|
||||
},
|
||||
submit: {
|
||||
nonValue: true,
|
||||
isDisabled: (r) => (!r.anyModified && !r.allValid)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
validator: new Validator({}, ResetPassword.validations),
|
||||
messageDialog: null,
|
||||
waitDialog: null
|
||||
}
|
||||
this.handleSubmit = this.handleSubmit.bind(this)
|
||||
this.handleMessageDialogDismiss = this.handleMessageDialogDismiss.bind(this)
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
const obj = this.state.validator.getValues()
|
||||
const passwordToken = new URLSearchParams(decodeURIComponent(window.location.search)).get('password-token')
|
||||
|
||||
this.setState({ waitDialog: { message: 'Setting Password...' } })
|
||||
api.resetPassword({ newPassword: obj.newPassword, passwordToken }).then(() => {
|
||||
this.setState({ waitDialog: null })
|
||||
this.props.history.replace('/login')
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
validator: new Validator({}, ResetPassword.validations), // Reset to avoid accidental rapid retries
|
||||
waitDialog: null,
|
||||
messageDialog: {
|
||||
title: 'We had a problem changing your password',
|
||||
message: error.message
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
handleMessageDialogDismiss() {
|
||||
this.setState({ messageDialog: null })
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Container className='password-reset-container'>
|
||||
<Header content='Reset Password' size='large' />
|
||||
<Form size='large' onSubmit={this.handleSubmit}>
|
||||
<ValidatedInput label='New Password' password name='newPassword'
|
||||
message='A new password, cannot be blank or the same as your old password'
|
||||
width={16} validator={this.state.validator} />
|
||||
<ValidatedInput label='Re-entered New Password' password name='reenteredNewPassword'
|
||||
message='The new password again, must match and cannot be blank'
|
||||
width={16} validator={this.state.validator} />
|
||||
<Message info>
|
||||
Passwords can contain special characters and are discouraged from being simple or reused from other sites or applications.
|
||||
<br /><br />
|
||||
Passwords must be at least 6 characters long.
|
||||
</Message>
|
||||
<ValidatedButton className='submit' name='submit' content='Submit'
|
||||
primary submit validator={this.state.validator} />
|
||||
</Form>
|
||||
|
||||
<MessageDialog error open={!!this.state.messageDialog}
|
||||
title={this.state.messageDialog ? this.state.messageDialog.title : ''}
|
||||
message={this.state.messageDialog ? this.state.messageDialog.message : ''}
|
||||
onDismiss={this.handleMessageDialogDismiss} />
|
||||
|
||||
<WaitDialog active={!!this.state.waitDialog}
|
||||
message={this.state.waitDialog ? this.state.waitDialog.message : ''} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
23
website/src/Auth/ResetPassword.scss
Normal file
23
website/src/Auth/ResetPassword.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
.ui.container.password-reset-container {
|
||||
height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ui.container.password-reset-container .header {
|
||||
border-bottom: 1px solid #d4d4d5;
|
||||
width: 40%;
|
||||
padding-bottom: 0.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.ui.container.password-reset-container form {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.ui.container.password-reset-container .message {
|
||||
margin: 2em 0;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
6
website/src/Auth/index.js
Normal file
6
website/src/Auth/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export { Login } from './Login'
|
||||
export { Logout } from './Logout'
|
||||
export { ResetPassword } from './ResetPassword'
|
||||
export { ForgotPassword } from './ForgotPassword'
|
||||
export { ConfirmEmail } from './ConfirmEmail'
|
||||
export { ProtectedRoute } from './ProtectedRoute'
|
||||
Reference in New Issue
Block a user