From e5ff11f65ae8d12328829d67bf872adaab774771 Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Tue, 18 Oct 2016 16:07:25 -0400 Subject: [PATCH] add basic password reset functionality, no error checking or styling --- .../modules/IDE/components/NewPasswordForm.js | 45 ++++++++++++++ .../modules/IDE/components/NewPasswordView.js | 61 +++++++++++++++++++ client/modules/IDE/pages/IDEView.js | 13 +++- client/modules/User/actions.js | 26 ++++++++ client/routes.js | 1 + client/styles/components/_new-password.scss | 9 +++ client/styles/main.scss | 1 + server/controllers/user.controller.js | 45 ++++++++++++-- server/routes/server.routes.js | 4 ++ server/routes/user.routes.js | 4 ++ 10 files changed, 202 insertions(+), 7 deletions(-) create mode 100644 client/modules/IDE/components/NewPasswordForm.js create mode 100644 client/modules/IDE/components/NewPasswordView.js create mode 100644 client/styles/components/_new-password.scss diff --git a/client/modules/IDE/components/NewPasswordForm.js b/client/modules/IDE/components/NewPasswordForm.js new file mode 100644 index 00000000..1a7bdd90 --- /dev/null +++ b/client/modules/IDE/components/NewPasswordForm.js @@ -0,0 +1,45 @@ +import React, { PropTypes } from 'react'; + +function NewPasswordForm(props) { + const { fields: { password, confirmPassword }, handleSubmit, submitting, invalid, pristine } = props; + return ( +
+

+ + {password.touched && password.error && {password.error}} +

+

+ + {confirmPassword.touched && confirmPassword.error && {confirmPassword.error}} +

+ +
+ ); +} + +NewPasswordForm.propTypes = { + fields: PropTypes.shape({ + password: PropTypes.object.isRequired, + confirmPassword: PropTypes.object.isRequired + }).isRequired, + handleSubmit: PropTypes.func.isRequired, + updatePassword: PropTypes.func.isRequired, + submitting: PropTypes.bool, + invalid: PropTypes.bool, + pristine: PropTypes.bool, + token: PropTypes.string.isRequired +}; + +export default NewPasswordForm; diff --git a/client/modules/IDE/components/NewPasswordView.js b/client/modules/IDE/components/NewPasswordView.js new file mode 100644 index 00000000..a76f8a30 --- /dev/null +++ b/client/modules/IDE/components/NewPasswordView.js @@ -0,0 +1,61 @@ +import React, { PropTypes } from 'react'; +import { reduxForm } from 'redux-form'; +import NewPasswordForm from './NewPasswordForm'; +import * as UserActions from '../../User/actions'; +import { bindActionCreators } from 'redux'; + +class NewPasswordView extends React.Component { + componentDidMount() { + this.refs.newPassword.focus(); + // need to check if this is a valid token + this.props.validateResetPasswordToken(this.props.token); + } + + render() { + return ( +
+

Set a New Password

+ +
+ ); + } +} + +NewPasswordView.propTypes = { + token: PropTypes.string.isRequired, + validateResetPasswordToken: PropTypes.func.isRequired +}; + +function validate(formProps) { + const errors = {}; + + if (!formProps.password) { + errors.password = 'Please enter a password'; + } + if (!formProps.confirmPassword) { + errors.confirmPassword = 'Please enter a password confirmation'; + } + + if (formProps.password !== formProps.confirmPassword) { + errors.password = 'Passwords must match'; + } + + return errors; +} + +function mapStateToProps(state, ownProps) { + return { + user: state.user, + token: ownProps.token + }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators(UserActions, dispatch); +} + +export default reduxForm({ + form: 'new-password', + fields: ['password', 'confirmPassword'], + validate +}, mapStateToProps, mapDispatchToProps)(NewPasswordView); diff --git a/client/modules/IDE/pages/IDEView.js b/client/modules/IDE/pages/IDEView.js index 6cc3fe76..93f99351 100644 --- a/client/modules/IDE/pages/IDEView.js +++ b/client/modules/IDE/pages/IDEView.js @@ -30,6 +30,7 @@ import About from '../components/About'; import LoginView from '../components/LoginView'; import SignupView from '../components/SignupView'; import ResetPasswordView from '../components/ResetPasswordView'; +import NewPasswordView from '../components/NewPasswordView'; class IDEView extends React.Component { constructor(props) { @@ -420,6 +421,15 @@ class IDEView extends React.Component { ); } })()} + {(() => { // eslint-disable-line + if (this.props.location.pathname.match(/\/reset-password\/[a-fA-F0-9]{40}/)) { + return ( + + + + ); + } + })()} ); @@ -429,7 +439,8 @@ class IDEView extends React.Component { IDEView.propTypes = { params: PropTypes.shape({ project_id: PropTypes.string, - username: PropTypes.string + username: PropTypes.string, + reset_password_token: PropTypes.string, }), location: PropTypes.shape({ pathname: PropTypes.string diff --git a/client/modules/User/actions.js b/client/modules/User/actions.js index b6422d78..8c3f9d14 100644 --- a/client/modules/User/actions.js +++ b/client/modules/User/actions.js @@ -125,3 +125,29 @@ export function resetPasswordReset() { type: ActionTypes.RESET_PASSWORD_RESET }; } + +export function validateResetPasswordToken(token) { + return (dispatch) => { + axios.get(`${ROOT_URL}/reset-password/${token}`) + .then(() => { + // do nothing if the token is valid + // add the token to the state? + }) + .catch(() => dispatch({ + type: ActionTypes.INVALID_RESET_PASSWORD_TOKEN + })); + }; +} + +export function updatePassword(token, formValues) { + return (dispatch) => { + axios.post(`${ROOT_URL}/reset-password/${token}`, formValues) + .then((response) => { + dispatch(loginUserSuccess(response.data)); + browserHistory.push('/'); + }) + .catch(() => dispatch({ + type: ActionTypes.INVALID_RESET_PASSWORD_TOKEN + })); + }; +} diff --git a/client/routes.js b/client/routes.js index 3a6ffdf1..ea3a8fe6 100644 --- a/client/routes.js +++ b/client/routes.js @@ -17,6 +17,7 @@ const routes = (store) => + diff --git a/client/styles/components/_new-password.scss b/client/styles/components/_new-password.scss new file mode 100644 index 00000000..d03e78df --- /dev/null +++ b/client/styles/components/_new-password.scss @@ -0,0 +1,9 @@ +.new-password { + @extend %modal; + text-align: center; + display: flex; + flex-direction: column; + justify-content: center; + padding: #{20 / $base-font-size}rem; + align-items: center; +} \ No newline at end of file diff --git a/client/styles/main.scss b/client/styles/main.scss index 22a53443..026eda2a 100644 --- a/client/styles/main.scss +++ b/client/styles/main.scss @@ -21,6 +21,7 @@ @import 'components/signup'; @import 'components/login'; @import 'components/reset-password'; +@import 'components/new-password'; @import 'components/sketch-list'; @import 'components/sidebar'; @import 'components/modal'; diff --git a/server/controllers/user.controller.js b/server/controllers/user.controller.js index e04220f6..c69d4774 100644 --- a/server/controllers/user.controller.js +++ b/server/controllers/user.controller.js @@ -109,13 +109,12 @@ export function resetPasswordInitiate(req, res) { const transporter = nodemailer.createTransport(mg(auth)); const message = { to: user.email, - from: 'passwordreset@mg.p5js.org', + from: 'p5.js Web Editor ', subject: 'p5.js Web Editor Password Reset', - text: `You are receiving this email because you (or someone else) have requested - the reset of the password for your account. \n\n Please click on the following link, - or paste this into your browser to complete the process: \n\n - http://${req.headers.host}/reset-password/${token}\n\n - If you did not request this, please ignore this email and your password will remain unchanged.\n` + text: `You are receiving this email because you (or someone else) have requested the reset of the password for your account. + \n\nPlease click on the following link, or paste this into your browser to complete the process: + \n\nhttp://${req.headers.host}/reset-password/${token} + \n\nIf you did not request this, please ignore this email and your password will remain unchanged.\n` }; transporter.sendMail(message, (error, info) => { done(error); @@ -130,3 +129,37 @@ export function resetPasswordInitiate(req, res) { return res.json({success: true, message: 'If the email is registered with the editor, an email has been sent.'}); }); } + +export function validateResetPasswordToken(req, res) { + User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, (err, user) => { + if (!user) { + return res.status(401).json({success: false, message: 'Password reset token is invalid or has expired.'}); + } + res.json({ success: true }); + }); +} + +export function updatePassword(req, res) { + User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, function(err, user) { + if (!user) { + return res.status(401).json({success: false, message: 'Password reset token is invalid or has expired.'}); + } + + user.password = req.body.password; + user.resetPasswordToken = undefined; + user.resetPasswordExpires = undefined; + + user.save(function(err) { + req.logIn(user, function(err) { + return res.json({ + email: req.user.email, + username: req.user.username, + preferences: req.user.preferences, + id: req.user._id + }); + }); + }); + }); + + //eventually send email that the password has been reset +} diff --git a/server/routes/server.routes.js b/server/routes/server.routes.js index 14dea1e4..a6d28b30 100644 --- a/server/routes/server.routes.js +++ b/server/routes/server.routes.js @@ -29,6 +29,10 @@ router.route('/reset-password').get((req, res) => { res.sendFile(path.resolve(`${__dirname}/../../index.html`)); }); +router.route('/reset-password/:reset_password_token').get((req, res) => { + res.sendFile(path.resolve(`${__dirname}/../../index.html`)); +}); + router.route('/sketches').get((req, res) => { res.sendFile(path.resolve(`${__dirname}/../../index.html`)); }); diff --git a/server/routes/user.routes.js b/server/routes/user.routes.js index 9abf70af..da8fd02b 100644 --- a/server/routes/user.routes.js +++ b/server/routes/user.routes.js @@ -10,4 +10,8 @@ router.route('/preferences').put(UserController.updatePreferences); router.route('/reset-password').post(UserController.resetPasswordInitiate); +router.route('/reset-password/:token').get(UserController.validateResetPasswordToken); + +router.route('/reset-password/:token').post(UserController.updatePassword); + export default router;