add basic password reset functionality, no error checking or styling

This commit is contained in:
Cassie Tarakajian 2016-10-18 16:07:25 -04:00
parent d055aa5af8
commit e5ff11f65a
10 changed files with 202 additions and 7 deletions

View file

@ -0,0 +1,45 @@
import React, { PropTypes } from 'react';
function NewPasswordForm(props) {
const { fields: { password, confirmPassword }, handleSubmit, submitting, invalid, pristine } = props;
return (
<form className="new-password-form" onSubmit={handleSubmit(props.updatePassword.bind(this, props.token))}>
<p className="new-password-form__field">
<input
className="new-password-form__password-input"
aria-label="password"
type="password"
placeholder="Password"
{...password}
/>
{password.touched && password.error && <span className="form-error">{password.error}</span>}
</p>
<p className="new-password-form__field">
<input
className="new-password-form__confirm-password-input"
type="password"
placeholder="Confirm Password"
aria-label="confirm password"
{...confirmPassword}
/>
{confirmPassword.touched && confirmPassword.error && <span className="form-error">{confirmPassword.error}</span>}
</p>
<input type="submit" disabled={submitting || invalid || pristine} value="Set New Password" aria-label="sign up" />
</form>
);
}
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;

View file

@ -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 (
<div className="new-password" ref="newPassword" tabIndex="0">
<h1>Set a New Password</h1>
<NewPasswordForm {...this.props} />
</div>
);
}
}
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);

View file

@ -30,6 +30,7 @@ import About from '../components/About';
import LoginView from '../components/LoginView'; import LoginView from '../components/LoginView';
import SignupView from '../components/SignupView'; import SignupView from '../components/SignupView';
import ResetPasswordView from '../components/ResetPasswordView'; import ResetPasswordView from '../components/ResetPasswordView';
import NewPasswordView from '../components/NewPasswordView';
class IDEView extends React.Component { class IDEView extends React.Component {
constructor(props) { 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 (
<Overlay>
<NewPasswordView token={this.props.params.reset_password_token} />
</Overlay>
);
}
})()}
</div> </div>
); );
@ -429,7 +439,8 @@ class IDEView extends React.Component {
IDEView.propTypes = { IDEView.propTypes = {
params: PropTypes.shape({ params: PropTypes.shape({
project_id: PropTypes.string, project_id: PropTypes.string,
username: PropTypes.string username: PropTypes.string,
reset_password_token: PropTypes.string,
}), }),
location: PropTypes.shape({ location: PropTypes.shape({
pathname: PropTypes.string pathname: PropTypes.string

View file

@ -125,3 +125,29 @@ export function resetPasswordReset() {
type: ActionTypes.RESET_PASSWORD_RESET 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
}));
};
}

View file

@ -17,6 +17,7 @@ const routes = (store) =>
<Route path="/login" component={IDEView} /> <Route path="/login" component={IDEView} />
<Route path="/signup" component={IDEView} /> <Route path="/signup" component={IDEView} />
<Route path="/reset-password" component={IDEView} /> <Route path="/reset-password" component={IDEView} />
<Route path="/reset-password/:reset_password_token" component={IDEView} />
<Route path="/projects/:project_id" component={IDEView} /> <Route path="/projects/:project_id" component={IDEView} />
<Route path="/full/:project_id" component={FullView} /> <Route path="/full/:project_id" component={FullView} />
<Route path="/sketches" component={IDEView} /> <Route path="/sketches" component={IDEView} />

View file

@ -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;
}

View file

@ -21,6 +21,7 @@
@import 'components/signup'; @import 'components/signup';
@import 'components/login'; @import 'components/login';
@import 'components/reset-password'; @import 'components/reset-password';
@import 'components/new-password';
@import 'components/sketch-list'; @import 'components/sketch-list';
@import 'components/sidebar'; @import 'components/sidebar';
@import 'components/modal'; @import 'components/modal';

View file

@ -109,13 +109,12 @@ export function resetPasswordInitiate(req, res) {
const transporter = nodemailer.createTransport(mg(auth)); const transporter = nodemailer.createTransport(mg(auth));
const message = { const message = {
to: user.email, to: user.email,
from: 'passwordreset@mg.p5js.org', from: 'p5.js Web Editor <support@p5js.org>',
subject: 'p5.js Web Editor Password Reset', subject: 'p5.js Web Editor Password Reset',
text: `You are receiving this email because you (or someone else) have requested text: `You are receiving this email because you (or someone else) have requested the reset of the password for your account.
the reset of the password for your account. \n\n Please click on the following link, \n\nPlease click on the following link, or paste this into your browser to complete the process:
or paste this into your browser to complete the process: \n\n \n\nhttp://${req.headers.host}/reset-password/${token}
http://${req.headers.host}/reset-password/${token}\n\n \n\nIf you did not request this, please ignore this email and your password will remain unchanged.\n`
If you did not request this, please ignore this email and your password will remain unchanged.\n`
}; };
transporter.sendMail(message, (error, info) => { transporter.sendMail(message, (error, info) => {
done(error); 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.'}); 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
}

View file

@ -29,6 +29,10 @@ router.route('/reset-password').get((req, res) => {
res.sendFile(path.resolve(`${__dirname}/../../index.html`)); 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) => { router.route('/sketches').get((req, res) => {
res.sendFile(path.resolve(`${__dirname}/../../index.html`)); res.sendFile(path.resolve(`${__dirname}/../../index.html`));
}); });

View file

@ -10,4 +10,8 @@ router.route('/preferences').put(UserController.updatePreferences);
router.route('/reset-password').post(UserController.resetPasswordInitiate); 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; export default router;