diff --git a/app/controllers/articles.server.controller.js b/app/controllers/articles.server.controller.js index dcbe5b8a0a..240f55692f 100644 --- a/app/controllers/articles.server.controller.js +++ b/app/controllers/articles.server.controller.js @@ -6,23 +6,21 @@ var mongoose = require('mongoose'), errorHandler = require('./errors'), Article = mongoose.model('Article'), - _ = require('lodash'); + _ = require('lodash'), + articleService = require('../services/articles.server.service'); /** * Create a article */ exports.create = function(req, res) { - var article = new Article(req.body); - article.user = req.user; + articleService.create(req.body, req.user, function(err, article){ + if (err) { + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); + } - article.save(function(err) { - if (err) { - return res.status(400).send({ - message: errorHandler.getErrorMessage(err) - }); - } else { - res.jsonp(article); - } + res.jsonp(article); }); }; @@ -37,18 +35,14 @@ exports.read = function(req, res) { * Update a article */ exports.update = function(req, res) { - var article = req.article; - - article = _.extend(article, req.body); - - article.save(function(err) { + articleService.update(req.article, req.body, function(err, article){ if (err) { return res.status(400).send({ message: errorHandler.getErrorMessage(err) }); - } else { - res.jsonp(article); } + + res.jsonp(article); }); }; @@ -56,16 +50,15 @@ exports.update = function(req, res) { * Delete an article */ exports.delete = function(req, res) { - var article = req.article; - article.remove(function(err) { + articleService.delete(req.article, function(err, article){ if (err) { return res.status(400).send({ message: errorHandler.getErrorMessage(err) }); - } else { - res.jsonp(article); } + + res.jsonp(article); }); }; @@ -73,14 +66,14 @@ exports.delete = function(req, res) { * List of Articles */ exports.list = function(req, res) { - Article.find().sort('-created').populate('user', 'displayName').exec(function(err, articles) { + articleService.list(req.user, function(err, articles){ if (err) { return res.status(400).send({ message: errorHandler.getErrorMessage(err) }); - } else { - res.jsonp(articles); } + + res.jsonp(articles); }); }; @@ -88,9 +81,11 @@ exports.list = function(req, res) { * Article middleware */ exports.articleByID = function(req, res, next, id) { - Article.findById(id).populate('user', 'displayName').exec(function(err, article) { - if (err) return next(err); - if (!article) return next(new Error('Failed to load article ' + id)); + articleService.articleById(id, function(err, article){ + if (err) { + return next(err); + } + req.article = article; next(); }); diff --git a/app/controllers/users/users.authentication.server.controller.js b/app/controllers/users/users.authentication.server.controller.js index 9558ed9734..b92580bd0f 100644 --- a/app/controllers/users/users.authentication.server.controller.js +++ b/app/controllers/users/users.authentication.server.controller.js @@ -3,46 +3,32 @@ /** * Module dependencies. */ -var _ = require('lodash'), - errorHandler = require('../errors'), - mongoose = require('mongoose'), +var errorHandler = require('../errors'), passport = require('passport'), - User = mongoose.model('User'); + userAuthenticationService = require('../../services/users.authentication.server.service'); /** * 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.status(400).send({ - message: errorHandler.getErrorMessage(err) - }); - } else { - // Remove sensitive data before login - user.password = undefined; - user.salt = undefined; - - req.login(user, function(err) { - if (err) { - res.status(400).send(err); - } else { - res.jsonp(user); - } - }); - } + // Signup user throughout service + userAuthenticationService.signup(req.body, function(err, user){ + if (err) { + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); + } + + // login user + req.login(user, function(err) { + if (err) { + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); + } + + return res.jsonp(user); + }); }); }; @@ -50,23 +36,24 @@ exports.signup = function(req, res) { * Signin after passport authentication */ exports.signin = function(req, res, next) { - passport.authenticate('local', function(err, user, info) { - if (err || !user) { - res.status(400).send(info); - } else { - // Remove sensitive data before login - user.password = undefined; - user.salt = undefined; - - req.login(user, function(err) { - if (err) { - res.status(400).send(err); - } else { - res.jsonp(user); - } - }); - } - })(req, res, next); + passport.authenticate('local', + function(err, user, info) { + userAuthenticationService.authenticate(err, user, info, function (err, user) { + if (err || !user) { + return res.status(400).send(info); + } + + req.login(user, function (err) { + if (err) { + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); + } + + return res.jsonp(user); + }); + }); + })(req, res, next); }; /** @@ -102,72 +89,24 @@ exports.oauthCallback = function(strategy) { */ 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); - } - } - }); + userAuthenticationService.saveNewOAuthUserProfile( + providerUserProfile, + function(err, user){ + 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); - } + userAuthenticationService.saveExistingOAuthUserProfile( + req.user, + providerUserProfile, + function(err, user){ + if (err){ + return done(err, user); + } + + return done(null, user, '/#!/settings/accounts'); + } + ); } }; @@ -175,32 +114,23 @@ exports.saveOAuthUserProfile = function(req, providerUserProfile, done) { * 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.status(400).send({ - message: errorHandler.getErrorMessage(err) - }); - } else { - req.login(user, function(err) { - if (err) { - res.status(400).send(err); - } else { - res.jsonp(user); - } - }); - } - }); - } + userAuthenticationService.removeOAuthProvider(req.user, provider, function(err, user){ + if (err){ + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); + } + + req.login(user, function(err) { + if (err) { + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); + } + + return res.jsonp(user); + }); + }); }; \ No newline at end of file diff --git a/app/controllers/users/users.authorization.server.controller.js b/app/controllers/users/users.authorization.server.controller.js index efa3b69524..d60e1bec3f 100644 --- a/app/controllers/users/users.authorization.server.controller.js +++ b/app/controllers/users/users.authorization.server.controller.js @@ -4,21 +4,18 @@ * Module dependencies. */ var _ = require('lodash'), - mongoose = require('mongoose'), - User = mongoose.model('User'); + userAuthorizationService = require('../../services/users.authorization.server.service'); /** * 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(); - }); + userAuthorizationService.userByID(id, function(err, user){ + if (err) return next(err); + if (!user) return next(new Error('Failed to load User ' + id)); + req.profile = user; + next(); + }); }; /** @@ -42,7 +39,7 @@ exports.hasAuthorization = function(roles) { return function(req, res, next) { _this.requiresLogin(req, res, function() { - if (_.intersection(req.user.roles, roles).length) { + if (req.user.hasRoles(roles)) { return next(); } else { return res.status(403).send({ diff --git a/app/controllers/users/users.password.server.controller.js b/app/controllers/users/users.password.server.controller.js index f5b1a6b505..94307c583e 100644 --- a/app/controllers/users/users.password.server.controller.js +++ b/app/controllers/users/users.password.server.controller.js @@ -4,101 +4,32 @@ * Module dependencies. */ var _ = require('lodash'), - errorHandler = require('../errors'), - mongoose = require('mongoose'), - passport = require('passport'), - User = mongoose.model('User'), - config = require('../../../config/config'), - nodemailer = require('nodemailer'), - crypto = require('crypto'), - async = require('async'), - crypto = require('crypto'); + errorHandler = require('../errors'), + userPassService = require('../../services/users.password.server.service'); /** * 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 - }, '-salt -password', function(err, user) { - if (!user) { - return res.status(400).send({ - message: 'No account with that username has been found' - }); - } else if (user.provider !== 'local') { - return res.status(400).send({ - message: 'It seems like you signed up using your ' + user.provider + ' account' - }); - } else { - user.resetPasswordToken = token; - user.resetPasswordExpires = Date.now() + 3600000; // 1 hour + userPassService.forgotPassword(req.body.username, req.headers.host, function(err, email){ + if (err){ + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); + } - user.save(function(err) { - done(err, token, user); - }); - } - }); - } else { - return res.status(400).send({ - 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.from, - subject: 'Password Reset', - html: emailHTML - }; - smtpTransport.sendMail(mailOptions, function(err) { - if (!err) { - res.send({ - message: 'An email has been sent to ' + user.email + ' with further instructions.' - }); - } - - done(err); - }); - } - ], function(err) { - if (err) return next(err); - }); + res.send({ + message: 'An email has been sent to ' + email + ' with further instructions.' + }); + }); }; /** * 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) { + userPassService.validateResetToken(req.params.token, function(err, user){ + if (err) { return res.redirect('/#!/password/reset/invalid'); } @@ -112,137 +43,49 @@ exports.validateResetToken = function(req, res) { 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; + userPassService.resetPassword(req.params.token, passwordDetails, function(err, user){ + if (err){ + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); + } - user.save(function(err) { - if (err) { - return res.status(400).send({ - message: errorHandler.getErrorMessage(err) - }); - } else { - req.login(user, function(err) { - if (err) { - res.status(400).send(err); - } else { - // Return authenticated user - res.jsonp(user); + req.login(user, function(err) { + if (err) { + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); + } - done(err, user); - } - }); - } - }); - } else { - return res.status(400).send({ - message: 'Passwords do not match' - }); - } - } else { - return res.status(400).send({ - message: 'Password reset token is invalid or has expired.' - }); - } - }); - }, - function(user, done) { - res.render('templates/reset-password-confirm-email', { - name: user.displayName, - appName: config.app.title - }, 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.from, - subject: 'Your password has been changed', - html: emailHTML - }; - - smtpTransport.sendMail(mailOptions, function(err) { - done(err, 'done'); - }); - } - ], function(err) { - if (err) return next(err); - }); + // Return authenticated user + res.jsonp(user); + }); + }); }; /** * Change Password */ exports.changePassword = function(req, res, next) { - // Init Variables - var passwordDetails = req.body; - var message = null; + userPassService.changePassowrd(req.user, req.body, function(err, user){ + if (err){ + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); + } - 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; + // Try to login + req.login(user, function(err) { + if (err) { + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); + } - user.save(function(err) { - if (err) { - return res.status(400).send({ - message: errorHandler.getErrorMessage(err) - }); - } else { - req.login(user, function(err) { - if (err) { - res.status(400).send(err); - } else { - res.send({ - message: 'Password changed successfully' - }); - } - }); - } - }); - } else { - res.status(400).send({ - message: 'Passwords do not match' - }); - } - } else { - res.status(400).send({ - message: 'Current password is incorrect' - }); - } - } else { - res.status(400).send({ - message: 'User is not found' - }); - } - }); - } else { - res.status(400).send({ - message: 'Please provide a new password' - }); - } - } else { - res.status(400).send({ - message: 'User is not signed in' - }); - } + res.send({ + message: 'Password changed successfully' + }); + }); + }); }; \ No newline at end of file diff --git a/app/controllers/users/users.profile.server.controller.js b/app/controllers/users/users.profile.server.controller.js index ee902cc1c0..2c7648838d 100644 --- a/app/controllers/users/users.profile.server.controller.js +++ b/app/controllers/users/users.profile.server.controller.js @@ -7,45 +7,28 @@ var _ = require('lodash'), errorHandler = require('../errors'), mongoose = require('mongoose'), passport = require('passport'), - User = mongoose.model('User'); + User = mongoose.model('User'), + userProfileService = require('../../services/users.profile.server.service'); /** * Update user details */ exports.update = function(req, res) { - // Init Variables - var user = req.user; - var message = null; + userProfileService.update(req.user, req.body, function(err, user) { + if (err) { + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); + } - // 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.status(400).send({ - message: errorHandler.getErrorMessage(err) - }); - } else { - req.login(user, function(err) { - if (err) { - res.status(400).send(err); - } else { - res.jsonp(user); - } - }); - } - }); - } else { - res.status(400).send({ - message: 'User is not signed in' - }); - } + req.login(user, function (err) { + if (err) { + res.status(400).send(err); + } else { + res.jsonp(user); + } + }); + }); }; /** diff --git a/app/models/user.server.model.js b/app/models/user.server.model.js index 76ff071ba2..16a9c98876 100755 --- a/app/models/user.server.model.js +++ b/app/models/user.server.model.js @@ -3,7 +3,8 @@ /** * Module dependencies. */ -var mongoose = require('mongoose'), +var _ = require('lodash'), + mongoose = require('mongoose'), Schema = mongoose.Schema, crypto = require('crypto'); @@ -121,6 +122,15 @@ UserSchema.methods.authenticate = function(password) { return this.password === this.hashPassword(password); }; +/** + * checks if user have all specified roles + * @param roles - {Array} of roles + * @returns {Boolean} + */ +UserSchema.methods.hasRoles = function(roles){ + return _.intersection(this.roles, roles).length > 0; +}; + /** * Find possible not used username */ @@ -143,4 +153,30 @@ UserSchema.statics.findUniqueUsername = function(username, suffix, callback) { }); }; +/** + * Find user by username + * @param username - {String} + * @param callback - {Function} in the form of callback(err, user) + * err - {Error} + * user - {User} + */ +UserSchema.statics.findByUsername = function(username, callback){ + var _this = this; + + _this.findOne({ + username: username + }, function(err, user) { + return callback(err, user); + }); +}; + +/** + * Transform this object without salt or password values + */ +if (!UserSchema.options.toObject) UserSchema.options.toObject = {}; +UserSchema.options.toObject.transform = function (doc, ret, options){ +// delete ret.salt; +// delete ret.password; +}; + mongoose.model('User', UserSchema); \ No newline at end of file diff --git a/app/services/articles.server.service.js b/app/services/articles.server.service.js new file mode 100644 index 0000000000..d37491522e --- /dev/null +++ b/app/services/articles.server.service.js @@ -0,0 +1,100 @@ +'use strict'; + +var mongoose = require('mongoose'), + Article = mongoose.model('Article'), + _ = require('lodash'); + +/** + * Create a new article from details + * @param details - {Object} details to create new article + * @param user - {User} the user that article belong to + * @param callback - {Function} in the form of callback(err, article) + * err - {Error} + * article - {Article} + */ +exports.create = function(details, user, callback){ + var article = new Article(details); + article.user = user; + + article.save(function(err) { + if (err) { + return callback(err, null); + } + + return callback(null, article); + }); +}; + +/** + * Update details for specific article + * @param article - {Article} to update + * @param details - {Object} details to update + * @param callback - {Function} in the form off callback(err, article) + * err - {Error} + * article - {Article} + */ +exports.update = function(article, details, callback){ + article = _.extend(article, details); + + article.save(function(err) { + if (err) { + return callback(err, null); + } + + return callback(null, article); + }); +}; + +/** + * Service to delete an Article + * @param article - {Aritcle} to delete + * @param callback - {Function} in the form of callback(err, article) + * err - {Error} + * article - {Article} that was deleted + */ +exports.delete = function(article, callback){ + article.remove(function(err) { + if (err) { + return callback(err, null); + } + + return callback(null, article); + }); +}; + +/** + * Return a list of article for a specific user + * @param user - {User} for him to find his articles + * @param callback - {Function} in the form of callback(err, articles) + * err - {Error} + * articles - {Array} of Articles + */ +exports.list = function(user, callback){ + Article.find().sort('-created').populate('user', 'displayName').exec(function(err, articles) { + if (err) { + return callback(err, null) + } + + return callback(null, articles); + }); +}; + +/** + * Return article by ID + * @param id - {String} article id + * @param callback - {Function} in the form of callback(err, article) + * err - {Error} + * article - {Article} + */ +exports.articleById = function(id, callback){ + Article.findById(id).populate('user', 'displayName').exec(function(err, article) { + if (err) { + return callback(err, null); + } + if (!article) { + return callback(new Error('Failed to load article ' + id), null); + } + + callback(null, article); + }); +}; \ No newline at end of file diff --git a/app/services/email.server.service.js b/app/services/email.server.service.js new file mode 100644 index 0000000000..170a88e782 --- /dev/null +++ b/app/services/email.server.service.js @@ -0,0 +1,46 @@ +'use strict'; + +var swig = require('swig'), + nodemailer = require('nodemailer'), + config = require('../../config/config'); + +/** + * Send Email Service + * @param options - {Object} with the properties : + * { + * template : { + * path - {String} - Path to the template to render + * renderOptions : {Object} - Template Rendering + * }, + * email : { + * to : {String} email to send to + * subject : {String} email Subject + * } + * } + * @param callback - {Function} in the form of callback(err) + */ +exports.sendTemplate = function(options, callback){ + if (!options || + !options.template || + !options.email){ + return callback(new Error('Error in sendTemplate parameters'), null); + } + + swig.renderFile(options.template.path, options, function(err, output){ + if (err){ + return callback(err, null); + } + + var smtpTransport = nodemailer.createTransport(config.mailer.options); + var mailOptions = { + to: options.email.to, + from: config.mailer.from, + subject: options.email.subject, + html: output + }; + + smtpTransport.sendMail(mailOptions, function(err) { + callback(err); + }); + }); +}; \ No newline at end of file diff --git a/app/services/users.authentication.server.service.js b/app/services/users.authentication.server.service.js new file mode 100644 index 0000000000..5c2a588b4f --- /dev/null +++ b/app/services/users.authentication.server.service.js @@ -0,0 +1,169 @@ +'use strict'; + +var mongoose = require('mongoose'), + User = mongoose.model('User'); + +/** + * Signs up a user + * @param usr - {Object} an user json object + * @param callback - {Function} in the form of callback(err, user) + * err - {Error} + * user - {User} + */ +exports.signup = function(usr, callback){ + // For security measurement we remove the roles from the req.body object + delete usr.roles; + + // Init Variables + var user = new User(usr); + + // Add missing user fields + user.provider = 'local'; + user.displayName = user.firstName + ' ' + user.lastName; + + // Then save the user + user.save(function(err) { + if (err) { + return callback(err, null); + } + + // Remove sensitive data before login + user.password = undefined; + user.salt = undefined; + + return callback(null, user); + }); +}; + +/** + * Authenticate user for passport usage + * @param err - {Error} + * @param user - {User} + * @param info - {String} + * @param callback - {Function} callback(err, user) + * err - {Error} + * user - {User} + */ +exports.authenticate = function(err, user, info, callback){ + if (err || !user) { + return callback(new Error(info), null); + } + + // Remove sensitive data before login + user.password = undefined; + user.salt = undefined; + + return callback(null, user); +}; + +/** + * Helper service to save new OAuth user profile + * @param providerUserProfile - {Object} + * @param callback - {Function} in the form of callback(err, user) + * err - {Error} + * user - {User} + */ +exports.saveNewOAuthUserProfile = function(providerUserProfile, callback) { + // 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 callback(err, null); + } + + 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 callback(err, null); + }); + }); + } else { + return callback(err, user); + } + }); +}; + + +/** + * Helper service to update existing OAuth user profile + * @param user - {User} + * @param providerUserProfile - {Object} + * @param callback - {Function} in the form of callback(err, user) + * err - {Error} + * user - {User} + */ +exports.saveExistingOAuthUserProfile = function(user, providerUserProfile, callback) { + // 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 callback(err, user); + }); + } else { + return callback(new Error('User is already connected using this provider'), null); + } +}; + +/** + * Remove OAuth provider + * @param user - {User} + * @param provider - {String} + * @param callback - {Function} callback(err, user) + * err - {Error} + * user - {User} + */ +exports.removeOAuthProvider = function(user, provider, callback) { + 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 callback(err, null); + } + + return callback(null, user); + }); + } +}; \ No newline at end of file diff --git a/app/services/users.authorization.server.service.js b/app/services/users.authorization.server.service.js new file mode 100644 index 0000000000..a48fd94742 --- /dev/null +++ b/app/services/users.authorization.server.service.js @@ -0,0 +1,23 @@ +'use strict'; + +var mongoose = require('mongoose'), + User = mongoose.model('User'); + +/** + * Find user by id + * @param id - {String} user id to find + * @param callback - {Function} callback(err, user) + * err - {Error} + * user - {User} + */ +exports.userByID = function(id, callback) { + User.findOne({ + _id: id + }).exec(function(err, user) { + if (err){ + return callback(err, null); + } + + return callback(null, user); + }); +}; \ No newline at end of file diff --git a/app/services/users.password.server.service.js b/app/services/users.password.server.service.js new file mode 100644 index 0000000000..c5a22792f7 --- /dev/null +++ b/app/services/users.password.server.service.js @@ -0,0 +1,216 @@ +'use strict'; + +var async = require('async'), + mongoose = require('mongoose'), + User = mongoose.model('User'), + emailService = require('./email.server.service'), + config = require('../../config/config'), + utilsService = require('./utils.server.service'); + +/** + * Forgot Password Service: + * 1. find the user + * 2. generate new token + * @param username - {String} + * @param callback - {Function} - in the form of function(err, email); + */ +exports.forgotPassword = function(username, hostname, callback){ + async.waterfall([ + // Search for the user + function(done) { + User.findByUsername(username, function(err, user){ + if (err){ + return done(err); + } + + if (user === null){ + return done (new Error('No user found')); + } + + done(null, user); + }); + }, + // Generate a new token + function(user, done){ + utilsService.generateToken(function(err, token){ + done(err, user, token); + }); + }, + // Save new token + function(user, token, done){ + user.resetPasswordToken = token; + user.resetPasswordExpires = Date.now() + 3600000; // 1 hour + + user.save(function(err) { + done(err, user, token); + }); + }, + // send email + function(user, token, done){ + var options = { + template : { + path : 'app/views/templates/reset-password-email.server.view.html', + renderOptions : { + name: user.displayName, + appName: config.app.title, + url: 'http://' + hostname + '/auth/reset/' + token + } + }, + email : { + to: user.email, + subject: 'Password Reset' + } + }; + emailService.sendTemplate(options, function(err){ + done(err, user.email); + }); + } + ], function(err, email){ + callback(err, email); + }); +}; + +/** + * Service validation for a specific token + * @param token - {String} + * @param callback - {Function} in the form of function(err, user) + * err - {Error} + * user - {User} + */ + function validateResetToken(token, callback){ + User.findOne({ + resetPasswordToken: token, + resetPasswordExpires: { + $gt: Date.now() + } + }, function(err, user) { + if (err){ + return callback(err, null); + } + if (!user) { + return callback(new Error('No user found'), null); + } + + return callback(null, user); + }); +}; +exports.validateResetToken = validateResetToken; + +/** + * Service to reset a password according to a specified token + * @param token - {String} - the token of the reset request + * @param passwordDetails - {Object} in the form of: + * { + * newPassword : {String} the new password + * verifyPassword {String} the password again + * } + * @param callback - {Function} in the form of callback(err, user) + * err - {Error} + * user - {User} + */ +exports.resetPassword = function(token, passwordDetails, callback){ + async.waterfall([ + + function(done) { + validateResetToken(token, function (err, user) { + done(err, user); + }); + }, + function(user, done){ + if (passwordDetails.newPassword !== passwordDetails.verifyPassword) { + done(new Error('Passwords do not match')); + } + + user.password = passwordDetails.newPassword; + user.resetPasswordToken = undefined; + user.resetPasswordExpires = undefined; + + user.save(function(err) { + if (err) { + return callback(err, null); + } + + done(err, user); + }); + }, + // send email + function(user, done){ + var options = { + template : { + path : 'app/views/templates/reset-password-confirm-email.server.view.html', + renderOptions : { + name: user.displayName, + appName: config.app.title + } + }, + email : { + to: user.email, + subject: 'Your password has been changed' + } + }; + emailService.sendTemplate(options, function(err){ + done(err, user); + }); + } + ], function(err, user) { + callback(err, user); + }); +}; + +/** + * Service to change a user passowrd + * @param user - {User} one to change his password + * @param passwordDetails - {Object} in the form of : + * { + * currentPassword : {String} the current password + * newPassword : {String} the new password + * verifyPassword : {String} the password again + * } + * @param callback - {Function} in the form of callback(err, user) + * err - {Error} + * user - {User} + */ +exports.changePassowrd = function(user, passwordDetails, callback){ + + async.waterfall([ + // Password exact verififcation + function(done){ + if (passwordDetails.newPassword && passwordDetails.newPassword === passwordDetails.verifyPassword){ + return done(null); + } + + return done(new Error('Password do not match or missing')); + }, + // Find the user + function(done){ + User.findById(user.id, function(err, user){ + if (err){ + return done(err); + } + + if (!user){ + return done(new Error('Could not find a user')); + } + + return done(err, user); + }) + }, + // Try to authenticate + function(user, done){ + if (!user.authenticate(passwordDetails.currentPassword)){ + return done(new Error('Could not authenticate user')); + } + return done(null, user); + }, + // Change Password + function(user, done){ + user.password = passwordDetails.newPassword; + + user.save(function(err){ + return done(err, user); + }); + } + ], function(err, user) { + callback(err, user); + }); +}; \ No newline at end of file diff --git a/app/services/users.profile.server.service.js b/app/services/users.profile.server.service.js new file mode 100644 index 0000000000..1a3cc3c9dd --- /dev/null +++ b/app/services/users.profile.server.service.js @@ -0,0 +1,31 @@ +'use strict'; + +/** + * Module dependencies. + */ +var _ = require('lodash'), + mongoose = require('mongoose'), + passport = require('passport'), + User = mongoose.model('User'); + +exports.update = function(user, details, callback){ + // For security measurement we remove the roles from the req.body object + delete user.roles; + + if (user) { + // Merge existing user + user = _.extend(user, details); + user.updated = Date.now(); + user.displayName = user.firstName + ' ' + user.lastName; + + user.save(function(err) { + if (err) { + return callback(err, null); + } + + callback(null, user); + }); + } else { + return callback(new Error('User is not signed in'), null); + } +}; \ No newline at end of file diff --git a/app/services/utils.server.service.js b/app/services/utils.server.service.js new file mode 100644 index 0000000000..09b1b6f5a6 --- /dev/null +++ b/app/services/utils.server.service.js @@ -0,0 +1,16 @@ +'use strict'; + +var crypto = require('crypto'); + +/** + * Service to generate 20 bytes token + * @param callback - {Function} in the form of function(err, token) + * err - {Error} + * token - {String} + */ +exports.generateToken = function(callback) { + crypto.randomBytes(20, function(err, buffer) { + var token = buffer.toString('hex'); + callback(err, token); + }); +}; \ No newline at end of file diff --git a/config/init.js b/config/init.js index d5bd17cc0e..a71498a8b6 100644 --- a/config/init.js +++ b/config/init.js @@ -34,7 +34,8 @@ module.exports = function() { /** * Add our server node extensions */ - require.extensions['.server.controller.js'] = require.extensions['.js']; + require.extensions['.server.controller.js'] = require.extensions['.js']; + require.extensions['.server.service.js'] = require.extensions['.js']; require.extensions['.server.model.js'] = require.extensions['.js']; require.extensions['.server.routes.js'] = require.extensions['.js']; }; \ No newline at end of file