diff --git a/app/controllers/users.js b/app/controllers/users.js index 407b6faa..9faaca57 100755 --- a/app/controllers/users.js +++ b/app/controllers/users.js @@ -8,6 +8,9 @@ var mongoose = require('mongoose'), User = mongoose.model('User'), _ = require('lodash'); +/** + * Get the error message from error object + */ var getErrorMessage = function(err) { var message = ''; @@ -153,7 +156,6 @@ exports.changePassword = function(req, res, next) { }); } }); - } else { res.send(400, { message: 'Passwords do not match' @@ -197,9 +199,8 @@ exports.me = function(req, res) { */ exports.oauthCallback = function(strategy) { return function(req, res, next) { - passport.authenticate(strategy, function(err, user, email) { + passport.authenticate(strategy, function(err, user, redirectURL) { if (err || !user) { - console.log(err); return res.redirect('/#!/signin'); } req.login(user, function(err) { @@ -207,7 +208,7 @@ exports.oauthCallback = function(strategy) { return res.redirect('/#!/signin'); } - return res.redirect('/'); + return res.redirect(redirectURL || '/'); }); })(req, res, next); }; @@ -250,94 +251,112 @@ exports.hasAuthorization = function(req, res, next) { }; /** - * Helper function to save or update a user. - * When the user is logged in, it joins the user data to the existing one. - * Otherwise it creates a new user. - * - * @author Kentaro Wakayama - * - * @date 2014-04-09 - * - * @param {Object} req This is the request object which contains the user when he is signed in. - * @param {String} token This is the accesstoken. - * @param {String} tokenSecret This is the refreshtoken. - * @param {Object} profile This is the user profile of the current provider. - * @param {Function} done Callback to supply Passport with the user that authenticated. - * - * @param {Object} providerData This Object contains all data which is specific for the provider - * @param {String} providerData.provider This is the passport provider name. - * @param {String} providerData.idKey This is the Key / Attribute name for saving / retrieving the provider id. - * @param {String} providerData.name This is the user's name. - * @param {String} [providerData.email] This is the user's email. - * @param {String} providerData.username This is the user's username. - * - * @return {[type]} [description] + * Helper function to save or update a OAuth user profile */ -exports.saveOrUpdate = function(req, token, tokenSecret, profile, done, providerData) { - var provider = providerData.provider; - var idKey = providerData.idKey; - var searchProviderKey = provider + '.' + idKey; - var searchObject = {}; - searchObject[searchProviderKey] = profile.id; - +exports.saveOAuthUserProfile = function(req, providerUserProfile, done) { if (!req.user) { - // no user active, this is a fresh login - User.findOne(searchObject, function(err, 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); - } - if (!user) { - - var possibleUsername = ''; - if (providerData.email) { - possibleUsername = providerData.email.split('@')[0]; - } else { - possibleUsername = profile.username; - } - - User.findUniqueUsername(possibleUsername, null, function(availableUsername) { - user = new User({ - firstName: providerData.firstName, - lastName: providerData.lastName, - username: availableUsername, - displayName: providerData.displayName, - email: providerData.email, - provider: provider, - }); - - user[provider] = profile._json; - user[provider].token = token; - user[provider].tokenSecret = tokenSecret; - user.save(function(err) { - if (err) console.log(err); - return done(err, user); - }); - }); } else { - user[provider].token = token; - user[provider].tokenSecret = tokenSecret; - user.save(function(err) { - if (err) console.log(err); + 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 { - // a user is already logged in, join the provider data to the existing user. - User.findById( req.user._id, function(err, user) { + // User is already logged in, join the provider data to the existing user + User.findById(req.user.id, function(err, user) { if (err) { return done(err); - } - if (user) { - user[provider] = profile._json; - user[provider].token = token; - user[provider].tokenSecret = tokenSecret; - user.save(function(err) { - if (err) console.log(err); + } else { + // Check if user exists, is not signed in using this provider, and doesn't have that provider data already configured + if (user && 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(err, 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 { - return done(err, user); + req.login(user, function(err) { + if (err) { + res.send(400, err); + } else { + res.jsonp(user); + } + }); } }); } diff --git a/app/models/user.js b/app/models/user.js index 2eebcbdb..cfd122a0 100755 --- a/app/models/user.js +++ b/app/models/user.js @@ -67,18 +67,14 @@ var UserSchema = new Schema({ required: 'Provider is required' }, providerData: {}, + additionalProvidersData: {}, updated: { type: Date }, created: { type: Date, default: Date.now - }, - facebook: {}, - twitter: {}, - github: {}, - google: {}, - linkedin: {} + } }); /** diff --git a/app/routes/users.js b/app/routes/users.js index 96e9903d..23f8126b 100644 --- a/app/routes/users.js +++ b/app/routes/users.js @@ -11,6 +11,7 @@ module.exports = function(app) { app.get('/users/me', users.me); app.put('/users', users.update); app.post('/users/password', users.changePassword); + app.del('/users/accounts', users.removeOAuthProvider); // Setting up the users api app.post('/auth/signup', users.signup); diff --git a/config/env/development.js b/config/env/development.js index dc583724..0e2e2871 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/strategies/facebook.js b/config/strategies/facebook.js index 80f684b7..113cb74b 100644 --- a/config/strategies/facebook.js +++ b/config/strategies/facebook.js @@ -2,7 +2,6 @@ var passport = require('passport'), FacebookStrategy = require('passport-facebook').Strategy, - User = require('mongoose').model('User'), config = require('../config'), users = require('../../app/controllers/users'); @@ -15,18 +14,25 @@ module.exports = function() { passReqToCallback: true }, function(req, accessToken, refreshToken, profile, done) { + // Set the provider data and include tokens + var providerData = profile._json; + providerData.accessToken = accessToken; + providerData.refreshToken = refreshToken; - var providerData = { + // Create the user OAuth profile + var providerUserProfile = { firstName: profile.name.givenName, lastName: profile.name.familyName, displayName: profile.displayName, - provider: 'facebook', - idKey: 'id', email: profile.emails[0].value, username: profile.username, + provider: 'facebook', + providerIdentifierField: 'id', + providerData: providerData }; - users.saveOrUpdate(req, accessToken, refreshToken, profile, done, providerData); + // Save the user OAuth profile + users.saveOAuthUserProfile(req, providerUserProfile, done); } )); }; \ No newline at end of file diff --git a/config/strategies/google.js b/config/strategies/google.js index fcbac8b2..f06c0998 100644 --- a/config/strategies/google.js +++ b/config/strategies/google.js @@ -2,7 +2,6 @@ var passport = require('passport'), GoogleStrategy = require('passport-google-oauth').OAuth2Strategy, - User = require('mongoose').model('User'), config = require('../config'), users = require('../../app/controllers/users'); @@ -15,18 +14,25 @@ module.exports = function() { passReqToCallback: true }, function(req, accessToken, refreshToken, profile, done) { - - var providerData = { + // Set the provider data and include tokens + var providerData = profile._json; + providerData.accessToken = accessToken; + providerData.refreshToken = refreshToken; + + // Create the user OAuth profile + var providerUserProfile = { firstName: profile.name.givenName, lastName: profile.name.familyName, displayName: profile.displayName, - provider: 'google', - idKey: 'id', email: profile.emails[0].value, - username: profile.username + username: profile.username, + provider: 'google', + providerIdentifierField: 'id', + providerData: providerData }; - users.saveOrUpdate(req, accessToken, refreshToken, profile, done, providerData); + // Save the user OAuth profile + users.saveOAuthUserProfile(req, providerUserProfile, done); } )); }; \ No newline at end of file diff --git a/config/strategies/linkedin.js b/config/strategies/linkedin.js index 80994fbf..2ba5736d 100644 --- a/config/strategies/linkedin.js +++ b/config/strategies/linkedin.js @@ -2,7 +2,6 @@ var passport = require('passport'), LinkedInStrategy = require('passport-linkedin').Strategy, - User = require('mongoose').model('User'), config = require('../config'), users = require('../../app/controllers/users'); @@ -16,17 +15,25 @@ module.exports = function() { profileFields: ['id', 'first-name', 'last-name', 'email-address'] }, function(req, accessToken, refreshToken, profile, done) { + // Set the provider data and include tokens + var providerData = profile._json; + providerData.accessToken = accessToken; + providerData.refreshToken = refreshToken; - var providerData = { + // Create the user OAuth profile + var providerUserProfile = { firstName: profile.name.givenName, lastName: profile.name.familyName, displayName: profile.displayName, + email: profile.emails[0].value, + username: profile.username, provider: 'linkedin', - idKey: 'id', - username: profile.displayName, + providerIdentifierField: 'id', + providerData: providerData }; - users.saveOrUpdate(req, accessToken, refreshToken, profile, done, providerData); + // Save the user OAuth profile + users.saveOAuthUserProfile(req, providerUserProfile, done); } )); }; \ No newline at end of file diff --git a/config/strategies/twitter.js b/config/strategies/twitter.js index 44e7b4d4..cf3e6cc2 100644 --- a/config/strategies/twitter.js +++ b/config/strategies/twitter.js @@ -2,7 +2,6 @@ var passport = require('passport'), TwitterStrategy = require('passport-twitter').Strategy, - User = require('mongoose').model('User'), config = require('../config'), users = require('../../app/controllers/users'); @@ -15,15 +14,22 @@ module.exports = function() { passReqToCallback: true }, function(req, token, tokenSecret, profile, done) { + // Set the provider data and include tokens + var providerData = profile._json; + providerData.token = token; + providerData.tokenSecret = tokenSecret; - var providerData = { + // Create the user OAuth profile + var providerUserProfile = { displayName: profile.displayName, - provider: 'twitter', - idKey: 'id_str', - username: profile.username, - }; + username: profile.username, + provider: 'twitter', + providerIdentifierField: 'id_str', + providerData: providerData + }; - users.saveOrUpdate(req, token, tokenSecret, profile, done, providerData); + // Save the user OAuth profile + users.saveOAuthUserProfile(req, providerUserProfile, done); } )); }; \ No newline at end of file diff --git a/public/modules/core/views/header.html b/public/modules/core/views/header.html index 8cf3169b..918c882b 100644 --- a/public/modules/core/views/header.html +++ b/public/modules/core/views/header.html @@ -32,6 +32,9 @@
  • Edit Profile
  • +
  • + Manage Social Accounts +
  • Change Password
  • diff --git a/public/modules/users/config/routes.js b/public/modules/users/config/routes.js index 12d8a0a0..f01bd699 100755 --- a/public/modules/users/config/routes.js +++ b/public/modules/users/config/routes.js @@ -13,6 +13,10 @@ angular.module('users').config(['$stateProvider', url: '/settings/password', templateUrl: 'modules/users/views/settings/password.html' }). + state('accounts', { + url: '/settings/accounts', + templateUrl: 'modules/users/views/settings/accounts.html' + }). state('signup', { url: '/signup', templateUrl: 'modules/users/views/signup.html' diff --git a/public/modules/users/controllers/settings.js b/public/modules/users/controllers/settings.js index 1715b2bb..c030e18c 100644 --- a/public/modules/users/controllers/settings.js +++ b/public/modules/users/controllers/settings.js @@ -4,9 +4,39 @@ angular.module('users').controller('SettingsController', ['$scope', '$http', '$l function($scope, $http, $location, Users, Authentication) { $scope.user = Authentication.user; - //If user is not signed in then redirect back home + // If user is not signed in then redirect back home if (!$scope.user) $location.path('/'); + // Check if there are additional accounts + $scope.hasConnectedAdditionalSocialAccounts = function(provider) { + for (var i in $scope.user.additionalProvidersData) { + return true; + } + + return false; + }; + + // Check if provider is already in use with current user + $scope.isConnectedSocialAccount = function(provider) { + return $scope.user.provider === provider || ($scope.user.additionalProvidersData && $scope.user.additionalProvidersData[provider]); + }; + + $scope.removeUserSocialAccount = function(provider) { + $scope.success = $scope.error = null; + + $http.delete('/users/accounts', { + params: { + provider: provider + } + }).success(function(response) { + // If successful show success message and clear form + $scope.success = true; + $scope.user = Authentication.user = response; + }).error(function(response) { + $scope.error = response.message; + }); + }; + $scope.updateUserProfile = function() { $scope.success = $scope.error = null; var user = new Users($scope.user); diff --git a/public/modules/users/css/users.css b/public/modules/users/css/users.css index 487df40a..7dd49e21 100644 --- a/public/modules/users/css/users.css +++ b/public/modules/users/css/users.css @@ -2,4 +2,15 @@ .nav-users { position: fixed; } -} \ No newline at end of file +} + +.remove-account-container { + display: inline-block; + position: relative; +} + +.btn-remove-account { + top: 10px; + right: 10px; + position: absolute; +} diff --git a/public/modules/users/views/settings/profile.html b/public/modules/users/views/settings/profile.html index aae41fff..5006dc9f 100644 --- a/public/modules/users/views/settings/profile.html +++ b/public/modules/users/views/settings/profile.html @@ -31,19 +31,4 @@ -

    Add an other Account

    -
    - - - - - - - - - - - - -
    \ No newline at end of file