diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..61617beec8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,49 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# Howto with your editor: +# Sublime: https://github.com/sindresorhus/editorconfig-sublime + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[**] +end_of_line = lf +insert_final_newline = true + +# Standard at: https://github.com/felixge/node-style-guide +[**.js, **.json] +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 +max_line_length = 80 +quote_type = single +curly_bracket_next_line = false +spaces_around_operators = true +space_after_control_statements = true +space_after_anonymous_functions = false +spaces_in_brackets = false + +# https://github.com/jedmao/codepainter +[node_modules/**.js] +codepaint = false + +# No Standard. Please document a standard if different from .js +[**.yml, **.html, **.css] +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +# No standard. Please document a standard if different from .js +[**.md] +indent_style = space + +# Standard at: +[**.py] +indent_style = space +indent_size = 4 + +# Standard at: +[Makefile] +indent_style = tab +indent_size = 8 diff --git a/.gitignore b/.gitignore index ade4e05e6c..775a12abad 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ npm-debug.log node_modules/ public/lib +public/dist app/tests/coverage/ .bower-*/ .idea/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..96dc98b13a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +FROM dockerfile/nodejs + +MAINTAINER Matthias Luebken, matthias@catalyst-zero.com + +WORKDIR /home/mean + +# Install Mean.JS Prerequisites +RUN npm install -g grunt-cli +RUN npm install -g bower + +# Install Mean.JS packages +ADD package.json /home/mean/package.json +RUN npm install + +# Manually trigger bower. Why doesnt this work via npm install? +ADD .bowerrc /home/mean/.bowerrc +ADD bower.json /home/mean/bower.json +RUN bower install --config.interactive=false --allow-root + +# Make everything available for start +ADD . /home/mean + +# currently only works for development +ENV NODE_ENV development + +# Port 3000 for server +# Port 35729 for livereload +EXPOSE 3000 35729 +CMD ["grunt"] \ No newline at end of file diff --git a/README.md b/README.md index 05ac4b3b0e..dec285d123 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,29 @@ Your application should run on the 3000 port so in your browser just go to [http That's it! your application should be running by now, to proceed with your development check the other sections in this documentation. If you encounter any problem try the Troubleshooting section. +## Development and deployment With Docker + +* Install [Docker](http://www.docker.com/) +* Install [Fig](https://github.com/orchardup/fig) + +* Local development and testing with fig: +```bash +$ fig up +``` + +* Local development and testing with just Docker: +```bash +$ docker build -t mean . +$ docker run -p 27017:27017 -d --name db mongo +$ docker run -p 3000:3000 --link db:db_1 mean +$ +``` + +* To enable live reload forward 35729 port and mount /app and /public as volumes: +```bash +$ docker run -p 3000:3000 -p 35729:35729 -v /Users/mdl/workspace/mean-stack/mean/public:/home/mean/public -v /Users/mdl/workspa/mean-stack/mean/app:/home/mean/app --link db:db_1 mean +``` + ## Getting Started With MEAN.JS You have your application running but there are a lot of stuff to understand, we recommend you'll go over the [Offical Documentation](http://meanjs.org/docs.html). In the docs we'll try to explain both general concepts of MEAN components and give you some guidelines to help you improve your development procees. We tried covering as many aspects as possible, and will keep update it by your request, you can also help us develop the documentation better by checking out the *gh-pages* branch of this repository. diff --git a/app/controllers/articles.server.controller.js b/app/controllers/articles.server.controller.js index c60e93035b..dcbe5b8a0a 100644 --- a/app/controllers/articles.server.controller.js +++ b/app/controllers/articles.server.controller.js @@ -4,33 +4,10 @@ * Module dependencies. */ var mongoose = require('mongoose'), + errorHandler = require('./errors'), Article = mongoose.model('Article'), _ = require('lodash'); -/** - * Get the error message from error object - */ -var getErrorMessage = function(err) { - var message = ''; - - if (err.code) { - switch (err.code) { - case 11000: - case 11001: - message = 'Article already exists'; - break; - default: - message = 'Something went wrong'; - } - } else { - for (var errName in err.errors) { - if (err.errors[errName].message) message = err.errors[errName].message; - } - } - - return message; -}; - /** * Create a article */ @@ -40,8 +17,8 @@ exports.create = function(req, res) { article.save(function(err) { if (err) { - return res.send(400, { - message: getErrorMessage(err) + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) }); } else { res.jsonp(article); @@ -66,8 +43,8 @@ exports.update = function(req, res) { article.save(function(err) { if (err) { - return res.send(400, { - message: getErrorMessage(err) + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) }); } else { res.jsonp(article); @@ -83,8 +60,8 @@ exports.delete = function(req, res) { article.remove(function(err) { if (err) { - return res.send(400, { - message: getErrorMessage(err) + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) }); } else { res.jsonp(article); @@ -98,8 +75,8 @@ exports.delete = function(req, res) { exports.list = function(req, res) { Article.find().sort('-created').populate('user', 'displayName').exec(function(err, articles) { if (err) { - return res.send(400, { - message: getErrorMessage(err) + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) }); } else { res.jsonp(articles); @@ -124,7 +101,7 @@ exports.articleByID = function(req, res, next, id) { */ exports.hasAuthorization = function(req, res, next) { if (req.article.user.id !== req.user.id) { - return res.send(403, { + return res.status(403).send({ message: 'User is not authorized' }); } diff --git a/app/controllers/errors.server.controller.js b/app/controllers/errors.server.controller.js new file mode 100644 index 0000000000..e4604f8c9c --- /dev/null +++ b/app/controllers/errors.server.controller.js @@ -0,0 +1,42 @@ +'use strict'; + +/** + * Get unique error field name + */ +var getUniqueErrorMessage = function(err) { + var output; + + try { + var fieldName = err.err.substring(err.err.lastIndexOf('.$') + 2, err.err.lastIndexOf('_1')); + output = fieldName.charAt(0).toUpperCase() + fieldName.slice(1) + ' already exist'; + + } catch(ex) { + output = 'Unique field already exist'; + } + + return output; +}; + +/** + * Get the error message from error object + */ +exports.getErrorMessage = function(err) { + var message = ''; + + if (err.code) { + switch (err.code) { + case 11000: + case 11001: + message = getUniqueErrorMessage(err); + break; + default: + message = 'Something went wrong'; + } + } else { + for (var errName in err.errors) { + if (err.errors[errName].message) message = err.errors[errName].message; + } + } + + return message; +}; \ No newline at end of file diff --git a/app/controllers/users.server.controller.js b/app/controllers/users.server.controller.js index 84f80bb920..94859e7227 100755 --- a/app/controllers/users.server.controller.js +++ b/app/controllers/users.server.controller.js @@ -3,380 +3,14 @@ /** * Module dependencies. */ -var mongoose = require('mongoose'), - passport = require('passport'), - User = mongoose.model('User'), - _ = require('lodash'); +var _ = require('lodash'); /** - * Get the error message from error object + * Extend user's controller */ -var getErrorMessage = function(err) { - var message = ''; - - if (err.code) { - switch (err.code) { - case 11000: - case 11001: - message = 'Username already exists'; - break; - default: - message = 'Something went wrong'; - } - } else { - for (var errName in err.errors) { - if (err.errors[errName].message) message = err.errors[errName].message; - } - } - - return message; -}; - -/** - * Signup - */ -exports.signup = function(req, res) { - // For security measurement we remove the roles from the req.body object - delete req.body.roles; - - // Init Variables - var user = new User(req.body); - var message = null; - - // Add missing user fields - user.provider = 'local'; - user.displayName = user.firstName + ' ' + user.lastName; - - // Then save the user - user.save(function(err) { - if (err) { - return res.send(400, { - message: getErrorMessage(err) - }); - } else { - // Remove sensitive data before login - user.password = undefined; - user.salt = undefined; - - req.login(user, function(err) { - if (err) { - res.send(400, err); - } else { - res.jsonp(user); - } - }); - } - }); -}; - -/** - * Signin after passport authentication - */ -exports.signin = function(req, res, next) { - passport.authenticate('local', function(err, user, info) { - if (err || !user) { - res.send(400, info); - } else { - // Remove sensitive data before login - user.password = undefined; - user.salt = undefined; - - req.login(user, function(err) { - if (err) { - res.send(400, err); - } else { - res.jsonp(user); - } - }); - } - })(req, res, next); -}; - -/** - * Update user details - */ -exports.update = function(req, res) { - // Init Variables - var user = req.user; - var message = null; - - // For security measurement we remove the roles from the req.body object - delete req.body.roles; - - if (user) { - // Merge existing user - user = _.extend(user, req.body); - user.updated = Date.now(); - user.displayName = user.firstName + ' ' + user.lastName; - - user.save(function(err) { - if (err) { - return res.send(400, { - message: getErrorMessage(err) - }); - } else { - req.login(user, function(err) { - if (err) { - res.send(400, err); - } else { - res.jsonp(user); - } - }); - } - }); - } else { - res.send(400, { - message: 'User is not signed in' - }); - } -}; - -/** - * Change Password - */ -exports.changePassword = function(req, res, next) { - // Init Variables - var passwordDetails = req.body; - var message = null; - - if (req.user) { - if (passwordDetails.newPassword) { - User.findById(req.user.id, function(err, user) { - if (!err && user) { - if (user.authenticate(passwordDetails.currentPassword)) { - if (passwordDetails.newPassword === passwordDetails.verifyPassword) { - user.password = passwordDetails.newPassword; - - user.save(function(err) { - if (err) { - return res.send(400, { - message: getErrorMessage(err) - }); - } else { - req.login(user, function(err) { - if (err) { - res.send(400, err); - } else { - res.send({ - message: 'Password changed successfully' - }); - } - }); - } - }); - } else { - res.send(400, { - message: 'Passwords do not match' - }); - } - } else { - res.send(400, { - message: 'Current password is incorrect' - }); - } - } else { - res.send(400, { - message: 'User is not found' - }); - } - }); - } else { - res.send(400, { - message: 'Please provide a new password' - }); - } - } else { - res.send(400, { - message: 'User is not signed in' - }); - } -}; - -/** - * Signout - */ -exports.signout = function(req, res) { - req.logout(); - res.redirect('/'); -}; - -/** - * Send User - */ -exports.me = function(req, res) { - res.jsonp(req.user || null); -}; - -/** - * OAuth callback - */ -exports.oauthCallback = function(strategy) { - return function(req, res, next) { - passport.authenticate(strategy, function(err, user, redirectURL) { - if (err || !user) { - return res.redirect('/#!/signin'); - } - req.login(user, function(err) { - if (err) { - return res.redirect('/#!/signin'); - } - - return res.redirect(redirectURL || '/'); - }); - })(req, res, next); - }; -}; - -/** - * User middleware - */ -exports.userByID = function(req, res, next, id) { - User.findOne({ - _id: id - }).exec(function(err, user) { - if (err) return next(err); - if (!user) return next(new Error('Failed to load User ' + id)); - req.profile = user; - next(); - }); -}; - -/** - * Require login routing middleware - */ -exports.requiresLogin = function(req, res, next) { - if (!req.isAuthenticated()) { - return res.send(401, { - message: 'User is not logged in' - }); - } - - next(); -}; - -/** - * User authorizations routing middleware - */ -exports.hasAuthorization = function(roles) { - var _this = this; - - return function(req, res, next) { - _this.requiresLogin(req, res, function() { - if (_.intersection(req.user.roles, roles).length) { - return next(); - } else { - return res.send(403, { - message: 'User is not authorized' - }); - } - }); - }; -}; - -/** - * Helper function to save or update a OAuth user profile - */ -exports.saveOAuthUserProfile = function(req, providerUserProfile, done) { - if (!req.user) { - // Define a search query fields - var searchMainProviderIdentifierField = 'providerData.' + providerUserProfile.providerIdentifierField; - var searchAdditionalProviderIdentifierField = 'additionalProvidersData.' + providerUserProfile.provider + '.' + providerUserProfile.providerIdentifierField; - - // Define main provider search query - var mainProviderSearchQuery = {}; - mainProviderSearchQuery.provider = providerUserProfile.provider; - mainProviderSearchQuery[searchMainProviderIdentifierField] = providerUserProfile.providerData[providerUserProfile.providerIdentifierField]; - - // Define additional provider search query - var additionalProviderSearchQuery = {}; - additionalProviderSearchQuery[searchAdditionalProviderIdentifierField] = providerUserProfile.providerData[providerUserProfile.providerIdentifierField]; - - // Define a search query to find existing user with current provider profile - var searchQuery = { - $or: [mainProviderSearchQuery, additionalProviderSearchQuery] - }; - - User.findOne(searchQuery, function(err, user) { - if (err) { - return done(err); - } else { - if (!user) { - var possibleUsername = providerUserProfile.username || ((providerUserProfile.email) ? providerUserProfile.email.split('@')[0] : ''); - - User.findUniqueUsername(possibleUsername, null, function(availableUsername) { - user = new User({ - firstName: providerUserProfile.firstName, - lastName: providerUserProfile.lastName, - username: availableUsername, - displayName: providerUserProfile.displayName, - email: providerUserProfile.email, - provider: providerUserProfile.provider, - providerData: providerUserProfile.providerData - }); - - // And save the user - user.save(function(err) { - return done(err, user); - }); - }); - } else { - return done(err, user); - } - } - }); - } else { - // User is already logged in, join the provider data to the existing user - var user = req.user; - - // Check if user exists, is not signed in using this provider, and doesn't have that provider data already configured - if (user.provider !== providerUserProfile.provider && (!user.additionalProvidersData || !user.additionalProvidersData[providerUserProfile.provider])) { - // Add the provider data to the additional provider data field - if (!user.additionalProvidersData) user.additionalProvidersData = {}; - user.additionalProvidersData[providerUserProfile.provider] = providerUserProfile.providerData; - - // Then tell mongoose that we've updated the additionalProvidersData field - user.markModified('additionalProvidersData'); - - // And save the user - user.save(function(err) { - return done(err, user, '/#!/settings/accounts'); - }); - } else { - return done(new Error('User is already connected using this provider'), user); - } - } -}; - -/** - * Remove OAuth provider - */ -exports.removeOAuthProvider = function(req, res, next) { - var user = req.user; - var provider = req.param('provider'); - - if (user && provider) { - // Delete the additional provider - if (user.additionalProvidersData[provider]) { - delete user.additionalProvidersData[provider]; - - // Then tell mongoose that we've updated the additionalProvidersData field - user.markModified('additionalProvidersData'); - } - - user.save(function(err) { - if (err) { - return res.send(400, { - message: getErrorMessage(err) - }); - } else { - req.login(user, function(err) { - if (err) { - res.send(400, err); - } else { - res.jsonp(user); - } - }); - } - }); - } -}; \ No newline at end of file +module.exports = _.extend( + require('./users/users.authentication'), + require('./users/users.authorization'), + require('./users/users.password'), + require('./users/users.profile') +); \ No newline at end of file diff --git a/app/controllers/users/users.authentication.server.controller.js b/app/controllers/users/users.authentication.server.controller.js new file mode 100644 index 0000000000..a912157a3b --- /dev/null +++ b/app/controllers/users/users.authentication.server.controller.js @@ -0,0 +1,98 @@ +'use strict'; + +/** + * Module dependencies. + */ +var _ = require('lodash'), + errorHandler = require('../errors'), + mongoose = require('mongoose'), + passport = require('passport'), + User = mongoose.model('User'); + +/** + * Signup + */ +exports.signup = function(req, res) { + // For security measurement we remove the roles from the req.body object + delete req.body.roles; + + // Init Variables + var user = new User(req.body); + var message = null; + + // Add missing user fields + user.provider = 'local'; + user.displayName = user.firstName + ' ' + user.lastName; + + // Then save the user + user.save(function(err) { + if (err) { + return res.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); + } + }); + } + }); +}; + +/** + * 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); +}; + +/** + * Signout + */ +exports.signout = function(req, res) { + req.logout(); + res.redirect('/'); +}; + +/** + * OAuth callback + */ +exports.oauthCallback = function(strategy) { + return function(req, res, next) { + passport.authenticate(strategy, function(err, user, redirectURL) { + if (err || !user) { + return res.redirect('/#!/signin'); + } + req.login(user, function(err) { + if (err) { + return res.redirect('/#!/signin'); + } + + return res.redirect(redirectURL || '/'); + }); + })(req, res, next); + }; +}; \ 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 new file mode 100644 index 0000000000..efa3b69524 --- /dev/null +++ b/app/controllers/users/users.authorization.server.controller.js @@ -0,0 +1,54 @@ +'use strict'; + +/** + * Module dependencies. + */ +var _ = require('lodash'), + mongoose = require('mongoose'), + User = mongoose.model('User'); + +/** + * User middleware + */ +exports.userByID = function(req, res, next, id) { + User.findOne({ + _id: id + }).exec(function(err, user) { + if (err) return next(err); + if (!user) return next(new Error('Failed to load User ' + id)); + req.profile = user; + next(); + }); +}; + +/** + * Require login routing middleware + */ +exports.requiresLogin = function(req, res, next) { + if (!req.isAuthenticated()) { + return res.status(401).send({ + message: 'User is not logged in' + }); + } + + next(); +}; + +/** + * User authorizations routing middleware + */ +exports.hasAuthorization = function(roles) { + var _this = this; + + return function(req, res, next) { + _this.requiresLogin(req, res, function() { + if (_.intersection(req.user.roles, roles).length) { + return next(); + } else { + return res.status(403).send({ + message: 'User is not authorized' + }); + } + }); + }; +}; \ No newline at end of file diff --git a/app/controllers/users/users.password.server.controller.js b/app/controllers/users/users.password.server.controller.js new file mode 100644 index 0000000000..70ffaced19 --- /dev/null +++ b/app/controllers/users/users.password.server.controller.js @@ -0,0 +1,245 @@ +'use strict'; + +/** + * Module dependencies. + */ +var _ = require('lodash'), + errorHandler = require('../errors'), + mongoose = require('mongoose'), + passport = require('passport'), + User = mongoose.model('User'), + config = require('../../../config/config'), + swig = require('swig'), + nodemailer = require('nodemailer'), + crypto = require('crypto'), + async = require('async'), + crypto = require('crypto'); + +/** + * Forgot for reset password (forgot POST) + */ +exports.forgot = function(req, res, next) { + async.waterfall([ + // Generate random token + function(done) { + crypto.randomBytes(20, function(err, buffer) { + var token = buffer.toString('hex'); + done(err, token); + }); + }, + // Lookup user by username + function(token, done) { + if (req.body.username) { + User.findOne({ + username: req.body.username + }, '-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 + + 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) { + res.send(200, { + message: 'An email has been sent to ' + user.email + ' with further instructions.' + }); + done(err, 'done'); + }); + } + ], function(err) { + if (err) return next(err); + }); +}; + +/** + * Reset password GET from email token + */ +exports.validateResetToken = function(req, res) { + User.findOne({ + resetPasswordToken: req.params.token, + resetPasswordExpires: { + $gt: Date.now() + } + }, function(err, user) { + if (!user) { + return res.redirect('/#!/password/reset/invalid'); + } + + res.redirect('/#!/password/reset/' + req.params.token); + }); +}; + +/** + * Reset password POST from email token + */ +exports.reset = function(req, res, next) { + // Init Variables + var passwordDetails = req.body; + var message = null; + + async.waterfall([ + + function(done) { + User.findOne({ + resetPasswordToken: req.params.token, + resetPasswordExpires: { + $gt: Date.now() + } + }, function(err, user) { + if (!err && user) { + if (passwordDetails.newPassword === passwordDetails.verifyPassword) { + user.password = passwordDetails.newPassword; + user.resetPasswordToken = undefined; + user.resetPasswordExpires = undefined; + + user.save(function(err) { + if (err) { + return res.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); + + 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); + }); +}; + +/** + * Change Password + */ +exports.changePassword = function(req, res, next) { + // Init Variables + var passwordDetails = req.body; + var message = null; + + if (req.user) { + if (passwordDetails.newPassword) { + User.findById(req.user.id, function(err, user) { + if (!err && user) { + if (user.authenticate(passwordDetails.currentPassword)) { + if (passwordDetails.newPassword === passwordDetails.verifyPassword) { + user.password = passwordDetails.newPassword; + + user.save(function(err) { + if (err) { + return res.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' + }); + } +}; \ 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 new file mode 100644 index 0000000000..3fcf730f5d --- /dev/null +++ b/app/controllers/users/users.profile.server.controller.js @@ -0,0 +1,164 @@ +'use strict'; + +/** + * Module dependencies. + */ +var _ = require('lodash'), + errorHandler = require('../errors'), + mongoose = require('mongoose'), + passport = require('passport'), + User = mongoose.model('User'); + +/** + * Update user details + */ +exports.update = function(req, res) { + // Init Variables + var user = req.user; + var message = null; + + // For security measurement we remove the roles from the req.body object + delete req.body.roles; + + if (user) { + // Merge existing user + user = _.extend(user, req.body); + user.updated = Date.now(); + user.displayName = user.firstName + ' ' + user.lastName; + + user.save(function(err) { + if (err) { + return res.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' + }); + } +}; + +/** + * Send User + */ +exports.me = function(req, res) { + res.jsonp(req.user || null); +}; + +/** + * Helper function to save or update a OAuth user profile + */ +exports.saveOAuthUserProfile = function(req, providerUserProfile, done) { + if (!req.user) { + // Define a search query fields + var searchMainProviderIdentifierField = 'providerData.' + providerUserProfile.providerIdentifierField; + var searchAdditionalProviderIdentifierField = 'additionalProvidersData.' + providerUserProfile.provider + '.' + providerUserProfile.providerIdentifierField; + + // Define main provider search query + var mainProviderSearchQuery = {}; + mainProviderSearchQuery.provider = providerUserProfile.provider; + mainProviderSearchQuery[searchMainProviderIdentifierField] = providerUserProfile.providerData[providerUserProfile.providerIdentifierField]; + + // Define additional provider search query + var additionalProviderSearchQuery = {}; + additionalProviderSearchQuery[searchAdditionalProviderIdentifierField] = providerUserProfile.providerData[providerUserProfile.providerIdentifierField]; + + // Define a search query to find existing user with current provider profile + var searchQuery = { + $or: [mainProviderSearchQuery, additionalProviderSearchQuery] + }; + + User.findOne(searchQuery, function(err, user) { + if (err) { + return done(err); + } else { + if (!user) { + var possibleUsername = providerUserProfile.username || ((providerUserProfile.email) ? providerUserProfile.email.split('@')[0] : ''); + + User.findUniqueUsername(possibleUsername, null, function(availableUsername) { + user = new User({ + firstName: providerUserProfile.firstName, + lastName: providerUserProfile.lastName, + username: availableUsername, + displayName: providerUserProfile.displayName, + email: providerUserProfile.email, + provider: providerUserProfile.provider, + providerData: providerUserProfile.providerData + }); + + // And save the user + user.save(function(err) { + return done(err, user); + }); + }); + } else { + return done(err, user); + } + } + }); + } else { + // User is already logged in, join the provider data to the existing user + var user = req.user; + + // Check if user exists, is not signed in using this provider, and doesn't have that provider data already configured + if (user.provider !== providerUserProfile.provider && (!user.additionalProvidersData || !user.additionalProvidersData[providerUserProfile.provider])) { + // Add the provider data to the additional provider data field + if (!user.additionalProvidersData) user.additionalProvidersData = {}; + user.additionalProvidersData[providerUserProfile.provider] = providerUserProfile.providerData; + + // Then tell mongoose that we've updated the additionalProvidersData field + user.markModified('additionalProvidersData'); + + // And save the user + user.save(function(err) { + return done(err, user, '/#!/settings/accounts'); + }); + } else { + return done(new Error('User is already connected using this provider'), user); + } + } +}; + +/** + * Remove OAuth provider + */ +exports.removeOAuthProvider = function(req, res, next) { + var user = req.user; + var provider = req.param('provider'); + + if (user && provider) { + // Delete the additional provider + if (user.additionalProvidersData[provider]) { + delete user.additionalProvidersData[provider]; + + // Then tell mongoose that we've updated the additionalProvidersData field + user.markModified('additionalProvidersData'); + } + + user.save(function(err) { + if (err) { + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); + } else { + req.login(user, function(err) { + if (err) { + res.status(400).send(err); + } else { + res.jsonp(user); + } + }); + } + }); + } +}; \ No newline at end of file diff --git a/app/models/user.server.model.js b/app/models/user.server.model.js index 6cfe08c231..76ff071ba2 100755 --- a/app/models/user.server.model.js +++ b/app/models/user.server.model.js @@ -50,7 +50,7 @@ var UserSchema = new Schema({ }, username: { type: String, - unique: true, + unique: 'testing error message', required: 'Please fill in a username', trim: true }, @@ -81,7 +81,14 @@ var UserSchema = new Schema({ created: { type: Date, default: Date.now - } + }, + /* For reset password */ + resetPasswordToken: { + type: String + }, + resetPasswordExpires: { + type: Date + } }); /** diff --git a/app/routes/users.server.routes.js b/app/routes/users.server.routes.js index c7bd094032..4ddb73946d 100644 --- a/app/routes/users.server.routes.js +++ b/app/routes/users.server.routes.js @@ -8,12 +8,19 @@ var passport = require('passport'); module.exports = function(app) { // User Routes var users = require('../../app/controllers/users'); + + // Setting up the users profile api app.route('/users/me').get(users.me); app.route('/users').put(users.update); - app.route('/users/password').post(users.changePassword); app.route('/users/accounts').delete(users.removeOAuthProvider); - // Setting up the users api + // Setting up the users password api + app.route('/users/password').post(users.changePassword); + app.route('/auth/forgot').post(users.forgot); + app.route('/auth/reset/:token').get(users.validateResetToken); + app.route('/auth/reset/:token').post(users.reset); + + // Setting up the users authentication api app.route('/auth/signup').post(users.signup); app.route('/auth/signin').post(users.signin); app.route('/auth/signout').get(users.signout); @@ -43,4 +50,4 @@ module.exports = function(app) { // Finish by binding the user middleware app.param('userId', users.userByID); -}; \ No newline at end of file +}; diff --git a/app/views/templates/reset-password-confirm-email.server.view.html b/app/views/templates/reset-password-confirm-email.server.view.html new file mode 100644 index 0000000000..eec61a672c --- /dev/null +++ b/app/views/templates/reset-password-confirm-email.server.view.html @@ -0,0 +1,16 @@ + + + +
+ + + +Dear {{name}},
+ +This is a confirmation that the password for your account has just been changed
+The {{appName}} Support Team
+ + + \ No newline at end of file diff --git a/app/views/templates/reset-password-email.server.view.html b/app/views/templates/reset-password-email.server.view.html new file mode 100644 index 0000000000..eb73cb83e3 --- /dev/null +++ b/app/views/templates/reset-password-email.server.view.html @@ -0,0 +1,22 @@ + + + + + + + + +Dear {{name}},
++ You have requested to have your password reset for your account at {{appName}} +
+Please visit this url to reset your password:
+{{url}}
+ If you didn't make this request, you can ignore this email. +The {{appName}} Support Team
+ + + \ No newline at end of file diff --git a/bower.json b/bower.json index 7335610f8c..4dcc0a6e0e 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "meanjs", - "version": "0.3.1", + "version": "0.3.2", "description": "Fullstack JavaScript with MongoDB, Express, AngularJS, and Node.js.", "dependencies": { "bootstrap": "~3", diff --git a/config/config.js b/config/config.js index 8ba8be42fb..3baa02e552 100644 --- a/config/config.js +++ b/config/config.js @@ -22,7 +22,7 @@ module.exports.getGlobbedFiles = function(globPatterns, removeRoot) { var _this = this; // URL paths regex - var urlRegex = new RegExp('^(?:[a-z]+:)?//', 'i'); + var urlRegex = new RegExp('^(?:[a-z]+:)?\/\/', 'i'); // The output array var output = []; diff --git a/config/env/development.js b/config/env/development.js index 17470972f2..5140111607 100644 --- a/config/env/development.js +++ b/config/env/development.js @@ -24,5 +24,15 @@ module.exports = { clientID: process.env.LINKEDIN_ID || 'APP_ID', clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', callbackURL: 'http://localhost:3000/auth/linkedin/callback' + }, + mailer: { + from: process.env.MAILER_FROM || 'MAILER_FROM', + options: { + service: process.env.MAILER_SERVICE_PROVIDER || 'MAILER_SERVICE_PROVIDER', + auth: { + user: process.env.MAILER_EMAIL_ID || 'MAILER_EMAIL_ID', + pass: process.env.MAILER_PASSWORD || 'MAILER_PASSWORD' + } + } } }; \ No newline at end of file diff --git a/config/env/production.js b/config/env/production.js index 26c97d775d..6a2bf89584 100644 --- a/config/env/production.js +++ b/config/env/production.js @@ -39,5 +39,15 @@ module.exports = { clientID: process.env.LINKEDIN_ID || 'APP_ID', clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', callbackURL: 'http://localhost:3000/auth/linkedin/callback' + }, + mailer: { + from: process.env.MAILER_FROM || 'MAILER_FROM', + options: { + service: process.env.MAILER_SERVICE_PROVIDER || 'MAILER_SERVICE_PROVIDER', + auth: { + user: process.env.MAILER_EMAIL_ID || 'MAILER_EMAIL_ID', + pass: process.env.MAILER_PASSWORD || 'MAILER_PASSWORD' + } + } } }; \ No newline at end of file diff --git a/config/env/test.js b/config/env/test.js index cc3f0aa4c4..060d67517c 100644 --- a/config/env/test.js +++ b/config/env/test.js @@ -25,5 +25,15 @@ module.exports = { clientID: process.env.LINKEDIN_ID || 'APP_ID', clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', callbackURL: 'http://localhost:3000/auth/linkedin/callback' + }, + mailer: { + from: process.env.MAILER_FROM || 'MAILER_FROM', + options: { + service: process.env.MAILER_SERVICE_PROVIDER || 'MAILER_SERVICE_PROVIDER', + auth: { + user: process.env.MAILER_EMAIL_ID || 'MAILER_EMAIL_ID', + pass: process.env.MAILER_PASSWORD || 'MAILER_PASSWORD' + } + } } }; \ No newline at end of file diff --git a/config/express.js b/config/express.js index 1614cc63e4..bea1c88dac 100755 --- a/config/express.js +++ b/config/express.js @@ -73,7 +73,9 @@ module.exports = function(db) { } // Request body parsing middleware should be above methodOverride - app.use(bodyParser.urlencoded()); + app.use(bodyParser.urlencoded({ + extended: true + })); app.use(bodyParser.json()); app.use(methodOverride()); @@ -85,6 +87,8 @@ module.exports = function(db) { // Express MongoDB session storage app.use(session({ + saveUninitialized: true, + resave: true, secret: config.sessionSecret, store: new mongoStore({ db: db.connection.db, @@ -101,8 +105,8 @@ module.exports = function(db) { // Use helmet to secure Express headers app.use(helmet.xframe()); - app.use(helmet.iexss()); - app.use(helmet.contentTypeOptions()); + app.use(helmet.xssFilter()); + app.use(helmet.nosniff()); app.use(helmet.ienoopen()); app.disable('x-powered-by'); diff --git a/config/init.js b/config/init.js index 4fb15d5c47..d5bd17cc0e 100644 --- a/config/init.js +++ b/config/init.js @@ -19,9 +19,9 @@ module.exports = function() { console.log(); if (!environmentFiles.length) { if (process.env.NODE_ENV) { - console.log('\x1b[31m', 'No configuration file found for "' + process.env.NODE_ENV + '" environment using development instead'); + console.error('\x1b[31m', 'No configuration file found for "' + process.env.NODE_ENV + '" environment using development instead'); } else { - console.log('\x1b[31m', 'NODE_ENV is not defined! Using default development environment'); + console.error('\x1b[31m', 'NODE_ENV is not defined! Using default development environment'); } process.env.NODE_ENV = 'development'; diff --git a/fig.yml b/fig.yml new file mode 100644 index 0000000000..4726a15100 --- /dev/null +++ b/fig.yml @@ -0,0 +1,12 @@ +web: + build: . + links: + - db + ports: + - "3000:3000" + environment: + NODE_ENV: development +db: + image: mongo + ports: + - "27017:27017" \ No newline at end of file diff --git a/gruntfile.js b/gruntfile.js index aab536a558..ca48bbd883 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -5,7 +5,7 @@ module.exports = function(grunt) { var watchFiles = { serverViews: ['app/views/**/*.*'], serverJS: ['gruntfile.js', 'server.js', 'config/**/*.js', 'app/**/*.js'], - clientViews: ['public/modules/**/views/*.html'], + clientViews: ['public/modules/**/views/**/*.html'], clientJS: ['public/js/*.js', 'public/modules/**/*.js'], clientCSS: ['public/modules/**/*.css'], mochaTests: ['app/tests/**/*.js'] diff --git a/package.json b/package.json index cd9eacc6af..f91d486c47 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.3.1", + "version": "0.3.2", "private": false, "author": "https://github.com/meanjs/mean/graphs/contributors", "repository": { @@ -18,18 +18,18 @@ "postinstall": "bower install --config.interactive=false" }, "dependencies": { - "express": "~4.2.0", - "express-session": "~1.1.0", - "body-parser": "~1.2.0", - "cookie-parser": "~1.1.0", - "compression": "~1.0.1", - "method-override": "~1.0.0", - "morgan": "~1.1.0", - "connect-mongo": "~0.4.0", + "express": "~4.7.2", + "express-session": "~1.7.2", + "body-parser": "~1.5.2", + "cookie-parser": "~1.3.2", + "compression": "~1.0.9", + "method-override": "~2.1.2", + "morgan": "~1.2.2", + "connect-mongo": "~0.4.1", "connect-flash": "~0.1.1", - "helmet": "~0.2.1", + "helmet": "~0.4.0", "consolidate": "~0.10.0", - "swig": "~1.3.2", + "swig": "~1.4.1", "mongoose": "~3.8.8", "passport": "~0.2.0", "passport-local": "~1.0.0", @@ -39,26 +39,28 @@ "passport-google-oauth": "~0.1.5", "lodash": "~2.4.1", "forever": "~0.11.0", - "bower": "~1.3.1", + "bower": "~1.3.8", "grunt-cli": "~0.1.13", - "glob": "~3.2.9" + "glob": "~4.0.5", + "async": "~0.9.0", + "nodemailer": "~1.1.1" }, "devDependencies": { - "supertest": "~0.12.1", - "should": "~3.3.1", + "supertest": "~0.13.0", + "should": "~4.0.4", "grunt-env": "~0.4.1", "grunt-node-inspector": "~0.1.3", "grunt-contrib-watch": "~0.6.1", "grunt-contrib-jshint": "~0.10.0", "grunt-contrib-csslint": "^0.2.0", "grunt-ngmin": "0.0.3", - "grunt-contrib-uglify": "~0.4.0", - "grunt-contrib-cssmin": "~0.9.0", - "grunt-nodemon": "~0.2.1", + "grunt-contrib-uglify": "~0.5.1", + "grunt-contrib-cssmin": "~0.10.0", + "grunt-nodemon": "~0.3.0", "grunt-concurrent": "~0.5.0", - "grunt-mocha-test": "~0.10.0", + "grunt-mocha-test": "~0.11.0", "grunt-karma": "~0.8.2", - "load-grunt-tasks": "~0.4.0", + "load-grunt-tasks": "~0.6.0", "karma": "~0.12.0", "karma-jasmine": "~0.2.1", "karma-coverage": "~0.2.0", diff --git a/public/config.js b/public/config.js index f61ff77ac9..75de1c4bce 100644 --- a/public/config.js +++ b/public/config.js @@ -7,9 +7,9 @@ var ApplicationConfiguration = (function() { var applicationModuleVendorDependencies = ['ngResource', 'ngAnimate', 'ui.router', 'ui.bootstrap', 'ui.utils']; // Add a new vertical module - var registerModule = function(moduleName) { + var registerModule = function(moduleName, dependencies) { // Create angular module - angular.module(moduleName, []); + angular.module(moduleName, dependencies || []); // Add the module to the AngularJS configuration file angular.module(applicationModuleName).requires.push(moduleName); diff --git a/public/dist/application.js b/public/dist/application.js deleted file mode 100644 index 305403c9bf..0000000000 --- a/public/dist/application.js +++ /dev/null @@ -1,461 +0,0 @@ -'use strict'; -// Init the application configuration module for AngularJS application -var ApplicationConfiguration = function () { - // Init module configuration options - var applicationModuleName = 'mean'; - var applicationModuleVendorDependencies = [ - 'ngResource', - 'ngAnimate', - 'ui.router', - 'ui.bootstrap', - 'ui.utils' - ]; - // Add a new vertical module - var registerModule = function (moduleName) { - // Create angular module - angular.module(moduleName, []); - // Add the module to the AngularJS configuration file - angular.module(applicationModuleName).requires.push(moduleName); - }; - return { - applicationModuleName: applicationModuleName, - applicationModuleVendorDependencies: applicationModuleVendorDependencies, - registerModule: registerModule - }; - }();'use strict'; -//Start by defining the main module and adding the module dependencies -angular.module(ApplicationConfiguration.applicationModuleName, ApplicationConfiguration.applicationModuleVendorDependencies); -// Setting HTML5 Location Mode -angular.module(ApplicationConfiguration.applicationModuleName).config([ - '$locationProvider', - function ($locationProvider) { - $locationProvider.hashPrefix('!'); - } -]); -//Then define the init function for starting up the application -angular.element(document).ready(function () { - //Fixing facebook bug with redirect - if (window.location.hash === '#_=_') - window.location.hash = '#!'; - //Then init the app - angular.bootstrap(document, [ApplicationConfiguration.applicationModuleName]); -});'use strict'; -// Use Applicaion configuration module to register a new module -ApplicationConfiguration.registerModule('articles');'use strict'; -// Use Applicaion configuration module to register a new module -ApplicationConfiguration.registerModule('core');'use strict'; -// Use Applicaion configuration module to register a new module -ApplicationConfiguration.registerModule('users');'use strict'; -// Configuring the Articles module -angular.module('articles').run([ - 'Menus', - function (Menus) { - // Set top bar menu items - Menus.addMenuItem('topbar', 'Articles', 'articles', 'dropdown', '/articles(/create)?'); - Menus.addSubMenuItem('topbar', 'articles', 'List Articles', 'articles'); - Menus.addSubMenuItem('topbar', 'articles', 'New Article', 'articles/create'); - } -]);'use strict'; -// Setting up route -angular.module('articles').config([ - '$stateProvider', - function ($stateProvider) { - // Articles state routing - $stateProvider.state('listArticles', { - url: '/articles', - templateUrl: 'modules/articles/views/list-articles.client.view.html' - }).state('createArticle', { - url: '/articles/create', - templateUrl: 'modules/articles/views/create-article.client.view.html' - }).state('viewArticle', { - url: '/articles/:articleId', - templateUrl: 'modules/articles/views/view-article.client.view.html' - }).state('editArticle', { - url: '/articles/:articleId/edit', - templateUrl: 'modules/articles/views/edit-article.client.view.html' - }); - } -]);'use strict'; -angular.module('articles').controller('ArticlesController', [ - '$scope', - '$stateParams', - '$location', - 'Authentication', - 'Articles', - function ($scope, $stateParams, $location, Authentication, Articles) { - $scope.authentication = Authentication; - $scope.create = function () { - var article = new Articles({ - title: this.title, - content: this.content - }); - article.$save(function (response) { - $location.path('articles/' + response._id); - }, function (errorResponse) { - $scope.error = errorResponse.data.message; - }); - this.title = ''; - this.content = ''; - }; - $scope.remove = function (article) { - if (article) { - article.$remove(); - for (var i in $scope.articles) { - if ($scope.articles[i] === article) { - $scope.articles.splice(i, 1); - } - } - } else { - $scope.article.$remove(function () { - $location.path('articles'); - }); - } - }; - $scope.update = function () { - var article = $scope.article; - article.$update(function () { - $location.path('articles/' + article._id); - }, function (errorResponse) { - $scope.error = errorResponse.data.message; - }); - }; - $scope.find = function () { - $scope.articles = Articles.query(); - }; - $scope.findOne = function () { - $scope.article = Articles.get({ articleId: $stateParams.articleId }); - }; - } -]);'use strict'; -//Articles service used for communicating with the articles REST endpoints -angular.module('articles').factory('Articles', [ - '$resource', - function ($resource) { - return $resource('articles/:articleId', { articleId: '@_id' }, { update: { method: 'PUT' } }); - } -]);'use strict'; -// Setting up route -angular.module('core').config([ - '$stateProvider', - '$urlRouterProvider', - function ($stateProvider, $urlRouterProvider) { - // Redirect to home view when route not found - $urlRouterProvider.otherwise('/'); - // Home state routing - $stateProvider.state('home', { - url: '/', - templateUrl: 'modules/core/views/home.client.view.html' - }); - } -]);'use strict'; -angular.module('core').controller('HeaderController', [ - '$scope', - 'Authentication', - 'Menus', - function ($scope, Authentication, Menus) { - $scope.authentication = Authentication; - $scope.isCollapsed = false; - $scope.menu = Menus.getMenu('topbar'); - $scope.toggleCollapsibleMenu = function () { - $scope.isCollapsed = !$scope.isCollapsed; - }; - // Collapsing the menu after navigation - $scope.$on('$stateChangeSuccess', function () { - $scope.isCollapsed = false; - }); - } -]);'use strict'; -angular.module('core').controller('HomeController', [ - '$scope', - 'Authentication', - function ($scope, Authentication) { - // This provides Authentication context. - $scope.authentication = Authentication; - } -]);'use strict'; -//Menu service used for managing menus -angular.module('core').service('Menus', [function () { - // Define a set of default roles - this.defaultRoles = ['user']; - // Define the menus object - this.menus = {}; - // A private function for rendering decision - var shouldRender = function (user) { - if (user) { - for (var userRoleIndex in user.roles) { - for (var roleIndex in this.roles) { - if (this.roles[roleIndex] === user.roles[userRoleIndex]) { - return true; - } - } - } - } else { - return this.isPublic; - } - return false; - }; - // Validate menu existance - this.validateMenuExistance = function (menuId) { - if (menuId && menuId.length) { - if (this.menus[menuId]) { - return true; - } else { - throw new Error('Menu does not exists'); - } - } else { - throw new Error('MenuId was not provided'); - } - return false; - }; - // Get the menu object by menu id - this.getMenu = function (menuId) { - // Validate that the menu exists - this.validateMenuExistance(menuId); - // Return the menu object - return this.menus[menuId]; - }; - // Add new menu object by menu id - this.addMenu = function (menuId, isPublic, roles) { - // Create the new menu - this.menus[menuId] = { - isPublic: isPublic || false, - roles: roles || this.defaultRoles, - items: [], - shouldRender: shouldRender - }; - // Return the menu object - return this.menus[menuId]; - }; - // Remove existing menu object by menu id - this.removeMenu = function (menuId) { - // Validate that the menu exists - this.validateMenuExistance(menuId); - // Return the menu object - delete this.menus[menuId]; - }; - // Add menu item object - this.addMenuItem = function (menuId, menuItemTitle, menuItemURL, menuItemType, menuItemUIRoute, isPublic, roles) { - // Validate that the menu exists - this.validateMenuExistance(menuId); - // Push new menu item - this.menus[menuId].items.push({ - title: menuItemTitle, - link: menuItemURL, - menuItemType: menuItemType || 'item', - menuItemClass: menuItemType, - uiRoute: menuItemUIRoute || '/' + menuItemURL, - isPublic: isPublic || this.menus[menuId].isPublic, - roles: roles || this.defaultRoles, - items: [], - shouldRender: shouldRender - }); - // Return the menu object - return this.menus[menuId]; - }; - // Add submenu item object - this.addSubMenuItem = function (menuId, rootMenuItemURL, menuItemTitle, menuItemURL, menuItemUIRoute, isPublic, roles) { - // Validate that the menu exists - this.validateMenuExistance(menuId); - // Search for menu item - for (var itemIndex in this.menus[menuId].items) { - if (this.menus[menuId].items[itemIndex].link === rootMenuItemURL) { - // Push new submenu item - this.menus[menuId].items[itemIndex].items.push({ - title: menuItemTitle, - link: menuItemURL, - uiRoute: menuItemUIRoute || '/' + menuItemURL, - isPublic: isPublic || this.menus[menuId].isPublic, - roles: roles || this.defaultRoles, - shouldRender: shouldRender - }); - } - } - // Return the menu object - return this.menus[menuId]; - }; - // Remove existing menu object by menu id - this.removeMenuItem = function (menuId, menuItemURL) { - // Validate that the menu exists - this.validateMenuExistance(menuId); - // Search for menu item to remove - for (var itemIndex in this.menus[menuId].items) { - if (this.menus[menuId].items[itemIndex].link === menuItemURL) { - this.menus[menuId].items.splice(itemIndex, 1); - } - } - // Return the menu object - return this.menus[menuId]; - }; - // Remove existing menu object by menu id - this.removeSubMenuItem = function (menuId, submenuItemURL) { - // Validate that the menu exists - this.validateMenuExistance(menuId); - // Search for menu item to remove - for (var itemIndex in this.menus[menuId].items) { - for (var subitemIndex in this.menus[menuId].items[itemIndex].items) { - if (this.menus[menuId].items[itemIndex].items[subitemIndex].link === submenuItemURL) { - this.menus[menuId].items[itemIndex].items.splice(subitemIndex, 1); - } - } - } - // Return the menu object - return this.menus[menuId]; - }; - //Adding the topbar menu - this.addMenu('topbar'); - }]);'use strict'; -// Config HTTP Error Handling -angular.module('users').config([ - '$httpProvider', - function ($httpProvider) { - // Set the httpProvider "not authorized" interceptor - $httpProvider.interceptors.push([ - '$q', - '$location', - 'Authentication', - function ($q, $location, Authentication) { - return { - responseError: function (rejection) { - switch (rejection.status) { - case 401: - // Deauthenticate the global user - Authentication.user = null; - // Redirect to signin page - $location.path('signin'); - break; - case 403: - // Add unauthorized behaviour - break; - } - return $q.reject(rejection); - } - }; - } - ]); - } -]);'use strict'; -// Setting up route -angular.module('users').config([ - '$stateProvider', - function ($stateProvider) { - // Users state routing - $stateProvider.state('profile', { - url: '/settings/profile', - templateUrl: 'modules/users/views/settings/edit-profile.client.view.html' - }).state('password', { - url: '/settings/password', - templateUrl: 'modules/users/views/settings/change-password.client.view.html' - }).state('accounts', { - url: '/settings/accounts', - templateUrl: 'modules/users/views/settings/social-accounts.client.view.html' - }).state('signup', { - url: '/signup', - templateUrl: 'modules/users/views/signup.client.view.html' - }).state('signin', { - url: '/signin', - templateUrl: 'modules/users/views/signin.client.view.html' - }); - } -]);'use strict'; -angular.module('users').controller('AuthenticationController', [ - '$scope', - '$http', - '$location', - 'Authentication', - function ($scope, $http, $location, Authentication) { - $scope.authentication = Authentication; - //If user is signed in then redirect back home - if ($scope.authentication.user) - $location.path('/'); - $scope.signup = function () { - $http.post('/auth/signup', $scope.credentials).success(function (response) { - //If successful we assign the response to the global user model - $scope.authentication.user = response; - //And redirect to the index page - $location.path('/'); - }).error(function (response) { - $scope.error = response.message; - }); - }; - $scope.signin = function () { - $http.post('/auth/signin', $scope.credentials).success(function (response) { - //If successful we assign the response to the global user model - $scope.authentication.user = response; - //And redirect to the index page - $location.path('/'); - }).error(function (response) { - $scope.error = response.message; - }); - }; - } -]);'use strict'; -angular.module('users').controller('SettingsController', [ - '$scope', - '$http', - '$location', - 'Users', - 'Authentication', - function ($scope, $http, $location, Users, Authentication) { - $scope.user = Authentication.user; - // 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); - user.$update(function (response) { - $scope.success = true; - Authentication.user = response; - }, function (response) { - $scope.error = response.data.message; - }); - }; - // Change user password - $scope.changeUserPassword = function () { - $scope.success = $scope.error = null; - $http.post('/users/password', $scope.passwordDetails).success(function (response) { - // If successful show success message and clear form - $scope.success = true; - $scope.passwordDetails = null; - }).error(function (response) { - $scope.error = response.message; - }); - }; - } -]);'use strict'; -// Authentication service for user variables -angular.module('users').factory('Authentication', [function () { - var _this = this; - _this._data = { user: window.user }; - return _this._data; - }]);'use strict'; -// Users service used for communicating with the users REST endpoint -angular.module('users').factory('Users', [ - '$resource', - function ($resource) { - return $resource('users', {}, { update: { method: 'PUT' } }); - } -]); \ No newline at end of file diff --git a/public/dist/application.min.css b/public/dist/application.min.css index 7769639b99..a5f5b12883 100644 --- a/public/dist/application.min.css +++ b/public/dist/application.min.css @@ -1 +1 @@ -.content{margin-top:50px}.undecorated-link:hover{text-decoration:none}.ng-cloak,.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}@media (min-width:992px){.nav-users{position:fixed}}.remove-account-container{display:inline-block;position:relative}.btn-remove-account{top:10px;right:10px;position:absolute} \ No newline at end of file +.content{margin-top:50px}.undecorated-link:hover{text-decoration:none}.ng-cloak,.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}@media (min-width:992px){.nav-users{position:fixed}}.remove-account-container{display:inline-block;position:relative}.btn-remove-account{top:10px;right:10px;position:absolute}.ng-invalid.ng-dirty{border-color:#FA787E;}.ng-valid.ng-dirty{border-color:#78FA89;} diff --git a/public/dist/application.min.js b/public/dist/application.min.js deleted file mode 100644 index aeb1fa1373..0000000000 --- a/public/dist/application.min.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";var ApplicationConfiguration=function(){var applicationModuleName="mean",applicationModuleVendorDependencies=["ngResource","ngAnimate","ui.router","ui.bootstrap","ui.utils"],registerModule=function(moduleName){angular.module(moduleName,[]),angular.module(applicationModuleName).requires.push(moduleName)};return{applicationModuleName:applicationModuleName,applicationModuleVendorDependencies:applicationModuleVendorDependencies,registerModule:registerModule}}();angular.module(ApplicationConfiguration.applicationModuleName,ApplicationConfiguration.applicationModuleVendorDependencies),angular.module(ApplicationConfiguration.applicationModuleName).config(["$locationProvider",function($locationProvider){$locationProvider.hashPrefix("!")}]),angular.element(document).ready(function(){"#_=_"===window.location.hash&&(window.location.hash="#!"),angular.bootstrap(document,[ApplicationConfiguration.applicationModuleName])}),ApplicationConfiguration.registerModule("articles"),ApplicationConfiguration.registerModule("core"),ApplicationConfiguration.registerModule("users"),angular.module("articles").run(["Menus",function(Menus){Menus.addMenuItem("topbar","Articles","articles","dropdown","/articles(/create)?"),Menus.addSubMenuItem("topbar","articles","List Articles","articles"),Menus.addSubMenuItem("topbar","articles","New Article","articles/create")}]),angular.module("articles").config(["$stateProvider",function($stateProvider){$stateProvider.state("listArticles",{url:"/articles",templateUrl:"modules/articles/views/list-articles.client.view.html"}).state("createArticle",{url:"/articles/create",templateUrl:"modules/articles/views/create-article.client.view.html"}).state("viewArticle",{url:"/articles/:articleId",templateUrl:"modules/articles/views/view-article.client.view.html"}).state("editArticle",{url:"/articles/:articleId/edit",templateUrl:"modules/articles/views/edit-article.client.view.html"})}]),angular.module("articles").controller("ArticlesController",["$scope","$stateParams","$location","Authentication","Articles",function($scope,$stateParams,$location,Authentication,Articles){$scope.authentication=Authentication,$scope.create=function(){var article=new Articles({title:this.title,content:this.content});article.$save(function(response){$location.path("articles/"+response._id)},function(errorResponse){$scope.error=errorResponse.data.message}),this.title="",this.content=""},$scope.remove=function(article){if(article){article.$remove();for(var i in $scope.articles)$scope.articles[i]===article&&$scope.articles.splice(i,1)}else $scope.article.$remove(function(){$location.path("articles")})},$scope.update=function(){var article=$scope.article;article.$update(function(){$location.path("articles/"+article._id)},function(errorResponse){$scope.error=errorResponse.data.message})},$scope.find=function(){$scope.articles=Articles.query()},$scope.findOne=function(){$scope.article=Articles.get({articleId:$stateParams.articleId})}}]),angular.module("articles").factory("Articles",["$resource",function($resource){return $resource("articles/:articleId",{articleId:"@_id"},{update:{method:"PUT"}})}]),angular.module("core").config(["$stateProvider","$urlRouterProvider",function($stateProvider,$urlRouterProvider){$urlRouterProvider.otherwise("/"),$stateProvider.state("home",{url:"/",templateUrl:"modules/core/views/home.client.view.html"})}]),angular.module("core").controller("HeaderController",["$scope","Authentication","Menus",function($scope,Authentication,Menus){$scope.authentication=Authentication,$scope.isCollapsed=!1,$scope.menu=Menus.getMenu("topbar"),$scope.toggleCollapsibleMenu=function(){$scope.isCollapsed=!$scope.isCollapsed},$scope.$on("$stateChangeSuccess",function(){$scope.isCollapsed=!1})}]),angular.module("core").controller("HomeController",["$scope","Authentication",function($scope,Authentication){$scope.authentication=Authentication}]),angular.module("core").service("Menus",[function(){this.defaultRoles=["user"],this.menus={};var shouldRender=function(user){if(!user)return this.isPublic;for(var userRoleIndex in user.roles)for(var roleIndex in this.roles)if(this.roles[roleIndex]===user.roles[userRoleIndex])return!0;return!1};this.validateMenuExistance=function(menuId){if(menuId&&menuId.length){if(this.menus[menuId])return!0;throw new Error("Menu does not exists")}throw new Error("MenuId was not provided")},this.getMenu=function(menuId){return this.validateMenuExistance(menuId),this.menus[menuId]},this.addMenu=function(menuId,isPublic,roles){return this.menus[menuId]={isPublic:isPublic||!1,roles:roles||this.defaultRoles,items:[],shouldRender:shouldRender},this.menus[menuId]},this.removeMenu=function(menuId){this.validateMenuExistance(menuId),delete this.menus[menuId]},this.addMenuItem=function(menuId,menuItemTitle,menuItemURL,menuItemType,menuItemUIRoute,isPublic,roles){return this.validateMenuExistance(menuId),this.menus[menuId].items.push({title:menuItemTitle,link:menuItemURL,menuItemType:menuItemType||"item",menuItemClass:menuItemType,uiRoute:menuItemUIRoute||"/"+menuItemURL,isPublic:isPublic||this.menus[menuId].isPublic,roles:roles||this.defaultRoles,items:[],shouldRender:shouldRender}),this.menus[menuId]},this.addSubMenuItem=function(menuId,rootMenuItemURL,menuItemTitle,menuItemURL,menuItemUIRoute,isPublic,roles){this.validateMenuExistance(menuId);for(var itemIndex in this.menus[menuId].items)this.menus[menuId].items[itemIndex].link===rootMenuItemURL&&this.menus[menuId].items[itemIndex].items.push({title:menuItemTitle,link:menuItemURL,uiRoute:menuItemUIRoute||"/"+menuItemURL,isPublic:isPublic||this.menus[menuId].isPublic,roles:roles||this.defaultRoles,shouldRender:shouldRender});return this.menus[menuId]},this.removeMenuItem=function(menuId,menuItemURL){this.validateMenuExistance(menuId);for(var itemIndex in this.menus[menuId].items)this.menus[menuId].items[itemIndex].link===menuItemURL&&this.menus[menuId].items.splice(itemIndex,1);return this.menus[menuId]},this.removeSubMenuItem=function(menuId,submenuItemURL){this.validateMenuExistance(menuId);for(var itemIndex in this.menus[menuId].items)for(var subitemIndex in this.menus[menuId].items[itemIndex].items)this.menus[menuId].items[itemIndex].items[subitemIndex].link===submenuItemURL&&this.menus[menuId].items[itemIndex].items.splice(subitemIndex,1);return this.menus[menuId]},this.addMenu("topbar")}]),angular.module("users").config(["$httpProvider",function($httpProvider){$httpProvider.interceptors.push(["$q","$location","Authentication",function($q,$location,Authentication){return{responseError:function(rejection){switch(rejection.status){case 401:Authentication.user=null,$location.path("signin");break;case 403:}return $q.reject(rejection)}}}])}]),angular.module("users").config(["$stateProvider",function($stateProvider){$stateProvider.state("profile",{url:"/settings/profile",templateUrl:"modules/users/views/settings/edit-profile.client.view.html"}).state("password",{url:"/settings/password",templateUrl:"modules/users/views/settings/change-password.client.view.html"}).state("accounts",{url:"/settings/accounts",templateUrl:"modules/users/views/settings/social-accounts.client.view.html"}).state("signup",{url:"/signup",templateUrl:"modules/users/views/signup.client.view.html"}).state("signin",{url:"/signin",templateUrl:"modules/users/views/signin.client.view.html"})}]),angular.module("users").controller("AuthenticationController",["$scope","$http","$location","Authentication",function($scope,$http,$location,Authentication){$scope.authentication=Authentication,$scope.authentication.user&&$location.path("/"),$scope.signup=function(){$http.post("/auth/signup",$scope.credentials).success(function(response){$scope.authentication.user=response,$location.path("/")}).error(function(response){$scope.error=response.message})},$scope.signin=function(){$http.post("/auth/signin",$scope.credentials).success(function(response){$scope.authentication.user=response,$location.path("/")}).error(function(response){$scope.error=response.message})}}]),angular.module("users").controller("SettingsController",["$scope","$http","$location","Users","Authentication",function($scope,$http,$location,Users,Authentication){$scope.user=Authentication.user,$scope.user||$location.path("/"),$scope.hasConnectedAdditionalSocialAccounts=function(){for(var i in $scope.user.additionalProvidersData)return!0;return!1},$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){$scope.success=!0,$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);user.$update(function(response){$scope.success=!0,Authentication.user=response},function(response){$scope.error=response.data.message})},$scope.changeUserPassword=function(){$scope.success=$scope.error=null,$http.post("/users/password",$scope.passwordDetails).success(function(){$scope.success=!0,$scope.passwordDetails=null}).error(function(response){$scope.error=response.message})}}]),angular.module("users").factory("Authentication",[function(){var _this=this;return _this._data={user:window.user},_this._data}]),angular.module("users").factory("Users",["$resource",function($resource){return $resource("users",{},{update:{method:"PUT"}})}]); \ No newline at end of file diff --git a/public/modules/articles/controllers/articles.client.controller.js b/public/modules/articles/controllers/articles.client.controller.js index 02ab64c908..364987eb9e 100644 --- a/public/modules/articles/controllers/articles.client.controller.js +++ b/public/modules/articles/controllers/articles.client.controller.js @@ -11,12 +11,12 @@ angular.module('articles').controller('ArticlesController', ['$scope', '$statePa }); article.$save(function(response) { $location.path('articles/' + response._id); + + $scope.title = ''; + $scope.content = ''; }, function(errorResponse) { $scope.error = errorResponse.data.message; }); - - this.title = ''; - this.content = ''; }; $scope.remove = function(article) { diff --git a/public/modules/articles/views/create-article.client.view.html b/public/modules/articles/views/create-article.client.view.html index cdb2abaf69..bd44131fce 100644 --- a/public/modules/articles/views/create-article.client.view.html +++ b/public/modules/articles/views/create-article.client.view.html @@ -3,18 +3,24 @@