diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..b366c5dc --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +## License +(The MIT License) + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 69dbe6fe..d8d04f6e 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,28 @@ $ npm install -g bower $ sudo npm install -g grunt-cli ``` +## Downloading MEAN.JS +There are several ways you can get the MEAN.JS boilerplate: + +### Yo Generator +The recommended way would be to use the [Official Yo Generator](http://meanjs.org/generator.html) which will generate the latest stable copy of the MEAN.JS boilerplate and supplies multiple sub-generators to ease your daily development cycles. + +### Cloning The GitHub Repository +You can also use Git to directly clone the MEAN.JS repository: +``` +$ git clone https://github.com/meanjs/mean.git meanjs +``` +This will clone the latest version of the MEAN.JS repository to a **meanjs** folder. + +### Downloading The Repository Zip File +Another way to use the MEAN.JS boilerplate is to download a zip copy from the [master branch on github](https://github.com/meanjs/mean/archive/master.zip). You can also do this using `wget` command: +``` +$ wget https://github.com/meanjs/mean/archive/master.zip -O meanjs.zip; unzip meanjs.zip; rm meanjs.zip +``` +Don't forget to rename **mean-master** after your project name. + ## Quick Install -Once you've installed all the prerequisites, you're just a few steps away from starting to develop you MEAN application. +Once you've downloaded the boilerplate and installed all the prerequisites, you're just a few steps away from starting to develop you MEAN application. The first thing you should do is install the Node.js dependencies. The boilerplate comes pre-bundled with a package.json file that contains the list of modules you need to start your application, to learn more about the modules installed visit the NPM & Package.json section. diff --git a/app/controllers/users.js b/app/controllers/users.js index e2ff8f4b..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); }; @@ -247,4 +248,116 @@ exports.hasAuthorization = function(req, res, next) { } next(); +}; + +/** + * 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 + User.findById(req.user.id, function(err, user) { + if (err) { + return done(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 { + req.login(user, function(err) { + if (err) { + res.send(400, err); + } else { + res.jsonp(user); + } + }); + } + }); + } }; \ No newline at end of file diff --git a/app/models/user.js b/app/models/user.js index 30d12bb1..cfd122a0 100755 --- a/app/models/user.js +++ b/app/models/user.js @@ -67,6 +67,7 @@ var UserSchema = new Schema({ required: 'Provider is required' }, providerData: {}, + additionalProvidersData: {}, updated: { type: Date }, 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/config.js b/config/config.js index 1823cae4..afe7eb09 100644 --- a/config/config.js +++ b/config/config.js @@ -3,6 +3,7 @@ var _ = require('lodash'), utilities = require('./utilities'); +// Look for a valid NODE_ENV variable and if one cannot be found load the development NODE_ENV process.env.NODE_ENV = ~utilities.walk('./config/env', /(.*)\.js$/).map(function(file) { return file.split('/').pop().slice(0, -3); }).indexOf(process.env.NODE_ENV) ? process.env.NODE_ENV : 'development'; @@ -11,4 +12,4 @@ process.env.NODE_ENV = ~utilities.walk('./config/env', /(.*)\.js$/).map(function module.exports = _.extend( require('./env/all'), require('./env/' + process.env.NODE_ENV) || {} -); +); \ No newline at end of file diff --git a/config/strategies/facebook.js b/config/strategies/facebook.js index 4802a46f..113cb74b 100644 --- a/config/strategies/facebook.js +++ b/config/strategies/facebook.js @@ -2,8 +2,8 @@ var passport = require('passport'), FacebookStrategy = require('passport-facebook').Strategy, - User = require('mongoose').model('User'), - config = require('../config'); + config = require('../config'), + users = require('../../app/controllers/users'); module.exports = function() { // Use facebook strategy @@ -14,36 +14,25 @@ module.exports = function() { passReqToCallback: true }, 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); - } - }); - } + // 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, + email: profile.emails[0].value, + username: profile.username, + provider: 'facebook', + providerIdentifierField: 'id', + providerData: 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 d86e0255..f06c0998 100644 --- a/config/strategies/google.js +++ b/config/strategies/google.js @@ -2,8 +2,8 @@ var passport = require('passport'), GoogleStrategy = require('passport-google-oauth').OAuth2Strategy, - User = require('mongoose').model('User'), - config = require('../config'); + config = require('../config'), + users = require('../../app/controllers/users'); module.exports = function() { // Use google strategy @@ -14,35 +14,25 @@ module.exports = function() { passReqToCallback: true }, 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]; + // 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, + email: profile.emails[0].value, + username: profile.username, + provider: 'google', + providerIdentifierField: 'id', + providerData: providerData + }; - 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); - } - }); - } + // 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 cbb460a6..2ba5736d 100644 --- a/config/strategies/linkedin.js +++ b/config/strategies/linkedin.js @@ -2,8 +2,8 @@ var passport = require('passport'), LinkedInStrategy = require('passport-linkedin').Strategy, - User = require('mongoose').model('User'), - config = require('../config'); + config = require('../config'), + users = require('../../app/controllers/users'); module.exports = function() { // Use linkedin strategy @@ -15,35 +15,25 @@ module.exports = function() { profileFields: ['id', 'first-name', 'last-name', 'email-address'] }, 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]; + // Set the provider data and include tokens + var providerData = profile._json; + providerData.accessToken = accessToken; + providerData.refreshToken = refreshToken; - 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); - } - }); - } + // 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', + providerIdentifierField: 'id', + providerData: 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 62301d44..cf3e6cc2 100644 --- a/config/strategies/twitter.js +++ b/config/strategies/twitter.js @@ -2,8 +2,8 @@ var passport = require('passport'), TwitterStrategy = require('passport-twitter').Strategy, - User = require('mongoose').model('User'), - config = require('../config'); + config = require('../config'), + users = require('../../app/controllers/users'); module.exports = function() { // Use twitter strategy @@ -14,33 +14,22 @@ module.exports = function() { passReqToCallback: true }, 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); - } - }); - } + // Set the provider data and include tokens + var providerData = profile._json; + providerData.token = token; + providerData.tokenSecret = tokenSecret; + + // Create the user OAuth profile + var providerUserProfile = { + displayName: profile.displayName, + username: profile.username, + provider: 'twitter', + providerIdentifierField: 'id_str', + providerData: providerData + }; + + // Save the user OAuth profile + users.saveOAuthUserProfile(req, providerUserProfile, done); } )); }; \ No newline at end of file diff --git a/package.json b/package.json index 52695071..2261bfaa 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "meanjs", "description": "Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js.", - "version": "0.2.2", + "version": "0.2.3", "private": false, "author": "https://github.com/meanjs/mean/graphs/contributors", "repository": { @@ -31,7 +31,7 @@ "passport-linkedin": "~0.1.3", "passport-google-oauth": "~0.1.5", "lodash": "~2.4.1", - "forever": "~0.10.11", + "forever": "~0.11.00", "bower": "~1.3.1", "grunt-cli": "~0.1.13" }, 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..02fd1fdd 100644 --- a/public/modules/users/controllers/settings.js +++ b/public/modules/users/controllers/settings.js @@ -4,9 +4,41 @@ 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]); + }; + + // Remove a user social account + $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; + }); + }; + + // Update a user profile $scope.updateUserProfile = function() { $scope.success = $scope.error = null; var user = new Users($scope.user); @@ -19,6 +51,7 @@ angular.module('users').controller('SettingsController', ['$scope', '$http', '$l }); }; + // Change user password $scope.changeUserPassword = function() { $scope.success = $scope.error = null; 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/accounts.html b/public/modules/users/views/settings/accounts.html new file mode 100644 index 00000000..ffc1c8f0 --- /dev/null +++ b/public/modules/users/views/settings/accounts.html @@ -0,0 +1,26 @@ +
    +

    Connected social accounts:

    +
    + +
    +

    Connect other social accounts:

    +
    + + + + + + + + + + + + +
    +
    \ No newline at end of file