Adding Password Reset

This commit is contained in:
Amos Haviv
2014-07-31 11:27:14 +03:00
parent 9fb6e411cf
commit f4b62ca819
33 changed files with 852 additions and 705 deletions

View File

@@ -4,33 +4,10 @@
* Module dependencies.
*/
var mongoose = require('mongoose'),
errorHandler = require('./errors'),
Article = mongoose.model('Article'),
_ = require('lodash');
/**
* Get the error message from error object
*/
var getErrorMessage = function(err) {
var message = '';
if (err.code) {
switch (err.code) {
case 11000:
case 11001:
message = 'Article already exists';
break;
default:
message = 'Something went wrong';
}
} else {
for (var errName in err.errors) {
if (err.errors[errName].message) message = err.errors[errName].message;
}
}
return message;
};
/**
* Create a article
*/
@@ -41,7 +18,7 @@ exports.create = function(req, res) {
article.save(function(err) {
if (err) {
return res.send(400, {
message: getErrorMessage(err)
message: errorHandler.getErrorMessage(err)
});
} else {
res.jsonp(article);
@@ -67,7 +44,7 @@ exports.update = function(req, res) {
article.save(function(err) {
if (err) {
return res.send(400, {
message: getErrorMessage(err)
message: errorHandler.getErrorMessage(err)
});
} else {
res.jsonp(article);
@@ -84,7 +61,7 @@ exports.delete = function(req, res) {
article.remove(function(err) {
if (err) {
return res.send(400, {
message: getErrorMessage(err)
message: errorHandler.getErrorMessage(err)
});
} else {
res.jsonp(article);
@@ -99,7 +76,7 @@ exports.list = function(req, res) {
Article.find().sort('-created').populate('user', 'displayName').exec(function(err, articles) {
if (err) {
return res.send(400, {
message: getErrorMessage(err)
message: errorHandler.getErrorMessage(err)
});
} else {
res.jsonp(articles);

View File

@@ -0,0 +1,42 @@
'use strict';
/**
* Get unique error field name
*/
var getUniqueErrorMessage = function(err) {
var output;
try {
var fieldName = err.err.substring(err.err.lastIndexOf('.$') + 2, err.err.lastIndexOf('_1'));
output = fieldName.charAt(0).toUpperCase() + fieldName.slice(1) + ' already exist';
} catch(ex) {
output = 'Unique field already exist';
}
return output;
};
/**
* Get the error message from error object
*/
exports.getErrorMessage = function(err) {
var message = '';
if (err.code) {
switch (err.code) {
case 11000:
case 11001:
message = getUniqueErrorMessage(err);
break;
default:
message = 'Something went wrong';
}
} else {
for (var errName in err.errors) {
if (err.errors[errName].message) message = err.errors[errName].message;
}
}
return message;
};

View File

@@ -1,542 +1,6 @@
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
passport = require('passport'),
User = mongoose.model('User'),
_ = require('lodash');
/* Requires for reset password */
var nodemailer = require('nodemailer');
var LocalStrategy = require('passport-local').Strategy;
var bcrypt = require('bcrypt-nodejs');
var async = require('async');
var crypto = require('crypto');
/**
* Get the error message from error object
*/
var getErrorMessage = function(err) {
var message = '';
if (err.code) {
switch (err.code) {
case 11000:
case 11001:
message = 'Username already exists';
break;
default:
message = 'Something went wrong';
}
} else {
for (var errName in err.errors) {
if (err.errors[errName].message) message = err.errors[errName].message;
}
}
return message;
};
/**
* Signup
*/
exports.signup = function(req, res) {
// For security measurement we remove the roles from the req.body object
delete req.body.roles;
// Init Variables
var user = new User(req.body);
var message = null;
// Add missing user fields
user.provider = 'local';
user.displayName = user.firstName + ' ' + user.lastName;
// Then save the user
user.save(function(err) {
if (err) {
return res.send(400, {
message: getErrorMessage(err)
});
} else {
// Remove sensitive data before login
user.password = undefined;
user.salt = undefined;
req.login(user, function(err) {
if (err) {
res.send(400, err);
} else {
res.jsonp(user);
}
});
}
});
};
/**
* Signin after passport authentication
*/
exports.signin = function(req, res, next) {
passport.authenticate('local', function(err, user, info) {
if (err || !user) {
res.send(400, info);
} else {
// Remove sensitive data before login
user.password = undefined;
user.salt = undefined;
req.login(user, function(err) {
if (err) {
res.send(400, err);
} else {
res.jsonp(user);
}
});
}
})(req, res, next);
};
/**
* Forgot for reset password (forgot POST)
*/
exports.forgot = function(req, res, next) {
async.waterfall([
// Generate random token
function(done) {
crypto.randomBytes(20, function(err, buf) {
var token = buf.toString('hex');
done(err, token);
});
},
// Lookup user by email address
function(token, done) {
if (req.body.email) {
User.findOne({ email: req.body.email }, function(err, user) {
if (!user) {
return res.send(400, {
message: 'No account with that email address exists'
});
}
user.resetPasswordToken = token;
user.resetPasswordExpires = Date.now() + 3600000; // 1 hour
user.save(function(err) {
done(err, token, user);
});
});
} else {
return res.send(400, {
message: 'Email field must not be blank'
});
}
},
// If valid email, send reset email using service
function(token, user, done) {
var smtpTransport = nodemailer.createTransport('SMTP', {
service: 'SendGrid', // Choose email service, default SendGrid
auth: {
user: 'your_sendgrid_email@domain.com',
pass: 'your_sendgrid_password'
}
});
var mailOptions = {
to: user.email,
from: 'your_email@domain.com',
subject: '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 + '/auth/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) {
res.send(200, {
message: 'An email has been sent to ' + user.email + ' with further instructions.'
});
done(err, 'done');
});
}
], function(err) {
if (err) return next(err);
});
};
/**
* Reset password GET from email token
*/
exports.resetGet = function(req, res) {
User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, function(err, user) {
if (!user) {
// res.render('404');
res.send(400, {
message: 'Password reset token is invalid or has expired.'
});
return res.redirect('/#!/forgot');
}
res.redirect('/#!/reset/' + req.params.token);
});
};
/**
* Reset password POST from email token
*/
exports.resetPost = function(req, res) {
// Init Variables
var passwordDetails = req.body;
var message = null;
async.waterfall([
function(done) {
User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, function(err, user) {
if (!err && user) {
if (passwordDetails.newPassword === passwordDetails.verifyPassword) {
user.password = passwordDetails.newPassword;
user.resetPasswordToken = undefined;
user.resetPasswordExpires = undefined;
user.save(function(err) {
if (err) {
return res.send(400, {
message: getErrorMessage(err)
});
} else {
req.login(user, function(err) {
if (err) {
res.send(400, err);
} else {
done(err, user);
}
});
}
});
} else {
return res.send(400, {
message: 'Passwords do not match'
});
}
} else {
return res.send(400, {
message: 'Password reset token is invalid or has expired.'
});
}
});
},
function(user, done) {
var smtpTransport = nodemailer.createTransport('SMTP', {
service: 'SendGrid',
auth: {
user: 'your_sendgrid_email@domain.com',
pass: 'your_sendgrid_password'
}
});
var mailOptions = {
to: user.email,
from: 'your_email@domain.com',
subject: 'Your 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) {
res.send(200, {
message: 'Password changed successfully'
});
});
}
], function(err) {
res.redirect('/');
});
};
/**
* Update user details
*/
exports.update = function(req, res) {
// Init Variables
var user = req.user;
var message = null;
// For security measurement we remove the roles from the req.body object
delete req.body.roles;
if (user) {
// Merge existing user
user = _.extend(user, req.body);
user.updated = Date.now();
user.displayName = user.firstName + ' ' + user.lastName;
user.save(function(err) {
if (err) {
return res.send(400, {
message: getErrorMessage(err)
});
} else {
req.login(user, function(err) {
if (err) {
res.send(400, err);
} else {
res.jsonp(user);
}
});
}
});
} else {
res.send(400, {
message: 'User is not signed in'
});
}
};
/**
* Change Password
*/
exports.changePassword = function(req, res, next) {
// Init Variables
var passwordDetails = req.body;
var message = null;
if (req.user) {
if (passwordDetails.newPassword) {
User.findById(req.user.id, function(err, user) {
if (!err && user) {
if (user.authenticate(passwordDetails.currentPassword)) {
if (passwordDetails.newPassword === passwordDetails.verifyPassword) {
user.password = passwordDetails.newPassword;
user.save(function(err) {
if (err) {
return res.send(400, {
message: getErrorMessage(err)
});
} else {
req.login(user, function(err) {
if (err) {
res.send(400, err);
} else {
res.send({
message: 'Password changed successfully'
});
}
});
}
});
} else {
res.send(400, {
message: 'Passwords do not match'
});
}
} else {
res.send(400, {
message: 'Current password is incorrect'
});
}
} else {
res.send(400, {
message: 'User is not found'
});
}
});
} else {
res.send(400, {
message: 'Please provide a new password'
});
}
} else {
res.send(400, {
message: 'User is not signed in'
});
}
};
/**
* Signout
*/
exports.signout = function(req, res) {
req.logout();
res.redirect('/');
};
/**
* Send User
*/
exports.me = function(req, res) {
res.jsonp(req.user || null);
};
/**
* OAuth callback
*/
exports.oauthCallback = function(strategy) {
return function(req, res, next) {
passport.authenticate(strategy, function(err, user, redirectURL) {
if (err || !user) {
return res.redirect('/#!/signin');
}
req.login(user, function(err) {
if (err) {
return res.redirect('/#!/signin');
}
return res.redirect(redirectURL || '/');
});
})(req, res, next);
};
};
/**
* User middleware
*/
exports.userByID = function(req, res, next, id) {
User.findOne({
_id: id
}).exec(function(err, user) {
if (err) return next(err);
if (!user) return next(new Error('Failed to load User ' + id));
req.profile = user;
next();
});
};
/**
* Require login routing middleware
*/
exports.requiresLogin = function(req, res, next) {
if (!req.isAuthenticated()) {
return res.send(401, {
message: 'User is not logged in'
});
}
next();
};
/**
* User authorizations routing middleware
*/
exports.hasAuthorization = function(roles) {
var _this = this;
return function(req, res, next) {
_this.requiresLogin(req, res, function() {
if (_.intersection(req.user.roles, roles).length) {
return next();
} else {
return res.send(403, {
message: 'User is not authorized'
});
}
});
};
};
/**
* Helper function to save or update a OAuth user profile
*/
exports.saveOAuthUserProfile = function(req, providerUserProfile, done) {
if (!req.user) {
// Define a search query fields
var searchMainProviderIdentifierField = 'providerData.' + providerUserProfile.providerIdentifierField;
var searchAdditionalProviderIdentifierField = 'additionalProvidersData.' + providerUserProfile.provider + '.' + providerUserProfile.providerIdentifierField;
// Define main provider search query
var mainProviderSearchQuery = {};
mainProviderSearchQuery.provider = providerUserProfile.provider;
mainProviderSearchQuery[searchMainProviderIdentifierField] = providerUserProfile.providerData[providerUserProfile.providerIdentifierField];
// Define additional provider search query
var additionalProviderSearchQuery = {};
additionalProviderSearchQuery[searchAdditionalProviderIdentifierField] = providerUserProfile.providerData[providerUserProfile.providerIdentifierField];
// Define a search query to find existing user with current provider profile
var searchQuery = {
$or: [mainProviderSearchQuery, additionalProviderSearchQuery]
};
User.findOne(searchQuery, function(err, user) {
if (err) {
return done(err);
} else {
if (!user) {
var possibleUsername = providerUserProfile.username || ((providerUserProfile.email) ? providerUserProfile.email.split('@')[0] : '');
User.findUniqueUsername(possibleUsername, null, function(availableUsername) {
user = new User({
firstName: providerUserProfile.firstName,
lastName: providerUserProfile.lastName,
username: availableUsername,
displayName: providerUserProfile.displayName,
email: providerUserProfile.email,
provider: providerUserProfile.provider,
providerData: providerUserProfile.providerData
});
// And save the user
user.save(function(err) {
return done(err, user);
});
});
} else {
return done(err, user);
}
}
});
} else {
// User is already logged in, join the provider data to the existing user
var user = req.user;
// Check if user exists, is not signed in using this provider, and doesn't have that provider data already configured
if (user.provider !== providerUserProfile.provider && (!user.additionalProvidersData || !user.additionalProvidersData[providerUserProfile.provider])) {
// Add the provider data to the additional provider data field
if (!user.additionalProvidersData) user.additionalProvidersData = {};
user.additionalProvidersData[providerUserProfile.provider] = providerUserProfile.providerData;
// Then tell mongoose that we've updated the additionalProvidersData field
user.markModified('additionalProvidersData');
// And save the user
user.save(function(err) {
return done(err, user, '/#!/settings/accounts');
});
} else {
return done(new Error('User is already connected using this provider'), user);
}
}
};
/**
* Remove OAuth provider
*/
exports.removeOAuthProvider = function(req, res, next) {
var user = req.user;
var provider = req.param('provider');
if (user && provider) {
// Delete the additional provider
if (user.additionalProvidersData[provider]) {
delete user.additionalProvidersData[provider];
// Then tell mongoose that we've updated the additionalProvidersData field
user.markModified('additionalProvidersData');
}
user.save(function(err) {
if (err) {
return res.send(400, {
message: getErrorMessage(err)
});
} else {
req.login(user, function(err) {
if (err) {
res.send(400, err);
} else {
res.jsonp(user);
}
});
}
});
}
};
module.exports.authentication = require('./users/users.authentication');
module.exports.authorization = require('./users/users.authorization');
module.exports.password = require('./users/users.password');
module.exports.profile = require('./users/users.profile');

View File

@@ -0,0 +1,98 @@
'use strict';
/**
* Module dependencies.
*/
var _ = require('lodash'),
errorHandler = require('../errors'),
mongoose = require('mongoose'),
passport = require('passport'),
User = mongoose.model('User');
/**
* Signup
*/
exports.signup = function(req, res) {
// For security measurement we remove the roles from the req.body object
delete req.body.roles;
// Init Variables
var user = new User(req.body);
var message = null;
// Add missing user fields
user.provider = 'local';
user.displayName = user.firstName + ' ' + user.lastName;
// Then save the user
user.save(function(err) {
if (err) {
return res.send(400, {
message: errorHandler.getErrorMessage(err)
});
} else {
// Remove sensitive data before login
user.password = undefined;
user.salt = undefined;
req.login(user, function(err) {
if (err) {
res.send(400, err);
} else {
res.jsonp(user);
}
});
}
});
};
/**
* Signin after passport authentication
*/
exports.signin = function(req, res, next) {
passport.authenticate('local', function(err, user, info) {
if (err || !user) {
res.send(400, info);
} else {
// Remove sensitive data before login
user.password = undefined;
user.salt = undefined;
req.login(user, function(err) {
if (err) {
res.send(400, err);
} else {
res.jsonp(user);
}
});
}
})(req, res, next);
};
/**
* Signout
*/
exports.signout = function(req, res) {
req.logout();
res.redirect('/');
};
/**
* OAuth callback
*/
exports.oauthCallback = function(strategy) {
return function(req, res, next) {
passport.authenticate(strategy, function(err, user, redirectURL) {
if (err || !user) {
return res.redirect('/#!/signin');
}
req.login(user, function(err) {
if (err) {
return res.redirect('/#!/signin');
}
return res.redirect(redirectURL || '/');
});
})(req, res, next);
};
};

View File

@@ -0,0 +1,54 @@
'use strict';
/**
* Module dependencies.
*/
var _ = require('lodash'),
mongoose = require('mongoose'),
User = mongoose.model('User');
/**
* User middleware
*/
exports.userByID = function(req, res, next, id) {
User.findOne({
_id: id
}).exec(function(err, user) {
if (err) return next(err);
if (!user) return next(new Error('Failed to load User ' + id));
req.profile = user;
next();
});
};
/**
* Require login routing middleware
*/
exports.requiresLogin = function(req, res, next) {
if (!req.isAuthenticated()) {
return res.send(401, {
message: 'User is not logged in'
});
}
next();
};
/**
* User authorizations routing middleware
*/
exports.hasAuthorization = function(roles) {
var _this = this;
return function(req, res, next) {
_this.requiresLogin(req, res, function() {
if (_.intersection(req.user.roles, roles).length) {
return next();
} else {
return res.send(403, {
message: 'User is not authorized'
});
}
});
};
};

View File

@@ -0,0 +1,244 @@
'use strict';
/**
* Module dependencies.
*/
var _ = require('lodash'),
errorHandler = require('../errors'),
mongoose = require('mongoose'),
passport = require('passport'),
User = mongoose.model('User'),
config = require('../../../config/config'),
swig = require('swig'),
nodemailer = require('nodemailer'),
crypto = require('crypto'),
async = require('async'),
crypto = require('crypto');
/**
* Forgot for reset password (forgot POST)
*/
exports.forgot = function(req, res, next) {
async.waterfall([
// Generate random token
function(done) {
crypto.randomBytes(20, function(err, buffer) {
var token = buffer.toString('hex');
done(err, token);
});
},
// Lookup user by username
function(token, done) {
if (req.body.username) {
User.findOne({
username: req.body.username
}, function(err, user) {
if (!user) {
return res.send(400, {
message: 'No account with that username has been found'
});
} else if (user.provider !== 'local') {
return res.send(400, {
message: 'It seems like you signed up using your ' + user.provider + ' account'
});
} else {
user.resetPasswordToken = token;
user.resetPasswordExpires = Date.now() + 3600000; // 1 hour
user.save(function(err) {
done(err, token, user);
});
}
});
} else {
return res.send(400, {
message: 'Username field must not be blank'
});
}
},
function(token, user, done) {
res.render('templates/reset-password-email', {
name: user.displayName,
appName: config.app.title,
url: 'http://' + req.headers.host + '/auth/reset/' + token
}, function(err, emailHTML) {
done(err, emailHTML, user);
});
},
// If valid email, send reset email using service
function(emailHTML, user, done) {
var smtpTransport = nodemailer.createTransport(config.mailer.options);
var mailOptions = {
to: user.email,
from: config.mailer.fromEmail,
subject: 'Password Reset',
html: emailHTML
};
smtpTransport.sendMail(mailOptions, function(err) {
res.send(200, {
message: 'An email has been sent to ' + user.email + ' with further instructions.'
});
done(err, 'done');
});
}
], function(err) {
if (err) return next(err);
});
};
/**
* Reset password GET from email token
*/
exports.validateResetToken = function(req, res) {
User.findOne({
resetPasswordToken: req.params.token,
resetPasswordExpires: {
$gt: Date.now()
}
}, function(err, user) {
if (!user) {
return res.redirect('/#!/password/reset/invalid');
}
res.redirect('/#!/password/reset/' + req.params.token);
});
};
/**
* Reset password POST from email token
*/
exports.reset = function(req, res, next) {
// Init Variables
var passwordDetails = req.body;
var message = null;
async.waterfall([
function(done) {
User.findOne({
resetPasswordToken: req.params.token,
resetPasswordExpires: {
$gt: Date.now()
}
}, function(err, user) {
if (!err && user) {
if (passwordDetails.newPassword === passwordDetails.verifyPassword) {
user.password = passwordDetails.newPassword;
user.resetPasswordToken = undefined;
user.resetPasswordExpires = undefined;
user.save(function(err) {
if (err) {
return res.send(400, {
message: errorHandler.getErrorMessage(err)
});
} else {
req.login(user, function(err) {
if (err) {
res.send(400, err);
} else {
// Return authenticated user
res.jsonp(user);
done(err, user);
}
});
}
});
} else {
return res.send(400, {
message: 'Passwords do not match'
});
}
} else {
return res.send(400, {
message: 'Password reset token is invalid or has expired.'
});
}
});
},
function(user, done) {
res.render('templates/reset-password-confirm-email', {
name: user.displayName
}, function(err, emailHTML) {
done(err, emailHTML, user);
});
},
// If valid email, send reset email using service
function(emailHTML, user, done) {
var smtpTransport = nodemailer.createTransport(config.mailer.options);
var mailOptions = {
to: user.email,
from: config.mailer.fromEmail,
subject: 'Your password has been changed',
html: emailHTML
};
smtpTransport.sendMail(mailOptions, function(err) {
done(err, 'done');
});
}
], function(err) {
if (err) return next(err);
});
};
/**
* Change Password
*/
exports.changePassword = function(req, res, next) {
// Init Variables
var passwordDetails = req.body;
var message = null;
if (req.user) {
if (passwordDetails.newPassword) {
User.findById(req.user.id, function(err, user) {
if (!err && user) {
if (user.authenticate(passwordDetails.currentPassword)) {
if (passwordDetails.newPassword === passwordDetails.verifyPassword) {
user.password = passwordDetails.newPassword;
user.save(function(err) {
if (err) {
return res.send(400, {
message: errorHandler.getErrorMessage(err)
});
} else {
req.login(user, function(err) {
if (err) {
res.send(400, err);
} else {
res.send({
message: 'Password changed successfully'
});
}
});
}
});
} else {
res.send(400, {
message: 'Passwords do not match'
});
}
} else {
res.send(400, {
message: 'Current password is incorrect'
});
}
} else {
res.send(400, {
message: 'User is not found'
});
}
});
} else {
res.send(400, {
message: 'Please provide a new password'
});
}
} else {
res.send(400, {
message: 'User is not signed in'
});
}
};

View File

@@ -0,0 +1,164 @@
'use strict';
/**
* Module dependencies.
*/
var _ = require('lodash'),
errorHandler = require('../errors'),
mongoose = require('mongoose'),
passport = require('passport'),
User = mongoose.model('User');
/**
* Update user details
*/
exports.update = function(req, res) {
// Init Variables
var user = req.user;
var message = null;
// For security measurement we remove the roles from the req.body object
delete req.body.roles;
if (user) {
// Merge existing user
user = _.extend(user, req.body);
user.updated = Date.now();
user.displayName = user.firstName + ' ' + user.lastName;
user.save(function(err) {
if (err) {
return res.send(400, {
message: errorHandler.getErrorMessage(err)
});
} else {
req.login(user, function(err) {
if (err) {
res.send(400, err);
} else {
res.jsonp(user);
}
});
}
});
} else {
res.send(400, {
message: 'User is not signed in'
});
}
};
/**
* Send User
*/
exports.me = function(req, res) {
res.jsonp(req.user || null);
};
/**
* Helper function to save or update a OAuth user profile
*/
exports.saveOAuthUserProfile = function(req, providerUserProfile, done) {
if (!req.user) {
// Define a search query fields
var searchMainProviderIdentifierField = 'providerData.' + providerUserProfile.providerIdentifierField;
var searchAdditionalProviderIdentifierField = 'additionalProvidersData.' + providerUserProfile.provider + '.' + providerUserProfile.providerIdentifierField;
// Define main provider search query
var mainProviderSearchQuery = {};
mainProviderSearchQuery.provider = providerUserProfile.provider;
mainProviderSearchQuery[searchMainProviderIdentifierField] = providerUserProfile.providerData[providerUserProfile.providerIdentifierField];
// Define additional provider search query
var additionalProviderSearchQuery = {};
additionalProviderSearchQuery[searchAdditionalProviderIdentifierField] = providerUserProfile.providerData[providerUserProfile.providerIdentifierField];
// Define a search query to find existing user with current provider profile
var searchQuery = {
$or: [mainProviderSearchQuery, additionalProviderSearchQuery]
};
User.findOne(searchQuery, function(err, user) {
if (err) {
return done(err);
} else {
if (!user) {
var possibleUsername = providerUserProfile.username || ((providerUserProfile.email) ? providerUserProfile.email.split('@')[0] : '');
User.findUniqueUsername(possibleUsername, null, function(availableUsername) {
user = new User({
firstName: providerUserProfile.firstName,
lastName: providerUserProfile.lastName,
username: availableUsername,
displayName: providerUserProfile.displayName,
email: providerUserProfile.email,
provider: providerUserProfile.provider,
providerData: providerUserProfile.providerData
});
// And save the user
user.save(function(err) {
return done(err, user);
});
});
} else {
return done(err, user);
}
}
});
} else {
// User is already logged in, join the provider data to the existing user
var user = req.user;
// Check if user exists, is not signed in using this provider, and doesn't have that provider data already configured
if (user.provider !== providerUserProfile.provider && (!user.additionalProvidersData || !user.additionalProvidersData[providerUserProfile.provider])) {
// Add the provider data to the additional provider data field
if (!user.additionalProvidersData) user.additionalProvidersData = {};
user.additionalProvidersData[providerUserProfile.provider] = providerUserProfile.providerData;
// Then tell mongoose that we've updated the additionalProvidersData field
user.markModified('additionalProvidersData');
// And save the user
user.save(function(err) {
return done(err, user, '/#!/settings/accounts');
});
} else {
return done(new Error('User is already connected using this provider'), user);
}
}
};
/**
* Remove OAuth provider
*/
exports.removeOAuthProvider = function(req, res, next) {
var user = req.user;
var provider = req.param('provider');
if (user && provider) {
// Delete the additional provider
if (user.additionalProvidersData[provider]) {
delete user.additionalProvidersData[provider];
// Then tell mongoose that we've updated the additionalProvidersData field
user.markModified('additionalProvidersData');
}
user.save(function(err) {
if (err) {
return res.send(400, {
message: errorHandler.getErrorMessage(err)
});
} else {
req.login(user, function(err) {
if (err) {
res.send(400, err);
} else {
res.jsonp(user);
}
});
}
});
}
};

View File

@@ -50,7 +50,7 @@ var UserSchema = new Schema({
},
username: {
type: String,
unique: true,
unique: 'testing error message',
required: 'Please fill in a username',
trim: true
},

View File

@@ -10,12 +10,12 @@ module.exports = function(app) {
// Article Routes
app.route('/articles')
.get(articles.list)
.post(users.requiresLogin, articles.create);
.post(users.authorization.requiresLogin, articles.create);
app.route('/articles/:articleId')
.get(articles.read)
.put(users.requiresLogin, articles.hasAuthorization, articles.update)
.delete(users.requiresLogin, articles.hasAuthorization, articles.delete);
.put(users.authorization.requiresLogin, articles.hasAuthorization, articles.update)
.delete(users.authorization.requiresLogin, articles.hasAuthorization, articles.delete);
// Finish by binding the article middleware
app.param('articleId', articles.articleByID);

View File

@@ -8,28 +8,32 @@ var passport = require('passport');
module.exports = function(app) {
// User Routes
var users = require('../../app/controllers/users');
app.route('/users/me').get(users.me);
app.route('/users').put(users.update);
app.route('/users/password').post(users.changePassword);
app.route('/users/accounts').delete(users.removeOAuthProvider);
// Setting up the users api
app.route('/auth/signup').post(users.signup);
app.route('/auth/signin').post(users.signin);
app.route('/auth/signout').get(users.signout);
app.route('/auth/forgot').post(users.forgot);
app.route('/auth/reset/:token').get(users.resetGet);
app.route('/auth/reset/:token').post(users.resetPost);
// Setting up the users profile api
app.route('/users/me').get(users.profile.me);
app.route('/users').put(users.profile.update);
app.route('/users/accounts').delete(users.profile.removeOAuthProvider);
// Setting up the users password api
app.route('/users/password').post(users.password.changePassword);
app.route('/auth/forgot').post(users.password.forgot);
app.route('/auth/reset/:token').get(users.password.validateResetToken);
app.route('/auth/reset/:token').post(users.password.reset);
// Setting up the users authentication api
app.route('/auth/signup').post(users.authentication.signup);
app.route('/auth/signin').post(users.authentication.signin);
app.route('/auth/signout').get(users.authentication.signout);
// Setting the facebook oauth routes
app.route('/auth/facebook').get(passport.authenticate('facebook', {
scope: ['email']
}));
app.route('/auth/facebook/callback').get(users.oauthCallback('facebook'));
app.route('/auth/facebook/callback').get(users.authentication.oauthCallback('facebook'));
// Setting the twitter oauth routes
app.route('/auth/twitter').get(passport.authenticate('twitter'));
app.route('/auth/twitter/callback').get(users.oauthCallback('twitter'));
app.route('/auth/twitter/callback').get(users.authentication.oauthCallback('twitter'));
// Setting the google oauth routes
app.route('/auth/google').get(passport.authenticate('google', {
@@ -38,12 +42,12 @@ module.exports = function(app) {
'https://www.googleapis.com/auth/userinfo.email'
]
}));
app.route('/auth/google/callback').get(users.oauthCallback('google'));
app.route('/auth/google/callback').get(users.authentication.oauthCallback('google'));
// Setting the linkedin oauth routes
app.route('/auth/linkedin').get(passport.authenticate('linkedin'));
app.route('/auth/linkedin/callback').get(users.oauthCallback('linkedin'));
app.route('/auth/linkedin/callback').get(users.authentication.oauthCallback('linkedin'));
// Finish by binding the user middleware
app.param('userId', users.userByID);
app.param('userId', users.authorization.userByID);
};

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
</head>
<body>
<p>Dear {{name}},</p>
<p></p>
<p>This is a confirmation that the password for your account has just been changed</p>
<br>
<br>
<p>The {{appName}} Support Team</p>
</body>
</html>

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
</head>
<body>
<p>Dear {{name}},</p>
<br>
<p>
You have requested to have your password reset for your account at {{appName}}
</p>
<p>Please visit this url to reset your password:</p>
<p>{{url}}</p>
<strong>If you didn't make this request, you can ignore this email.</strong>
<br>
<br>
<p>The {{appName}} Support Team</p>
</body>
</html>

View File

@@ -1,6 +1,6 @@
{
"name": "meanjs",
"version": "0.3.1",
"version": "0.4.0",
"description": "Fullstack JavaScript with MongoDB, Express, AngularJS, and Node.js.",
"dependencies": {
"bootstrap": "~3",

View File

@@ -22,7 +22,7 @@ module.exports.getGlobbedFiles = function(globPatterns, removeRoot) {
var _this = this;
// URL paths regex
var urlRegex = new RegExp('^(?:[a-z]+:)?//', 'i');
var urlRegex = new RegExp('^(?:[a-z]+:)?\/\/', 'i');
// The output array
var output = [];

View File

@@ -1,9 +1,7 @@
'use strict';
var DB_HOST = process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost';
module.exports = {
db: 'mongodb://' + DB_HOST + '/mean-dev',
db: 'mongodb://' + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + '/mean-dev',
app: {
title: 'MEAN.JS - Development Environment'
},
@@ -26,5 +24,15 @@ module.exports = {
clientID: process.env.LINKEDIN_ID || 'APP_ID',
clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET',
callbackURL: 'http://localhost:3000/auth/linkedin/callback'
},
mailer: {
fromEmail: process.env.MAILER_FROM_EMAIL || 'MAILER_FROM_EMAIL',
options: {
service: process.env.MAILER_SERVICE_PROVIDER || 'MAILER_SERVICE_PROVIDER',
auth: {
user: process.env.MAILER_EMAIL_ID || 'MAILER_EMAIL_ID',
pass: process.env.MAILER_PASSWORD || 'MAILER_PASSWORD'
}
}
}
};

View File

@@ -1,9 +1,7 @@
'use strict';
var DB_HOST = process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost';
module.exports = {
db: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://' + DB_HOST + '/mean',
db: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://' + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + '/mean',
assets: {
lib: {
css: [
@@ -41,5 +39,15 @@ module.exports = {
clientID: process.env.LINKEDIN_ID || 'APP_ID',
clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET',
callbackURL: 'http://localhost:3000/auth/linkedin/callback'
},
mailer: {
fromEmail: process.env.MAILER_FROM_EMAIL || 'MAILER_FROM_EMAIL',
options: {
service: process.env.MAILER_SERVICE_PROVIDER || 'MAILER_SERVICE_PROVIDER',
auth: {
user: process.env.MAILER_EMAIL_ID || 'MAILER_EMAIL_ID',
pass: process.env.MAILER_PASSWORD || 'MAILER_PASSWORD'
}
}
}
};

14
config/env/test.js vendored
View File

@@ -1,9 +1,7 @@
'use strict';
var DB_HOST = process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost';
module.exports = {
db: 'mongodb://' + DB_HOST + '/mean-test',
db: 'mongodb://' + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + '/mean-test',
port: 3001,
app: {
title: 'MEAN.JS - Test Environment'
@@ -27,5 +25,15 @@ module.exports = {
clientID: process.env.LINKEDIN_ID || 'APP_ID',
clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET',
callbackURL: 'http://localhost:3000/auth/linkedin/callback'
},
mailer: {
fromEmail: process.env.MAILER_FROM_EMAIL || 'MAILER_FROM_EMAIL',
options: {
service: process.env.MAILER_SERVICE_PROVIDER || 'MAILER_SERVICE_PROVIDER',
auth: {
user: process.env.MAILER_EMAIL_ID || 'MAILER_EMAIL_ID',
pass: process.env.MAILER_PASSWORD || 'MAILER_PASSWORD'
}
}
}
};

View File

@@ -73,7 +73,9 @@ module.exports = function(db) {
}
// Request body parsing middleware should be above methodOverride
app.use(bodyParser.urlencoded());
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(bodyParser.json());
app.use(methodOverride());
@@ -85,6 +87,8 @@ module.exports = function(db) {
// Express MongoDB session storage
app.use(session({
saveUninitialized: true,
resave: true,
secret: config.sessionSecret,
store: new mongoStore({
db: db.connection.db,
@@ -101,8 +105,8 @@ module.exports = function(db) {
// Use helmet to secure Express headers
app.use(helmet.xframe());
app.use(helmet.iexss());
app.use(helmet.contentTypeOptions());
app.use(helmet.xssFilter());
app.use(helmet.nosniff());
app.use(helmet.ienoopen());
app.disable('x-powered-by');

View File

@@ -19,9 +19,9 @@ module.exports = function() {
console.log();
if (!environmentFiles.length) {
if (process.env.NODE_ENV) {
console.log('\x1b[31m', 'No configuration file found for "' + process.env.NODE_ENV + '" environment using development instead');
console.error('\x1b[31m', 'No configuration file found for "' + process.env.NODE_ENV + '" environment using development instead');
} else {
console.log('\x1b[31m', 'NODE_ENV is not defined! Using default development environment');
console.error('\x1b[31m', 'NODE_ENV is not defined! Using default development environment');
}
process.env.NODE_ENV = 'development';

View File

@@ -5,7 +5,7 @@ module.exports = function(grunt) {
var watchFiles = {
serverViews: ['app/views/**/*.*'],
serverJS: ['gruntfile.js', 'server.js', 'config/**/*.js', 'app/**/*.js'],
clientViews: ['public/modules/**/views/*.html'],
clientViews: ['public/modules/**/views/**/*.html'],
clientJS: ['public/js/*.js', 'public/modules/**/*.js'],
clientCSS: ['public/modules/**/*.css'],
mochaTests: ['app/tests/**/*.js']

View File

@@ -1,7 +1,7 @@
{
"name": "meanjs",
"description": "Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js.",
"version": "0.3.1",
"version": "0.4.0",
"private": false,
"author": "https://github.com/meanjs/mean/graphs/contributors",
"repository": {
@@ -18,18 +18,18 @@
"postinstall": "bower install --config.interactive=false"
},
"dependencies": {
"express": "~4.2.0",
"express-session": "~1.1.0",
"body-parser": "~1.2.0",
"cookie-parser": "~1.1.0",
"compression": "~1.0.1",
"method-override": "~1.0.0",
"morgan": "~1.1.0",
"connect-mongo": "~0.4.0",
"express": "~4.7.2",
"express-session": "~1.7.2",
"body-parser": "~1.5.2",
"cookie-parser": "~1.3.2",
"compression": "~1.0.9",
"method-override": "~2.1.2",
"morgan": "~1.2.2",
"connect-mongo": "~0.4.1",
"connect-flash": "~0.1.1",
"helmet": "~0.2.1",
"helmet": "~0.4.0",
"consolidate": "~0.10.0",
"swig": "~1.3.2",
"swig": "~1.4.1",
"mongoose": "~3.8.8",
"passport": "~0.2.0",
"passport-local": "~1.0.0",
@@ -39,29 +39,28 @@
"passport-google-oauth": "~0.1.5",
"lodash": "~2.4.1",
"forever": "~0.11.0",
"bower": "~1.3.1",
"bower": "~1.3.8",
"grunt-cli": "~0.1.13",
"glob": "~3.2.9",
"bcrypt-nodejs": "0.0.3",
"async": "~0.8.0",
"nodemailer": "~0.6.3"
"glob": "~4.0.5",
"async": "~0.9.0",
"nodemailer": "~1.1.1"
},
"devDependencies": {
"supertest": "~0.12.1",
"should": "~3.3.1",
"supertest": "~0.13.0",
"should": "~4.0.4",
"grunt-env": "~0.4.1",
"grunt-node-inspector": "~0.1.3",
"grunt-contrib-watch": "~0.6.1",
"grunt-contrib-jshint": "~0.10.0",
"grunt-contrib-csslint": "^0.2.0",
"grunt-ngmin": "0.0.3",
"grunt-contrib-uglify": "~0.4.0",
"grunt-contrib-cssmin": "~0.9.0",
"grunt-nodemon": "~0.2.1",
"grunt-contrib-uglify": "~0.5.1",
"grunt-contrib-cssmin": "~0.10.0",
"grunt-nodemon": "~0.3.0",
"grunt-concurrent": "~0.5.0",
"grunt-mocha-test": "~0.10.0",
"grunt-mocha-test": "~0.11.0",
"grunt-karma": "~0.8.2",
"load-grunt-tasks": "~0.4.0",
"load-grunt-tasks": "~0.6.0",
"karma": "~0.12.0",
"karma-jasmine": "~0.2.1",
"karma-coverage": "~0.2.0",

View File

@@ -7,9 +7,9 @@ var ApplicationConfiguration = (function() {
var applicationModuleVendorDependencies = ['ngResource', 'ngAnimate', 'ui.router', 'ui.bootstrap', 'ui.utils'];
// Add a new vertical module
var registerModule = function(moduleName) {
var registerModule = function(moduleName, dependencies) {
// Create angular module
angular.module(moduleName, []);
angular.module(moduleName, dependencies || []);
// Add the module to the AngularJS configuration file
angular.module(applicationModuleName).requires.push(moduleName);

View File

@@ -49,7 +49,7 @@ angular.module('articles').controller('ArticlesController', ['$scope', '$statePa
$scope.error = errorResponse.data.message;
});
} else {
scope.submitted = true;
$scope.submitted = true;
}
};

View File

@@ -19,19 +19,27 @@ angular.module('users').config(['$stateProvider',
}).
state('signup', {
url: '/signup',
templateUrl: 'modules/users/views/signup.client.view.html'
templateUrl: 'modules/users/views/authentication/signup.client.view.html'
}).
state('signin', {
url: '/signin',
templateUrl: 'modules/users/views/signin.client.view.html'
templateUrl: 'modules/users/views/authentication/signin.client.view.html'
}).
state('forgot', {
url: '/forgot',
templateUrl: 'modules/users/views/forgot.client.view.html'
url: '/password/forgot',
templateUrl: 'modules/users/views/password/forgot-password.client.view.html'
}).
state('reset-invlaid', {
url: '/password/reset/invalid',
templateUrl: 'modules/users/views/password/reset-password-invalid.client.view.html'
}).
state('reset-success', {
url: '/password/reset/success',
templateUrl: 'modules/users/views/password/reset-password-success.client.view.html'
}).
state('reset', {
url: '/reset/:token',
templateUrl: 'modules/users/views/reset.client.view.html'
url: '/password/reset/:token',
templateUrl: 'modules/users/views/password/reset-password.client.view.html'
});
}
]);

View File

@@ -1,19 +1,19 @@
'use strict';
angular.module('users').controller('AuthenticationController', ['$scope', '$stateParams', '$http', '$location', 'Authentication',
function($scope, $stateParams, $http, $location, Authentication) {
$scope.authentication = Authentication;
function($scope, $stateParams, $http, $location, Authentication) {
$scope.authentication = Authentication;
//If user is signed in then redirect back home
// If user is signed in then redirect back home
if ($scope.authentication.user) $location.path('/');
$scope.signup = function(isValid) {
if (isValid){
if (isValid) {
$http.post('/auth/signup', $scope.credentials).success(function(response) {
//If successful we assign the response to the global user model
// If successful we assign the response to the global user model
$scope.authentication.user = response;
//And redirect to the index page
// And redirect to the index page
$location.path('/');
}).error(function(response) {
$scope.error = response.message;
@@ -25,45 +25,14 @@ angular.module('users').controller('AuthenticationController', ['$scope', '$stat
$scope.signin = function() {
$http.post('/auth/signin', $scope.credentials).success(function(response) {
//If successful we assign the response to the global user model
// If successful we assign the response to the global user model
$scope.authentication.user = response;
//And redirect to the index page
$location.path('/');
}).error(function(response) {
$scope.error = response.message;
});
};
$scope.forgot = function() {
$scope.success = $scope.error = null;
$http.post('/auth/forgot', $scope.credentials).success(function(response) {
// Show user success message and clear form
$scope.credentials = null;
$scope.success = response.message;
}).error(function(response) {
// Show user error message and clear form
$scope.credentials = null;
$scope.error = response.message;
});
};
// Change user password
$scope.reset = function() {
$scope.success = $scope.error = null;
$http.post('/auth/reset/' + $stateParams.token,
$scope.passwordDetails).success(function(response) {
// If successful show success message and clear form
$scope.success = response.message;
$scope.passwordDetails = null;
}).error(function(response) {
$scope.error = response.message;
});
};
}
]);
// And redirect to the index page
$location.path('/');
}).error(function(response) {
$scope.error = response.message;
});
};
}
]);

View File

@@ -0,0 +1,44 @@
'use strict';
angular.module('users').controller('PasswordController', ['$scope', '$stateParams', '$http', '$location', 'Authentication',
function($scope, $stateParams, $http, $location, Authentication) {
$scope.authentication = Authentication;
//If user is signed in then redirect back home
if ($scope.authentication.user) $location.path('/');
// Submit forgotten password account id
$scope.askForPasswordReset = function() {
$scope.success = $scope.error = null;
$http.post('/auth/forgot', $scope.credentials).success(function(response) {
// Show user success message and clear form
$scope.credentials = null;
$scope.success = response.message;
}).error(function(response) {
// Show user error message and clear form
$scope.credentials = null;
$scope.error = response.message;
});
};
// Change user password
$scope.resetUserPassword = function() {
$scope.success = $scope.error = null;
$http.post('/auth/reset/' + $stateParams.token, $scope.passwordDetails).success(function(response) {
// If successful show success message and clear form
$scope.passwordDetails = null;
// Attach user profile
Authentication.user = response;
// And redirect to the index page
$location.path('/password/reset/success');
}).error(function(response) {
$scope.error = response.message;
});
};
}
]);

View File

@@ -31,7 +31,7 @@
<a href="/#!/signup">Sign up</a>
</div>
<div class"forgot-password">
<a href="/#!/forgot">Forgot your password?</a>
<a href="/#!/password/forgot">Forgot your password?</a>
</div>
<div data-ng-show="error" class="text-center text-danger">
<strong data-ng-bind="error"></strong>

View File

@@ -1,11 +1,12 @@
<section class="row" data-ng-controller="AuthenticationController">
<h3 class="col-md-12 text-center">Forgot your password?</h3>
<section class="row" data-ng-controller="PasswordController">
<h3 class="col-md-12 text-center">Restore your password</h3>
<p class="small text-center">Enter your account username.</p>
<div class="col-xs-offset-2 col-xs-8 col-md-offset-5 col-md-2">
<form data-ng-submit="forgot()" class="signin form-horizontal" autocomplete="off">
<form data-ng-submit="askForPasswordReset()" class="signin form-horizontal" autocomplete="off">
<fieldset>
<div class="form-group">
<input type="text" id="email" name="email" class="form-control" data-ng-model="credentials.email" placeholder="Account Email">
</div>
<div class="form-group">
<input type="text" id="username" name="username" class="form-control" data-ng-model="credentials.username" placeholder="Username">
</div>
<div class="text-center form-group">
<button type="submit" class="btn btn-primary">Submit</button>
</div>

View File

@@ -0,0 +1,4 @@
<section class="row text-center">
<h3 class="col-md-12">Password reset is invalid</h3>
<a href="/#!/password/forgot" class="col-md-12">Ask for a new password reset</a>
</section>

View File

@@ -0,0 +1,4 @@
<section class="row text-center">
<h3 class="col-md-12">Password successfully reset</h3>
<a href="/#!/" class="col-md-12">Continue to home page</a>
</section>

View File

@@ -1,7 +1,7 @@
<section class="row" data-ng-controller="AuthenticationController">
<section class="row" data-ng-controller="PasswordController">
<h3 class="col-md-12 text-center">Reset your password</h3>
<div class="col-xs-offset-2 col-xs-8 col-md-offset-5 col-md-2">
<form data-ng-submit="reset()" class="signin form-horizontal" autocomplete="off">
<form data-ng-submit="resetUserPassword()" class="signin form-horizontal" autocomplete="off">
<fieldset>
<div class="form-group">
<label for="newPassword">New Password</label>

View File

@@ -12,19 +12,24 @@ var init = require('./config/init')(),
*/
// Bootstrap db connection
var db = mongoose.connect(config.db);
var db = mongoose.connect(config.db, function(err) {
if (err) {
console.log(err);
console.error('\x1b[31m', 'Could not connect to MongoDB!');
} else {
// Init the express application
var app = require('./config/express')(db);
// Init the express application
var app = require('./config/express')(db);
// Bootstrap passport config
require('./config/passport')();
// Bootstrap passport config
require('./config/passport')();
// Start the app by listening on <port>
app.listen(config.port);
// Start the app by listening on <port>
app.listen(config.port);
// Expose app
exports = module.exports = app;
// Expose app
exports = module.exports = app;
// Logging initialization
console.log('MEAN.JS application started on port ' + config.port);
// Logging initialization
console.log('MEAN.JS application started on port ' + config.port);
}
});