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 (
+
+ );
+}
+
+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;