Fixing last couple of auth dialogs

This commit is contained in:
John Lyon-Smith
2018-03-24 10:53:34 -07:00
parent ce25d56dfe
commit cb708c720f
16 changed files with 312 additions and 274 deletions

View File

@@ -43,6 +43,10 @@ export class AuthRoutes {
app.route('/auth/password/send') app.route('/auth/password/send')
.post(this.sendPasswordResetEmail) .post(this.sendPasswordResetEmail)
// Confirm a password reset token is valid
app.route('/auth/password/confirm')
.post(this.confirmPasswordToken)
// Finish a password reset // Finish a password reset
app.route('/auth/password/reset') app.route('/auth/password/reset')
.post(this.resetPassword) .post(this.resetPassword)
@@ -52,110 +56,104 @@ export class AuthRoutes {
.get(passport.authenticate('bearer', { session: false }), this.whoAmI) .get(passport.authenticate('bearer', { session: false }), this.whoAmI)
} }
login(req, res, next) { async login(req, res, next) {
const email = req.body.email const email = req.body.email
const password = req.body.password const password = req.body.password
if (!email || !password) {
return next(new createError.BadRequest('Must supply user name and password'))
}
let User = this.db.User let User = this.db.User
// Lookup the user try {
User.findOne({ email }).then((user) => { if (!email || !password) {
createError.BadRequest('Must supply user name and password')
}
// Lookup the user
const user = await User.findOne({ email })
if (!user) { if (!user) {
// NOTE: Don't return NotFound as that gives too much information away to hackers // NOTE: Don't return NotFound as that gives too much information away to hackers
return Promise.reject(createError.BadRequest("Email or password incorrect")) throw createError.BadRequest("Email or password incorrect")
} else if (user.emailToken || !user.passwordHash) { } else if (user.emailToken || !user.passwordHash) {
return Promise.reject(createError.Forbidden("Must confirm email and set password")) throw createError.Forbidden("Must confirm email and set password")
} else {
let cr = credential()
return Promise.all([
Promise.resolve(user),
cr.verify(JSON.stringify(user.passwordHash), req.body.password)
])
} }
}).then((arr) => {
const [user, isValid] = arr let cr = credential()
const isValid = cr.verify(JSON.stringify(user.passwordHash), req.body.password)
if (isValid) { if (isValid) {
user.loginToken = loginToken.pack(user._id.toString(), user.email) user.loginToken = loginToken.pack(user._id.toString(), user.email)
} else { } else {
user.loginToken = null // A bad login removes existing token for this user... user.loginToken = null // A bad login removes existing token for this user...
} }
return user.save()
}).then((savedUser) => { const savedUser = await user.save()
if (savedUser.loginToken) { if (savedUser.loginToken) {
res.set('Authorization', `Bearer ${savedUser.loginToken}`) res.set('Authorization', `Bearer ${savedUser.loginToken}`)
res.json(savedUser.toClient()) res.json(savedUser.toClient())
} else { } else {
return Promise.reject(createError.BadRequest('email or password incorrect')) throw createError.BadRequest('email or password incorrect')
} }
}).catch((err) => { } catch(err) {
if (err instanceof createError.HttpError) { if (err instanceof createError.HttpError) {
next(err) next(err)
} else { } else {
next(createError.InternalServerError(`${err ? err.message : ''}`)) next(createError.InternalServerError(err.message))
} }
}) }
} }
logout(req, res, next) { async logout(req, res, next) {
let User = this.db.User let User = this.db.User
User.findById({ _id: req.user._id }).then((user) => { try {
if (!user) { const user = await User.findById({ _id: req.user._id })
return next(createError.BadRequest())
if (user) {
user.loginToken = null
await user.save()
} }
user.loginToken = null res.json({})
user.save().then((savedUser) => { } catch(err) {
res.json({}) next(createError.InternalServerError(err.message))
}) }
}).catch((err) => {
next(createError.InternalServerError(`Unable to login. ${err ? err.message : ''}`))
})
} }
whoAmI(req, res, next) { whoAmI(req, res, next) {
res.json(req.user.toClient()) res.json(req.user.toClient())
} }
sendChangeEmailEmail(req, res, next) { async sendChangeEmailEmail(req, res, next) {
let existingEmail = req.body.existingEmail let existingEmail = req.body.existingEmail
const newEmail = req.body.newEmail const newEmail = req.body.newEmail
let User = this.db.User let User = this.db.User
const isAdmin = !!req.user.administrator const isAdmin = !!req.user.administrator
if (existingEmail) { try {
if (!isAdmin) { if (existingEmail) {
return next(createError.Forbidden('Only admins can resend change email to any user')) if (!isAdmin) {
throw createError.Forbidden('Only admins can resend change email to any user')
}
} else {
existingEmail = req.user.email
} }
} else {
existingEmail = req.user.email
}
let promiseArray = [User.findOne({ email: existingEmail })] const user = await User.findOne({ email: existingEmail })
let conflictingUser = null
if (newEmail) { if (newEmail) {
promiseArray.push(User.findOne({ email: newEmail })) conflictingUser = await User.findOne({ email: newEmail })
} }
Promise.all(promiseArray).then((arr) => {
const [user, conflictingUser] = arr
if (!user) { if (!user) {
return Promise.reject(createError.NotFound(`User with email '${existingEmail}' was not found`)) throw createError.NotFound(`User with email '${existingEmail}' was not found`)
} else if (conflictingUser) { } else if (conflictingUser) {
return Promise.reject(createError.BadRequest(`A user with '${newEmail}' already exists`)) throw createError.BadRequest(`A user with '${newEmail}' already exists`)
} else if (!isAdmin && user.emailToken && (new Date() - user.emailToken.created) < this.sendEmailDelayInSeconds) { } else if (!isAdmin && user.emailToken && (new Date() - user.emailToken.created) < this.sendEmailDelayInSeconds) {
return Promise.reject(createError.BadRequest('Cannot request email confirmation again so soon')) throw createError.BadRequest('Cannot request email confirmation again so soon')
} }
return Promise.all([Promise.resolve(user), util.promisify(crypto.randomBytes)(32)]) const buf = await util.promisify(crypto.randomBytes)(32)
}).then((arr) => {
let [ user, buf ] = arr
user.emailToken = { user.emailToken = {
value: urlSafeBase64.encode(buf), value: urlSafeBase64.encode(buf),
@@ -166,8 +164,7 @@ export class AuthRoutes {
user.newEmail = newEmail user.newEmail = newEmail
} }
return user.save() const savedUser = await user.save()
}).then((savedUser) => {
const userFullName = `${savedUser.firstName} ${savedUser.lastName}` const userFullName = `${savedUser.firstName} ${savedUser.lastName}`
const siteUrl = url.parse(req.headers.referer) const siteUrl = url.parse(req.headers.referer)
let msgs = [] let msgs = []
@@ -193,36 +190,41 @@ export class AuthRoutes {
supportEmail: this.supportEmail supportEmail: this.supportEmail
} }
}) })
return this.sendEmail ? this.mq.request('dar-email', 'sendEmail', msgs) : Promise.resolve()
}).then(() => { if (this.sendEmail) {
await this.mq.request('dar-email', 'sendEmail', msgs)
}
res.json({}) res.json({})
}).catch((err) => { } catch(err) {
if (err instanceof createError.HttpError) { if (err instanceof createError.HttpError) {
next(err) next(err)
} else { } else {
next(createError.InternalServerError(`Unable to send change email email. ${err.message}`)) next(createError.InternalServerError(err.message))
} }
}) }
} }
confirmEmail(req, res, next) { async confirmEmail(req, res, next) {
const token = req.body.emailToken const token = req.body.emailToken
let User = this.db.User let User = this.db.User
if (!token) { try {
return next(createError.BadRequest('Invalid request parameters')) if (!token) {
} throw createError.BadRequest('Invalid request parameters')
}
const user = await User.findOne({ 'emailToken.value': token })
User.findOne({ 'emailToken.value': token }).then((user) => {
if (!user) { if (!user) {
return Promise.reject(createError.BadRequest(`The token was not found`)) throw createError.BadRequest(`The token was not found`)
} }
// Token must not be too old // Token must not be too old
const ageInHours = (new Date() - user.emailToken.created) / 3600000 const ageInHours = (new Date() - user.emailToken.created) / 3600000
if (ageInHours > this.maxEmailTokenAgeInHours) { if (ageInHours > this.maxEmailTokenAgeInHours) {
return Promise.reject(createError.BadRequest(`Token has expired`)) throw createError.BadRequest(`Token has expired`)
} }
// Remove the email token & any login token as it will become invalid // Remove the email token & any login token as it will become invalid
@@ -235,86 +237,109 @@ export class AuthRoutes {
user.newEmail = undefined user.newEmail = undefined
} }
let promiseArray = [ Promise.resolve(user) ] let buf = null
// If user has no password, create reset token for them
if (!user.passwordHash) { if (!user.passwordHash) {
// User has no password, create reset token for them buf = await util.promisify(crypto.randomBytes)(32)
promiseArray.push(util.promisify(crypto.randomBytes)(32))
}
return Promise.all(promiseArray)
}).then((arr) => {
let [ user, buf ] = arr
if (buf) {
user.passwordToken = { user.passwordToken = {
value: urlSafeBase64.encode(buf), value: urlSafeBase64.encode(buf),
created: new Date() created: new Date()
} }
} }
return user.save() const savedUser = await user.save()
}).then((savedUser) => {
let obj = {} let obj = {}
// Only because the user has sent us a valid email reset token can we respond with an password reset token // Only because the user has sent us a valid email reset token
// can we respond with an password reset token.
if (savedUser.passwordToken) { if (savedUser.passwordToken) {
obj.passwordToken = savedUser.passwordToken.value obj.passwordToken = savedUser.passwordToken.value
} }
res.json(obj) res.json(obj)
}).catch((err) => { } catch(err) {
if (err instanceof createError.HttpError) { if (err instanceof createError.HttpError) {
next(err) next(err)
} else { } else {
next(createError.InternalServerError(`Unable to confirm set email token. ${err.message}`)) next(createError.InternalServerError(err.message))
} }
}) }
} }
resetPassword(req, res, next) { async confirmPasswordToken(req, res, next) {
const token = req.body.passwordToken const token = req.body.passwordToken
const newPassword = req.body.newPassword
let User = this.db.User let User = this.db.User
let cr = credential()
if (!token || !newPassword) { try {
return next(createError.BadRequest('Invalid request parameters')) if (!token) {
} throw createError.BadRequest('Invalid request parameters')
}
const user = await User.findOne({ 'passwordToken.value': token })
User.findOne({ 'passwordToken.value': token }).then((user) => {
if (!user) { if (!user) {
return Promise.reject(createError.BadRequest(`The token was not found`)) throw createError.BadRequest(`The token was not found`)
} }
// Token must not be too old // Token must not be too old
const ageInHours = (new Date() - user.passwordToken.created) / (3600 * 1000) const ageInHours = (new Date() - user.passwordToken.created) / (3600 * 1000)
if (ageInHours > this.maxPasswordTokenAgeInHours) { if (ageInHours > this.maxPasswordTokenAgeInHours) {
return Promise.reject(createError.BadRequest(`Token has expired`)) throw createError.BadRequest(`Token has expired`)
}
res.json({})
} catch (err) {
if (err instanceof createError.HttpError) {
next(err)
} else {
next(createError.InternalServerError(err.message))
}
}
}
async resetPassword(req, res, next) {
const token = req.body.passwordToken
const newPassword = req.body.newPassword
let User = this.db.User
let cr = credential()
try {
if (!token || !newPassword) {
throw createError.BadRequest('Invalid request parameters')
}
const user = await User.findOne({ 'passwordToken.value': token })
if (!user) {
throw createError.BadRequest(`The token was not found`)
}
// Token must not be too old
const ageInHours = (new Date() - user.passwordToken.created) / (3600 * 1000)
if (ageInHours > this.maxPasswordTokenAgeInHours) {
throw createError.BadRequest(`Token has expired`)
} }
// Remove the password token & any login token // Remove the password token & any login token
user.passwordToken = undefined user.passwordToken = undefined
user.loginToken = undefined user.loginToken = undefined
return Promise.all([ const json = await cr.hash(newPassword)
Promise.resolve(user),
cr.hash(newPassword)
])
}).then((arr) => {
const [user, json] = arr
user.passwordHash = JSON.parse(json) user.passwordHash = JSON.parse(json)
return user.save() await user.save()
}).then((savedUser) => {
res.json({}) res.json({})
}).catch((err) => { } catch(err) {
if (err instanceof createError.HttpError) { if (err instanceof createError.HttpError) {
next(err) next(err)
} else { } else {
next(createError.InternalServerError(`Unable to confirm password reset token. ${err.message}`)) next(createError.InternalServerError(err.message))
} }
}) }
} }
async changePassword(req, res, next) { async changePassword(req, res, next) {

View File

@@ -230,6 +230,9 @@ class API extends EventEmitter {
sendResetPassword(email) { sendResetPassword(email) {
return this.post('/auth/password/send', { email }) return this.post('/auth/password/send', { email })
} }
confirmResetPassword(passwordToken) {
return this.post('/auth/password/confirm', { passwordToken })
}
resetPassword(passwords) { resetPassword(passwords) {
return this.post('/auth/password/reset', passwords) return this.post('/auth/password/reset', passwords)
} }

View File

@@ -97,11 +97,11 @@ export class App extends Component {
<Route exact path='/confirm-email' component={ConfirmEmail} /> <Route exact path='/confirm-email' component={ConfirmEmail} />
<Route exact path='/reset-password' component={ResetPassword} /> <Route exact path='/reset-password' component={ResetPassword} />
<Route exact path='/forgot-password' component={ForgotPassword} /> <Route exact path='/forgot-password' component={ForgotPassword} />
<ProtectedRoute exact path='/profile' component={Profile} /> <ProtectedRoute exact path='/profile' render={props => (<Profile {...props} changeTitle={this.handleChangeTitle} />)} />
<ProtectedRoute exact admin path='/users' render={props => (<Users {...props} onChangeTitle={this.handleChangeTitle} />)} /> <ProtectedRoute exact admin path='/users' render={props => (<Users {...props} changeTitle={this.handleChangeTitle} />)} />
<ProtectedRoute exact admin path='/teams' component={Users} /> <ProtectedRoute exact admin path='/teams' component={Users} />
<ProtectedRoute exact admin path='/system' component={Users} /> <ProtectedRoute exact admin path='/system' component={Users} />
<ProtectedRoute exact admin path='/home' render={props => (<Home {...props} onChangeTitle={this.handleChangeTitle} />)} /> <ProtectedRoute exact admin path='/home' render={props => (<Home {...props} changeTitle={this.handleChangeTitle} />)} />
<DefaultRoute /> <DefaultRoute />
</Switch> </Switch>
<Column.Item> <Column.Item>

View File

@@ -2,13 +2,13 @@ import React from 'react'
import { api } from 'src/API' import { api } from 'src/API'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { MessageModal, WaitModal } from '../Modal' import { MessageModal, WaitModal } from '../Modal'
import { Logout } from '.'
import autobind from 'autobind-decorator' import autobind from 'autobind-decorator'
export class ConfirmEmail extends React.Component { export class ConfirmEmail extends React.Component {
static propTypes = { static propTypes = {
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]) history: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
} }
constructor() { constructor() {
super() super()
this.state = { this.state = {
@@ -22,7 +22,9 @@ export class ConfirmEmail extends React.Component {
this.setState({ waitModal: { message: 'Validating Email...' } }) this.setState({ waitModal: { message: 'Validating Email...' } })
if (emailToken) { if (emailToken) {
api.confirmEmail(emailToken).then((response) => { api.logout().then(() => {
return api.confirmEmail(emailToken)
}).then((response) => {
this.setState({ waitModal: null }) this.setState({ waitModal: null })
if (response && response.passwordToken) { if (response && response.passwordToken) {
// API will send a password reset token if this is the first time loggin on // API will send a password reset token if this is the first time loggin on
@@ -54,10 +56,6 @@ export class ConfirmEmail extends React.Component {
render() { render() {
const { messageModal, waitModal } = this.state const { messageModal, waitModal } = this.state
if (api.loggedInUser) {
return <Logout redirect={`${window.location.pathname}${window.location.search}`} />
}
return ( return (
<div> <div>
<WaitModal <WaitModal

View File

@@ -5,7 +5,6 @@ import { Image, Text, Column, Row, BoundInput, BoundButton, Box } from 'ui'
import { MessageModal, WaitModal } from '../Modal' import { MessageModal, WaitModal } from '../Modal'
import { api } from 'src/API' import { api } from 'src/API'
import { FormBinder } from 'react-form-binder' import { FormBinder } from 'react-form-binder'
import { Logout } from '.'
import headerLogo from 'images/deighton.png' import headerLogo from 'images/deighton.png'
import { sizeInfo, colorInfo } from 'ui/style' import { sizeInfo, colorInfo } from 'ui/style'
import autobind from 'autobind-decorator' import autobind from 'autobind-decorator'
@@ -35,6 +34,10 @@ export class ForgotPassword extends Component {
} }
} }
componentDidMount() {
api.logout()
}
@autobind @autobind
handleSubmit(e) { handleSubmit(e) {
e.preventDefault() e.preventDefault()
@@ -66,10 +69,6 @@ export class ForgotPassword extends Component {
render() { render() {
const { binder, waitModal, messageModal } = this.state const { binder, waitModal, messageModal } = this.state
if (api.loggedInUser) {
return <Logout redirect={`${window.location.pathname}${window.location.search}`} />
}
return ( return (
<Fragment> <Fragment>
<Column.Item grow /> <Column.Item grow />

View File

@@ -7,6 +7,7 @@ import autobind from 'autobind-decorator'
export class ProtectedRoute extends React.Component { export class ProtectedRoute extends React.Component {
static propTypes = { static propTypes = {
location: PropTypes.shape({ pathname: PropTypes.string, search: PropTypes.string }), location: PropTypes.shape({ pathname: PropTypes.string, search: PropTypes.string }),
admin: PropTypes.bool,
} }
@autobind @autobind
@@ -30,11 +31,11 @@ export class ProtectedRoute extends React.Component {
// The API might be in the middle of fetching the user information // The API might be in the middle of fetching the user information
// Return something and wait for login evint to fire to re-render // Return something and wait for login evint to fire to re-render
return <div /> return <div />
} else if (user.administrator) { } else if (!this.props.admin || (this.props.admin && user.administrator)) {
return <Route {...this.props} /> return <Route {...this.props} />
} }
} else {
return <Redirect to={`/login?redirect=${this.props.location.pathname}${this.props.location.search}`} />
} }
return <Redirect to={`/login?redirect=${this.props.location.pathname}${this.props.location.search}`} />
} }
} }

View File

@@ -1,7 +1,6 @@
import React, { Component, Fragment } from 'react' import React, { Component, Fragment } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Box, Text, Image, Column, Row, BoundInput, BoundButton } from 'ui' import { Box, Text, Image, Column, Row, BoundInput, BoundButton } from 'ui'
import { Logout } from '.'
import { MessageModal, WaitModal } from '../Modal' import { MessageModal, WaitModal } from '../Modal'
import { api } from 'src/API' import { api } from 'src/API'
import { FormBinder } from 'react-form-binder' import { FormBinder } from 'react-form-binder'
@@ -47,13 +46,10 @@ export class ResetPassword extends Component {
this.setState({ waitModal: { message: 'Confirming password reset...' } }) this.setState({ waitModal: { message: 'Confirming password reset...' } })
if (passwordToken) { if (passwordToken) {
api.confirmResetPassword(passwordToken).then((response) => { api.logout().then(() => {
this.setState({ waitModal: null }) return api.confirmResetPassword(passwordToken)
if (response && response.valid) { }).then((response) => {
this.setState({ tokenConfirmed: true }) this.setState({ waitModal: null, tokenConfirmed: true })
} else {
this.props.history.replace('/')
}
}).catch((err) => { }).catch((err) => {
this.setState({ this.setState({
waitModal: null, waitModal: null,
@@ -107,10 +103,6 @@ export class ResetPassword extends Component {
render() { render() {
const { messageModal, waitModal, binder } = this.state const { messageModal, waitModal, binder } = this.state
if (api.loggedInUser) {
return <Logout redirect={`${window.location.pathname}${window.location.search}`} />
}
return ( return (
<Fragment> <Fragment>
<Column.Item grow /> <Column.Item grow />

View File

@@ -6,15 +6,15 @@ import { sizeInfo } from 'ui/style'
export class Home extends Component { export class Home extends Component {
static propTypes = { static propTypes = {
history: PropTypes.object, history: PropTypes.object,
onChangeTitle: PropTypes.func.isRequired, changeTitle: PropTypes.func.isRequired,
} }
componentDidMount() { componentDidMount() {
this.props.onChangeTitle('Home') this.props.changeTitle('Home')
} }
componentWillUnmount() { componentWillUnmount() {
this.props.onChangeTitle('') this.props.changeTitle('')
} }
render() { render() {

View File

@@ -14,8 +14,11 @@ export class ChangeEmailModal extends React.Component {
} }
static bindings = { static bindings = {
oldEmail: {
noValue: true,
},
newEmail: { newEmail: {
isValid: (r, v) => (v !== '' && regExpPattern.email.test(v)) isValid: (r, v) => (v !== '' && regExpPattern.email.test(v) && v !== r.getFieldValue('oldEmail'))
}, },
submit: { submit: {
isDisabled: (r) => (!r.allValid), isDisabled: (r) => (!r.allValid),
@@ -26,7 +29,9 @@ export class ChangeEmailModal extends React.Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
binder: new FormBinder({}, ChangeEmailModal.bindings) binder: new FormBinder({
oldEmail: props.oldEmail,
}, ChangeEmailModal.bindings)
} }
} }
@@ -43,12 +48,7 @@ export class ChangeEmailModal extends React.Component {
@autobind @autobind
handleSubmit(e) { handleSubmit(e) {
e.preventDefault() e.preventDefault()
let newEmail = null let newEmail = this.state.binder.getFieldValue('newEmail')
if (this.state.binder.anyModified && this.state.binder.allValid) {
newEmail = this.state.binder.getFieldValue('newEmail')
}
this.close(newEmail) this.close(newEmail)
} }
@@ -58,10 +58,11 @@ export class ChangeEmailModal extends React.Component {
} }
render() { render() {
const { binder } = this.state
return ( return (
<Modal dimmer='inverted' open={this.props.open} onClose={this.handleClose} <Modal open={this.props.open} width={sizeInfo.modalWidth}>
closeOnDimmerClick={false}> <form id='changeEmailForm' onSubmit={this.handleSubmit}>
<form id='emailForm' onSubmit={this.handleSubmit}>
<Column> <Column>
<Column.Item height={sizeInfo.formColumnSpacing} /> <Column.Item height={sizeInfo.formColumnSpacing} />
<Column.Item> <Column.Item>
@@ -69,6 +70,11 @@ export class ChangeEmailModal extends React.Component {
<Row.Item width={sizeInfo.formRowSpacing} /> <Row.Item width={sizeInfo.formRowSpacing} />
<Row.Item grow> <Row.Item grow>
<Column> <Column>
<Column.Item height={sizeInfo.formColumnSpacing} />
<Column.Item color='black' icon='edit'>
<Text size='large'>Change Password</Text>
</Column.Item>
<Column.Item height={sizeInfo.formColumnSpacing} />
<Column.Item> <Column.Item>
<Text>{this.props.oldEmail}</Text> <Text>{this.props.oldEmail}</Text>
</Column.Item> </Column.Item>
@@ -76,7 +82,7 @@ export class ChangeEmailModal extends React.Component {
<Column.Item> <Column.Item>
<BoundInput label='New Email' name='newEmail' <BoundInput label='New Email' name='newEmail'
message='Your new email address, e.g. xyz@abc.com, cannot be blank' message='Your new email address, e.g. xyz@abc.com, cannot be blank'
binder={this.state.binder} /> binder={binder} />
</Column.Item> </Column.Item>
<Column.Item height={sizeInfo.formColumnSpacing} /> <Column.Item height={sizeInfo.formColumnSpacing} />
<Column.Item> <Column.Item>
@@ -87,7 +93,7 @@ export class ChangeEmailModal extends React.Component {
</Row.Item> </Row.Item>
<Row.Item width={sizeInfo.formRowSpacing} /> <Row.Item width={sizeInfo.formRowSpacing} />
<Row.Item> <Row.Item>
<BoundButton submit='emailForm' name='submit' binder={this.state.binder} text='OK' /> <BoundButton submit='changeEmailForm' name='submit' binder={binder} text='OK' />
</Row.Item> </Row.Item>
</Row> </Row>
</Column.Item> </Column.Item>

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import autobind from 'autobind-decorator' import autobind from 'autobind-decorator'
import { Modal, Button, Icon, Column, Row, Text, BoundInput, BoundButton } from 'ui' import { Modal, Button, Column, Row, Text, BoundInput, BoundButton } from 'ui'
import { FormBinder } from 'react-form-binder' import { FormBinder } from 'react-form-binder'
import { sizeInfo } from 'ui/style' import { sizeInfo } from 'ui/style'
@@ -21,8 +21,7 @@ export class ChangePasswordModal extends React.Component {
isValid: (r, v) => (v !== '' && v !== r.fields.oldPassword.value) isValid: (r, v) => (v !== '' && v !== r.fields.oldPassword.value)
}, },
reenteredNewPassword: { reenteredNewPassword: {
alwaysGet: true, isValid: (r, v) => (v !== '' && v === r.getFieldValue('newPassword')),
isValid: (r, v) => (v !== '' && v === r.fields.newPassword.value)
}, },
submit: { submit: {
isDisabled: (r) => (!r.allValid), isDisabled: (r) => (!r.allValid),
@@ -51,10 +50,11 @@ export class ChangePasswordModal extends React.Component {
handleSubmit(e) { handleSubmit(e) {
e.preventDefault() e.preventDefault()
let passwords = null let passwords = null
const { binder } = this.state
if (this.state.binder.allValid) { if (binder.allValid) {
const oldPassword = this.state.binder.getField('oldPassword').value const oldPassword = binder.getFieldValue('oldPassword')
const newPassword = this.state.binder.getField('newPassword').value const newPassword = binder.getFieldValue('newPassword')
passwords = { oldPassword, newPassword } passwords = { oldPassword, newPassword }
} }
this.close(passwords) this.close(passwords)
@@ -66,41 +66,57 @@ export class ChangePasswordModal extends React.Component {
} }
render() { render() {
const { binder } = this.state
return ( return (
<Modal dimmer='inverted' open={this.props.open} width={sizeInfo.modalWidth}> <Modal open={this.props.open} width={sizeInfo.modalWidth}>
<form id='passwordForm' onSubmit={this.handleSubmit}> <form id='changePasswordForm' onSubmit={this.handleSubmit}>
<Column.Item color='black' icon='edit'> <Row>
<Text size='large'>Change Password</Text> <Row.Item width={sizeInfo.formRowSpacing} />
</Column.Item> <Row.Item grow>
<Column.Item> <Column>
<Column> <Column.Item height={sizeInfo.formColumnSpacing} />
<Column.Item> <Column.Item color='black' icon='edit'>
<BoundInput label='Current Password' password name='oldPassword' <Text size='large'>Change Password</Text>
message='Your existing password, cannot be blank' </Column.Item>
binder={this.state.binder} /> <Column.Item height={sizeInfo.formColumnSpacing} />
</Column.Item> <Column.Item>
<Column.Item> <Column>
<BoundInput label='New Password' password name='newPassword' <Column.Item>
message='A new password, cannot be blank or the same as your old password' <BoundInput label='Current Password' password name='oldPassword'
binder={this.state.binder} /> message='Your existing password, cannot be blank'
</Column.Item> binder={binder} />
<Column.Item> </Column.Item>
<BoundInput label='Re-entered New Password' password name='reenteredNewPassword' <Column.Item>
message='The new password again, must match and cannot be blank' <BoundInput label='New Password' password name='newPassword'
binder={this.state.binder} /> message='A new password, cannot be blank or the same as your old password'
</Column.Item> binder={binder} />
</Column> </Column.Item>
</Column.Item> <Column.Item>
<Column.Item> <BoundInput label='Re-entered New Password' password name='reenteredNewPassword'
<Row> message='The new password again, must match and cannot be blank'
<BoundButton primary submit form='passwordForm' name='submit' binder={this.state.binder}> binder={binder} />
<Icon name='checkmark' /> OK </Column.Item>
</BoundButton> </Column>
<Button color='red' onClick={this.handleClick}> </Column.Item>
<Icon name='close' /> Cancel <Column.Item height={sizeInfo.formColumnSpacing} />
</Button> <Column.Item>
</Row> <Row>
</Column.Item> <Row.Item grow />
<Row.Item>
<Button onClick={this.handleClick} text='Cancel' />
</Row.Item>
<Row.Item width={sizeInfo.formRowSpacing} />
<Row.Item>
<BoundButton text='Submit' submit='changePasswordForm' name='submit' binder={binder} />
</Row.Item>
</Row>
</Column.Item>
<Column.Item height={sizeInfo.formColumnSpacing} />
</Column>
</Row.Item>
<Row.Item width={sizeInfo.formRowSpacing} />
</Row>
</form> </form>
</Modal> </Modal>
) )

View File

@@ -4,9 +4,14 @@ import { api } from 'src/API'
import { WaitModal, MessageModal, ChangePasswordModal, ChangeEmailModal } from '../Modal' import { WaitModal, MessageModal, ChangePasswordModal, ChangeEmailModal } from '../Modal'
import { Column, Row } from 'ui' import { Column, Row } from 'ui'
import { sizeInfo } from 'ui/style' import { sizeInfo } from 'ui/style'
import PropTypes from 'prop-types'
import autobind from 'autobind-decorator' import autobind from 'autobind-decorator'
export class Profile extends Component { export class Profile extends Component {
static propTypes = {
changeTitle: PropTypes.func.isRequired,
}
constructor(props) { constructor(props) {
super(props) super(props)
@@ -23,11 +28,11 @@ export class Profile extends Component {
} }
componentDidMount() { componentDidMount() {
api.addListener('newProfileImage', this.handleNewProfileImage) this.props.changeTitle('Profile')
} }
componentWillUnmount() { componentWillUnmount() {
api.removeListener('newProfileImage', this.handleNewProfileImage) this.props.changeTitle('')
} }
@autobind @autobind
@@ -41,7 +46,11 @@ export class Profile extends Component {
}).catch((error) => { }).catch((error) => {
this.setState({ this.setState({
waitModal: null, waitModal: null,
messageModal: { title: 'Update Error...', message: `Unable to save the profile changes. ${error.message}` } messageModal: {
icon: 'hand',
message: 'Unable to save the profile changes.',
detail: error.message,
},
}) })
}) })
} }
@@ -56,21 +65,6 @@ export class Profile extends Component {
this.setState({ changePasswordModal: true }) this.setState({ changePasswordModal: true })
} }
@autobind
handleProgress(uploadData) {
if (this.state.progressModal) {
this.setState({ uploadPercent: Math.round(uploadData.uploadedChunks / uploadData.numberOfChunks * 100) })
return true
} else {
return false
}
}
@autobind
handleUploadCancel(result) {
this.setState({ progressModal: null })
}
@autobind @autobind
handleChangePasswordDismiss(passwords) { handleChangePasswordDismiss(passwords) {
this.setState({ changePasswordModal: false }) this.setState({ changePasswordModal: false })
@@ -85,8 +79,9 @@ export class Profile extends Component {
this.setState({ this.setState({
waitModal: false, waitModal: false,
messageModal: { messageModal: {
title: 'Changing Password Error', icon: 'hand',
message: `Unable to change password. ${error.message}.` message: 'Unable to change password',
detail: error.message,
} }
}) })
}) })
@@ -94,8 +89,8 @@ export class Profile extends Component {
} }
@autobind @autobind
handleChangeEmail() { handleChangeEmail(oldEmail) {
this.setState({ changeEmailModal: {} }) this.setState({ changeEmailModal: { oldEmail } })
} }
@autobind @autobind
@@ -111,32 +106,32 @@ export class Profile extends Component {
this.setState({ this.setState({
waitModal: null, waitModal: null,
messageModal: { messageModal: {
error: false, icon: 'thumb',
title: 'Email Change Requested...',
message: `An email has been sent to '${newEmail}' with a link that you need to click on to finish changing your email.` message: `An email has been sent to '${newEmail}' with a link that you need to click on to finish changing your email.`
} }
}) })
}).catch((error) => { }).catch((error) => {
this.setState({ this.setState({
error: true,
waitModal: null, waitModal: null,
messageModal: { messageModal: {
error: true, icon: 'hand',
title: 'Email Change Error...', message: 'Unable to request email change.',
message: `Unable to request email change. ${error ? error.message : ''}` detail: error.message
} }
}) })
}) })
} }
render() { render() {
const { messageModal, waitModal, changeEmailModal, changePasswordModal } = this.state
return ( return (
<Fragment> <Fragment>
<Column.Item grow /> <Column.Item grow />
<Column.Item> <Column.Item>
<Row> <Row>
<Row.Item grow /> <Row.Item grow />
<Row.Item width={sizeInfo.modalWidth}> <Row.Item width={sizeInfo.profileWidth}>
<ProfileForm <ProfileForm
user={this.state.user} user={this.state.user}
onSaved={this.handleSaved} onSaved={this.handleSaved}
@@ -149,16 +144,25 @@ export class Profile extends Component {
</Row> </Row>
</Column.Item> </Column.Item>
<Column.Item> <Column.Item>
<MessageModal error open={!!this.state.messageModal} <MessageModal
title={this.state.messageModal ? this.state.messageModal.title : ''} open={!!messageModal}
message={this.state.messageModal ? this.state.messageModal.message : ''} icon={messageModal ? messageModal.icon : ''}
title={messageModal ? messageModal.title : ''}
message={messageModal ? messageModal.message : ''}
onDismiss={this.handleMessageModalDismiss} /> onDismiss={this.handleMessageModalDismiss} />
<ChangeEmailModal open={!!this.state.changeEmailModal} onDismiss={this.handleChangeEmailDismiss} /> <ChangeEmailModal
open={!!changeEmailModal}
oldEmail={changeEmailModal ? changeEmailModal.oldEmail : ''}
onDismiss={this.handleChangeEmailDismiss} />
<WaitModal active={!!this.state.waitModal} message={this.state.waitModal ? this.state.waitModal.message : ''} /> <WaitModal
active={!!waitModal}
message={waitModal ? waitModal.message : ''} />
<ChangePasswordModal open={!!this.state.changePasswordModal} onDismiss={this.handleChangePasswordDismiss} /> <ChangePasswordModal
open={!!changePasswordModal}
onDismiss={this.handleChangePasswordDismiss} />
</Column.Item> </Column.Item>
<Column.Item grow /> <Column.Item grow />
</Fragment> </Fragment>

View File

@@ -2,7 +2,6 @@ import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Column, Row, Box, Button, BoundInput, BoundButton } from 'ui' import { Column, Row, Box, Button, BoundInput, BoundButton } from 'ui'
import { sizeInfo, colorInfo } from 'ui/style' import { sizeInfo, colorInfo } from 'ui/style'
import { regExpPattern } from 'regexp-pattern'
import { FormBinder } from 'react-form-binder' import { FormBinder } from 'react-form-binder'
import autobind from 'autobind-decorator' import autobind from 'autobind-decorator'
@@ -26,36 +25,6 @@ export class ProfileForm extends React.Component {
lastName: { lastName: {
isValid: (r, v) => (v !== '') isValid: (r, v) => (v !== '')
}, },
zip: {
isValid: (r, v) => (v === '' || regExpPattern.zip.test(v))
},
state: {
isValid: (r, v) => (v === '' || regExpPattern.state.test(v))
},
city: {
isValid: true
},
address1: {
isValid: true
},
address2: {
isValid: true
},
homePhone: {
isValid: (r, v) => (v === '' || regExpPattern.phone.test(v))
},
cellPhone: {
isValid: (r, v) => (v === '' || regExpPattern.phone.test(v))
},
dateOfBirth: {
isValid: true
},
dateOfHire: {
isValid: true
},
ssn: {
isValid: (r, v) => (v === '' || regExpPattern.ssn.test(v))
},
save: { save: {
noValue: true, noValue: true,
isDisabled: (r) => (!r.anyModified || !r.allValid) isDisabled: (r) => (!r.anyModified || !r.allValid)
@@ -90,38 +59,59 @@ export class ProfileForm extends React.Component {
} }
} }
@autobind
handleChangeEmail() {
this.props.onChangeEmail(this.state.binder.getFieldValue('email'))
}
render() { render() {
const { binder } = this.state const { binder } = this.state
return ( return (
<form onSubmit={this.handleSubmit} id='profileForm'> <form onSubmit={this.handleSubmit} id='profileForm'>
<Box border={{ width: sizeInfo.headerBorderWidth, color: colorInfo.headerBorder }} radius={sizeInfo.formBoxRadius}> <Box border={{ width: sizeInfo.headerBorderWidth, color: colorInfo.headerBorder }}
radius={sizeInfo.formBoxRadius}>
<Row> <Row>
<Row.Item width={sizeInfo.formRowSpacing} /> <Row.Item width={sizeInfo.formRowSpacing} />
<Row.Item> <Row.Item grow>
<Column stackable> <Column>
<Column.Item height={sizeInfo.formColumnSpacing} />
<Column.Item> <Column.Item>
<BoundInput label='First Name' name='firstName' <BoundInput label='First Name' name='firstName'
binder={binder} /> message='First name is required' binder={binder} />
</Column.Item> </Column.Item>
<Column.Item> <Column.Item>
<BoundInput label='Last Name' name='lastName' <BoundInput label='Last Name' name='lastName'
binder={binder} /> binder={binder} />
</Column.Item> </Column.Item>
<Column.Item> <Column.Item>
<BoundInput label='Email' name='email' message='Required. Must be a valid email address.' <BoundInput label='Email' name='email'
message='Required. Must be a valid email address.'
binder={binder} /> binder={binder} />
</Column.Item> </Column.Item>
<Column.Item> <Column.Item height={sizeInfo.formColumnSpacing} />
<Button text={'Change Email'} label='&nbsp;' <Column.Item height={sizeInfo.buttonHeight}>
onClick={this.props.onChangeEmail} /> <Row>
<Button text={'Change Password'} label='&nbsp;' <Row.Item>
onClick={this.props.onChangePassword} /> <Button text={'Change Email'} label='&nbsp;'
<BoundButton submit size='medium' text='Save' label='&nbsp;' name='save' width={sizeInfo.buttonWideWidth} onClick={this.handleChangeEmail} />
binder={binder} /> </Row.Item>
<Row.Item width={sizeInfo.formRowSpacing} />
<Row.Item>
<Button text={'Change Password'} label='&nbsp;'
width={sizeInfo.buttonWideWidth} onClick={this.props.onChangePassword} />
</Row.Item>
<Row.Item grow />
<Row.Item>
<BoundButton submit='profileForm' size='medium' text='Save' label='&nbsp;' name='save'
binder={binder} />
</Row.Item>
</Row>
</Column.Item> </Column.Item>
<Column.Item height={sizeInfo.formColumnSpacing} />
</Column> </Column>
</Row.Item> </Row.Item>
<Row.Item width={sizeInfo.formRowSpacing} />
</Row> </Row>
</Box> </Box>
</form> </form>

View File

@@ -149,12 +149,12 @@ export class UserForm extends React.Component {
<Row> <Row>
<Row.Item> <Row.Item>
<BoundButton text='Change Email' name='changeEmail' binder={binder} <BoundButton text='Change Email' name='changeEmail' binder={binder}
width={sizeInfo.formButtonLarge} onClick={this.handleChangeEmail} /> width={sizeInfo.buttonWideWidth} onClick={this.handleChangeEmail} />
</Row.Item> </Row.Item>
<Row.Item grow /> <Row.Item grow />
<Row.Item> <Row.Item>
<BoundButton text='Resend Confirmation Email' name='resendEmail' binder={binder} <BoundButton text='Resend Confirmation Email' name='resendEmail' binder={binder}
width={sizeInfo.formButtonLarge} onClick={this.handleResendEmail} /> width={sizeInfo.buttonWideWidth} onClick={this.handleResendEmail} />
</Row.Item> </Row.Item>
</Row> </Row>
</Column.Item> </Column.Item>

View File

@@ -11,7 +11,7 @@ import { sizeInfo, colorInfo } from 'ui/style'
export class Users extends Component { export class Users extends Component {
static propTypes = { static propTypes = {
onChangeTitle: PropTypes.func.isRequired, changeTitle: PropTypes.func.isRequired,
} }
constructor(props) { constructor(props) {
@@ -28,7 +28,7 @@ export class Users extends Component {
} }
componentDidMount() { componentDidMount() {
this.props.onChangeTitle('Users') this.props.changeTitle('Users')
api.listUsers().then((list) => { api.listUsers().then((list) => {
list.items.sort((userA, userB) => (userA.lastName.localeCompare(userB.lastName))) list.items.sort((userA, userB) => (userA.lastName.localeCompare(userB.lastName)))
@@ -45,7 +45,7 @@ export class Users extends Component {
} }
componentWillUnmount() { componentWillUnmount() {
this.props.onChangeTitle('') this.props.changeTitle('')
} }
removeUnfinishedNewUser() { removeUnfinishedNewUser() {

View File

@@ -18,6 +18,7 @@ export class Button extends Component {
static defaultProps = { static defaultProps = {
visible: true, visible: true,
disabled: false, disabled: false,
width: sizeInfo.buttonWidth,
} }
static style = { static style = {

View File

@@ -54,6 +54,8 @@ const sizeInfo = {
buttonHeight: 40, buttonHeight: 40,
buttonPadding: '0 15px 0 15px', buttonPadding: '0 15px 0 15px',
buttonWidth: 125,
buttonWideWidth: 225,
checkboxSize: 25, checkboxSize: 25,
checkmarkBorder: '0 3px 3px 0', checkmarkBorder: '0 3px 3px 0',
@@ -72,7 +74,6 @@ const sizeInfo = {
formRowSpacing: 20, formRowSpacing: 20,
formBoundIcon: 30, formBoundIcon: 30,
formBoundIconMargin: 0, formBoundIconMargin: 0,
formButtonLarge: 225,
listBorderWidth: 1, listBorderWidth: 1,
listTopBottomGap: 10, listTopBottomGap: 10,
@@ -83,6 +84,8 @@ const sizeInfo = {
modalShadowWidth: 25, modalShadowWidth: 25,
modalMessageIcon: 150, modalMessageIcon: 150,
profileWidth: '65vw',
inputPadding: 5, inputPadding: 5,
inputBorderRadius: 5, inputBorderRadius: 5,