add basic password reset functionality, no error checking or styling
This commit is contained in:
parent
d055aa5af8
commit
e5ff11f65a
10 changed files with 202 additions and 7 deletions
45
client/modules/IDE/components/NewPasswordForm.js
Normal file
45
client/modules/IDE/components/NewPasswordForm.js
Normal 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;
|
61
client/modules/IDE/components/NewPasswordView.js
Normal file
61
client/modules/IDE/components/NewPasswordView.js
Normal 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);
|
|
@ -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 (
|
||||
<Overlay>
|
||||
<NewPasswordView token={this.props.params.reset_password_token} />
|
||||
</Overlay>
|
||||
);
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
|
||||
);
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ const routes = (store) =>
|
|||
<Route path="/login" component={IDEView} />
|
||||
<Route path="/signup" 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="/full/:project_id" component={FullView} />
|
||||
<Route path="/sketches" component={IDEView} />
|
||||
|
|
9
client/styles/components/_new-password.scss
Normal file
9
client/styles/components/_new-password.scss
Normal 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;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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 <support@p5js.org>',
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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`));
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue