diff --git a/app/controllers/users.js b/app/controllers/users.js index a291b73383..e97bdd6227 100755 --- a/app/controllers/users.js +++ b/app/controllers/users.js @@ -5,7 +5,29 @@ */ var mongoose = require('mongoose'), passport = require('passport'), - User = mongoose.model('User'); + User = mongoose.model('User'), + _ = require('lodash'); + +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 @@ -21,26 +43,22 @@ exports.signup = function(req, res) { user.save(function(err) { if (err) { - switch (err.code) { - case 11000: - case 11001: - message = 'Email or username already exists'; - break; - default: - message = 'Please fill all the required fields'; - } - return res.send(400, { - message: message + 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); + } }); } - req.logIn(user, function(err) { - if (err) { - res.send(400, err); - } else { - res.jsonp(user); - } - }); }); }; @@ -52,7 +70,11 @@ exports.signin = function(req, res, next) { if (err || !user) { res.send(400, info); } else { - req.logIn(user, function(err) { + // Remove sensitive data before login + user.password = undefined; + user.salt = undefined; + + req.login(user, function(err) { if (err) { res.send(400, err); } else { @@ -63,6 +85,98 @@ exports.signin = function(req, res, next) { })(req, res, next); }; +/** + * Update user details + */ +exports.update = function(req, res) { + // Init Variables + var user = req.user; + var message = null; + + 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) { + 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: 'User is not signed in' + }); + } +}; + /** * Signout */ @@ -79,10 +193,24 @@ exports.me = function(req, res) { }; /** - * Auth callback + * OAuth callback */ -exports.authCallback = function(req, res) { - res.redirect('/'); +exports.oauthCallback = function(strategy) { + return function(req, res, next) { + passport.authenticate(strategy, function(err, user, email) { + if (err || !user) { + console.log(err); + return res.redirect('/#!/signin'); + } + req.login(user, function(err) { + if (err) { + return res.redirect('/#!/signin'); + } + + return res.redirect('/'); + }); + })(req, res, next); + }; }; /** @@ -106,6 +234,7 @@ exports.requiresLogin = function(req, res, next) { if (!req.isAuthenticated()) { return res.send(401, 'User is not logged in'); } + next(); }; @@ -116,5 +245,6 @@ exports.hasAuthorization = function(req, res, next) { if (req.profile.id !== req.user.id) { return res.send(403, 'User is not authorized'); } + next(); }; \ No newline at end of file diff --git a/app/models/user.js b/app/models/user.js index 9b0398f1be..6549d2508b 100755 --- a/app/models/user.js +++ b/app/models/user.js @@ -7,131 +7,119 @@ var mongoose = require('mongoose'), Schema = mongoose.Schema, crypto = require('crypto'); +/** + * A Validation function for local strategy properties + */ +var validateLocalStrategyProperty = function(property) { + return ((this.provider !== 'local' && !this.updated) || property.length); +}; + +/** + * A Validation function for local strategy password + */ +var validateLocalStrategyPassword = function(password) { + return (this.provider !== 'local' || (password && password.length > 6)); +}; + /** * User Schema */ var UserSchema = new Schema({ firstName: { type: String, + trim: true, default: '', - trim: true + validate: [validateLocalStrategyProperty, 'Please fill in your first name'] }, lastName: { type: String, + trim: true, default: '', - trim: true + validate: [validateLocalStrategyProperty, 'Please fill in your last name'] }, displayName: { type: String, - default: '', trim: true }, email: { type: String, - default: '', trim: true, + default: '', + validate: [validateLocalStrategyProperty, 'Please fill in your email'], + match: [/.+\@.+\..+/, 'Please fill a valid email address'] }, username: { + type: String, + unique: true, + required: 'Please fill in a username', + trim: true + }, + password: { type: String, default: '', - trim: true, - unique: true + validate: [validateLocalStrategyPassword, 'Password should be longer'] + }, + salt: { + type: String }, provider: { type: String, - required: true + required: 'Provider is required' }, - hashed_password: String, - salt: String, - providerData: {} + providerData: {}, + updated: { + type: Date + }, + created: { + type: Date, + default: Date.now + } }); /** - * Virtuals + * Hook a pre save method to hash the password */ -UserSchema.virtual('password').set(function(password) { - this._password = password; - this.salt = this.makeSalt(); - this.hashed_password = this.encryptPassword(password); -}).get(function() { - return this._password; +UserSchema.pre('save', function(next) { + if (this.password && this.password.length > 6) { + this.salt = new Buffer(crypto.randomBytes(16).toString('base64'), 'base64'); + this.password = this.hashPassword(this.password); + } + + next(); }); /** - * Validations + * Create instance method for hashing a password */ -var validatePresenceOf = function(value) { - return value && value.length; +UserSchema.methods.hashPassword = function(password) { + return crypto.pbkdf2Sync(password, this.salt, 10000, 64).toString('base64'); }; -// The below 5 validations only apply if you are signing up traditionally -UserSchema.path('firstName').validate(function(firstName) { - if (this.provider !== 'local') return true; - else return firstName.length; -}, 'First name cannot be blank'); -UserSchema.path('lastName').validate(function(lastName) { - if (this.provider !== 'local') return true; - else return lastName.length; -}, 'Last name cannot be blank'); -UserSchema.path('email').validate(function(email) { - if (this.provider !== 'local') return true; - else return email.length; -}, 'Email cannot be blank'); -UserSchema.path('username').validate(function(username) { - if (this.provider !== 'local') return true; - else return username.length; -}, 'Username cannot be blank'); -UserSchema.path('hashed_password').validate(function(hashed_password) { - if (this.provider !== 'local') return true; - else return hashed_password.length; -}, 'Password cannot be blank'); - /** - * Pre-save hook + * Create instance method for authenticating user */ -UserSchema.pre('save', function(next) { - if (!this.isNew) return next(); - - if (!validatePresenceOf(this.password) && this.provider === 'local') next(new Error('Invalid password')); - else next(); -}); +UserSchema.methods.authenticate = function(password) { + return this.password === this.hashPassword(password); +}; /** - * Methods + * Find possible not used username */ -UserSchema.methods = { - /** - * Authenticate - check if the passwords are the same - * - * @param {String} plainText - * @return {Boolean} - * @api public - */ - authenticate: function(plainText) { - return this.encryptPassword(plainText) === this.hashed_password; - }, - - /** - * Make salt - * - * @return {String} - * @api public - */ - makeSalt: function() { - return Math.round((new Date().valueOf() * Math.random())) + ''; - }, +UserSchema.statics.findUniqueUsername = function(username, suffix, callback) { + var _this = this; + var possibleUsername = username + (suffix || ''); - /** - * Encrypt password - * - * @param {String} password - * @return {String} - * @api public - */ - encryptPassword: function(password) { - if (!password) return ''; - return crypto.createHmac('sha1', this.salt).update(password).digest('hex'); - } + _this.findOne({username: possibleUsername}, function(err, user) { + if(!err) { + if (!user) { + callback(possibleUsername); + } else { + return _this.findUniqueUsername(username, (suffix || 0) + 1, callback); + } + } else { + callback(null); + } + }); }; mongoose.model('User', UserSchema); \ No newline at end of file diff --git a/app/routes/users.js b/app/routes/users.js index 32f076f346..c5c3590660 100644 --- a/app/routes/users.js +++ b/app/routes/users.js @@ -6,6 +6,8 @@ module.exports = function(app) { // User Routes var users = require('../../app/controllers/users'); app.get('/users/me', users.me); + app.put('/users', users.update); + app.post('/users/password', users.changePassword); // Setting up the users api app.post('/auth/signup', users.signup); @@ -14,40 +16,26 @@ module.exports = function(app) { // Setting the facebook oauth routes app.get('/auth/facebook', passport.authenticate('facebook', { - scope: ['email'], - failureRedirect: '/#!/signin' - }), users.signin); - app.get('/auth/facebook/callback', passport.authenticate('facebook', { - failureRedirect: '/#!/signin' - }), users.authCallback); + scope: ['email'] + })); + app.get('/auth/facebook/callback', users.oauthCallback('facebook')); // Setting the twitter oauth routes - app.get('/auth/twitter', passport.authenticate('twitter', { - failureRedirect: '/#!/signin' - }), users.signin); - app.get('/auth/twitter/callback', passport.authenticate('twitter', { - failureRedirect: '/#!/signin' - }), users.authCallback); + app.get('/auth/twitter', passport.authenticate('twitter')); + app.get('/auth/twitter/callback', users.oauthCallback('twitter')); // Setting the google oauth routes app.get('/auth/google', passport.authenticate('google', { - failureRedirect: '/#!/signin', scope: [ 'https://www.googleapis.com/auth/userinfo.profile', 'https://www.googleapis.com/auth/userinfo.email' ] - }), users.signin); - app.get('/auth/google/callback', passport.authenticate('google', { - failureRedirect: '/#!/signin' - }), users.authCallback); + })); + app.get('/auth/google/callback', users.oauthCallback('google')); // Setting the linkedin oauth routes - app.get('/auth/linkedin', passport.authenticate('linkedin', { - failureRedirect: '/#!/signin' - }), users.signin); - app.get('/auth/linkedin/callback', passport.authenticate('linkedin', { - failureRedirect: '/#!/signin' - }), users.authCallback); + app.get('/auth/linkedin', passport.authenticate('linkedin')); + app.get('/auth/linkedin/callback', users.oauthCallback('linkedin')); // Finish by binding the user middleware app.param('userId', users.userByID); diff --git a/config/env/development.js b/config/env/development.js index dc5837249b..165f88e030 100644 --- a/config/env/development.js +++ b/config/env/development.js @@ -6,23 +6,23 @@ module.exports = { title: 'MEAN.JS - Development Environment' }, facebook: { - clientID: 'APP_ID', - clientSecret: 'APP_SECRET', - callbackURL: 'http://localhost:3000/auth/facebook/callback' + clientID: '588647347851720', + clientSecret: 'd2870185a0b41ab0ec32ac9d023be5b0', + callbackURL: 'http://local.meanjs.herokuapp.com:3000/auth/facebook/callback' }, twitter: { - clientID: 'CONSUMER_KEY', - clientSecret: 'CONSUMER_SECRET', + clientID: 'f9JcCc0xSzEUkwF5E5ZKLQ', + clientSecret: 'E9zzKZhZlZuy5T1qMsu3c75EkGf9yVwp0uAIOwtI0oM', callbackURL: 'http://localhost:3000/auth/twitter/callback' }, google: { - clientID: 'APP_ID', - clientSecret: 'APP_SECRET', + clientID: '751147574067-kt9q7nnkvns3b8cg742nsddk9d77k0bt.apps.googleusercontent.com', + clientSecret: '-7acCDhnsbf22HoHB_8CkAHi', callbackURL: 'http://localhost:3000/auth/google/callback' }, linkedin: { - clientID: 'APP_ID', - clientSecret: 'APP_SECRET', + clientID: '77f1ywm1byjwpm', + clientSecret: 'K6D9cufcuNIjcqUr', callbackURL: 'http://localhost:3000/auth/linkedin/callback' } }; \ No newline at end of file diff --git a/config/passport.js b/config/passport.js index a931afc0c8..9941f783ab 100755 --- a/config/passport.js +++ b/config/passport.js @@ -15,7 +15,7 @@ module.exports = function() { passport.deserializeUser(function(id, done) { User.findOne({ _id: id - }, '-salt -hashed_password', function(err, user) { + }, '-salt -password', function(err, user) { done(err, user); }); }); diff --git a/config/strategies/facebook.js b/config/strategies/facebook.js index ef6c98667f..4802a46f1f 100644 --- a/config/strategies/facebook.js +++ b/config/strategies/facebook.js @@ -11,32 +11,39 @@ module.exports = function() { clientID: config.facebook.clientID, clientSecret: config.facebook.clientSecret, callbackURL: config.facebook.callbackURL, + passReqToCallback: true }, - function(accessToken, refreshToken, profile, done) { - User.findOne({ - 'providerData.id': profile.id - }, function(err, user) { - if (err) { - return done(err); - } - if (!user) { - user = new User({ - firstName: profile.name.givenName, - lastName: profile.name.familyName, - displayName: profile.displayName, - email: profile.emails[0].value, - username: profile.username, - provider: 'facebook', - providerData: profile._json - }); - user.save(function(err) { - if (err) console.log(err); + function(req, accessToken, refreshToken, profile, done) { + if (req.user) { + return done(new Error('User is already signed in'), req.user); + } else { + User.findOne({ + 'provider': 'facebook', + 'providerData.id': profile.id + }, function(err, user) { + if (err) { + return done(err); + } + if (!user) { + User.findUniqueUsername(profile.username, null, function(availableUsername) { + user = new User({ + firstName: profile.name.givenName, + lastName: profile.name.familyName, + displayName: profile.displayName, + email: profile.emails[0].value, + username: availableUsername, + provider: 'facebook', + providerData: profile._json + }); + user.save(function(err) { + return done(err, user); + }); + }); + } else { return done(err, user); - }); - } else { - return done(err, user); - } - }); + } + }); + } } )); }; \ No newline at end of file diff --git a/config/strategies/google.js b/config/strategies/google.js index 15b5acbade..d86e025559 100644 --- a/config/strategies/google.js +++ b/config/strategies/google.js @@ -10,30 +10,39 @@ module.exports = function() { passport.use(new GoogleStrategy({ clientID: config.google.clientID, clientSecret: config.google.clientSecret, - callbackURL: config.google.callbackURL + callbackURL: config.google.callbackURL, + passReqToCallback: true }, - function(accessToken, refreshToken, profile, done) { - User.findOne({ - 'providerData.id': profile.id - }, function(err, user) { - if (!user) { - user = new User({ - firstName: profile.name.givenName, - lastName: profile.name.familyName, - displayName: profile.displayName, - email: profile.emails[0].value, - username: profile.emails[0].value, - provider: 'google', - providerData: profile._json - }); - user.save(function(err) { - if (err) console.log(err); + function(req, accessToken, refreshToken, profile, done) { + if (req.user) { + return done(new Error('User is already signed in'), req.user); + } else { + User.findOne({ + 'provider': 'google', + 'providerData.id': profile.id + }, function(err, user) { + if (!user) { + var possibleUsername = profile.emails[0].value.split('@')[0]; + + User.findUniqueUsername(possibleUsername, null, function(availableUsername) { + user = new User({ + firstName: profile.name.givenName, + lastName: profile.name.familyName, + displayName: profile.displayName, + email: profile.emails[0].value, + username: availableUsername, + provider: 'google', + providerData: profile._json + }); + user.save(function(err) { + return done(err, user); + }); + }); + } else { return done(err, user); - }); - } else { - return done(err, user); - } - }); + } + }); + } } )); }; \ No newline at end of file diff --git a/config/strategies/linkedin.js b/config/strategies/linkedin.js index a7f38b0709..cbb460a62b 100644 --- a/config/strategies/linkedin.js +++ b/config/strategies/linkedin.js @@ -11,30 +11,39 @@ module.exports = function() { consumerKey: config.linkedin.clientID, consumerSecret: config.linkedin.clientSecret, callbackURL: config.linkedin.callbackURL, + passReqToCallback: true, profileFields: ['id', 'first-name', 'last-name', 'email-address'] }, - function(accessToken, refreshToken, profile, done) { - User.findOne({ - 'providerData.id': profile.id - }, function(err, user) { - if (!user) { - user = new User({ - firstName: profile.name.givenName, - lastName: profile.name.familyName, - displayName: profile.displayName, - email: profile.emails[0].value, - username: profile.emails[0].value, - provider: 'linkedin', - providerData: profile._json - }); - user.save(function(err) { - if (err) console.log(err); + function(req, accessToken, refreshToken, profile, done) { + if (req.user) { + return done(new Error('User is already signed in'), req.user); + } else { + User.findOne({ + 'provider': 'linkedin', + 'providerData.id': profile.id + }, function(err, user) { + if (!user) { + var possibleUsername = profile.emails[0].value.split('@')[0]; + + User.findUniqueUsername(possibleUsername, null, function(availableUsername) { + user = new User({ + firstName: profile.name.givenName, + lastName: profile.name.familyName, + displayName: profile.displayName, + email: profile.emails[0].value, + username: availableUsername, + provider: 'linkedin', + providerData: profile._json + }); + user.save(function(err) { + return done(err, user); + }); + }); + } else { return done(err, user); - }); - } else { - return done(err, user); - } - }); + } + }); + } } )); }; \ No newline at end of file diff --git a/config/strategies/local.js b/config/strategies/local.js index 0c4a1256b6..c4850a6e5d 100644 --- a/config/strategies/local.js +++ b/config/strategies/local.js @@ -8,12 +8,12 @@ var passport = require('passport'), module.exports = function() { // Use local strategy passport.use(new LocalStrategy({ - usernameField: 'email', + usernameField: 'username', passwordField: 'password' }, - function(email, password, done) { + function(username, password, done) { User.findOne({ - email: email + username: username }, function(err, user) { if (err) { return done(err); @@ -28,6 +28,7 @@ module.exports = function() { message: 'Invalid password' }); } + return done(null, user); }); } diff --git a/config/strategies/twitter.js b/config/strategies/twitter.js index 02ebd4cf2a..62301d4458 100644 --- a/config/strategies/twitter.js +++ b/config/strategies/twitter.js @@ -10,30 +10,37 @@ module.exports = function() { passport.use(new TwitterStrategy({ consumerKey: config.twitter.clientID, consumerSecret: config.twitter.clientSecret, - callbackURL: config.twitter.callbackURL + callbackURL: config.twitter.callbackURL, + passReqToCallback: true }, - function(token, tokenSecret, profile, done) { - User.findOne({ - 'providerData.id_str': profile.id - }, function(err, user) { - if (err) { - return done(err); - } - if (!user) { - user = new User({ - displayName: profile.displayName, - username: profile.username, - provider: 'twitter', - providerData: profile._json - }); - user.save(function(err) { - if (err) console.log(err); + function(req, token, tokenSecret, profile, done) { + if (req.user) { + return done(new Error('User is already signed in'), req.user); + } else { + User.findOne({ + 'provider': 'twitter', + 'providerData.id_str': profile.id + }, function(err, user) { + if (err) { + return done(err); + } + if (!user) { + User.findUniqueUsername(profile.username, null, function(availableUsername) { + user = new User({ + displayName: profile.displayName, + username: availableUsername, + provider: 'twitter', + providerData: profile._json + }); + user.save(function(err) { + return done(err, user); + }); + }); + } else { return done(err, user); - }); - } else { - return done(err, user); - } - }); + } + }); + } } )); -}; +}; \ No newline at end of file diff --git a/public/modules/core/views/header.html b/public/modules/core/views/header.html index e19b906cf0..8cf3169be2 100644 --- a/public/modules/core/views/header.html +++ b/public/modules/core/views/header.html @@ -29,6 +29,13 @@ {{authentication.user.displayName}}