Merge branch 'dstroot-reset'
* dstroot-reset: (27 commits) Added comments to User model on instance methods and mongoose middleware. Updated login template Add success flash notification on successful password reset Forgot password token changed to hex instead of base64 to avoid having slashes in the url Merged first and second waterfall steps into one, added var secrets = require('../config/secrets');, and mongoose query now returns a user object instead of an array Converted reset controller from eventemitter to async.waterfall. Added callback to async.waterfall for error handling via express middleware Renamed forgot password link Updated reset password template Cleaned up and refactored reset password template Updated schema's default values for password token and expires fields Update flash message on successful forgot password request Update contributing section Update POST /forgot description. Refactor Forgot controller Converted workflow/eventemitter code to async.waterfall Update error flash message, redirect to /forgot if no reset token is found or if it has expired Updated expiration of password token to 1hr, updated flash message when email is sent with password recovery instructions. Removed user _id from reset route. Use only token value. It's random enough that you don't need to include user id as well. Updated email template text, removed token salting, changed token to base64 (24bit) ...
This commit is contained in:
@ -959,7 +959,7 @@ TODO
|
||||
|
||||
Contributing
|
||||
------------
|
||||
If something is unclear, confusing, or needs to be refactored, please let me know. Pull requests are always welcome, but due to the opinionated nature of this project, I cannot accept every pull request. Please open an issue before submitting a pull request. This project uses [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) with a few exceptions.
|
||||
If something is unclear, confusing, or needs to be refactored, please let me know. Pull requests are always welcome, but due to the opinionated nature of this project, I cannot accept every pull request. Please open an issue before submitting a pull request. This project uses [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) with a few minor exceptions. If you are submitting a pull request that involves Jade templates, please make sure you are using *spaces*, not tabs.
|
||||
|
||||
License
|
||||
-------
|
||||
|
6
app.js
6
app.js
@ -18,6 +18,8 @@ var homeController = require('./controllers/home');
|
||||
var userController = require('./controllers/user');
|
||||
var apiController = require('./controllers/api');
|
||||
var contactController = require('./controllers/contact');
|
||||
var forgotController = require('./controllers/forgot');
|
||||
var resetController = require('./controllers/reset');
|
||||
|
||||
/**
|
||||
* API keys + Passport configuration.
|
||||
@ -98,6 +100,10 @@ app.get('/', homeController.index);
|
||||
app.get('/login', userController.getLogin);
|
||||
app.post('/login', userController.postLogin);
|
||||
app.get('/logout', userController.logout);
|
||||
app.get('/forgot', forgotController.getForgot);
|
||||
app.post('/forgot', forgotController.postForgot);
|
||||
app.get('/reset/:token', resetController.getReset);
|
||||
app.post('/reset/:token', resetController.postReset);
|
||||
app.get('/signup', userController.getSignup);
|
||||
app.post('/signup', userController.postSignup);
|
||||
app.get('/contact', contactController.getContact);
|
||||
|
85
controllers/forgot.js
Normal file
85
controllers/forgot.js
Normal file
@ -0,0 +1,85 @@
|
||||
var async = require('async');
|
||||
var crypto = require('crypto');
|
||||
var nodemailer = require("nodemailer");
|
||||
var User = require('../models/User');
|
||||
var secrets = require('../config/secrets');
|
||||
|
||||
/**
|
||||
* GET /forgot
|
||||
* Forgot Password page.
|
||||
*/
|
||||
|
||||
exports.getForgot = function(req, res) {
|
||||
if (req.isAuthenticated()) {
|
||||
return res.redirect('/');
|
||||
}
|
||||
res.render('account/forgot', {
|
||||
title: 'Forgot Password'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* POST /forgot
|
||||
* Create a random token, then the send user an email with a reset link.
|
||||
* @param email
|
||||
*/
|
||||
|
||||
exports.postForgot = function(req, res, next) {
|
||||
req.assert('email', 'Please enter a valid email address.').isEmail();
|
||||
|
||||
var errors = req.validationErrors();
|
||||
|
||||
if (errors) {
|
||||
req.flash('errors', errors);
|
||||
return res.redirect('/forgot');
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function(done) {
|
||||
crypto.randomBytes(20, function(err, buf) {
|
||||
var token = buf.toString('hex');
|
||||
done(err, token);
|
||||
});
|
||||
},
|
||||
function(token, done) {
|
||||
User.findOne({ email: req.body.email.toLowerCase() }, function(err, user) {
|
||||
if (!user) {
|
||||
req.flash('errors', { msg: 'No account with that email address exists.' });
|
||||
return res.redirect('/forgot');
|
||||
}
|
||||
|
||||
user.resetPasswordToken = token;
|
||||
user.resetPasswordExpires = Date.now() + 3600000; // 1 hour
|
||||
|
||||
user.save(function(err) {
|
||||
done(err, token, user);
|
||||
});
|
||||
});
|
||||
},
|
||||
function(token, user, done) {
|
||||
var smtpTransport = nodemailer.createTransport('SMTP', {
|
||||
service: 'SendGrid',
|
||||
auth: {
|
||||
user: secrets.sendgrid.user,
|
||||
pass: secrets.sendgrid.password
|
||||
}
|
||||
});
|
||||
var mailOptions = {
|
||||
to: user.profile.name + ' <' + user.email + '>',
|
||||
from: 'hackathon@starter.com',
|
||||
subject: 'Hackathon Starter Password Reset',
|
||||
text: 'You are receiving this 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/' + token + '\n\n' +
|
||||
'If you did not request this, please ignore this email and your password will remain unchanged.\n'
|
||||
};
|
||||
smtpTransport.sendMail(mailOptions, function(err) {
|
||||
req.flash('info', { msg: 'An e-mail has been sent to ' + user.email + ' with further instructions.' });
|
||||
done(err, 'done');
|
||||
});
|
||||
}
|
||||
], function(err) {
|
||||
if (err) return next(err);
|
||||
res.redirect('/forgot');
|
||||
});
|
||||
};
|
93
controllers/reset.js
Normal file
93
controllers/reset.js
Normal file
@ -0,0 +1,93 @@
|
||||
var async = require('async');
|
||||
var nodemailer = require('nodemailer');
|
||||
var User = require('../models/User');
|
||||
var secrets = require('../config/secrets');
|
||||
|
||||
/**
|
||||
* GET /reset/:token
|
||||
* Reset Password page.
|
||||
*/
|
||||
|
||||
exports.getReset = function(req, res) {
|
||||
if (req.isAuthenticated()) {
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
User
|
||||
.findOne({ resetPasswordToken: req.params.token })
|
||||
.where('resetPasswordExpires').gt(Date.now())
|
||||
.exec(function(err, user) {
|
||||
if (!user) {
|
||||
req.flash('errors', { msg: 'Password reset token is invalid or has expired.' });
|
||||
return res.redirect('/forgot');
|
||||
}
|
||||
res.render('account/reset', {
|
||||
title: 'Password Reset'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* POST /reset/:token
|
||||
* Process the reset password request.
|
||||
*/
|
||||
|
||||
exports.postReset = function(req, res, next) {
|
||||
req.assert('password', 'Password must be at least 4 characters long.').len(4);
|
||||
req.assert('confirm', 'Passwords must match.').equals(req.body.password);
|
||||
|
||||
var errors = req.validationErrors();
|
||||
|
||||
if (errors) {
|
||||
req.flash('errors', errors);
|
||||
return res.redirect('back');
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function(done) {
|
||||
User
|
||||
.findOne({ resetPasswordToken: req.params.token })
|
||||
.where('resetPasswordExpires').gt(Date.now())
|
||||
.exec(function(err, user) {
|
||||
if (!user) {
|
||||
req.flash('errors', { msg: 'Password reset token is invalid or has expired.' });
|
||||
return res.redirect('back');
|
||||
}
|
||||
|
||||
user.password = req.body.password;
|
||||
user.resetPasswordToken = undefined;
|
||||
user.resetPasswordExpires = undefined;
|
||||
|
||||
user.save(function(err) {
|
||||
if (err) return next(err);
|
||||
req.logIn(user, function(err) {
|
||||
done(err, user);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
function(user, done) {
|
||||
var smtpTransport = nodemailer.createTransport('SMTP', {
|
||||
service: 'SendGrid',
|
||||
auth: {
|
||||
user: secrets.sendgrid.user,
|
||||
pass: secrets.sendgrid.password
|
||||
}
|
||||
});
|
||||
var mailOptions = {
|
||||
to: user.profile.name + ' <' + user.email + '>',
|
||||
from: 'hackathon@starter.com',
|
||||
subject: 'Your Hackathon Starter password has been changed',
|
||||
text: 'Hello,\n\n' +
|
||||
'This is a confirmation that the password for your account ' + user.email + ' has just been changed.\n'
|
||||
};
|
||||
smtpTransport.sendMail(mailOptions, function(err) {
|
||||
req.flash('success', { msg: 'Success! Your password has been changed.' });
|
||||
done(err);
|
||||
});
|
||||
}
|
||||
], function(err) {
|
||||
if (err) return next(err);
|
||||
res.redirect('/');
|
||||
});
|
||||
};
|
@ -18,20 +18,23 @@ var userSchema = new mongoose.Schema({
|
||||
location: { type: String, default: '' },
|
||||
website: { type: String, default: '' },
|
||||
picture: { type: String, default: '' }
|
||||
}
|
||||
},
|
||||
|
||||
resetPasswordToken: String,
|
||||
resetPasswordExpires: Date
|
||||
});
|
||||
|
||||
/**
|
||||
* Hash the password for security.
|
||||
* "Pre" is a Mongoose middleware that executes before each user.save() call.
|
||||
*/
|
||||
|
||||
userSchema.pre('save', function(next) {
|
||||
var user = this;
|
||||
var SALT_FACTOR = 5;
|
||||
|
||||
if (!user.isModified('password')) return next();
|
||||
|
||||
bcrypt.genSalt(SALT_FACTOR, function(err, salt) {
|
||||
bcrypt.genSalt(5, function(err, salt) {
|
||||
if (err) return next(err);
|
||||
|
||||
bcrypt.hash(user.password, salt, null, function(err, hash) {
|
||||
@ -42,6 +45,11 @@ userSchema.pre('save', function(next) {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Validate user's password.
|
||||
* Used by Passport-Local Strategy for password validation.
|
||||
*/
|
||||
|
||||
userSchema.methods.comparePassword = function(candidatePassword, cb) {
|
||||
bcrypt.compare(candidatePassword, this.password, function(err, isMatch) {
|
||||
if (err) return cb(err);
|
||||
@ -50,7 +58,8 @@ userSchema.methods.comparePassword = function(candidatePassword, cb) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a URL to a user's Gravatar email.
|
||||
* Get URL to a user's gravatar.
|
||||
* Used in Navbar and Account Management page.
|
||||
*/
|
||||
|
||||
userSchema.methods.gravatar = function(size, defaults) {
|
||||
|
@ -34,6 +34,7 @@
|
||||
"paypal-rest-sdk": "~0.6.4",
|
||||
"connect-mongo": "~0.4.0",
|
||||
"twilio": "~1.5.0",
|
||||
"validator": "~3.2.1"
|
||||
"validator": "~3.2.1",
|
||||
"crypto": "0.0.3"
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,10 @@
|
||||
background-image: linear-gradient(to bottom, #ffffff 60%, #f8f8f8 100%);
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
// Forms
|
||||
// -------------------------
|
||||
|
||||
|
15
views/account/forgot.jade
Normal file
15
views/account/forgot.jade
Normal file
@ -0,0 +1,15 @@
|
||||
extends ../layout
|
||||
|
||||
block content
|
||||
.col-sm-8.col-sm-offset-2
|
||||
form(method='POST')
|
||||
legend Forgot Password
|
||||
input(type='hidden', name='_csrf', value=token)
|
||||
.form-group
|
||||
p Enter your email address below and we will send you password reset instructions.
|
||||
label.control-label(for='email') Email
|
||||
input.form-control(type='email', name='email', id='email', placeholder='Email', autofocus=true)
|
||||
.form-group
|
||||
button.btn.btn-primary(type='submit')
|
||||
i.fa.fa-key
|
||||
| Reset Password
|
@ -3,8 +3,8 @@ extends ../layout
|
||||
block content
|
||||
.col-sm-8.col-sm-offset-2
|
||||
form(method='POST')
|
||||
input(type='hidden', name='_csrf', value=token)
|
||||
legend Sign In
|
||||
input(type='hidden', name='_csrf', value=token)
|
||||
.form-group
|
||||
.btn-group.btn-group-justified
|
||||
if secrets.facebookAuth
|
||||
@ -34,4 +34,4 @@ block content
|
||||
button.btn.btn-primary(type='submit')
|
||||
i.fa.fa-unlock-alt
|
||||
| Login
|
||||
|
||||
a.btn.btn-link(href='/forgot') Forgot your password?
|
||||
|
17
views/account/reset.jade
Normal file
17
views/account/reset.jade
Normal file
@ -0,0 +1,17 @@
|
||||
extends ../layout
|
||||
|
||||
block content
|
||||
.col-sm-8.col-sm-offset-2
|
||||
form(method='POST')
|
||||
legend Reset Password
|
||||
input(type='hidden', name='_csrf', value=token)
|
||||
.form-group
|
||||
label(for='password') New Password
|
||||
input.form-control(type='password', name='password', value='', placeholder='New password', autofocus=true)
|
||||
.form-group
|
||||
label(for='confirm') Confirm Password
|
||||
input.form-control(type='password', name='confirm', value='', placeholder='Confirm password')
|
||||
.form-group
|
||||
button.btn.btn-primary.btn-reset(type='submit')
|
||||
i.fa.fa-keyboard-o
|
||||
| Update Password
|
Reference in New Issue
Block a user