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 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
|
||||||
|
|
|
@ -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
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
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/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';
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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`));
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue