diff --git a/server/config/templates/accountDeleted.txt b/server/config/templates/accountDeleted.txt index b5e28e6..f20efda 100644 --- a/server/config/templates/accountDeleted.txt +++ b/server/config/templates/accountDeleted.txt @@ -1,6 +1,6 @@ Hello {{recipientFullName}}. -This email is for your records to indicated that your account for the Deighton AR system has been deleted. +Your account for the Deighton AR system has been deleted. Please contact {{supportEmail}} if you have any questions. diff --git a/server/config/templates/changeEmailNew.txt b/server/config/templates/changeEmailNew.txt index a1f68fa..a5ea217 100644 --- a/server/config/templates/changeEmailNew.txt +++ b/server/config/templates/changeEmailNew.txt @@ -1,8 +1,8 @@ Hello {{recipientFullName}}, -This message allows you to complete the process of changing your email. If you did not make this request please do not worry. Just ignore this email and your account will remain unchanged. +This message allows you to complete the process of changing your email on the Deighton AR system. If you did not make this request please do not worry, but we would request that you change your password immediately just to be safe. If you ignore this email your account will remain unchanged. -If you did make this request, please click on the following link to confirm your new email: +If you _did_ make this request, please click on the following link to confirm your new email: {{confirmEmailLink}} @@ -10,4 +10,4 @@ If you have any questions, please contact us at {{supportEmail}}. Regards, -Deighton +{{senderFullName}} diff --git a/server/config/templates/changeEmailOld.txt b/server/config/templates/changeEmailOld.txt index e52d33b..7ca6c33 100644 --- a/server/config/templates/changeEmailOld.txt +++ b/server/config/templates/changeEmailOld.txt @@ -1,6 +1,6 @@ Hello {{recipientFullName}}, -This message is to inform you that a request was made to change your email to {{recipientNewEmail}}. If you did not make this request please do not worry. Just ignore this email and your account will remain unchanged. +This message is to inform you that a request was made to change your email to {{recipientNewEmail}}. If you did not make this request please do not worry, but we would request that you change your password immediately just to be safe. If you did make this request, please see the message sent to your new email account for further instructions. @@ -8,4 +8,4 @@ If you have any questions, please contact us at {{supportEmail}}. Regards, -Deighton +{{senderFullName}} diff --git a/server/config/templates/forgotPassword.txt b/server/config/templates/forgotPassword.txt index 69d8685..0983f3c 100644 --- a/server/config/templates/forgotPassword.txt +++ b/server/config/templates/forgotPassword.txt @@ -1,6 +1,6 @@ Hello {{recipientFullName}}, -The following link will allow you to reset your password. Please paste it into your browser and you will be redirected to the Deighton AR site to set your new password: +The following link will allow you to reset your password. Please click on it or paste it into your browser and you will be redirected to the Deighton AR site to set your new password: {{resetPasswordLink}} @@ -8,4 +8,4 @@ Please contact {{supportEmail}} if you have any questions or problems. Regards, -Deighton +{{senderFullName}} diff --git a/server/src/api/routes/AuthRoutes.js b/server/src/api/routes/AuthRoutes.js index 9c721b9..b90fe6f 100644 --- a/server/src/api/routes/AuthRoutes.js +++ b/server/src/api/routes/AuthRoutes.js @@ -317,59 +317,60 @@ export class AuthRoutes { }) } - changePassword(req, res, next) { + async changePassword(req, res, next) { let User = this.db.User let cr = credential() - User.findById({ _id: req.user._id }).then((user) => { + + try { + const user = await User.findById({ _id: req.user._id }) + if (!user) { - return next(createError.NotFound(`User ${req.user._id} not found`)) + throw createError.NotFound(`User ${req.user._id} not found`) } - return Promise.all([ - Promise.resolve(user), - cr.verify(JSON.stringify(user.passwordHash), req.body.oldPassword) - ]) - }).then((arr) => { - const [user, ok] = arr - return Promise.all([Promise.resolve(user), cr.hash(req.body.newPassword)]) - }).then((arr) => { - const [user, obj] = arr + + const ok = await cr.verify(JSON.stringify(user.passwordHash), req.body.oldPassword) + const obj = await cr.hash(req.body.newPassword) + user.passwordHash = JSON.parse(obj) - return user.save() - }).then((savedUser) => { + await user.save() res.json({}) - }).catch((err) => { - return next(createError.InternalServerError(err.message)) - }) + } catch(err) { + if (err instanceof createError.HttpError) { + next(err) + } else { + next(createError.InternalServerError(err.message)) + } + } } - sendPasswordResetEmail(req, res, next){ + async sendPasswordResetEmail(req, res, next){ const email = req.body.email let User = this.db.User - if (!email) { - return next(createError.BadRequest('Invalid request parameters')) - } - - User.findOne({ email }).then((user) => { - // User must exist their email must be confirmed - if (!user || user.emailToken) { - // Don't give away any information about why we rejected the request - return Promise.reject(createError.BadRequest('Not a valid request')) - } else if (user.passwordToken && (new Date() - user.emailToken.created) < this.sendEmailDelayInSeconds) { - return Promise.reject(createError.BadRequest('Cannot request password reset so soon')) + try { + if (!email) { + throw createError.BadRequest('Invalid request parameters') } - return Promise.all([Promise.resolve(user), util.promisify(crypto.randomBytes)(32)]) - }).then((arr) => { - let [ user, buf ] = arr + const user = await User.findOne({ email }) + + // User must exist and their email must be confirmed + if (!user || user.emailToken) { + // Don't give away any information about why we rejected the request + throw createError.BadRequest('Not a valid request') + } else if (user.passwordToken && user.passwordToken.created && + (new Date() - user.passwordToken.created) < this.sendEmailDelayInSeconds) { + throw createError.BadRequest('Cannot request password reset so soon') + } + + const buf = await util.promisify(crypto.randomBytes)(32) user.passwordToken = { value: urlSafeBase64.encode(buf), created: new Date() } - return user.save() - }).then((savedUser) => { + const savedUser = await user.save() const userFullName = `${savedUser.firstName} ${savedUser.lastName}` const siteUrl = url.parse(req.headers.referer) const msg = { @@ -381,15 +382,17 @@ export class AuthRoutes { supportEmail: this.supportEmail } } - return this.sendEmail ? this.mq.request('dar-email', 'sendEmail', msg) : Promise.resolve() - }).then(() => { + if (this.sendEmail) { + await this.mq.request('dar-email', 'sendEmail', msg) + } + res.json({}) - }).catch((err) => { + } catch(err) { if (err instanceof createError.HttpError) { next(err) } else { next(createError.InternalServerError(`Unable to send password reset email. ${err.message}`)) } - }) + } } } diff --git a/website/config/webpack.config.dev.js b/website/config/webpack.config.dev.js index ebb7aea..462eae9 100644 --- a/website/config/webpack.config.dev.js +++ b/website/config/webpack.config.dev.js @@ -153,6 +153,7 @@ module.exports = { // It enables caching results in ./node_modules/.cache/babel-loader/ // directory for faster rebuilds. cacheDirectory: true, + plugins: [ 'transform-decorators-legacy' ] }, }, // "postcss" loader applies autoprefixer to our CSS. diff --git a/website/config/webpack.config.prod.js b/website/config/webpack.config.prod.js index 5287940..794e807 100644 --- a/website/config/webpack.config.prod.js +++ b/website/config/webpack.config.prod.js @@ -157,6 +157,7 @@ module.exports = { options: { compact: true, }, + plugins: [ 'transform-decorators-legacy' ] }, // The notation here is somewhat confusing. // "postcss" loader applies autoprefixer to our CSS. diff --git a/website/package-lock.json b/website/package-lock.json index 9af9495..bb6d16f 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -375,6 +375,11 @@ "resolved": "https://registry.npmjs.org/auto-bind2/-/auto-bind2-1.0.3.tgz", "integrity": "sha512-+br9nya9M8ayHjai7m9rdpRxuEr8xcYRDrIp7HybNe0ixUHbc1kDiWXKMb0ldsfWb9Zi+SqJ9JfjW8nTkYD0QQ==" }, + "autobind-decorator": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/autobind-decorator/-/autobind-decorator-2.1.0.tgz", + "integrity": "sha512-bgyxeRi1R2Q8kWpHsb1c+lXCulbIAHsyZRddaS+agAUX3hFUVZMociwvRgeZi1zWvfqEEjybSv4zxWvFV8ydQQ==" + }, "autoprefixer": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-7.1.0.tgz", @@ -795,6 +800,12 @@ "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=", "dev": true }, + "babel-plugin-syntax-decorators": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz", + "integrity": "sha1-MSVjtNvePMgGzuPkFszurd0RrAs=", + "dev": true + }, "babel-plugin-syntax-dynamic-import": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", @@ -854,6 +865,17 @@ "babel-template": "6.26.0" } }, + "babel-plugin-transform-decorators-legacy": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators-legacy/-/babel-plugin-transform-decorators-legacy-1.3.4.tgz", + "integrity": "sha1-dBtY9sW86eYCfgiC2cmU8E82aSU=", + "dev": true, + "requires": { + "babel-plugin-syntax-decorators": "6.13.0", + "babel-runtime": "6.23.0", + "babel-template": "6.26.0" + } + }, "babel-plugin-transform-es2015-arrow-functions": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", diff --git a/website/package.json b/website/package.json index 97a7e9a..0b44dc4 100644 --- a/website/package.json +++ b/website/package.json @@ -5,6 +5,7 @@ "dependencies": { "animejs": "^2.2.0", "auto-bind2": "^1.0.2", + "autobind-decorator": "^2.1.0", "eventemitter3": "^2.0.3", "npm": "^5.7.1", "prop-types": "^15.5.10", @@ -21,6 +22,7 @@ "babel-core": "6.24.1", "babel-eslint": "7.2.3", "babel-loader": "7.0.0", + "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-preset-react-app": "^3.0.1", "babel-runtime": "6.23.0", "case-sensitive-paths-webpack-plugin": "1.1.4", diff --git a/website/src/Auth/ForgotPassword.js b/website/src/Auth/ForgotPassword.js index 2d6e73c..6cd2e5b 100644 --- a/website/src/Auth/ForgotPassword.js +++ b/website/src/Auth/ForgotPassword.js @@ -1,12 +1,15 @@ -import React from 'react' +import React, { Component, Fragment } from 'react' import PropTypes from 'prop-types' import { regExpPattern } from 'regexp-pattern' -import { Text, Column, BoundInput, BoundButton } from 'ui' +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 headerLogo from 'images/deighton.png' +import { sizeInfo, colorInfo } from 'ui/style' +import autobind from 'autobind-decorator' -export class ForgotPassword extends React.Component { +export class ForgotPassword extends Component { static propTypes = { history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]) } @@ -29,74 +32,109 @@ export class ForgotPassword extends React.Component { messageModal: null, waitModal: null } - this.handleSubmit = this.handleSubmit.bind(this) - this.handleMessageModalDismiss = this.handleMessageModalDismiss.bind(this) } - handleSubmit() { - const obj = this.state.binder.getValues() + @autobind + handleSubmit(e) { + e.preventDefault() + e.stopPropagation() + + const obj = this.state.binder.getModifiedFieldValues() this.setState({ waitModal: { message: 'Requesting Reset Email' } }) - api.sendResetPassword(obj.email).then((res) => { - const email = this.state.binder.getField('email').value + + const cb = (res) => { this.setState({ waitModal: null, messageModal: { - error: false, + icon: 'thumb', 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.` + message: `If everything checks out, an email will be sent to '${obj.email}' with a reset link. Please click on it to finish resetting the password.` } }) - }).catch((error) => { - this.setState({ - binder: new FormBinder({}, ForgotPassword.bindings), // Reset to avoid rapid retries - waitModal: null, - messageModal: { - error: true, - title: 'Password Reset Failed', - message: `There was a problem requesting the password reset. ${error ? error.message : ''}` - } - }) - }) + } + + api.sendResetPassword(obj.email).then(cb, cb) } + @autobind handleMessageModalDismiss() { this.props.history.replace('/') } render() { + const { binder, waitModal, messageModal } = this.state + return ( -