diff --git a/.bowerrc b/.bowerrc index 1b84277940..47ad66735b 100644 --- a/.bowerrc +++ b/.bowerrc @@ -1,3 +1,3 @@ { - "directory": "public/lib" + "directory": "public/lib" } diff --git a/.csslintrc b/.csslintrc index 0dab227ebb..09f7bf209a 100644 --- a/.csslintrc +++ b/.csslintrc @@ -1,15 +1,15 @@ { - "adjoining-classes": false, - "box-model": false, - "box-sizing": false, - "floats": false, - "font-sizes": false, - "important": false, - "known-properties": false, - "overqualified-elements": false, - "qualified-headings": false, - "regex-selectors": false, - "unique-headings": false, - "universal-selector": false, - "unqualified-attributes": false + "adjoining-classes": false, + "box-model": false, + "box-sizing": false, + "floats": false, + "font-sizes": false, + "important": false, + "known-properties": false, + "overqualified-elements": false, + "qualified-headings": false, + "regex-selectors": false, + "unique-headings": false, + "universal-selector": false, + "unqualified-attributes": false } diff --git a/.editorconfig b/.editorconfig index ed90a06898..d27bd05b8a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,25 +1,43 @@ # EditorConfig is awesome: http://EditorConfig.org -# How-to with your editor: http://editorconfig.org/#download +# Howto with your editor: http://editorconfig.org/#download +# 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 -indent_style = tab insert_final_newline = true -[{Dockerfile,Procfile}] -trim_trailing_whitespace = true - -# Standard at: https://github.com/felixge/node-style-guide -[{*.js,*.json}] +# Standard at: https://github.com/felixge/node-style-guide +[**.js, **.json] trim_trailing_whitespace = true +indent_style = space +indent_size = 2 quote_type = single curly_bracket_next_line = false spaces_around_operators = true space_after_control_statements = true -space_after_anonymous_functions = false +space_after_anonymous_functions = true spaces_in_brackets = false + +# No Standard. Please document a standard if different from .js +[**.yml, **.html, **.css] +trim_trailing_whitespace = true +indent_style = tab + +# No standard. Please document a standard if different from .js +[**.md] +indent_style = tab + +# Standard at: +[Makefile] +indent_style = tab + +# The indentation in package.json will always need to be 2 spaces +# https://github.com/npm/npm/issues/4718 +[package.json, bower.json] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/.gitignore b/.gitignore index c519f47471..e030a1718b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,45 @@ -# iOS / Apple +# OS # =========== .DS_Store ehthumbs.db Icon? Thumbs.db -config/env/local.js # Node and related ecosystem # ========================== .nodemonignore .sass-cache/ -npm-debug.log node_modules/ public/lib/ app/tests/coverage/ .bower-*/ .idea/ +coverage/ # MEAN.js app and assets # ====================== -config/sslcerts/*.pem -access.log public/dist/ +uploads +modules/users/client/img/profile/uploads +config/env/local.js +*.pem + +# Ignoring MEAN.JS's gh-pages branch for documenation +_site/ + +# General +# ======= +*.log +*.csv +*.dat +*.out +*.pid +*.gz +*.tmp +*.bak +*.swp +logs/ +build/ # Sublime editor # ============== @@ -48,16 +66,9 @@ local.properties data/ mongod -# General -# ======= -*.log -*.csv -*.dat -*.out -*.pid -*.gz -*.tmp -*.bak -*.swp -logs/ -build/ +# Visual Studio +# ========= +*.suo +*.ntvs* +*.njsproj +*.sln diff --git a/.jshintrc b/.jshintrc index 4cd07cdcab..b1c735f190 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,42 +1,36 @@ { - "node": true, // Enable globals available when code is running inside of the NodeJS runtime environment. - "browser": true, // Standard browser globals e.g. `window`, `document`. - "esnext": true, // Allow ES.next specific features such as `const` and `let`. - "bitwise": false, // Prohibit bitwise operators (&, |, ^, etc.). - "camelcase": false, // Permit only camelcase for `var` and `object indexes`. - "curly": false, // Require {} for every new block or scope. - "eqeqeq": true, // Require triple equals i.e. `===`. - "immed": true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` - "latedef": true, // Prohibit variable use before definition. - "newcap": true, // Require capitalization of all constructor functions e.g. `new F()`. - "noarg": true, // Prohibit use of `arguments.caller` and `arguments.callee`. - "quotmark": "single", // Define quotes to string values. - "regexp": true, // Prohibit `.` and `[^...]` in regular expressions. - "undef": true, // Require all non-global variables be declared before they are used. - "unused": false, // Warn unused variables. - "strict": true, // Require `use strict` pragma in every file. - "trailing": true, // Prohibit trailing whitespaces. - "smarttabs": false, // Suppresses warnings about mixed tabs and spaces - "globals": { // Globals variables. - "jasmine": true, - "angular": true, - "ApplicationConfiguration": true - }, - "predef": [ // Extra globals. - "define", - "require", - "exports", - "module", - "describe", - "before", - "beforeEach", - "after", - "afterEach", - "it", - "inject", - "expect" - ], - "indent": 4, // Specify indentation spacing - "devel": true, // Allow development statements e.g. `console.log();`. - "noempty": true // Prohibit use of empty blocks. -} \ No newline at end of file + "node": true, // Enable globals available when code is running inside of the NodeJS runtime environment. + "mocha": true, // Enable globals available when code is running inside of the Mocha tests. + "jasmine": true, // Enable globals available when code is running inside of the Jasmine tests. + "browser": true, // Standard browser globals e.g. `window`, `document`. + "esnext": true, // Allow ES.next specific features such as `const` and `let`. + "bitwise": false, // Prohibit bitwise operators (&, |, ^, etc.). + "camelcase": false, // Permit only camelcase for `var` and `object indexes`. + "curly": false, // Require {} for every new block or scope. + "eqeqeq": true, // Require triple equals i.e. `===`. + "immed": true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` + "latedef": true, // Prohibit variable use before definition. + "newcap": true, // Require capitalization of all constructor functions e.g. `new F()`. + "noarg": true, // Prohibit use of `arguments.caller` and `arguments.callee`. + "quotmark": "single", // Define quotes to string values. + "regexp": true, // Prohibit `.` and `[^...]` in regular expressions. + "undef": true, // Require all non-global variables be declared before they are used. + "unused": false, // Warn unused variables. + "strict": true, // Require `use strict` pragma in every file. + "trailing": true, // Prohibit trailing whitespaces. + "smarttabs": false, // Suppresses warnings about mixed tabs and spaces + "globals": { // Globals variables. + "angular": true, + "io": true, + "ApplicationConfiguration": true + }, + "predef": [ // Extra globals. + "inject", + "by", + "browser", + "element" + ], + "indent": 4, // Specify indentation spacing + "devel": true, // Allow development statements e.g. `console.log();`. + "noempty": true // Prohibit use of empty blocks. +} diff --git a/.slugignore b/.slugignore index e4e50baab8..4611d35f44 100644 --- a/.slugignore +++ b/.slugignore @@ -1 +1 @@ -/app/tests \ No newline at end of file +/app/tests diff --git a/.travis.yml b/.travis.yml index 708607e326..eef75fbf97 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: node_js node_js: - "0.10" - - "0.11" + - "0.12" env: - NODE_ENV=travis services: diff --git a/Dockerfile b/Dockerfile index de61a99205..d6a654b74b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM dockerfile/nodejs +FROM node:0.10 MAINTAINER Matthias Luebken, matthias@catalyst-zero.com @@ -20,7 +20,7 @@ RUN bower install --config.interactive=false --allow-root # Make everything available for start ADD . /home/mean -# currently only works for development +# Set development environment as default ENV NODE_ENV development # Port 3000 for server diff --git a/Procfile b/Procfile old mode 100755 new mode 100644 diff --git a/README.md b/README.md index 4b1a5accbe..a54801a20f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,14 @@ [![MEAN.JS Logo](http://meanjs.org/img/logo-small.png)](http://meanjs.org/) +[![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/meanjs/mean?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +Master Branch: [![Build Status](https://travis-ci.org/meanjs/mean.svg?branch=master)](https://travis-ci.org/meanjs/mean) [![Dependencies Status](https://david-dm.org/meanjs/mean.svg)](https://david-dm.org/meanjs/mean) -[![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/meanjs/mean?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +Dev Branch: +[![Build Status](https://travis-ci.org/meanjs/mean.svg?branch=0.4.1)](https://travis-ci.org/meanjs/mean) +[![Dependencies Status](https://david-dm.org/meanjs/mean/0.4.1.svg)](https://david-dm.org/meanjs/mean/0.4.1) MEAN.JS is a full-stack JavaScript open-source solution, which provides a solid starting point for [MongoDB](http://www.mongodb.org/), [Node.js](http://www.nodejs.org/), [Express](http://expressjs.com/), and [AngularJS](http://angularjs.org/) based applications. The idea is to solve the common issues with connecting those frameworks, build a robust framework to support daily development needs, and help developers use better practices while working with popular JavaScript components. @@ -33,23 +39,27 @@ $ npm install -g grunt-cli ## Downloading MEAN.JS There are several ways you can get the MEAN.JS boilerplate: -### Yo Generator -The recommended way would be to use the [Official Yo Generator](http://meanjs.org/generator.html), which generates the latest stable copy of the MEAN.JS boilerplate and supplies multiple sub-generators to ease your daily development cycles. - ### Cloning The GitHub Repository -You can also use Git to directly clone the MEAN.JS repository: +The recommended way to get MEAN.js is to use git to directly clone the MEAN.JS repository: + ```bash $ git clone https://github.com/meanjs/mean.git meanjs ``` + This will clone the latest version of the MEAN.JS repository to a **meanjs** folder. ### Downloading The Repository Zip File Another way to use the MEAN.JS boilerplate is to download a zip copy from the [master branch on GitHub](https://github.com/meanjs/mean/archive/master.zip). You can also do this using `wget` command: + ```bash $ wget https://github.com/meanjs/mean/archive/master.zip -O meanjs.zip; unzip meanjs.zip; rm meanjs.zip ``` + Don't forget to rename **mean-master** after your project name. +### Yo Generator +-Another way would be to use the [Official Yo Generator](http://meanjs.org/generator.html), which generates a copy of the MEAN.JS 0.3.x boilerplate and supplies multiple sub-generators to ease your daily development cycles. + ## Quick Install Once you've downloaded the boilerplate and installed all the prerequisites, you're just a few steps away from starting to develop your MEAN application. @@ -64,24 +74,50 @@ $ npm install This command does a few things: * First it will install the dependencies needed for the application to run. * If you're running in a development environment, it will then also install development dependencies needed for testing and running your application. -* Finally, when the install process is over, npm will initiate a bower install command to install all the front-end modules needed for the application. +* Finally, when the install process is over, npm will initiate a bower install command to install all the front-end modules needed for the application ## Running Your Application -After the install process is over, you'll be able to run your application using Grunt. Just run grunt default task: +After the install process is over, you'll be able to run your application using Grunt, just run grunt default task: -```bash +``` $ grunt ``` -Your application should run on port 3000, so in your browser just go to [http://localhost:3000](http://localhost:3000) +Your application should run on port 3000 with the *development* environment configuration, so in your browser just go to [http://localhost:3000](http://localhost:3000) That's it! Your application should be running. To proceed with your development, check the other sections in this documentation. If you encounter any problems, try the Troubleshooting section. +* explore `config/env/development.js` for development environment configuration options + +### Running in Production mode +To run your application with *production* environment configuration, execute grunt as follows: + +```bash +$ grunt prod +``` + +* explore `config/env/production.js` for production environment configuration options + +### Running with TLS (SSL) +Application will start by default with secure configuration (SSL mode) turned on and listen on port 8443. +To run your application in a secure manner you'll need to use OpenSSL and generate a set of self-signed certificates. Unix-based users can use the following command: + +```bash +$ sh ./scripts/generate-ssl-certs.sh +``` + +Windows users can follow instructions found [here](http://www.websense.com/support/article/kbarticle/How-to-use-OpenSSL-and-Microsoft-Certification-Authority). +After you've generated the key and certificate, place them in the *config/sslcerts* folder. + +Finally, execute grunt's prod task `grunt prod` +* enable/disable SSL mode in production environment change the `secure` option in `config/env/production.js` + + ## Testing Your Application You can run the full test suite included with MEAN.JS with the test task: -``` +```bash $ grunt test ``` @@ -89,24 +125,24 @@ This will run both the server-side tests (located in the app/tests/ directory) a To execute only the server tests, run the test:server task: -``` +```bash $ grunt test:server ``` And to run only the client tests, run the test:client task: -``` +```bash $ grunt test:client ``` ## Development and deployment With Docker -* Install [Docker](http://www.docker.com/) -* Install [Fig](https://github.com/orchardup/fig) +* Install [Docker](https://docs.docker.com/installation/#installation) +* Install [Compose](https://docs.docker.com/compose/install/) -* Local development and testing with fig: +* Local development and testing with compose: ```bash -$ fig up +$ docker-compose up ``` * Local development and testing with just Docker: @@ -122,14 +158,6 @@ $ $ docker run -p 3000:3000 -p 35729:35729 -v /Users/mdl/workspace/mean-stack/mean/public:/home/mean/public -v /Users/mdl/workspace/mean-stack/mean/app:/home/mean/app --link db:db_1 mean ``` -## Running in a secure environment -To run your application in a secure manner you'll need to use OpenSSL and generate a set of self-signed certificates. Unix-based users can use the following command: -```bash -$ sh ./scripts/generate-ssl-certs.sh -``` -Windows users can follow instructions found [here](http://www.websense.com/support/article/kbarticle/How-to-use-OpenSSL-and-Microsoft-Certification-Authority). -After you've generated the key and certificate, place them in the *config/sslcerts* folder. - ## Getting Started With MEAN.JS You have your application running, but there is a lot of stuff to understand. We recommend you go over the [Official 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 process. We tried covering as many aspects as possible, and will keep it updated by your request. You can also help us develop and improve the documentation 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 deleted file mode 100644 index f5b4d27f12..0000000000 --- a/app/controllers/articles.server.controller.js +++ /dev/null @@ -1,120 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var mongoose = require('mongoose'), - errorHandler = require('./errors.server.controller'), - Article = mongoose.model('Article'), - _ = require('lodash'); - -/** - * Create a article - */ -exports.create = function(req, res) { - var article = new Article(req.body); - article.user = req.user; - - article.save(function(err) { - if (err) { - return res.status(400).send({ - message: errorHandler.getErrorMessage(err) - }); - } else { - res.json(article); - } - }); -}; - -/** - * Show the current article - */ -exports.read = function(req, res) { - res.json(req.article); -}; - -/** - * Update a article - */ -exports.update = function(req, res) { - var article = req.article; - - article = _.extend(article, req.body); - - article.save(function(err) { - if (err) { - return res.status(400).send({ - message: errorHandler.getErrorMessage(err) - }); - } else { - res.json(article); - } - }); -}; - -/** - * Delete an article - */ -exports.delete = function(req, res) { - var article = req.article; - - article.remove(function(err) { - if (err) { - return res.status(400).send({ - message: errorHandler.getErrorMessage(err) - }); - } else { - res.json(article); - } - }); -}; - -/** - * List of Articles - */ -exports.list = function(req, res) { - Article.find().sort('-created').populate('user', 'displayName').exec(function(err, articles) { - if (err) { - return res.status(400).send({ - message: errorHandler.getErrorMessage(err) - }); - } else { - res.json(articles); - } - }); -}; - -/** - * Article middleware - */ -exports.articleByID = function(req, res, next, id) { - - if (!mongoose.Types.ObjectId.isValid(id)) { - return res.status(400).send({ - message: 'Article is invalid' - }); - } - - Article.findById(id).populate('user', 'displayName').exec(function(err, article) { - if (err) return next(err); - if (!article) { - return res.status(404).send({ - message: 'Article not found' - }); - } - req.article = article; - next(); - }); -}; - -/** - * Article authorization middleware - */ -exports.hasAuthorization = function(req, res, next) { - if (req.article.user.id !== req.user.id) { - return res.status(403).send({ - message: 'User is not authorized' - }); - } - next(); -}; diff --git a/app/controllers/core.server.controller.js b/app/controllers/core.server.controller.js deleted file mode 100644 index 5dfdd5e494..0000000000 --- a/app/controllers/core.server.controller.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -exports.index = function(req, res) { - res.render('index', { - user: req.user || null, - request: req - }); -}; diff --git a/app/controllers/errors.server.controller.js b/app/controllers/errors.server.controller.js deleted file mode 100644 index 5944d786af..0000000000 --- a/app/controllers/errors.server.controller.js +++ /dev/null @@ -1,42 +0,0 @@ -'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 exists'; - - } catch (ex) { - output = 'Unique field already exists'; - } - - 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; -}; diff --git a/app/controllers/users.server.controller.js b/app/controllers/users.server.controller.js deleted file mode 100755 index 06ef00ea31..0000000000 --- a/app/controllers/users.server.controller.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var _ = require('lodash'); - -/** - * Extend user's controller - */ -module.exports = _.extend( - require('./users/users.authentication.server.controller'), - require('./users/users.authorization.server.controller'), - require('./users/users.password.server.controller'), - require('./users/users.profile.server.controller') -); diff --git a/app/controllers/users/users.authentication.server.controller.js b/app/controllers/users/users.authentication.server.controller.js deleted file mode 100644 index d34642b5b6..0000000000 --- a/app/controllers/users/users.authentication.server.controller.js +++ /dev/null @@ -1,206 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var _ = require('lodash'), - errorHandler = require('../errors.server.controller'), - 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.json(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.json(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); - }; -}; - -/** - * 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.json(user); - } - }); - } - }); - } -}; diff --git a/app/controllers/users/users.authorization.server.controller.js b/app/controllers/users/users.authorization.server.controller.js deleted file mode 100644 index 932e49061e..0000000000 --- a/app/controllers/users/users.authorization.server.controller.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var _ = require('lodash'), - mongoose = require('mongoose'), - User = mongoose.model('User'); - -/** - * User middleware - */ -exports.userByID = function(req, res, next, id) { - User.findById(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' - }); - } - }); - }; -}; diff --git a/app/controllers/users/users.password.server.controller.js b/app/controllers/users/users.password.server.controller.js deleted file mode 100644 index e246baed8f..0000000000 --- a/app/controllers/users/users.password.server.controller.js +++ /dev/null @@ -1,249 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var _ = require('lodash'), - errorHandler = require('../errors.server.controller'), - mongoose = require('mongoose'), - passport = require('passport'), - User = mongoose.model('User'), - config = require('../../../config/config'), - nodemailer = require('nodemailer'), - async = require('async'), - crypto = require('crypto'); - -var smtpTransport = nodemailer.createTransport(config.mailer.options); - -/** - * 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 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.' - }); - } else { - return res.status(400).send({ - message: 'Failure sending email' - }); - } - - done(err); - }); - } - ], 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; - - 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.json(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 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) { - // Init Variables - var passwordDetails = req.body; - - 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' - }); - } -}; diff --git a/app/controllers/users/users.profile.server.controller.js b/app/controllers/users/users.profile.server.controller.js deleted file mode 100644 index 8e438f7c17..0000000000 --- a/app/controllers/users/users.profile.server.controller.js +++ /dev/null @@ -1,56 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var _ = require('lodash'), - errorHandler = require('../errors.server.controller.js'), - 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.json(user); - } - }); - } - }); - } else { - res.status(400).send({ - message: 'User is not signed in' - }); - } -}; - -/** - * Send User - */ -exports.me = function(req, res) { - res.json(req.user || null); -}; diff --git a/app/models/article.server.model.js b/app/models/article.server.model.js deleted file mode 100644 index f2b89db815..0000000000 --- a/app/models/article.server.model.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var mongoose = require('mongoose'), - Schema = mongoose.Schema; - -/** - * Article Schema - */ -var ArticleSchema = new Schema({ - created: { - type: Date, - default: Date.now - }, - title: { - type: String, - default: '', - trim: true, - required: 'Title cannot be blank' - }, - content: { - type: String, - default: '', - trim: true - }, - user: { - type: Schema.ObjectId, - ref: 'User' - } -}); - -mongoose.model('Article', ArticleSchema); diff --git a/app/models/user.server.model.js b/app/models/user.server.model.js deleted file mode 100755 index 39dff15a89..0000000000 --- a/app/models/user.server.model.js +++ /dev/null @@ -1,146 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var mongoose = require('mongoose'), - Schema = mongoose.Schema, - crypto = require('crypto'); - -/** - * A Validation function for local strategy properties - */ -var validateLocalStrategyProperty = function(property) { - return ((this.provider !== 'local' && !this.updated) || property.length); -}; - -/** - * A Validation function for local strategy password - */ -var validateLocalStrategyPassword = function(password) { - return (this.provider !== 'local' || (password && password.length > 6)); -}; - -/** - * User Schema - */ -var UserSchema = new Schema({ - firstName: { - type: String, - trim: true, - default: '', - validate: [validateLocalStrategyProperty, 'Please fill in your first name'] - }, - lastName: { - type: String, - trim: true, - default: '', - validate: [validateLocalStrategyProperty, 'Please fill in your last name'] - }, - displayName: { - type: String, - trim: true - }, - email: { - type: String, - trim: true, - default: '', - validate: [validateLocalStrategyProperty, 'Please fill in your email'], - match: [/.+\@.+\..+/, 'Please fill a valid email address'] - }, - username: { - type: String, - unique: 'Username already exists', - required: 'Please fill in a username', - trim: true - }, - password: { - type: String, - default: '', - validate: [validateLocalStrategyPassword, 'Password should be longer'] - }, - salt: { - type: String - }, - provider: { - type: String, - required: 'Provider is required' - }, - providerData: {}, - additionalProvidersData: {}, - roles: { - type: [{ - type: String, - enum: ['user', 'admin'] - }], - default: ['user'] - }, - updated: { - type: Date - }, - created: { - type: Date, - default: Date.now - }, - /* For reset password */ - resetPasswordToken: { - type: String - }, - resetPasswordExpires: { - type: Date - } -}); - -/** - * Hook a pre save method to hash the password - */ -UserSchema.pre('save', function(next) { - if (this.password && this.password.length > 6) { - this.salt = crypto.randomBytes(16).toString('base64'); - this.password = this.hashPassword(this.password); - } - - next(); -}); - -/** - * Create instance method for hashing a password - */ -UserSchema.methods.hashPassword = function(password) { - if (this.salt && password) { - return crypto.pbkdf2Sync(password, new Buffer(this.salt, 'base64'), 10000, 64).toString('base64'); - } else { - return password; - } -}; - -/** - * Create instance method for authenticating user - */ -UserSchema.methods.authenticate = function(password) { - return this.password === this.hashPassword(password); -}; - -/** - * Find possible not used username - */ -UserSchema.statics.findUniqueUsername = function(username, suffix, callback) { - var _this = this; - var possibleUsername = username + (suffix || ''); - - _this.findOne({ - username: possibleUsername - }, function(err, user) { - if (!err) { - if (!user) { - callback(possibleUsername); - } else { - return _this.findUniqueUsername(username, (suffix || 0) + 1, callback); - } - } else { - callback(null); - } - }); -}; - -mongoose.model('User', UserSchema); diff --git a/app/routes/articles.server.routes.js b/app/routes/articles.server.routes.js deleted file mode 100644 index 9a93d05985..0000000000 --- a/app/routes/articles.server.routes.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var users = require('../../app/controllers/users.server.controller'), - articles = require('../../app/controllers/articles.server.controller'); - -module.exports = function(app) { - // Article Routes - app.route('/articles') - .get(articles.list) - .post(users.requiresLogin, articles.create); - - app.route('/articles/:articleId') - .get(articles.read) - .put(users.requiresLogin, articles.hasAuthorization, articles.update) - .delete(users.requiresLogin, articles.hasAuthorization, articles.delete); - - // Finish by binding the article middleware - app.param('articleId', articles.articleByID); -}; diff --git a/app/routes/core.server.routes.js b/app/routes/core.server.routes.js deleted file mode 100644 index 7138822689..0000000000 --- a/app/routes/core.server.routes.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -module.exports = function(app) { - // Root routing - var core = require('../../app/controllers/core.server.controller'); - app.route('/').get(core.index); -}; diff --git a/app/routes/users.server.routes.js b/app/routes/users.server.routes.js deleted file mode 100644 index a3005346d8..0000000000 --- a/app/routes/users.server.routes.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var passport = require('passport'); - -module.exports = function(app) { - // User Routes - var users = require('../../app/controllers/users.server.controller'); - - // Setting up the users profile api - app.route('/users/me').get(users.me); - app.route('/users').put(users.update); - app.route('/users/accounts').delete(users.removeOAuthProvider); - - // 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); - - // Setting the facebook oauth routes - app.route('/auth/facebook').get(passport.authenticate('facebook', { - scope: ['email'] - })); - app.route('/auth/facebook/callback').get(users.oauthCallback('facebook')); - - // Setting the twitter oauth routes - app.route('/auth/twitter').get(passport.authenticate('twitter')); - app.route('/auth/twitter/callback').get(users.oauthCallback('twitter')); - - // Setting the google oauth routes - app.route('/auth/google').get(passport.authenticate('google', { - scope: [ - 'https://www.googleapis.com/auth/userinfo.profile', - 'https://www.googleapis.com/auth/userinfo.email' - ] - })); - app.route('/auth/google/callback').get(users.oauthCallback('google')); - - // Setting the linkedin oauth routes - app.route('/auth/linkedin').get(passport.authenticate('linkedin')); - app.route('/auth/linkedin/callback').get(users.oauthCallback('linkedin')); - - // Setting the github oauth routes - app.route('/auth/github').get(passport.authenticate('github')); - app.route('/auth/github/callback').get(users.oauthCallback('github')); - - // Finish by binding the user middleware - app.param('userId', users.userByID); -}; diff --git a/app/tests/article.server.model.test.js b/app/tests/article.server.model.test.js deleted file mode 100644 index b4b76b8a93..0000000000 --- a/app/tests/article.server.model.test.js +++ /dev/null @@ -1,64 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var should = require('should'), - mongoose = require('mongoose'), - User = mongoose.model('User'), - Article = mongoose.model('Article'); - -/** - * Globals - */ -var user, article; - -/** - * Unit tests - */ -describe('Article Model Unit Tests:', function() { - beforeEach(function(done) { - user = new User({ - firstName: 'Full', - lastName: 'Name', - displayName: 'Full Name', - email: 'test@test.com', - username: 'username', - password: 'password' - }); - - user.save(function() { - article = new Article({ - title: 'Article Title', - content: 'Article Content', - user: user - }); - - done(); - }); - }); - - describe('Method Save', function() { - it('should be able to save without problems', function(done) { - return article.save(function(err) { - should.not.exist(err); - done(); - }); - }); - - it('should be able to show an error when try to save without title', function(done) { - article.title = ''; - - return article.save(function(err) { - should.exist(err); - done(); - }); - }); - }); - - afterEach(function(done) { - Article.remove().exec(function() { - User.remove().exec(done); - }); - }); -}); diff --git a/app/tests/article.server.routes.test.js b/app/tests/article.server.routes.test.js deleted file mode 100644 index c583c3f3af..0000000000 --- a/app/tests/article.server.routes.test.js +++ /dev/null @@ -1,280 +0,0 @@ -'use strict'; - -var should = require('should'), - request = require('supertest'), - app = require('../../server'), - mongoose = require('mongoose'), - User = mongoose.model('User'), - Article = mongoose.model('Article'), - agent = request.agent(app); - -/** - * Globals - */ -var credentials, user, article; - -/** - * Article routes tests - */ -describe('Article CRUD tests', function() { - beforeEach(function(done) { - // Create user credentials - credentials = { - username: 'username', - password: 'password' - }; - - // Create a new user - user = new User({ - firstName: 'Full', - lastName: 'Name', - displayName: 'Full Name', - email: 'test@test.com', - username: credentials.username, - password: credentials.password, - provider: 'local' - }); - - // Save a user to the test db and create new article - user.save(function() { - article = { - title: 'Article Title', - content: 'Article Content' - }; - - done(); - }); - }); - - it('should be able to save an article if logged in', function(done) { - agent.post('/auth/signin') - .send(credentials) - .expect(200) - .end(function(signinErr, signinRes) { - // Handle signin error - if (signinErr) done(signinErr); - - // Get the userId - var userId = user.id; - - // Save a new article - agent.post('/articles') - .send(article) - .expect(200) - .end(function(articleSaveErr, articleSaveRes) { - // Handle article save error - if (articleSaveErr) done(articleSaveErr); - - // Get a list of articles - agent.get('/articles') - .end(function(articlesGetErr, articlesGetRes) { - // Handle article save error - if (articlesGetErr) done(articlesGetErr); - - // Get articles list - var articles = articlesGetRes.body; - - // Set assertions - (articles[0].user._id).should.equal(userId); - (articles[0].title).should.match('Article Title'); - - // Call the assertion callback - done(); - }); - }); - }); - }); - - it('should not be able to save an article if not logged in', function(done) { - agent.post('/articles') - .send(article) - .expect(401) - .end(function(articleSaveErr, articleSaveRes) { - // Call the assertion callback - done(articleSaveErr); - }); - }); - - it('should not be able to save an article if no title is provided', function(done) { - // Invalidate title field - article.title = ''; - - agent.post('/auth/signin') - .send(credentials) - .expect(200) - .end(function(signinErr, signinRes) { - // Handle signin error - if (signinErr) done(signinErr); - - // Get the userId - var userId = user.id; - - // Save a new article - agent.post('/articles') - .send(article) - .expect(400) - .end(function(articleSaveErr, articleSaveRes) { - // Set message assertion - (articleSaveRes.body.message).should.match('Title cannot be blank'); - - // Handle article save error - done(articleSaveErr); - }); - }); - }); - - it('should be able to update an article if signed in', function(done) { - agent.post('/auth/signin') - .send(credentials) - .expect(200) - .end(function(signinErr, signinRes) { - // Handle signin error - if (signinErr) done(signinErr); - - // Get the userId - var userId = user.id; - - // Save a new article - agent.post('/articles') - .send(article) - .expect(200) - .end(function(articleSaveErr, articleSaveRes) { - // Handle article save error - if (articleSaveErr) done(articleSaveErr); - - // Update article title - article.title = 'WHY YOU GOTTA BE SO MEAN?'; - - // Update an existing article - agent.put('/articles/' + articleSaveRes.body._id) - .send(article) - .expect(200) - .end(function(articleUpdateErr, articleUpdateRes) { - // Handle article update error - if (articleUpdateErr) done(articleUpdateErr); - - // Set assertions - (articleUpdateRes.body._id).should.equal(articleSaveRes.body._id); - (articleUpdateRes.body.title).should.match('WHY YOU GOTTA BE SO MEAN?'); - - // Call the assertion callback - done(); - }); - }); - }); - }); - - it('should be able to get a list of articles if not signed in', function(done) { - // Create new article model instance - var articleObj = new Article(article); - - // Save the article - articleObj.save(function() { - // Request articles - request(app).get('/articles') - .end(function(req, res) { - // Set assertion - res.body.should.be.an.Array.with.lengthOf(1); - - // Call the assertion callback - done(); - }); - - }); - }); - - - it('should be able to get a single article if not signed in', function(done) { - // Create new article model instance - var articleObj = new Article(article); - - // Save the article - articleObj.save(function() { - request(app).get('/articles/' + articleObj._id) - .end(function(req, res) { - // Set assertion - res.body.should.be.an.Object.with.property('title', article.title); - - // Call the assertion callback - done(); - }); - }); - }); - - it('should return proper error for single article which doesnt exist, if not signed in', function(done) { - request(app).get('/articles/test') - .end(function(req, res) { - // Set assertion - res.body.should.be.an.Object.with.property('message', 'Article is invalid'); - - // Call the assertion callback - done(); - }); - }); - - it('should be able to delete an article if signed in', function(done) { - agent.post('/auth/signin') - .send(credentials) - .expect(200) - .end(function(signinErr, signinRes) { - // Handle signin error - if (signinErr) done(signinErr); - - // Get the userId - var userId = user.id; - - // Save a new article - agent.post('/articles') - .send(article) - .expect(200) - .end(function(articleSaveErr, articleSaveRes) { - // Handle article save error - if (articleSaveErr) done(articleSaveErr); - - // Delete an existing article - agent.delete('/articles/' + articleSaveRes.body._id) - .send(article) - .expect(200) - .end(function(articleDeleteErr, articleDeleteRes) { - // Handle article error error - if (articleDeleteErr) done(articleDeleteErr); - - // Set assertions - (articleDeleteRes.body._id).should.equal(articleSaveRes.body._id); - - // Call the assertion callback - done(); - }); - }); - }); - }); - - it('should not be able to delete an article if not signed in', function(done) { - // Set article user - article.user = user; - - // Create new article model instance - var articleObj = new Article(article); - - // Save the article - articleObj.save(function() { - // Try deleting article - request(app).delete('/articles/' + articleObj._id) - .expect(401) - .end(function(articleDeleteErr, articleDeleteRes) { - // Set message assertion - (articleDeleteRes.body.message).should.match('User is not logged in'); - - // Handle article error error - done(articleDeleteErr); - }); - - }); - }); - - afterEach(function(done) { - User.remove().exec(function() { - Article.remove().exec(done); - }); - }); -}); diff --git a/app/tests/user.server.model.test.js b/app/tests/user.server.model.test.js deleted file mode 100644 index e43848764d..0000000000 --- a/app/tests/user.server.model.test.js +++ /dev/null @@ -1,75 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var should = require('should'), - mongoose = require('mongoose'), - User = mongoose.model('User'); - -/** - * Globals - */ -var user, user2; - -/** - * Unit tests - */ -describe('User Model Unit Tests:', function() { - before(function(done) { - user = new User({ - firstName: 'Full', - lastName: 'Name', - displayName: 'Full Name', - email: 'test@test.com', - username: 'username', - password: 'password', - provider: 'local' - }); - user2 = new User({ - firstName: 'Full', - lastName: 'Name', - displayName: 'Full Name', - email: 'test@test.com', - username: 'username', - password: 'password', - provider: 'local' - }); - - done(); - }); - - describe('Method Save', function() { - it('should begin with no users', function(done) { - User.find({}, function(err, users) { - users.should.have.length(0); - done(); - }); - }); - - it('should be able to save without problems', function(done) { - user.save(done); - }); - - it('should fail to save an existing user again', function(done) { - user.save(function() { - user2.save(function(err) { - should.exist(err); - done(); - }); - }); - }); - - it('should be able to show an error when try to save without first name', function(done) { - user.firstName = ''; - return user.save(function(err) { - should.exist(err); - done(); - }); - }); - }); - - after(function(done) { - User.remove().exec(done); - }); -}); diff --git a/app/views/404.server.view.html b/app/views/404.server.view.html deleted file mode 100644 index 404076174c..0000000000 --- a/app/views/404.server.view.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends 'layout.server.view.html' %} - -{% block content %} -

Page Not Found

-
-	{{url}} is not a valid path.
-
-{% endblock %} diff --git a/app/views/layout.server.view.html b/app/views/layout.server.view.html deleted file mode 100644 index 9e9cebd41d..0000000000 --- a/app/views/layout.server.view.html +++ /dev/null @@ -1,81 +0,0 @@ - - - - - {{title}} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {% for cssFile in cssFiles %} - - {% endfor %} - - - - - - - -
-
- {% block content %}{% endblock %} -
-
- - - - - - {% for jsFile in jsFiles %} - - {% endfor %} - - {% if process.env.NODE_ENV === 'development' %} - - - {% endif %} - - - - - diff --git a/app/views/templates/reset-password-confirm-email.server.view.html b/app/views/templates/reset-password-confirm-email.server.view.html deleted file mode 100644 index bfbcb157fc..0000000000 --- a/app/views/templates/reset-password-confirm-email.server.view.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - -

Dear {{name}},

-

-

This is a confirmation that the password for your account has just been changed

-
-
-

The {{appName}} Support Team

- - diff --git a/app/views/templates/reset-password-email.server.view.html b/app/views/templates/reset-password-email.server.view.html deleted file mode 100644 index 4869dfd859..0000000000 --- a/app/views/templates/reset-password-email.server.view.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - -

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

- - diff --git a/bower.json b/bower.json index 51e865536a..5e87d4d990 100644 --- a/bower.json +++ b/bower.json @@ -1,16 +1,19 @@ { - "name": "meanjs", - "version": "0.3.2", - "description": "Fullstack JavaScript with MongoDB, Express, AngularJS, and Node.js.", - "dependencies": { - "bootstrap": "~3", - "angular": "~1.2", - "angular-resource": "~1.2", - "angular-animate": "~1.2", - "angular-mocks": "~1.2", - "angular-bootstrap": "~0.11.2", - "angular-bootstrap": "~0.12.0", - "angular-ui-utils": "~0.1.1", - "angular-ui-router": "~0.2.11" - } + "name": "meanjs", + "version": "0.4.0", + "description": "Fullstack JavaScript with MongoDB, Express, AngularJS, and Node.js.", + "dependencies": { + "bootstrap": "~3", + "angular": "~1.3", + "angular-resource": "~1.3", + "angular-animate": "~1.3", + "angular-mocks": "~1.3", + "angular-bootstrap": "~0.13", + "angular-ui-utils": "bower", + "angular-ui-router": "~0.2", + "angular-file-upload": "1.1.5" + }, + "resolutions": { + "angular": "~1.3" + } } diff --git a/config/assets/default.js b/config/assets/default.js new file mode 100644 index 0000000000..ce8d70a65d --- /dev/null +++ b/config/assets/default.js @@ -0,0 +1,49 @@ +'use strict'; + +module.exports = { + client: { + lib: { + css: [ + 'public/lib/bootstrap/dist/css/bootstrap.css', + 'public/lib/bootstrap/dist/css/bootstrap-theme.css' + ], + js: [ + 'public/lib/angular/angular.js', + 'public/lib/angular-resource/angular-resource.js', + 'public/lib/angular-animate/angular-animate.js', + 'public/lib/angular-ui-router/release/angular-ui-router.js', + 'public/lib/angular-ui-utils/ui-utils.js', + 'public/lib/angular-bootstrap/ui-bootstrap-tpls.js', + 'public/lib/angular-file-upload/angular-file-upload.js' + ], + tests: ['public/lib/angular-mocks/angular-mocks.js'] + }, + css: [ + 'modules/*/client/css/*.css' + ], + less: [ + 'modules/*/client/less/*.less' + ], + sass: [ + 'modules/*/client/scss/*.scss' + ], + js: [ + 'modules/core/client/app/config.js', + 'modules/core/client/app/init.js', + 'modules/*/client/*.js', + 'modules/*/client/**/*.js' + ], + views: ['modules/*/client/views/**/*.html'] + }, + server: { + gruntConfig: 'gruntfile.js', + gulpConfig: 'gulpfile.js', + allJS: ['server.js', 'config/**/*.js', 'modules/*/server/**/*.js'], + models: 'modules/*/server/models/**/*.js', + routes: ['modules/!(core)/server/routes/**/*.js', 'modules/core/server/routes/**/*.js'], + sockets: 'modules/*/server/sockets/**/*.js', + config: 'modules/*/server/config/*.js', + policies: 'modules/*/server/policies/*.js', + views: 'modules/*/server/views/*.html' + } +}; diff --git a/config/assets/development.js b/config/assets/development.js new file mode 100644 index 0000000000..bdd0b25c01 --- /dev/null +++ b/config/assets/development.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + // Development assets +}; diff --git a/config/assets/production.js b/config/assets/production.js new file mode 100644 index 0000000000..ed4f296173 --- /dev/null +++ b/config/assets/production.js @@ -0,0 +1,23 @@ +'use strict'; + +module.exports = { + client: { + lib: { + css: [ + 'public/lib/bootstrap/dist/css/bootstrap.min.css', + 'public/lib/bootstrap/dist/css/bootstrap-theme.min.css', + ], + js: [ + 'public/lib/angular/angular.min.js', + 'public/lib/angular-resource/angular-resource.min.js', + 'public/lib/angular-animate/angular-animate.min.js', + 'public/lib/angular-ui-router/release/angular-ui-router.min.js', + 'public/lib/angular-ui-utils/ui-utils.min.js', + 'public/lib/angular-bootstrap/ui-bootstrap-tpls.min.js', + 'public/lib/angular-file-upload/angular-file-upload.min.js' + ] + }, + css: 'public/dist/application.min.css', + js: 'public/dist/application.min.js' + } +}; diff --git a/config/assets/test.js b/config/assets/test.js new file mode 100644 index 0000000000..365d6510ee --- /dev/null +++ b/config/assets/test.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = { + tests: { + client: ['modules/*/tests/client/**/*.js'], + server: ['modules/*/tests/server/**/*.js'], + e2e: ['modules/*/tests/e2e/**/*.js'] + } +}; diff --git a/config/config.js b/config/config.js index 3a22a2cdb3..f2db078212 100644 --- a/config/config.js +++ b/config/config.js @@ -4,87 +4,183 @@ * Module dependencies. */ var _ = require('lodash'), - glob = require('glob'), - fs = require('fs'); + chalk = require('chalk'), + glob = require('glob'), + fs = require('fs'), + path = require('path'); /** - * Resolve environment configuration by extending each env configuration file, - * and lastly merge/override that with any local repository configuration that exists - * in local.js + * Get files by glob patterns */ -var resolvingConfig = function() { - var conf = {}; +var getGlobbedPaths = function (globPatterns, excludes) { + // URL paths regex + var urlRegex = new RegExp('^(?:[a-z]+:)?\/\/', 'i'); + + // The output array + var output = []; - conf = _.extend( - require('./env/all'), - require('./env/' + process.env.NODE_ENV) || {} - ); + // If glob pattern is array so we use each pattern in a recursive way, otherwise we use glob + if (_.isArray(globPatterns)) { + globPatterns.forEach(function (globPattern) { + output = _.union(output, getGlobbedPaths(globPattern, excludes)); + }); + } else if (_.isString(globPatterns)) { + if (urlRegex.test(globPatterns)) { + output.push(globPatterns); + } else { + var files = glob.sync(globPatterns); + if (excludes) { + files = files.map(function (file) { + if (_.isArray(excludes)) { + for (var i in excludes) { + file = file.replace(excludes[i], ''); + } + } else { + file = file.replace(excludes, ''); + } + return file; + }); + } + output = _.union(output, files); + } + } - return _.merge(conf, (fs.existsSync('./config/env/local.js') && require('./env/local.js')) || {}); + return output; }; /** - * Load app configurations + * Validate NODE_ENV existance */ -module.exports = resolvingConfig(); +var validateEnvironmentVariable = function () { + var environmentFiles = glob.sync('./config/env/' + process.env.NODE_ENV + '.js'); + console.log(); + if (!environmentFiles.length) { + if (process.env.NODE_ENV) { + console.error(chalk.red('+ Error: No configuration file found for "' + process.env.NODE_ENV + '" environment using development instead')); + } else { + console.error(chalk.red('+ Error: NODE_ENV is not defined! Using default development environment')); + } + process.env.NODE_ENV = 'development'; + } + // Reset console color + console.log(chalk.white('')); +}; /** - * Get files by glob patterns + * Validate Secure=true parameter can actually be turned on + * because it requires certs and key files to be available */ -module.exports.getGlobbedFiles = function(globPatterns, removeRoot) { - // For context switching - var _this = this; - - // URL paths regex - var urlRegex = new RegExp('^(?:[a-z]+:)?\/\/', 'i'); - - // The output array - var output = []; - - // If glob pattern is array so we use each pattern in a recursive way, otherwise we use glob - if (_.isArray(globPatterns)) { - globPatterns.forEach(function(globPattern) { - output = _.union(output, _this.getGlobbedFiles(globPattern, removeRoot)); - }); - } else if (_.isString(globPatterns)) { - if (urlRegex.test(globPatterns)) { - output.push(globPatterns); - } else { - glob(globPatterns, { - sync: true - }, function(err, files) { - if (removeRoot) { - files = files.map(function(file) { - return file.replace(removeRoot, ''); - }); - } - - output = _.union(output, files); - }); - } - } - - return output; +var validateSecureMode = function (config) { + + if (config.secure !== true) { + return true; + } + + var privateKey = fs.existsSync('./config/sslcerts/key.pem'); + var certificate = fs.existsSync('./config/sslcerts/cert.pem'); + + if (!privateKey || !certificate) { + console.log(chalk.red('+ Error: Certificate file or key file is missing, falling back to non-SSL mode')); + console.log(chalk.red(' To create them, simply run the following from your shell: sh ./scripts/generate-ssl-certs.sh')); + console.log(); + config.secure = false; + } }; /** - * Get the modules JavaScript files + * Initialize global configuration files */ -module.exports.getJavaScriptAssets = function(includeTests) { - var output = this.getGlobbedFiles(this.assets.lib.js.concat(this.assets.js), 'public/'); +var initGlobalConfigFolders = function (config, assets) { + // Appending files + config.folders = { + server: {}, + client: {} + }; + + // Setting globbed client paths + config.folders.client = getGlobbedPaths(path.join(process.cwd(), 'modules/*/client/'), process.cwd().replace(new RegExp(/\\/g), '/')); +}; + +/** + * Initialize global configuration files + */ +var initGlobalConfigFiles = function (config, assets) { + // Appending files + config.files = { + server: {}, + client: {} + }; + + // Setting Globbed model files + config.files.server.models = getGlobbedPaths(assets.server.models); + + // Setting Globbed route files + config.files.server.routes = getGlobbedPaths(assets.server.routes); + + // Setting Globbed config files + config.files.server.configs = getGlobbedPaths(assets.server.config); + + // Setting Globbed socket files + config.files.server.sockets = getGlobbedPaths(assets.server.sockets); + + // Setting Globbed policies files + config.files.server.policies = getGlobbedPaths(assets.server.policies); - // To include tests - if (includeTests) { - output = _.union(output, this.getGlobbedFiles(this.assets.tests)); - } + // Setting Globbed js files + config.files.client.js = getGlobbedPaths(assets.client.lib.js, 'public/').concat(getGlobbedPaths(assets.client.js, ['client/', 'public/'])); - return output; + // Setting Globbed css files + config.files.client.css = getGlobbedPaths(assets.client.lib.css, 'public/').concat(getGlobbedPaths(assets.client.css, ['client/', 'public/'])); + + // Setting Globbed test files + config.files.client.tests = getGlobbedPaths(assets.client.tests); }; /** - * Get the modules CSS files + * Initialize global configuration */ -module.exports.getCSSAssets = function() { - var output = this.getGlobbedFiles(this.assets.lib.css.concat(this.assets.css), 'public/'); - return output; +var initGlobalConfig = function () { + // Validate NDOE_ENV existance + validateEnvironmentVariable(); + + // Get the default assets + var defaultAssets = require(path.join(process.cwd(), 'config/assets/default')); + + // Get the current assets + var environmentAssets = require(path.join(process.cwd(), 'config/assets/', process.env.NODE_ENV)) || {}; + + // Merge assets + var assets = _.merge(defaultAssets, environmentAssets); + + // Get the default config + var defaultConfig = require(path.join(process.cwd(), 'config/env/default')); + + // Get the current config + var environmentConfig = require(path.join(process.cwd(), 'config/env/', process.env.NODE_ENV)) || {}; + + // Merge config files + var envConf = _.merge(defaultConfig, environmentConfig); + + var config = _.merge(envConf, (fs.existsSync(path.join(process.cwd(), 'config/env/local.js')) && require(path.join(process.cwd(), 'config/env/local.js'))) || {}); + + // Initialize global globbed files + initGlobalConfigFiles(config, assets); + + // Initialize global globbed folders + initGlobalConfigFolders(config, assets); + + // Validate Secure SSL mode can be used + validateSecureMode(config); + + // Expose configuration utilities + config.utils = { + getGlobbedPaths: getGlobbedPaths + }; + + return config; }; + +/** + * Set configuration object + */ +module.exports = initGlobalConfig(); diff --git a/config/env/all.js b/config/env/all.js deleted file mode 100644 index 34bc24e7a6..0000000000 --- a/config/env/all.js +++ /dev/null @@ -1,72 +0,0 @@ -'use strict'; - -module.exports = { - app: { - title: 'MEAN.JS', - description: 'Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js', - keywords: 'mongodb, express, angularjs, node.js, mongoose, passport' - }, - port: process.env.PORT || 3000, - templateEngine: 'swig', - // The secret should be set to a non-guessable string that - // is used to compute a session hash - sessionSecret: 'MEAN', - // The name of the MongoDB collection to store sessions in - sessionCollection: 'sessions', - // The session cookie settings - sessionCookie: { - path: '/', - httpOnly: true, - // If secure is set to true then it will cause the cookie to be set - // only when SSL-enabled (HTTPS) is used, and otherwise it won't - // set a cookie. 'true' is recommended yet it requires the above - // mentioned pre-requisite. - secure: false, - // Only set the maxAge to null if the cookie shouldn't be expired - // at all. The cookie will expunge when the browser is closed. - maxAge: null, - // To set the cookie in a specific domain uncomment the following - // setting: - // domain: 'yourdomain.com' - }, - // The session cookie name - sessionName: 'connect.sid', - log: { - // Can specify one of 'combined', 'common', 'dev', 'short', 'tiny' - format: 'combined', - // Stream defaults to process.stdout - // Uncomment to enable logging to a log on the file system - options: { - stream: 'access.log' - } - }, - assets: { - lib: { - css: [ - 'public/lib/bootstrap/dist/css/bootstrap.css', - 'public/lib/bootstrap/dist/css/bootstrap-theme.css', - ], - js: [ - 'public/lib/angular/angular.js', - 'public/lib/angular-resource/angular-resource.js', - 'public/lib/angular-animate/angular-animate.js', - 'public/lib/angular-ui-router/release/angular-ui-router.js', - 'public/lib/angular-ui-utils/ui-utils.js', - 'public/lib/angular-bootstrap/ui-bootstrap-tpls.js' - ] - }, - css: [ - 'public/modules/**/css/*.css' - ], - js: [ - 'public/config.js', - 'public/application.js', - 'public/modules/*/*.js', - 'public/modules/*/*[!tests]*/*.js' - ], - tests: [ - 'public/lib/angular-mocks/angular-mocks.js', - 'public/modules/*/tests/*.js' - ] - } -}; diff --git a/config/env/default.js b/config/env/default.js new file mode 100644 index 0000000000..bc3e6e6010 --- /dev/null +++ b/config/env/default.js @@ -0,0 +1,16 @@ +'use strict'; + +module.exports = { + app: { + title: 'MEAN.JS', + description: 'Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js', + keywords: 'mongodb, express, angularjs, node.js, mongoose, passport', + googleAnalyticsTrackingID: process.env.GOOGLE_ANALYTICS_TRACKING_ID || 'GOOGLE_ANALYTICS_TRACKING_ID' + }, + port: process.env.PORT || 3000, + templateEngine: 'swig', + sessionSecret: 'MEAN', + sessionCollection: 'sessions', + logo: 'modules/core/img/brand/logo.png', + favicon: 'modules/core/img/brand/favicon.ico' +}; diff --git a/config/env/development.js b/config/env/development.js index 35c08b7e8e..1f31ec53f6 100644 --- a/config/env/development.js +++ b/config/env/development.js @@ -1,58 +1,69 @@ 'use strict'; +var defaultEnvConfig = require('./default'); + module.exports = { - db: { - uri: 'mongodb://localhost/mean-dev', - options: { - user: '', - pass: '' - } - }, - log: { - // Can specify one of 'combined', 'common', 'dev', 'short', 'tiny' - format: 'dev', - // Stream defaults to process.stdout - // Uncomment to enable logging to a log on the file system - options: { - //stream: 'access.log' - } - }, - app: { - title: 'MEAN.JS - Development Environment' - }, - facebook: { - clientID: process.env.FACEBOOK_ID || 'APP_ID', - clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET', - callbackURL: '/auth/facebook/callback' - }, - twitter: { - clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY', - clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET', - callbackURL: '/auth/twitter/callback' - }, - google: { - clientID: process.env.GOOGLE_ID || 'APP_ID', - clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET', - callbackURL: '/auth/google/callback' - }, - linkedin: { - clientID: process.env.LINKEDIN_ID || 'APP_ID', - clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', - callbackURL: '/auth/linkedin/callback' - }, - github: { - clientID: process.env.GITHUB_ID || 'APP_ID', - clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET', - callbackURL: '/auth/github/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' - } - } - } + db: { + uri: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://' + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + '/mean-dev', + options: { + user: '', + pass: '' + }, + // Enable mongoose debug mode + debug: process.env.MONGODB_DEBUG || false + }, + log: { + // Can specify one of 'combined', 'common', 'dev', 'short', 'tiny' + format: 'dev', + // Stream defaults to process.stdout + // Uncomment to enable logging to a log on the file system + options: { + //stream: 'access.log' + } + }, + app: { + title: defaultEnvConfig.app.title + ' - Development Environment' + }, + facebook: { + clientID: process.env.FACEBOOK_ID || 'APP_ID', + clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET', + callbackURL: '/api/auth/facebook/callback' + }, + twitter: { + clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY', + clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET', + callbackURL: '/api/auth/twitter/callback' + }, + google: { + clientID: process.env.GOOGLE_ID || 'APP_ID', + clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET', + callbackURL: '/api/auth/google/callback' + }, + linkedin: { + clientID: process.env.LINKEDIN_ID || 'APP_ID', + clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', + callbackURL: '/api/auth/linkedin/callback' + }, + github: { + clientID: process.env.GITHUB_ID || 'APP_ID', + clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET', + callbackURL: '/api/auth/github/callback' + }, + paypal: { + clientID: process.env.PAYPAL_ID || 'CLIENT_ID', + clientSecret: process.env.PAYPAL_SECRET || 'CLIENT_SECRET', + callbackURL: '/api/auth/paypal/callback', + sandbox: true + }, + 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' + } + } + }, + livereload: true }; diff --git a/config/env/local.example.js b/config/env/local.example.js index 824a29930f..8fd65b2f6f 100644 --- a/config/env/local.example.js +++ b/config/env/local.example.js @@ -7,17 +7,17 @@ /* For example: module.exports = { - db: { - uri: 'mongodb://localhost/local-dev', - options: { - user: '', - pass: '' - } - }, - facebook: { - clientID: process.env.FACEBOOK_ID || 'APP_ID', - clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET', - callbackURL: '/auth/facebook/callback' - } + db: { + uri: 'mongodb://localhost/local-dev', + options: { + user: '', + pass: '' + } + }, + facebook: { + clientID: process.env.FACEBOOK_ID || 'APP_ID', + clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET', + callbackURL: '/auth/facebook/callback' + } }; -*/ \ No newline at end of file +*/ diff --git a/config/env/production.js b/config/env/production.js index a56a48f0e5..3bf482cc85 100644 --- a/config/env/production.js +++ b/config/env/production.js @@ -1,73 +1,65 @@ 'use strict'; module.exports = { - db: { - uri: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://' + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + '/mean', - options: { - user: '', - pass: '' - } - }, - log: { - // Can specify one of 'combined', 'common', 'dev', 'short', 'tiny' - format: 'combined', - // Stream defaults to process.stdout - // Uncomment to enable logging to a log on the file system - options: { - stream: 'access.log' - } - }, - assets: { - lib: { - css: [ - 'public/lib/bootstrap/dist/css/bootstrap.min.css', - 'public/lib/bootstrap/dist/css/bootstrap-theme.min.css', - ], - js: [ - 'public/lib/angular/angular.min.js', - 'public/lib/angular-resource/angular-resource.min.js', - 'public/lib/angular-animate/angular-animate.min.js', - 'public/lib/angular-ui-router/release/angular-ui-router.min.js', - 'public/lib/angular-ui-utils/ui-utils.min.js', - 'public/lib/angular-bootstrap/ui-bootstrap-tpls.min.js' - ] - }, - css: 'public/dist/application.min.css', - js: 'public/dist/application.min.js' - }, - facebook: { - clientID: process.env.FACEBOOK_ID || 'APP_ID', - clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET', - callbackURL: '/auth/facebook/callback' - }, - twitter: { - clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY', - clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET', - callbackURL: '/auth/twitter/callback' - }, - google: { - clientID: process.env.GOOGLE_ID || 'APP_ID', - clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET', - callbackURL: '/auth/google/callback' - }, - linkedin: { - clientID: process.env.LINKEDIN_ID || 'APP_ID', - clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', - callbackURL: '/auth/linkedin/callback' - }, - github: { - clientID: process.env.GITHUB_ID || 'APP_ID', - clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET', - callbackURL: '/auth/github/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' - } - } - } + secure: true, + port: process.env.PORT || 8443, + db: { + uri: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://' + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + '/mean', + options: { + user: '', + pass: '' + }, + // Enable mongoose debug mode + debug: process.env.MONGODB_DEBUG || false + }, + log: { + // Can specify one of 'combined', 'common', 'dev', 'short', 'tiny' + format: 'combined', + // Stream defaults to process.stdout + // Uncomment to enable logging to a log on the file system + options: { + stream: 'access.log' + } + }, + facebook: { + clientID: process.env.FACEBOOK_ID || 'APP_ID', + clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET', + callbackURL: '/api/auth/facebook/callback' + }, + twitter: { + clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY', + clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET', + callbackURL: '/api/auth/twitter/callback' + }, + google: { + clientID: process.env.GOOGLE_ID || 'APP_ID', + clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET', + callbackURL: '/api/auth/google/callback' + }, + linkedin: { + clientID: process.env.LINKEDIN_ID || 'APP_ID', + clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', + callbackURL: '/api/auth/linkedin/callback' + }, + github: { + clientID: process.env.GITHUB_ID || 'APP_ID', + clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET', + callbackURL: '/api/auth/github/callback' + }, + paypal: { + clientID: process.env.PAYPAL_ID || 'CLIENT_ID', + clientSecret: process.env.PAYPAL_SECRET || 'CLIENT_SECRET', + callbackURL: '/api/auth/paypal/callback', + sandbox: false + }, + 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' + } + } + } }; diff --git a/config/env/secure.js b/config/env/secure.js deleted file mode 100644 index 320d0fb5d7..0000000000 --- a/config/env/secure.js +++ /dev/null @@ -1,74 +0,0 @@ -'use strict'; - -module.exports = { - port: 8443, - db: { - uri: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://localhost/mean', - options: { - user: '', - pass: '' - } - }, - log: { - // Can specify one of 'combined', 'common', 'dev', 'short', 'tiny' - format: 'combined', - // Stream defaults to process.stdout - // Uncomment to enable logging to a log on the file system - options: { - stream: 'access.log' - } - }, - assets: { - lib: { - css: [ - 'public/lib/bootstrap/dist/css/bootstrap.min.css', - 'public/lib/bootstrap/dist/css/bootstrap-theme.min.css', - ], - js: [ - 'public/lib/angular/angular.min.js', - 'public/lib/angular-resource/angular-resource.min.js', - 'public/lib/angular-animate/angular-animate.min.js', - 'public/lib/angular-ui-router/release/angular-ui-router.min.js', - 'public/lib/angular-ui-utils/ui-utils.min.js', - 'public/lib/angular-bootstrap/ui-bootstrap-tpls.min.js' - ] - }, - css: 'public/dist/application.min.css', - js: 'public/dist/application.min.js' - }, - facebook: { - clientID: process.env.FACEBOOK_ID || 'APP_ID', - clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET', - callbackURL: 'https://localhost:443/auth/facebook/callback' - }, - twitter: { - clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY', - clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET', - callbackURL: 'https://localhost:443/auth/twitter/callback' - }, - google: { - clientID: process.env.GOOGLE_ID || 'APP_ID', - clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET', - callbackURL: 'https://localhost:443/auth/google/callback' - }, - linkedin: { - clientID: process.env.LINKEDIN_ID || 'APP_ID', - clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', - callbackURL: 'https://localhost:443/auth/linkedin/callback' - }, - github: { - clientID: process.env.GITHUB_ID || 'APP_ID', - clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET', - callbackURL: 'https://localhost:443/auth/github/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' - } - } - } -}; diff --git a/config/env/test.js b/config/env/test.js index f9e3116f73..cbd9821795 100644 --- a/config/env/test.js +++ b/config/env/test.js @@ -1,59 +1,60 @@ 'use strict'; +var defaultEnvConfig = require('./default'); + module.exports = { - db: { - uri: 'mongodb://localhost/mean-test', - options: { - user: '', - pass: '' - } - }, - port: 3001, - log: { - // Can specify one of 'combined', 'common', 'dev', 'short', 'tiny' - format: 'dev', - // Stream defaults to process.stdout - // Uncomment to enable logging to a log on the file system - options: { - //stream: 'access.log' - } - }, - app: { - title: 'MEAN.JS - Test Environment' - }, - facebook: { - clientID: process.env.FACEBOOK_ID || 'APP_ID', - clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET', - callbackURL: '/auth/facebook/callback' - }, - twitter: { - clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY', - clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET', - callbackURL: '/auth/twitter/callback' - }, - google: { - clientID: process.env.GOOGLE_ID || 'APP_ID', - clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET', - callbackURL: '/auth/google/callback' - }, - linkedin: { - clientID: process.env.LINKEDIN_ID || 'APP_ID', - clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', - callbackURL: '/auth/linkedin/callback' - }, - github: { - clientID: process.env.GITHUB_ID || 'APP_ID', - clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET', - callbackURL: '/auth/github/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' - } - } - } + db: { + uri: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://' + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + '/mean-test', + options: { + user: '', + pass: '' + }, + // Enable mongoose debug mode + debug: process.env.MONGODB_DEBUG || false + }, + port: process.env.PORT || 3001, + app: { + title: defaultEnvConfig.app.title + ' - Test Environment' + }, + facebook: { + clientID: process.env.FACEBOOK_ID || 'APP_ID', + clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET', + callbackURL: '/api/auth/facebook/callback' + }, + twitter: { + clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY', + clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET', + callbackURL: '/api/auth/twitter/callback' + }, + google: { + clientID: process.env.GOOGLE_ID || 'APP_ID', + clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET', + callbackURL: '/api/auth/google/callback' + }, + linkedin: { + clientID: process.env.LINKEDIN_ID || 'APP_ID', + clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', + callbackURL: '/api/auth/linkedin/callback' + }, + github: { + clientID: process.env.GITHUB_ID || 'APP_ID', + clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET', + callbackURL: '/api/auth/github/callback' + }, + paypal: { + clientID: process.env.PAYPAL_ID || 'CLIENT_ID', + clientSecret: process.env.PAYPAL_SECRET || 'CLIENT_SECRET', + callbackURL: '/api/auth/paypal/callback', + sandbox: true + }, + 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' + } + } + } }; diff --git a/config/express.js b/config/express.js deleted file mode 100755 index 6b5e1b851a..0000000000 --- a/config/express.js +++ /dev/null @@ -1,165 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var fs = require('fs'), - http = require('http'), - https = require('https'), - express = require('express'), - morgan = require('morgan'), - logger = require('./logger'), - bodyParser = require('body-parser'), - session = require('express-session'), - compression = require('compression'), - methodOverride = require('method-override'), - cookieParser = require('cookie-parser'), - helmet = require('helmet'), - passport = require('passport'), - mongoStore = require('connect-mongo')({ - session: session - }), - flash = require('connect-flash'), - config = require('./config'), - consolidate = require('consolidate'), - path = require('path'); - -module.exports = function(db) { - // Initialize express app - var app = express(); - - // Globbing model files - config.getGlobbedFiles('./app/models/**/*.js').forEach(function(modelPath) { - require(path.resolve(modelPath)); - }); - - // Setting application local variables - app.locals.title = config.app.title; - app.locals.description = config.app.description; - app.locals.keywords = config.app.keywords; - app.locals.facebookAppId = config.facebook.clientID; - app.locals.jsFiles = config.getJavaScriptAssets(); - app.locals.cssFiles = config.getCSSAssets(); - - // Passing the request url to environment locals - app.use(function(req, res, next) { - res.locals.url = req.protocol + '://' + req.headers.host + req.url; - next(); - }); - - // Should be placed before express.static - app.use(compression({ - // only compress files for the following content types - filter: function(req, res) { - return (/json|text|javascript|css/).test(res.getHeader('Content-Type')); - }, - // zlib option for compression level - level: 3 - })); - - // Showing stack errors - app.set('showStackError', true); - - // Set swig as the template engine - app.engine('server.view.html', consolidate[config.templateEngine]); - - // Set views path and view engine - app.set('view engine', 'server.view.html'); - app.set('views', './app/views'); - - // Enable logger (morgan) - app.use(morgan(logger.getLogFormat(), logger.getLogOptions())); - - // Environment dependent middleware - if (process.env.NODE_ENV === 'development') { - // Disable views cache - app.set('view cache', false); - } else if (process.env.NODE_ENV === 'production') { - app.locals.cache = 'memory'; - } - - // Request body parsing middleware should be above methodOverride - app.use(bodyParser.urlencoded({ - extended: true - })); - app.use(bodyParser.json()); - app.use(methodOverride()); - - // Use helmet to secure Express headers - app.use(helmet.xframe()); - app.use(helmet.xssFilter()); - app.use(helmet.nosniff()); - app.use(helmet.ienoopen()); - app.disable('x-powered-by'); - - // Setting the app router and static folder - app.use(express.static(path.resolve('./public'))); - - // CookieParser should be above session - app.use(cookieParser()); - - // Express MongoDB session storage - app.use(session({ - saveUninitialized: true, - resave: true, - secret: config.sessionSecret, - store: new mongoStore({ - db: db.connection.db, - collection: config.sessionCollection - }), - cookie: config.sessionCookie, - name: config.sessionName - })); - - // use passport session - app.use(passport.initialize()); - app.use(passport.session()); - - // connect flash for flash messages - app.use(flash()); - - // Globbing routing files - config.getGlobbedFiles('./app/routes/**/*.js').forEach(function(routePath) { - require(path.resolve(routePath))(app); - }); - - // Assume 'not found' in the error msgs is a 404. this is somewhat silly, but valid, you can do whatever you like, set properties, use instanceof etc. - app.use(function(err, req, res, next) { - // If the error object doesn't exists - if (!err) return next(); - - // Log it - console.error(err.stack); - - // Error page - res.status(500).render('500', { - error: err.stack - }); - }); - - // Assume 404 since no middleware responded - app.use(function(req, res) { - res.status(404).render('404', { - url: req.originalUrl, - error: 'Not Found' - }); - }); - - if (process.env.NODE_ENV === 'secure') { - // Load SSL key and certificate - var privateKey = fs.readFileSync('./config/sslcerts/key.pem', 'utf8'); - var certificate = fs.readFileSync('./config/sslcerts/cert.pem', 'utf8'); - - // Create HTTPS Server - var httpsServer = https.createServer({ - key: privateKey, - cert: certificate - }, app); - - // Return HTTPS server instance - return httpsServer; - } - - // Return Express server instance - return app; -}; diff --git a/config/init.js b/config/init.js deleted file mode 100644 index 3a5b1e5203..0000000000 --- a/config/init.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var glob = require('glob'), - chalk = require('chalk'); - -/** - * Module init function. - */ -module.exports = function() { - /** - * Before we begin, lets set the environment variable - * We'll Look for a valid NODE_ENV variable and if one cannot be found load the development NODE_ENV - */ - glob('./config/env/' + process.env.NODE_ENV + '.js', { - sync: true - }, function(err, environmentFiles) { - if (!environmentFiles.length) { - if (process.env.NODE_ENV) { - console.error(chalk.red('No configuration file found for "' + process.env.NODE_ENV + '" environment using development instead')); - } else { - console.error(chalk.red('NODE_ENV is not defined! Using default development environment')); - } - - process.env.NODE_ENV = 'development'; - } - }); - -}; diff --git a/config/lib/app.js b/config/lib/app.js new file mode 100644 index 0000000000..fb32c2071b --- /dev/null +++ b/config/lib/app.js @@ -0,0 +1,52 @@ +'use strict'; + +/** + * Module dependencies. + */ +var config = require('../config'), + mongoose = require('./mongoose'), + express = require('./express'), + chalk = require('chalk'); + +// Initialize Models +mongoose.loadModels(); + +module.exports.loadModels = function loadModels() { + mongoose.loadModels(); +}; + +module.exports.init = function init(callback) { + + mongoose.connect(function (db) { + // Initialize express + var app = express.init(db); + if (callback) callback(app, db, config); + + }); +}; + +module.exports.start = function start(callback) { + var _this = this; + + _this.init(function(app, db, config) { + + // Start the app by listening on + app.listen(config.port, function() { + + // Logging initialization + console.log('--'); + console.log(chalk.green(config.app.title)); + console.log(chalk.green('Environment:\t\t\t' + process.env.NODE_ENV)); + console.log(chalk.green('Port:\t\t\t\t' + config.port)); + console.log(chalk.green('Database:\t\t\t\t' + config.db.uri)); + if (process.env.NODE_ENV === 'secure') { + console.log(chalk.green('HTTPs:\t\t\t\ton')); + } + console.log('--'); + + if (callback) callback(app, db, config); + }); + + }); + +}; diff --git a/config/lib/express.js b/config/lib/express.js new file mode 100644 index 0000000000..70b4e3e644 --- /dev/null +++ b/config/lib/express.js @@ -0,0 +1,249 @@ +'use strict'; + +/** + * Module dependencies. + */ +var config = require('../config'), + express = require('express'), + morgan = require('morgan'), + bodyParser = require('body-parser'), + session = require('express-session'), + MongoStore = require('connect-mongo')(session), + multer = require('multer'), + favicon = require('serve-favicon'), + compress = require('compression'), + methodOverride = require('method-override'), + cookieParser = require('cookie-parser'), + helmet = require('helmet'), + flash = require('connect-flash'), + consolidate = require('consolidate'), + path = require('path'); + +/** + * Initialize local variables + */ +module.exports.initLocalVariables = function (app) { + // Setting application local variables + app.locals.title = config.app.title; + app.locals.description = config.app.description; + app.locals.secure = config.secure; + app.locals.keywords = config.app.keywords; + app.locals.googleAnalyticsTrackingID = config.app.googleAnalyticsTrackingID; + app.locals.facebookAppId = config.facebook.clientID; + app.locals.jsFiles = config.files.client.js; + app.locals.cssFiles = config.files.client.css; + app.locals.livereload = config.livereload; + app.locals.logo = config.logo; + app.locals.favicon = config.favicon; + + // Passing the request url to environment locals + app.use(function (req, res, next) { + res.locals.host = req.protocol + '://' + req.hostname; + res.locals.url = req.protocol + '://' + req.headers.host + req.originalUrl; + next(); + }); +}; + +/** + * Initialize application middleware + */ +module.exports.initMiddleware = function (app) { + // Showing stack errors + app.set('showStackError', true); + + // Enable jsonp + app.enable('jsonp callback'); + + // Should be placed before express.static + app.use(compress({ + filter: function (req, res) { + return (/json|text|javascript|css|font|svg/).test(res.getHeader('Content-Type')); + }, + level: 9 + })); + + // Initialize favicon middleware + app.use(favicon('./modules/core/client/img/brand/favicon.ico')); + + // Environment dependent middleware + if (process.env.NODE_ENV === 'development') { + // Enable logger (morgan) + app.use(morgan('dev')); + + // Disable views cache + app.set('view cache', false); + } else if (process.env.NODE_ENV === 'production') { + app.locals.cache = 'memory'; + } + + // Request body parsing middleware should be above methodOverride + app.use(bodyParser.urlencoded({ + extended: true + })); + app.use(bodyParser.json()); + app.use(methodOverride()); + + // Add the cookie parser and flash middleware + app.use(cookieParser()); + app.use(flash()); + + // Add multipart handling middleware + app.use(multer({ + dest: './uploads/', + inMemory: true + })); +}; + +/** + * Configure view engine + */ +module.exports.initViewEngine = function (app) { + // Set swig as the template engine + app.engine('server.view.html', consolidate[config.templateEngine]); + + // Set views path and view engine + app.set('view engine', 'server.view.html'); + app.set('views', './'); +}; + +/** + * Configure Express session + */ +module.exports.initSession = function (app, db) { + // Express MongoDB session storage + app.use(session({ + saveUninitialized: true, + resave: true, + secret: config.sessionSecret, + store: new MongoStore({ + mongooseConnection: db.connection, + collection: config.sessionCollection + }) + })); +}; + +/** + * Invoke modules server configuration + */ +module.exports.initModulesConfiguration = function (app, db) { + config.files.server.configs.forEach(function (configPath) { + require(path.resolve(configPath))(app, db); + }); +}; + +/** + * Configure Helmet headers configuration + */ +module.exports.initHelmetHeaders = function (app) { + // Use helmet to secure Express headers + app.use(helmet.xframe()); + app.use(helmet.xssFilter()); + app.use(helmet.nosniff()); + app.use(helmet.ienoopen()); + app.disable('x-powered-by'); +}; + +/** + * Configure the modules static routes + */ +module.exports.initModulesClientRoutes = function (app) { + // Setting the app router and static folder + app.use('/', express.static(path.resolve('./public'))); + + // Globbing static routing + config.folders.client.forEach(function (staticPath) { + app.use(staticPath.replace('/client', ''), express.static(path.resolve('./' + staticPath))); + }); +}; + +/** + * Configure the modules ACL policies + */ +module.exports.initModulesServerPolicies = function (app) { + // Globbing policy files + config.files.server.policies.forEach(function (policyPath) { + require(path.resolve(policyPath)).invokeRolesPolicies(); + }); +}; + +/** + * Configure the modules server routes + */ +module.exports.initModulesServerRoutes = function (app) { + // Globbing routing files + config.files.server.routes.forEach(function (routePath) { + require(path.resolve(routePath))(app); + }); +}; + +/** + * Configure error handling + */ +module.exports.initErrorRoutes = function (app) { + app.use(function (err, req, res, next) { + // If the error object doesn't exists + if (!err) { + return next(); + } + + // Log it + console.error(err.stack); + + // Redirect to error page + res.redirect('/server-error'); + }); +}; + +/** + * Configure Socket.io + */ +module.exports.configureSocketIO = function (app, db) { + // Load the Socket.io configuration + var server = require('./socket.io')(app, db); + + // Return server object + return server; +}; + +/** + * Initialize the Express application + */ +module.exports.init = function (db) { + // Initialize express app + var app = express(); + + // Initialize local variables + this.initLocalVariables(app); + + // Initialize Express middleware + this.initMiddleware(app); + + // Initialize Express view engine + this.initViewEngine(app); + + // Initialize Express session + this.initSession(app, db); + + // Initialize Modules configuration + this.initModulesConfiguration(app); + + // Initialize Helmet security headers + this.initHelmetHeaders(app); + + // Initialize modules static client routes + this.initModulesClientRoutes(app); + + // Initialize modules server authorization policies + this.initModulesServerPolicies(app); + + // Initialize modules server routes + this.initModulesServerRoutes(app); + + // Initialize error routes + this.initErrorRoutes(app); + + // Configure Socket.io + app = this.configureSocketIO(app, db); + + return app; +}; diff --git a/config/lib/mongoose.js b/config/lib/mongoose.js new file mode 100644 index 0000000000..9f48135366 --- /dev/null +++ b/config/lib/mongoose.js @@ -0,0 +1,44 @@ +'use strict'; + +/** + * Module dependencies. + */ +var config = require('../config'), + chalk = require('chalk'), + path = require('path'), + mongoose = require('mongoose'); + +// Load the mongoose models +module.exports.loadModels = function () { + // Globbing model files + config.files.server.models.forEach(function (modelPath) { + require(path.resolve(modelPath)); + }); +}; + +// Initialize Mongoose +module.exports.connect = function (cb) { + var _this = this; + + var db = mongoose.connect(config.db.uri, config.db.options, function (err) { + // Log Error + if (err) { + console.error(chalk.red('Could not connect to MongoDB!')); + console.log(err); + } else { + + // Enabling mongoose debug mode if required + mongoose.set('debug', config.db.debug); + + // Call callback FN + if (cb) cb(db); + } + }); +}; + +module.exports.disconnect = function (cb) { + mongoose.disconnect(function (err) { + console.info(chalk.yellow('Disconnected from MongoDB.')); + cb(err); + }); +}; diff --git a/config/lib/socket.io.js b/config/lib/socket.io.js new file mode 100644 index 0000000000..e3e7659b6e --- /dev/null +++ b/config/lib/socket.io.js @@ -0,0 +1,76 @@ +'use strict'; + +// Load the module dependencies +var config = require('../config'), + path = require('path'), + fs = require('fs'), + http = require('http'), + https = require('https'), + cookieParser = require('cookie-parser'), + passport = require('passport'), + socketio = require('socket.io'), + session = require('express-session'), + MongoStore = require('connect-mongo')(session); + +// Define the Socket.io configuration method +module.exports = function (app, db) { + var server; + if (config.secure === true) { + // Load SSL key and certificate + var privateKey = fs.readFileSync('./config/sslcerts/key.pem', 'utf8'); + var certificate = fs.readFileSync('./config/sslcerts/cert.pem', 'utf8'); + var options = { + key: privateKey, + cert: certificate + }; + + // Create new HTTPS Server + server = https.createServer(options, app); + } else { + // Create a new HTTP server + server = http.createServer(app); + } + // Create a new Socket.io server + var io = socketio.listen(server); + + // Create a MongoDB storage object + var mongoStore = new MongoStore({ + mongooseConnection: db.connection, + collection: config.sessionCollection + }); + + // Intercept Socket.io's handshake request + io.use(function (socket, next) { + // Use the 'cookie-parser' module to parse the request cookies + cookieParser(config.sessionSecret)(socket.request, {}, function (err) { + // Get the session id from the request cookies + var sessionId = socket.request.signedCookies['connect.sid']; + + // Use the mongoStorage instance to get the Express session information + mongoStore.get(sessionId, function (err, session) { + // Set the Socket.io session information + socket.request.session = session; + + // Use Passport to populate the user details + passport.initialize()(socket.request, {}, function () { + passport.session()(socket.request, {}, function () { + if (socket.request.user) { + next(null, true); + } else { + next(new Error('User is not authenticated'), false); + } + }); + }); + }); + }); + }); + + // Add an event listener to the 'connection' event + io.on('connection', function (socket) { + config.files.server.sockets.forEach(function (socketConfiguration) { + require(path.resolve(socketConfiguration))(io, socket); + }); + }); + + return server; +}; diff --git a/config/logger.js b/config/logger.js deleted file mode 100644 index e6d1f1dcbc..0000000000 --- a/config/logger.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var morgan = require('morgan'); -var config = require('./config'); -var fs = require('fs'); - -/** - * Module init function. - */ -module.exports = { - - getLogFormat: function() { - return config.log.format; - }, - - getLogOptions: function() { - var options = {}; - - try { - if ('stream' in config.log.options) { - options = { - stream: fs.createWriteStream(process.cwd() + '/' + config.log.options.stream, {flags: 'a'}) - }; - } - } catch (e) { - options = {}; - } - - return options; - } - -}; diff --git a/config/passport.js b/config/passport.js deleted file mode 100755 index 5abfae766e..0000000000 --- a/config/passport.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var passport = require('passport'), - User = require('mongoose').model('User'), - path = require('path'), - config = require('./config'); - -/** - * Module init function. - */ -module.exports = function() { - // Serialize sessions - passport.serializeUser(function(user, done) { - done(null, user.id); - }); - - // Deserialize sessions - passport.deserializeUser(function(id, done) { - User.findOne({ - _id: id - }, '-salt -password', function(err, user) { - done(err, user); - }); - }); - - // Initialize strategies - config.getGlobbedFiles('./config/strategies/**/*.js').forEach(function(strategy) { - require(path.resolve(strategy))(); - }); -}; diff --git a/config/strategies/facebook.js b/config/strategies/facebook.js deleted file mode 100644 index 34ddc68f16..0000000000 --- a/config/strategies/facebook.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var passport = require('passport'), - FacebookStrategy = require('passport-facebook').Strategy, - config = require('../config'), - users = require('../../app/controllers/users.server.controller'); - -module.exports = function() { - // Use facebook strategy - passport.use(new FacebookStrategy({ - clientID: config.facebook.clientID, - clientSecret: config.facebook.clientSecret, - callbackURL: config.facebook.callbackURL, - passReqToCallback: true - }, - function(req, accessToken, refreshToken, profile, done) { - // Set the provider data and include tokens - var providerData = profile._json; - providerData.accessToken = accessToken; - providerData.refreshToken = refreshToken; - - // Create the user OAuth profile - var providerUserProfile = { - firstName: profile.name.givenName, - lastName: profile.name.familyName, - displayName: profile.displayName, - email: profile.emails[0].value, - username: profile.username, - provider: 'facebook', - providerIdentifierField: 'id', - providerData: providerData - }; - - // Save the user OAuth profile - users.saveOAuthUserProfile(req, providerUserProfile, done); - } - )); -}; diff --git a/config/strategies/github.js b/config/strategies/github.js deleted file mode 100644 index f10a413e20..0000000000 --- a/config/strategies/github.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var passport = require('passport'), - GithubStrategy = require('passport-github').Strategy, - config = require('../config'), - users = require('../../app/controllers/users.server.controller'); - -module.exports = function() { - // Use github strategy - passport.use(new GithubStrategy({ - clientID: config.github.clientID, - clientSecret: config.github.clientSecret, - callbackURL: config.github.callbackURL, - passReqToCallback: true - }, - function(req, accessToken, refreshToken, profile, done) { - // Set the provider data and include tokens - var providerData = profile._json; - providerData.accessToken = accessToken; - providerData.refreshToken = refreshToken; - - // Create the user OAuth profile - var displayName = profile.displayName.trim(); - var iSpace = displayName.indexOf(' '); // index of the whitespace following the firstName - var firstName = iSpace !== -1 ? displayName.substring(0, iSpace) : displayName; - var lastName = iSpace !== -1 ? displayName.substring(iSpace + 1) : ''; - - var providerUserProfile = { - firstName: firstName, - lastName: lastName, - displayName: displayName, - email: profile.emails[0].value, - username: profile.username, - provider: 'github', - providerIdentifierField: 'id', - providerData: providerData - }; - - // Save the user OAuth profile - users.saveOAuthUserProfile(req, providerUserProfile, done); - } - )); -}; diff --git a/config/strategies/google.js b/config/strategies/google.js deleted file mode 100644 index 8044ed4eec..0000000000 --- a/config/strategies/google.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var passport = require('passport'), - GoogleStrategy = require('passport-google-oauth').OAuth2Strategy, - config = require('../config'), - users = require('../../app/controllers/users.server.controller'); - -module.exports = function() { - // Use google strategy - passport.use(new GoogleStrategy({ - clientID: config.google.clientID, - clientSecret: config.google.clientSecret, - callbackURL: config.google.callbackURL, - passReqToCallback: true - }, - function(req, accessToken, refreshToken, profile, done) { - // Set the provider data and include tokens - var providerData = profile._json; - providerData.accessToken = accessToken; - providerData.refreshToken = refreshToken; - - // Create the user OAuth profile - var providerUserProfile = { - firstName: profile.name.givenName, - lastName: profile.name.familyName, - displayName: profile.displayName, - email: profile.emails[0].value, - username: profile.username, - provider: 'google', - providerIdentifierField: 'id', - providerData: providerData - }; - - // Save the user OAuth profile - users.saveOAuthUserProfile(req, providerUserProfile, done); - } - )); -}; diff --git a/config/strategies/linkedin.js b/config/strategies/linkedin.js deleted file mode 100644 index 1ee5b3f5bc..0000000000 --- a/config/strategies/linkedin.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var passport = require('passport'), - LinkedInStrategy = require('passport-linkedin').Strategy, - config = require('../config'), - users = require('../../app/controllers/users.server.controller'); - -module.exports = function() { - // Use linkedin strategy - passport.use(new LinkedInStrategy({ - consumerKey: config.linkedin.clientID, - consumerSecret: config.linkedin.clientSecret, - callbackURL: config.linkedin.callbackURL, - passReqToCallback: true, - profileFields: ['id', 'first-name', 'last-name', 'email-address'] - }, - function(req, accessToken, refreshToken, profile, done) { - // Set the provider data and include tokens - var providerData = profile._json; - providerData.accessToken = accessToken; - providerData.refreshToken = refreshToken; - - // Create the user OAuth profile - var providerUserProfile = { - firstName: profile.name.givenName, - lastName: profile.name.familyName, - displayName: profile.displayName, - email: profile.emails[0].value, - username: profile.username, - provider: 'linkedin', - providerIdentifierField: 'id', - providerData: providerData - }; - - // Save the user OAuth profile - users.saveOAuthUserProfile(req, providerUserProfile, done); - } - )); -}; diff --git a/config/strategies/local.js b/config/strategies/local.js deleted file mode 100644 index ad56052422..0000000000 --- a/config/strategies/local.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var passport = require('passport'), - LocalStrategy = require('passport-local').Strategy, - User = require('mongoose').model('User'); - -module.exports = function() { - // Use local strategy - passport.use(new LocalStrategy({ - usernameField: 'username', - passwordField: 'password' - }, - function(username, password, done) { - User.findOne({ - username: username - }, function(err, user) { - if (err) { - return done(err); - } - if (!user) { - return done(null, false, { - message: 'Unknown user or invalid password' - }); - } - if (!user.authenticate(password)) { - return done(null, false, { - message: 'Unknown user or invalid password' - }); - } - - return done(null, user); - }); - } - )); -}; diff --git a/config/strategies/twitter.js b/config/strategies/twitter.js deleted file mode 100644 index 5dcc93f4ed..0000000000 --- a/config/strategies/twitter.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var passport = require('passport'), - TwitterStrategy = require('passport-twitter').Strategy, - config = require('../config'), - users = require('../../app/controllers/users.server.controller'); - -module.exports = function() { - // Use twitter strategy - passport.use(new TwitterStrategy({ - consumerKey: config.twitter.clientID, - consumerSecret: config.twitter.clientSecret, - callbackURL: config.twitter.callbackURL, - passReqToCallback: true - }, - function(req, token, tokenSecret, profile, done) { - // Set the provider data and include tokens - var providerData = profile._json; - providerData.token = token; - providerData.tokenSecret = tokenSecret; - - // Create the user OAuth profile - var displayName = profile.displayName.trim(); - var iSpace = displayName.indexOf(' '); // index of the whitespace following the firstName - var firstName = iSpace !== -1 ? displayName.substring(0, iSpace) : displayName; - var lastName = iSpace !== -1 ? displayName.substring(iSpace + 1) : ''; - - var providerUserProfile = { - firstName: firstName, - lastName: lastName, - displayName: displayName, - username: profile.username, - provider: 'twitter', - providerIdentifierField: 'id_str', - providerData: providerData - }; - - // Save the user OAuth profile - users.saveOAuthUserProfile(req, providerUserProfile, done); - } - )); -}; diff --git a/fig.yml b/docker-compose.yml similarity index 100% rename from fig.yml rename to docker-compose.yml diff --git a/gruntfile.js b/gruntfile.js index c028ff7751..39dfceee6e 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -1,194 +1,266 @@ 'use strict'; -var fs = require('fs'); - -module.exports = function(grunt) { - // Unified Watch Object - var watchFiles = { - serverViews: ['app/views/**/*.*'], - serverJS: ['gruntfile.js', 'server.js', 'config/**/*.js', 'app/**/*.js', '!app/tests/'], - clientViews: ['public/modules/**/views/**/*.html'], - clientJS: ['public/js/*.js', 'public/modules/**/*.js'], - clientCSS: ['public/modules/**/*.css'], - mochaTests: ['app/tests/**/*.js'] - }; - - // Project Configuration - grunt.initConfig({ - pkg: grunt.file.readJSON('package.json'), - watch: { - serverViews: { - files: watchFiles.serverViews, - options: { - livereload: true - } - }, - serverJS: { - files: watchFiles.serverJS, - tasks: ['jshint'], - options: { - livereload: true - } - }, - clientViews: { - files: watchFiles.clientViews, - options: { - livereload: true - } - }, - clientJS: { - files: watchFiles.clientJS, - tasks: ['jshint'], - options: { - livereload: true - } - }, - clientCSS: { - files: watchFiles.clientCSS, - tasks: ['csslint'], - options: { - livereload: true - } - }, - mochaTests: { - files: watchFiles.mochaTests, - tasks: ['test:server'], - } - }, - jshint: { - all: { - src: watchFiles.clientJS.concat(watchFiles.serverJS), - options: { - jshintrc: true - } - } - }, - csslint: { - options: { - csslintrc: '.csslintrc' - }, - all: { - src: watchFiles.clientCSS - } - }, - uglify: { - production: { - options: { - mangle: false - }, - files: { - 'public/dist/application.min.js': 'public/dist/application.js' - } - } - }, - cssmin: { - combine: { - files: { - 'public/dist/application.min.css': '<%= applicationCSSFiles %>' - } - } - }, - nodemon: { - dev: { - script: 'server.js', - options: { - nodeArgs: ['--debug'], - ext: 'js,html', - watch: watchFiles.serverViews.concat(watchFiles.serverJS) - } - } - }, - 'node-inspector': { - custom: { - options: { - 'web-port': 1337, - 'web-host': 'localhost', - 'debug-port': 5858, - 'save-live-edit': true, - 'no-preload': true, - 'stack-trace-limit': 50, - 'hidden': [] - } - } - }, - ngAnnotate: { - production: { - files: { - 'public/dist/application.js': '<%= applicationJavaScriptFiles %>' - } - } - }, - concurrent: { - default: ['nodemon', 'watch'], - debug: ['nodemon', 'watch', 'node-inspector'], - options: { - logConcurrentOutput: true, - limit: 10 - } - }, - env: { - test: { - NODE_ENV: 'test' - }, - secure: { - NODE_ENV: 'secure' - } - }, - mochaTest: { - src: watchFiles.mochaTests, - options: { - reporter: 'spec', - require: 'server.js' - } - }, - karma: { - unit: { - configFile: 'karma.conf.js' - } - }, - copy: { - localConfig: { - src: 'config/env/local.example.js', - dest: 'config/env/local.js', - filter: function() { - return !fs.existsSync('config/env/local.js'); - } - } - } - }); - - // Load NPM tasks - require('load-grunt-tasks')(grunt); - - // Making grunt default to force in order not to break the project. - grunt.option('force', true); - - // A Task for loading the configuration object - grunt.task.registerTask('loadConfig', 'Task that loads the config into a grunt option.', function() { - var init = require('./config/init')(); - var config = require('./config/config'); - - grunt.config.set('applicationJavaScriptFiles', config.assets.js); - grunt.config.set('applicationCSSFiles', config.assets.css); - }); - - // Default task(s). - grunt.registerTask('default', ['lint', 'copy:localConfig', 'concurrent:default']); - - // Debug task. - grunt.registerTask('debug', ['lint', 'copy:localConfig', 'concurrent:debug']); - - // Secure task(s). - grunt.registerTask('secure', ['env:secure', 'lint', 'copy:localConfig', 'concurrent:default']); - - // Lint task(s). - grunt.registerTask('lint', ['jshint', 'csslint']); - - // Build task(s). - grunt.registerTask('build', ['lint', 'loadConfig', 'ngAnnotate', 'uglify', 'cssmin']); - - // Test task. - grunt.registerTask('test', ['copy:localConfig', 'test:server', 'test:client']); - grunt.registerTask('test:server', ['env:test', 'mochaTest']); - grunt.registerTask('test:client', ['env:test', 'karma:unit']); +/** + * Module dependencies. + */ +var _ = require('lodash'), + defaultAssets = require('./config/assets/default'), + testAssets = require('./config/assets/test'), + fs = require('fs'), + path = require('path'); + +module.exports = function (grunt) { + // Project Configuration + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + env: { + test: { + NODE_ENV: 'test' + }, + dev: { + NODE_ENV: 'development' + }, + prod: { + NODE_ENV: 'production' + } + }, + watch: { + serverViews: { + files: defaultAssets.server.views, + options: { + livereload: true + } + }, + serverJS: { + files: _.union(defaultAssets.server.gruntConfig, defaultAssets.server.allJS), + tasks: ['jshint'], + options: { + livereload: true + } + }, + clientViews: { + files: defaultAssets.client.views, + options: { + livereload: true + } + }, + clientJS: { + files: defaultAssets.client.js, + tasks: ['jshint'], + options: { + livereload: true + } + }, + clientCSS: { + files: defaultAssets.client.css, + tasks: ['csslint'], + options: { + livereload: true + } + }, + clientSCSS: { + files: defaultAssets.client.sass, + tasks: ['sass', 'csslint'], + options: { + livereload: true + } + }, + clientLESS: { + files: defaultAssets.client.less, + tasks: ['less', 'csslint'], + options: { + livereload: true + } + } + }, + nodemon: { + dev: { + script: 'server.js', + options: { + nodeArgs: ['--debug'], + ext: 'js,html', + watch: _.union(defaultAssets.server.gruntConfig, defaultAssets.server.views, defaultAssets.server.allJS, defaultAssets.server.config) + } + } + }, + concurrent: { + default: ['nodemon', 'watch'], + debug: ['nodemon', 'watch', 'node-inspector'], + options: { + logConcurrentOutput: true + } + }, + jshint: { + all: { + src: _.union(defaultAssets.server.gruntConfig, defaultAssets.server.allJS, defaultAssets.client.js, testAssets.tests.server, testAssets.tests.client, testAssets.tests.e2e), + options: { + jshintrc: true, + node: true, + mocha: true, + jasmine: true + } + } + }, + csslint: { + options: { + csslintrc: '.csslintrc' + }, + all: { + src: defaultAssets.client.css + } + }, + ngAnnotate: { + production: { + files: { + 'public/dist/application.js': defaultAssets.client.js + } + } + }, + uglify: { + production: { + options: { + mangle: false + }, + files: { + 'public/dist/application.min.js': 'public/dist/application.js' + } + } + }, + cssmin: { + combine: { + files: { + 'public/dist/application.min.css': defaultAssets.client.css + } + } + }, + sass: { + dist: { + files: [{ + expand: true, + src: defaultAssets.client.sass, + ext: '.css', + rename: function (base, src) { + return src.replace('/scss/', '/css/'); + } + }] + } + }, + less: { + dist: { + files: [{ + expand: true, + src: defaultAssets.client.less, + ext: '.css', + rename: function (base, src) { + return src.replace('/less/', '/css/'); + } + }] + } + }, + 'node-inspector': { + custom: { + options: { + 'web-port': 1337, + 'web-host': 'localhost', + 'debug-port': 5858, + 'save-live-edit': true, + 'no-preload': true, + 'stack-trace-limit': 50, + 'hidden': [] + } + } + }, + mochaTest: { + src: testAssets.tests.server, + options: { + reporter: 'spec' + } + }, + karma: { + unit: { + configFile: 'karma.conf.js' + } + }, + protractor: { + options: { + configFile: 'protractor.conf.js', + keepAlive: true, + noColor: false + }, + e2e: { + options: { + args: {} // Target-specific arguments + } + } + }, + copy: { + localConfig: { + src: 'config/env/local.example.js', + dest: 'config/env/local.js', + filter: function () { + return !fs.existsSync('config/env/local.js'); + } + } + } + }); + + // Load NPM tasks + require('load-grunt-tasks')(grunt); + + // Making grunt default to force in order not to break the project. + grunt.option('force', true); + + // Make sure upload directory exists + grunt.task.registerTask('mkdir:upload', 'Task that makes sure upload directory exists.', function () { + // Get the callback + var done = this.async(); + + grunt.file.mkdir(path.normalize(__dirname + '/modules/users/client/img/profile/uploads')); + + done(); + }); + + // Connect to the MongoDB instance and load the models + grunt.task.registerTask('mongoose', 'Task that connects to the MongoDB instance and loads the application models.', function () { + // Get the callback + var done = this.async(); + + // Use mongoose configuration + var mongoose = require('./config/lib/mongoose.js'); + + // Connect to database + mongoose.connect(function (db) { + done(); + }); + }); + + grunt.task.registerTask('server', 'Starting the server', function () { + // Get the callback + var done = this.async(); + + var path = require('path'); + var app = require(path.resolve('./config/lib/app')); + var server = app.start(function () { + done(); + }); + }); + + // Lint CSS and JavaScript files. + grunt.registerTask('lint', ['sass', 'less', 'jshint', 'csslint']); + + // Lint project files and minify them into two production files. + grunt.registerTask('build', ['env:dev', 'lint', 'ngAnnotate', 'uglify', 'cssmin']); + + // Run the project tests + grunt.registerTask('test', ['env:test', 'lint', 'mkdir:upload', 'copy:localConfig', 'server', 'mochaTest', 'karma:unit']); + grunt.registerTask('test:server', ['env:test', 'lint', 'server', 'mochaTest']); + grunt.registerTask('test:client', ['env:test', 'lint', 'server', 'karma:unit']); + // Run the project in development mode + grunt.registerTask('default', ['env:dev', 'lint', 'mkdir:upload', 'copy:localConfig', 'concurrent:default']); + + // Run the project in debug mode + grunt.registerTask('debug', ['env:dev', 'lint', 'mkdir:upload', 'copy:localConfig', 'concurrent:debug']); + + // Run the project in production mode + grunt.registerTask('prod', ['build', 'env:prod', 'mkdir:upload', 'copy:localConfig', 'concurrent:default']); }; diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000000..2c260cebcf --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,195 @@ +'use strict'; + +/** + * Module dependencies. + */ +var _ = require('lodash'), + defaultAssets = require('./config/assets/default'), + testAssets = require('./config/assets/test'), + gulp = require('gulp'), + gulpLoadPlugins = require('gulp-load-plugins'), + runSequence = require('run-sequence'), + plugins = gulpLoadPlugins(), + path = require('path'); + +// Set NODE_ENV to 'test' +gulp.task('env:test', function () { + process.env.NODE_ENV = 'test'; +}); + +// Set NODE_ENV to 'development' +gulp.task('env:dev', function () { + process.env.NODE_ENV = 'development'; +}); + +// Set NODE_ENV to 'production' +gulp.task('env:prod', function () { + process.env.NODE_ENV = 'production'; +}); + +// Nodemon task +gulp.task('nodemon', function () { + return plugins.nodemon({ + script: 'server.js', + nodeArgs: ['--debug'], + ext: 'js,html', + watch: _.union(defaultAssets.server.views, defaultAssets.server.allJS, defaultAssets.server.config) + }); +}); + +// Watch Files For Changes +gulp.task('watch', function() { + // Start livereload + plugins.livereload.listen(); + + // Add watch rules + gulp.watch(defaultAssets.server.gulpConfig, ['jshint']); + gulp.watch(defaultAssets.server.views).on('change', plugins.livereload.changed); + gulp.watch(defaultAssets.server.allJS, ['jshint']).on('change', plugins.livereload.changed); + gulp.watch(defaultAssets.client.views).on('change', plugins.livereload.changed); + gulp.watch(defaultAssets.client.js, ['jshint']).on('change', plugins.livereload.changed); + gulp.watch(defaultAssets.client.css, ['csslint']).on('change', plugins.livereload.changed); + gulp.watch(defaultAssets.client.sass, ['sass', 'csslint']).on('change', plugins.livereload.changed); + gulp.watch(defaultAssets.client.less, ['less', 'csslint']).on('change', plugins.livereload.changed); +}); + +// CSS linting task +gulp.task('csslint', function (done) { + return gulp.src(defaultAssets.client.css) + .pipe(plugins.csslint('.csslintrc')) + .pipe(plugins.csslint.reporter()) + .pipe(plugins.csslint.reporter(function (file) { + if (!file.csslint.errorCount) { + done(); + } + })); +}); + +// JS linting task +gulp.task('jshint', function () { + return gulp.src(_.union(defaultAssets.server.gulpConfig, defaultAssets.server.allJS, defaultAssets.client.js, testAssets.tests.server, testAssets.tests.client, testAssets.tests.e2e)) + .pipe(plugins.jshint()) + .pipe(plugins.jshint.reporter('default')) + .pipe(plugins.jshint.reporter('fail')); +}); + + +// JS minifying task +gulp.task('uglify', function () { + return gulp.src(defaultAssets.client.js) + .pipe(plugins.ngAnnotate()) + .pipe(plugins.uglify({ + mangle: false + })) + .pipe(plugins.concat('application.min.js')) + .pipe(gulp.dest('public/dist')); +}); + +// CSS minifying task +gulp.task('cssmin', function () { + return gulp.src(defaultAssets.client.css) + .pipe(plugins.cssmin()) + .pipe(plugins.concat('application.min.css')) + .pipe(gulp.dest('public/dist')); +}); + +// Sass task +gulp.task('sass', function () { + return gulp.src(defaultAssets.client.sass) + .pipe(plugins.sass()) + .pipe(plugins.rename(function (file) { + file.dirname = file.dirname.replace(path.sep + 'scss', path.sep + 'css'); + })) + .pipe(gulp.dest('./modules/')); +}); + +// Less task +gulp.task('less', function () { + return gulp.src(defaultAssets.client.less) + .pipe(plugins.less()) + .pipe(plugins.rename(function (file) { + file.dirname = file.dirname.replace(path.sep + 'less', path.sep + 'css'); + })) + .pipe(gulp.dest('./modules/')); +}); + +// Mocha tests task +gulp.task('mocha', function (done) { + // Open mongoose connections + var mongoose = require('./config/lib/mongoose.js'); + var error; + + // Connect mongoose + mongoose.connect(function() { + // Run the tests + gulp.src(testAssets.tests.server) + .pipe(plugins.mocha({ + reporter: 'spec' + })) + .on('error', function (err) { + // If an error occurs, save it + error = err; + }) + .on('end', function() { + // When the tests are done, disconnect mongoose and pass the error state back to gulp + mongoose.disconnect(function() { + done(error); + }); + }); + }); + +}); + +// Karma test runner task +gulp.task('karma', function (done) { + return gulp.src([]) + .pipe(plugins.karma({ + configFile: 'karma.conf.js', + action: 'run', + singleRun: true + })); +}); + +// Selenium standalone WebDriver update task +gulp.task('webdriver-update', plugins.protractor.webdriver_update); + +// Protractor test runner task +gulp.task('protractor', function () { + gulp.src([]) + .pipe(plugins.protractor.protractor({ + configFile: 'protractor.conf.js' + })) + .on('error', function (e) { + throw e; + }); +}); + +// Lint CSS and JavaScript files. +gulp.task('lint', function(done) { + runSequence('less', 'sass', ['csslint', 'jshint'], done); +}); + +// Lint project files and minify them into two production files. +gulp.task('build', function(done) { + runSequence('env:dev' ,'lint', ['uglify', 'cssmin'], done); +}); + +// Run the project tests +gulp.task('test', function(done) { + runSequence('env:test', ['karma', 'mocha'], done); +}); + +// Run the project in development mode +gulp.task('default', function(done) { + runSequence('env:dev', 'lint', ['nodemon', 'watch'], done); +}); + +// Run the project in debug mode +gulp.task('debug', function(done) { + runSequence('env:dev', 'lint', ['nodemon', 'watch'], done); +}); + +// Run the project in production mode +gulp.task('prod', function(done) { + runSequence('build', 'lint', ['nodemon', 'watch'], done); +}); diff --git a/karma.conf.js b/karma.conf.js index 0f5ab311fd..cdf1cd2421 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -3,49 +3,63 @@ /** * Module dependencies. */ -var applicationConfiguration = require('./config/config'); +var _ = require('lodash'), + defaultAssets = require('./config/assets/default'), + testAssets = require('./config/assets/test'); // Karma configuration -module.exports = function(config) { - config.set({ - // Frameworks to use - frameworks: ['jasmine'], - - // List of files / patterns to load in the browser - files: applicationConfiguration.assets.lib.js.concat(applicationConfiguration.assets.js, applicationConfiguration.assets.tests), - - // Test results reporter to use - // Possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' - reporters: ['progress'], - - // Web server port - port: 9876, - - // Enable / disable colors in the output (reporters and logs) - colors: true, - - // Level of logging - // Possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG - logLevel: config.LOG_INFO, - - // Enable / disable watching file and executing tests whenever any file changes - autoWatch: true, - - // Start these browsers, currently available: - // - Chrome - // - ChromeCanary - // - Firefox - // - Opera - // - Safari (only Mac) - // - PhantomJS - // - IE (only Windows) - browsers: ['PhantomJS'], - - // If browser does not capture in given timeout [ms], kill it - captureTimeout: 60000, - - // Continuous Integration mode - // If true, it capture browsers, run tests and exit - singleRun: true - }); +module.exports = function (karmaConfig) { + karmaConfig.set({ + // Frameworks to use + frameworks: ['jasmine'], + + preprocessors: { + 'modules/*/client/views/**/*.html': ['ng-html2js'] + }, + + ngHtml2JsPreprocessor: { + moduleName: 'mean', + + cacheIdFromPath: function (filepath) { + return filepath.replace('/client', ''); + }, + }, + + // List of files / patterns to load in the browser + files: _.union(defaultAssets.client.lib.js, defaultAssets.client.lib.tests, defaultAssets.client.js, testAssets.tests.client, defaultAssets.client.views), + + // Test results reporter to use + // Possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' + reporters: ['progress'], + + // Web server port + port: 9876, + + // Enable / disable colors in the output (reporters and logs) + colors: true, + + // Level of logging + // Possible values: karmaConfig.LOG_DISABLE || karmaConfig.LOG_ERROR || karmaConfig.LOG_WARN || karmaConfig.LOG_INFO || karmaConfig.LOG_DEBUG + logLevel: karmaConfig.LOG_INFO, + + // Enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + // Start these browsers, currently available: + // - Chrome + // - ChromeCanary + // - Firefox + // - Opera + // - Safari (only Mac) + // - PhantomJS + // - IE (only Windows) + browsers: ['PhantomJS'], + + // If browser does not capture in given timeout [ms], kill it + captureTimeout: 60000, + + // Continuous Integration mode + // If true, it capture browsers, run tests and exit + singleRun: true + }); }; diff --git a/public/modules/articles/articles.client.module.js b/modules/articles/client/articles.client.module.js old mode 100755 new mode 100644 similarity index 51% rename from public/modules/articles/articles.client.module.js rename to modules/articles/client/articles.client.module.js index 3f4c63fdbb..3c94d0cb57 --- a/public/modules/articles/articles.client.module.js +++ b/modules/articles/client/articles.client.module.js @@ -1,4 +1,4 @@ 'use strict'; -// Use Application configuration module to register a new module +// Use Applicaion configuration module to register a new module ApplicationConfiguration.registerModule('articles'); diff --git a/modules/articles/client/config/articles.client.config.js b/modules/articles/client/config/articles.client.config.js new file mode 100644 index 0000000000..5ad71e82dd --- /dev/null +++ b/modules/articles/client/config/articles.client.config.js @@ -0,0 +1,25 @@ +'use strict'; + +// Configuring the Articles module +angular.module('articles').run(['Menus', + function (Menus) { + // Add the articles dropdown item + Menus.addMenuItem('topbar', { + title: 'Articles', + state: 'articles', + type: 'dropdown' + }); + + // Add the dropdown list item + Menus.addSubMenuItem('topbar', 'articles', { + title: 'List Articles', + state: 'articles.list' + }); + + // Add the dropdown create item + Menus.addSubMenuItem('topbar', 'articles', { + title: 'Create Articles', + state: 'articles.create' + }); + } +]); diff --git a/modules/articles/client/config/articles.client.routes.js b/modules/articles/client/config/articles.client.routes.js new file mode 100644 index 0000000000..df7024c4c2 --- /dev/null +++ b/modules/articles/client/config/articles.client.routes.js @@ -0,0 +1,33 @@ +'use strict'; + +// Setting up route +angular.module('articles').config(['$stateProvider', + function ($stateProvider) { + // Articles state routing + $stateProvider + .state('articles', { + abstract: true, + url: '/articles', + template: '', + data: { + roles: ['user', 'admin'] + } + }) + .state('articles.list', { + url: '', + templateUrl: 'modules/articles/views/list-articles.client.view.html' + }) + .state('articles.create', { + url: '/create', + templateUrl: 'modules/articles/views/create-article.client.view.html' + }) + .state('articles.view', { + url: '/:articleId', + templateUrl: 'modules/articles/views/view-article.client.view.html' + }) + .state('articles.edit', { + url: '/:articleId/edit', + templateUrl: 'modules/articles/views/edit-article.client.view.html' + }); + } +]); diff --git a/modules/articles/client/controllers/articles.client.controller.js b/modules/articles/client/controllers/articles.client.controller.js new file mode 100644 index 0000000000..4a47d5a27b --- /dev/null +++ b/modules/articles/client/controllers/articles.client.controller.js @@ -0,0 +1,68 @@ +'use strict'; + +// Articles controller +angular.module('articles').controller('ArticlesController', ['$scope', '$stateParams', '$location', 'Authentication', 'Articles', + function ($scope, $stateParams, $location, Authentication, Articles) { + $scope.authentication = Authentication; + + // Create new Article + $scope.create = function () { + // Create new Article object + var article = new Articles({ + title: this.title, + content: this.content + }); + + // Redirect after save + article.$save(function (response) { + $location.path('articles/' + response._id); + + // Clear form fields + $scope.title = ''; + $scope.content = ''; + }, function (errorResponse) { + $scope.error = errorResponse.data.message; + }); + }; + + // Remove existing Article + $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'); + }); + } + }; + + // Update existing Article + $scope.update = function () { + var article = $scope.article; + + article.$update(function () { + $location.path('articles/' + article._id); + }, function (errorResponse) { + $scope.error = errorResponse.data.message; + }); + }; + + // Find a list of Articles + $scope.find = function () { + $scope.articles = Articles.query(); + }; + + // Find existing Article + $scope.findOne = function () { + $scope.article = Articles.get({ + articleId: $stateParams.articleId + }); + }; + } +]); diff --git a/modules/articles/client/services/articles.client.service.js b/modules/articles/client/services/articles.client.service.js new file mode 100644 index 0000000000..73a8251221 --- /dev/null +++ b/modules/articles/client/services/articles.client.service.js @@ -0,0 +1,14 @@ +'use strict'; + +//Articles service used for communicating with the articles REST endpoints +angular.module('articles').factory('Articles', ['$resource', + function ($resource) { + return $resource('api/articles/:articleId', { + articleId: '@_id' + }, { + update: { + method: 'PUT' + } + }); + } +]); diff --git a/modules/articles/client/views/create-article.client.view.html b/modules/articles/client/views/create-article.client.view.html new file mode 100644 index 0000000000..235e6b37af --- /dev/null +++ b/modules/articles/client/views/create-article.client.view.html @@ -0,0 +1,29 @@ +
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+ +
+
+
+
+
diff --git a/modules/articles/client/views/edit-article.client.view.html b/modules/articles/client/views/edit-article.client.view.html new file mode 100644 index 0000000000..69eb621b2e --- /dev/null +++ b/modules/articles/client/views/edit-article.client.view.html @@ -0,0 +1,29 @@ +
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+ +
+
+
+
+
diff --git a/modules/articles/client/views/list-articles.client.view.html b/modules/articles/client/views/list-articles.client.view.html new file mode 100644 index 0000000000..689c9c030a --- /dev/null +++ b/modules/articles/client/views/list-articles.client.view.html @@ -0,0 +1,20 @@ +
+ + +
+ No articles yet, why don't you create one? +
+
diff --git a/modules/articles/client/views/view-article.client.view.html b/modules/articles/client/views/view-article.client.view.html new file mode 100644 index 0000000000..83a0c40b89 --- /dev/null +++ b/modules/articles/client/views/view-article.client.view.html @@ -0,0 +1,22 @@ +
+ + + + + Posted on + + by + + + +

+
diff --git a/modules/articles/server/controllers/articles.server.controller.js b/modules/articles/server/controllers/articles.server.controller.js new file mode 100644 index 0000000000..2c146c64d8 --- /dev/null +++ b/modules/articles/server/controllers/articles.server.controller.js @@ -0,0 +1,110 @@ +'use strict'; + +/** + * Module dependencies. + */ +var path = require('path'), + mongoose = require('mongoose'), + Article = mongoose.model('Article'), + errorHandler = require(path.resolve('./modules/core/server/controllers/errors.server.controller')); + +/** + * Create a article + */ +exports.create = function (req, res) { + var article = new Article(req.body); + article.user = req.user; + + article.save(function (err) { + if (err) { + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); + } else { + res.json(article); + } + }); +}; + +/** + * Show the current article + */ +exports.read = function (req, res) { + res.json(req.article); +}; + +/** + * Update a article + */ +exports.update = function (req, res) { + var article = req.article; + + article.title = req.body.title; + article.content = req.body.content; + + article.save(function (err) { + if (err) { + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); + } else { + res.json(article); + } + }); +}; + +/** + * Delete an article + */ +exports.delete = function (req, res) { + var article = req.article; + + article.remove(function (err) { + if (err) { + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); + } else { + res.json(article); + } + }); +}; + +/** + * List of Articles + */ +exports.list = function (req, res) { + Article.find().sort('-created').populate('user', 'displayName').exec(function (err, articles) { + if (err) { + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); + } else { + res.json(articles); + } + }); +}; + +/** + * Article middleware + */ +exports.articleByID = function (req, res, next, id) { + + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).send({ + message: 'Article is invalid' + }); + } + + Article.findById(id).populate('user', 'displayName').exec(function (err, article) { + if (err) { + return next(err); + } else if (!article) { + return res.status(404).send({ + message: 'No article with that identifier has been found' + }); + } + req.article = article; + next(); + }); +}; diff --git a/modules/articles/server/models/article.server.model.js b/modules/articles/server/models/article.server.model.js new file mode 100644 index 0000000000..2ccb91e370 --- /dev/null +++ b/modules/articles/server/models/article.server.model.js @@ -0,0 +1,34 @@ +'use strict'; + +/** + * Module dependencies. + */ +var mongoose = require('mongoose'), + Schema = mongoose.Schema; + +/** + * Article Schema + */ +var ArticleSchema = new Schema({ + created: { + type: Date, + default: Date.now + }, + title: { + type: String, + default: '', + trim: true, + required: 'Title cannot be blank' + }, + content: { + type: String, + default: '', + trim: true + }, + user: { + type: Schema.ObjectId, + ref: 'User' + } +}); + +mongoose.model('Article', ArticleSchema); diff --git a/modules/articles/server/policies/articles.server.policy.js b/modules/articles/server/policies/articles.server.policy.js new file mode 100644 index 0000000000..536f8bccb1 --- /dev/null +++ b/modules/articles/server/policies/articles.server.policy.js @@ -0,0 +1,72 @@ +'use strict'; + +/** + * Module dependencies. + */ +var acl = require('acl'); + +// Using the memory backend +acl = new acl(new acl.memoryBackend()); + +/** + * Invoke Articles Permissions + */ +exports.invokeRolesPolicies = function () { + acl.allow([{ + roles: ['admin'], + allows: [{ + resources: '/api/articles', + permissions: '*' + }, { + resources: '/api/articles/:articleId', + permissions: '*' + }] + }, { + roles: ['user'], + allows: [{ + resources: '/api/articles', + permissions: ['get', 'post'] + }, { + resources: '/api/articles/:articleId', + permissions: ['get'] + }] + }, { + roles: ['guest'], + allows: [{ + resources: '/api/articles', + permissions: ['get'] + }, { + resources: '/api/articles/:articleId', + permissions: ['get'] + }] + }]); +}; + +/** + * Check If Articles Policy Allows + */ +exports.isAllowed = function (req, res, next) { + var roles = (req.user) ? req.user.roles : ['guest']; + + // If an article is being processed and the current user created it then allow any manipulation + if (req.article && req.user && req.article.user.id === req.user.id) { + return next(); + } + + // Check for user roles + acl.areAnyRolesAllowed(roles, req.route.path, req.method.toLowerCase(), function (err, isAllowed) { + if (err) { + // An authorization error occurred. + return res.status(500).send('Unexpected authorization error'); + } else { + if (isAllowed) { + // Access granted! Invoke next middleware + return next(); + } else { + return res.status(403).json({ + message: 'User is not authorized' + }); + } + } + }); +}; diff --git a/modules/articles/server/routes/articles.server.routes.js b/modules/articles/server/routes/articles.server.routes.js new file mode 100644 index 0000000000..dd7bab41e4 --- /dev/null +++ b/modules/articles/server/routes/articles.server.routes.js @@ -0,0 +1,23 @@ +'use strict'; + +/** + * Module dependencies. + */ +var articlesPolicy = require('../policies/articles.server.policy'), + articles = require('../controllers/articles.server.controller'); + +module.exports = function (app) { + // Articles collection routes + app.route('/api/articles').all(articlesPolicy.isAllowed) + .get(articles.list) + .post(articles.create); + + // Single article routes + app.route('/api/articles/:articleId').all(articlesPolicy.isAllowed) + .get(articles.read) + .put(articles.update) + .delete(articles.delete); + + // Finish by binding the article middleware + app.param('articleId', articles.articleByID); +}; diff --git a/modules/articles/tests/client/articles.client.controller.tests.js b/modules/articles/tests/client/articles.client.controller.tests.js new file mode 100644 index 0000000000..2e6f317f34 --- /dev/null +++ b/modules/articles/tests/client/articles.client.controller.tests.js @@ -0,0 +1,210 @@ +'use strict'; + +(function () { + // Articles Controller Spec + describe('Articles Controller Tests', function () { + // Initialize global variables + var ArticlesController, + scope, + $httpBackend, + $stateParams, + $location, + Authentication, + Articles, + mockArticle; + + // The $resource service augments the response object with methods for updating and deleting the resource. + // If we were to use the standard toEqual matcher, our tests would fail because the test values would not match + // the responses exactly. To solve the problem, we define a new toEqualData Jasmine matcher. + // When the toEqualData matcher compares two objects, it takes only object properties into + // account and ignores methods. + beforeEach(function () { + jasmine.addMatchers({ + toEqualData: function (util, customEqualityTesters) { + return { + compare: function (actual, expected) { + return { + pass: angular.equals(actual, expected) + }; + } + }; + } + }); + }); + + // Then we can start by loading the main application module + beforeEach(module(ApplicationConfiguration.applicationModuleName)); + + // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). + // This allows us to inject a service but then attach it to a variable + // with the same name as the service. + beforeEach(inject(function ($controller, $rootScope, _$location_, _$stateParams_, _$httpBackend_, _Authentication_, _Articles_) { + // Set a new global scope + scope = $rootScope.$new(); + + // Point global variables to injected services + $stateParams = _$stateParams_; + $httpBackend = _$httpBackend_; + $location = _$location_; + Authentication = _Authentication_; + Articles = _Articles_; + + // create mock article + mockArticle = new Articles({ + _id: '525a8422f6d0f87f0e407a33', + title: 'An Article about MEAN', + content: 'MEAN rocks!' + }); + + // Mock logged in user + Authentication.user = { + roles: ['user'] + }; + + // Initialize the Articles controller. + ArticlesController = $controller('ArticlesController', { + $scope: scope + }); + })); + + it('$scope.find() should create an array with at least one article object fetched from XHR', inject(function (Articles) { + // Create a sample articles array that includes the new article + var sampleArticles = [mockArticle]; + + // Set GET response + $httpBackend.expectGET('api/articles').respond(sampleArticles); + + // Run controller functionality + scope.find(); + $httpBackend.flush(); + + // Test scope value + expect(scope.articles).toEqualData(sampleArticles); + })); + + it('$scope.findOne() should create an array with one article object fetched from XHR using a articleId URL parameter', inject(function (Articles) { + // Set the URL parameter + $stateParams.articleId = mockArticle._id; + + // Set GET response + $httpBackend.expectGET(/api\/articles\/([0-9a-fA-F]{24})$/).respond(mockArticle); + + // Run controller functionality + scope.findOne(); + $httpBackend.flush(); + + // Test scope value + expect(scope.article).toEqualData(mockArticle); + })); + + describe('$scope.craete()', function () { + var sampleArticlePostData; + + beforeEach(function () { + // Create a sample article object + sampleArticlePostData = new Articles({ + title: 'An Article about MEAN', + content: 'MEAN rocks!' + }); + + // Fixture mock form input values + scope.title = 'An Article about MEAN'; + scope.content = 'MEAN rocks!'; + + spyOn($location, 'path'); + }); + + it('should send a POST request with the form input values and then locate to new object URL', inject(function (Articles) { + // Set POST response + $httpBackend.expectPOST('api/articles', sampleArticlePostData).respond(mockArticle); + + // Run controller functionality + scope.create(); + $httpBackend.flush(); + + // Test form inputs are reset + expect(scope.title).toEqual(''); + expect(scope.content).toEqual(''); + + // Test URL redirection after the article was created + expect($location.path.calls.mostRecent().args[0]).toBe('articles/' + mockArticle._id); + })); + + it('should set scope.error if save error', function () { + var errorMessage = 'this is an error message'; + $httpBackend.expectPOST('api/articles', sampleArticlePostData).respond(400, { + message: errorMessage + }); + + scope.create(); + $httpBackend.flush(); + + expect(scope.error).toBe(errorMessage); + }); + }); + + describe('$scope.update()', function () { + beforeEach(function () { + // Mock article in scope + scope.article = mockArticle; + }); + + it('should update a valid article', inject(function (Articles) { + // Set PUT response + $httpBackend.expectPUT(/api\/articles\/([0-9a-fA-F]{24})$/).respond(); + + // Run controller functionality + scope.update(); + $httpBackend.flush(); + + // Test URL location to new object + expect($location.path()).toBe('/articles/' + mockArticle._id); + })); + + it('should set scope.error to error response message', inject(function (Articles) { + var errorMessage = 'error'; + $httpBackend.expectPUT(/api\/articles\/([0-9a-fA-F]{24})$/).respond(400, { + message: errorMessage + }); + + scope.update(); + $httpBackend.flush(); + + expect(scope.error).toBe(errorMessage); + })); + }); + + describe('$scope.remove(article)', function () { + beforeEach(function () { + // Create new articles array and include the article + scope.articles = [mockArticle, {}]; + + // Set expected DELETE response + $httpBackend.expectDELETE(/api\/articles\/([0-9a-fA-F]{24})$/).respond(204); + + // Run controller functionality + scope.remove(mockArticle); + }); + + it('should send a DELETE request with a valid articleId and remove the article from the scope', inject(function (Articles) { + expect(scope.articles.length).toBe(1); + })); + }); + + describe('scope.remove()', function () { + beforeEach(function () { + spyOn($location, 'path'); + scope.article = mockArticle; + + $httpBackend.expectDELETE(/api\/articles\/([0-9a-fA-F]{24})$/).respond(204); + + scope.remove(); + $httpBackend.flush(); + }); + + it('should redirect to articles', function () { + expect($location.path).toHaveBeenCalledWith('articles'); + }); + }); + }); +}()); diff --git a/modules/articles/tests/e2e/articles.e2e.tests.js b/modules/articles/tests/e2e/articles.e2e.tests.js new file mode 100644 index 0000000000..96b0791361 --- /dev/null +++ b/modules/articles/tests/e2e/articles.e2e.tests.js @@ -0,0 +1,10 @@ +'use strict'; + +describe('Articles E2E Tests:', function () { + describe('Test articles page', function () { + it('Should report missing credentials', function () { + browser.get('http://localhost:3000/articles'); + expect(element.all(by.repeater('article in articles')).count()).toEqual(0); + }); + }); +}); diff --git a/modules/articles/tests/server/article.server.model.tests.js b/modules/articles/tests/server/article.server.model.tests.js new file mode 100644 index 0000000000..0031f1191d --- /dev/null +++ b/modules/articles/tests/server/article.server.model.tests.js @@ -0,0 +1,64 @@ +'use strict'; + +/** + * Module dependencies. + */ +var should = require('should'), + mongoose = require('mongoose'), + User = mongoose.model('User'), + Article = mongoose.model('Article'); + +/** + * Globals + */ +var user, article; + +/** + * Unit tests + */ +describe('Article Model Unit Tests:', function () { + beforeEach(function (done) { + user = new User({ + firstName: 'Full', + lastName: 'Name', + displayName: 'Full Name', + email: 'test@test.com', + username: 'username', + password: 'password' + }); + + user.save(function () { + article = new Article({ + title: 'Article Title', + content: 'Article Content', + user: user + }); + + done(); + }); + }); + + describe('Method Save', function () { + it('should be able to save without problems', function (done) { + return article.save(function (err) { + should.not.exist(err); + done(); + }); + }); + + it('should be able to show an error when try to save without title', function (done) { + article.title = ''; + + return article.save(function (err) { + should.exist(err); + done(); + }); + }); + }); + + afterEach(function (done) { + Article.remove().exec(function () { + User.remove().exec(done); + }); + }); +}); diff --git a/modules/articles/tests/server/article.server.routes.tests.js b/modules/articles/tests/server/article.server.routes.tests.js new file mode 100644 index 0000000000..ef0f1ac781 --- /dev/null +++ b/modules/articles/tests/server/article.server.routes.tests.js @@ -0,0 +1,320 @@ +'use strict'; + +var should = require('should'), + request = require('supertest'), + path = require('path'), + mongoose = require('mongoose'), + User = mongoose.model('User'), + Article = mongoose.model('Article'), + express = require(path.resolve('./config/lib/express')); + +/** + * Globals + */ +var app, agent, credentials, user, article; + +/** + * Article routes tests + */ +describe('Article CRUD tests', function () { + before(function (done) { + // Get application + app = express.init(mongoose); + agent = request.agent(app); + + done(); + }); + + beforeEach(function (done) { + // Create user credentials + credentials = { + username: 'username', + password: 'password' + }; + + // Create a new user + user = new User({ + firstName: 'Full', + lastName: 'Name', + displayName: 'Full Name', + email: 'test@test.com', + username: credentials.username, + password: credentials.password, + provider: 'local' + }); + + // Save a user to the test db and create new article + user.save(function () { + article = { + title: 'Article Title', + content: 'Article Content' + }; + + done(); + }); + }); + + it('should be able to save an article if logged in', function (done) { + agent.post('/api/auth/signin') + .send(credentials) + .expect(200) + .end(function (signinErr, signinRes) { + // Handle signin error + if (signinErr) { + return done(signinErr); + } + + // Get the userId + var userId = user.id; + + // Save a new article + agent.post('/api/articles') + .send(article) + .expect(200) + .end(function (articleSaveErr, articleSaveRes) { + // Handle article save error + if (articleSaveErr) { + return done(articleSaveErr); + } + + // Get a list of articles + agent.get('/api/articles') + .end(function (articlesGetErr, articlesGetRes) { + // Handle article save error + if (articlesGetErr) { + return done(articlesGetErr); + } + + // Get articles list + var articles = articlesGetRes.body; + + // Set assertions + (articles[0].user._id).should.equal(userId); + (articles[0].title).should.match('Article Title'); + + // Call the assertion callback + done(); + }); + }); + }); + }); + + it('should not be able to save an article if not logged in', function (done) { + agent.post('/api/articles') + .send(article) + .expect(403) + .end(function (articleSaveErr, articleSaveRes) { + // Call the assertion callback + done(articleSaveErr); + }); + }); + + it('should not be able to save an article if no title is provided', function (done) { + // Invalidate title field + article.title = ''; + + agent.post('/api/auth/signin') + .send(credentials) + .expect(200) + .end(function (signinErr, signinRes) { + // Handle signin error + if (signinErr) { + return done(signinErr); + } + + // Get the userId + var userId = user.id; + + // Save a new article + agent.post('/api/articles') + .send(article) + .expect(400) + .end(function (articleSaveErr, articleSaveRes) { + // Set message assertion + (articleSaveRes.body.message).should.match('Title cannot be blank'); + + // Handle article save error + done(articleSaveErr); + }); + }); + }); + + it('should be able to update an article if signed in', function (done) { + agent.post('/api/auth/signin') + .send(credentials) + .expect(200) + .end(function (signinErr, signinRes) { + // Handle signin error + if (signinErr) { + return done(signinErr); + } + + // Get the userId + var userId = user.id; + + // Save a new article + agent.post('/api/articles') + .send(article) + .expect(200) + .end(function (articleSaveErr, articleSaveRes) { + // Handle article save error + if (articleSaveErr) { + return done(articleSaveErr); + } + + // Update article title + article.title = 'WHY YOU GOTTA BE SO MEAN?'; + + // Update an existing article + agent.put('/api/articles/' + articleSaveRes.body._id) + .send(article) + .expect(200) + .end(function (articleUpdateErr, articleUpdateRes) { + // Handle article update error + if (articleUpdateErr) { + return done(articleUpdateErr); + } + + // Set assertions + (articleUpdateRes.body._id).should.equal(articleSaveRes.body._id); + (articleUpdateRes.body.title).should.match('WHY YOU GOTTA BE SO MEAN?'); + + // Call the assertion callback + done(); + }); + }); + }); + }); + + it('should be able to get a list of articles if not signed in', function (done) { + // Create new article model instance + var articleObj = new Article(article); + + // Save the article + articleObj.save(function () { + // Request articles + request(app).get('/api/articles') + .end(function (req, res) { + // Set assertion + res.body.should.be.instanceof(Array).and.have.lengthOf(1); + + // Call the assertion callback + done(); + }); + + }); + }); + + it('should be able to get a single article if not signed in', function (done) { + // Create new article model instance + var articleObj = new Article(article); + + // Save the article + articleObj.save(function () { + request(app).get('/api/articles/' + articleObj._id) + .end(function (req, res) { + // Set assertion + res.body.should.be.instanceof(Object).and.have.property('title', article.title); + + // Call the assertion callback + done(); + }); + }); + }); + + it('should return proper error for single article with an invalid Id, if not signed in', function (done) { + // test is not a valid mongoose Id + request(app).get('/api/articles/test') + .end(function (req, res) { + // Set assertion + res.body.should.be.instanceof(Object).and.have.property('message', 'Article is invalid'); + + // Call the assertion callback + done(); + }); + }); + + it('should return proper error for single article which doesnt exist, if not signed in', function (done) { + // This is a valid mongoose Id but a non-existent article + request(app).get('/api/articles/559e9cd815f80b4c256a8f41') + .end(function (req, res) { + // Set assertion + res.body.should.be.instanceof(Object).and.have.property('message', 'No article with that identifier has been found'); + + // Call the assertion callback + done(); + }); + }); + + it('should be able to delete an article if signed in', function (done) { + agent.post('/api/auth/signin') + .send(credentials) + .expect(200) + .end(function (signinErr, signinRes) { + // Handle signin error + if (signinErr) { + return done(signinErr); + } + + // Get the userId + var userId = user.id; + + // Save a new article + agent.post('/api/articles') + .send(article) + .expect(200) + .end(function (articleSaveErr, articleSaveRes) { + // Handle article save error + if (articleSaveErr) { + return done(articleSaveErr); + } + + // Delete an existing article + agent.delete('/api/articles/' + articleSaveRes.body._id) + .send(article) + .expect(200) + .end(function (articleDeleteErr, articleDeleteRes) { + // Handle article error error + if (articleDeleteErr) { + return done(articleDeleteErr); + } + + // Set assertions + (articleDeleteRes.body._id).should.equal(articleSaveRes.body._id); + + // Call the assertion callback + done(); + }); + }); + }); + }); + + it('should not be able to delete an article if not signed in', function (done) { + // Set article user + article.user = user; + + // Create new article model instance + var articleObj = new Article(article); + + // Save the article + articleObj.save(function () { + // Try deleting article + request(app).delete('/api/articles/' + articleObj._id) + .expect(403) + .end(function (articleDeleteErr, articleDeleteRes) { + // Set message assertion + (articleDeleteRes.body.message).should.match('User is not authorized'); + + // Handle article error error + done(articleDeleteErr); + }); + + }); + }); + + afterEach(function (done) { + User.remove().exec(function () { + Article.remove().exec(done); + }); + }); +}); diff --git a/modules/chat/client/chat.client.module.js b/modules/chat/client/chat.client.module.js new file mode 100644 index 0000000000..80ef9c2941 --- /dev/null +++ b/modules/chat/client/chat.client.module.js @@ -0,0 +1,4 @@ +'use strict'; + +// Use Applicaion configuration module to register a new module +ApplicationConfiguration.registerModule('chat'); diff --git a/modules/chat/client/config/chat.client.config.js b/modules/chat/client/config/chat.client.config.js new file mode 100644 index 0000000000..381ef5d6e3 --- /dev/null +++ b/modules/chat/client/config/chat.client.config.js @@ -0,0 +1,12 @@ +'use strict'; + +// Configuring the Chat module +angular.module('chat').run(['Menus', + function (Menus) { + // Set top bar menu items + Menus.addMenuItem('topbar', { + title: 'Chat', + state: 'chat' + }); + } +]); diff --git a/modules/chat/client/config/chat.client.routes.js b/modules/chat/client/config/chat.client.routes.js new file mode 100644 index 0000000000..d289e22d01 --- /dev/null +++ b/modules/chat/client/config/chat.client.routes.js @@ -0,0 +1,15 @@ +'use strict'; + +// Configure the 'chat' module routes +angular.module('chat').config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('chat', { + url: '/chat', + templateUrl: 'modules/chat/views/chat.client.view.html', + data: { + roles: ['user', 'admin'] + } + }); + } +]); diff --git a/modules/chat/client/controllers/chat.client.controller.js b/modules/chat/client/controllers/chat.client.controller.js new file mode 100644 index 0000000000..edb9b2727f --- /dev/null +++ b/modules/chat/client/controllers/chat.client.controller.js @@ -0,0 +1,43 @@ +'use strict'; + +// Create the 'chat' controller +angular.module('chat').controller('ChatController', ['$scope', '$location', 'Authentication', 'Socket', + function ($scope, $location, Authentication, Socket) { + // Create a messages array + $scope.messages = []; + + // If user is not signed in then redirect back home + if (!Authentication.user) { + $location.path('/'); + } + + // Make sure the Socket is connected + if (!Socket.socket) { + Socket.connect(); + } + + // Add an event listener to the 'chatMessage' event + Socket.on('chatMessage', function (message) { + $scope.messages.unshift(message); + }); + + // Create a controller method for sending messages + $scope.sendMessage = function () { + // Create a new message object + var message = { + text: this.messageText + }; + + // Emit a 'chatMessage' message event + Socket.emit('chatMessage', message); + + // Clear the message text + this.messageText = ''; + }; + + // Remove the event listener when the controller instance is destroyed + $scope.$on('$destroy', function () { + Socket.removeListener('chatMessage'); + }); + } +]); diff --git a/modules/chat/client/css/chat.css b/modules/chat/client/css/chat.css new file mode 100644 index 0000000000..a64df1fa64 --- /dev/null +++ b/modules/chat/client/css/chat.css @@ -0,0 +1,15 @@ +.chat-message { + margin-top: 10px; + padding-top: 10px; +} +.chat-message:not(:first-child) { + border-top: 1px solid #e7e7e7; +} +.chat-message-details { + margin-left: 10px; +} +.chat-profile-image { + height: 28px; + width: 28px; + border-radius: 50%; +} diff --git a/modules/chat/client/views/chat.client.view.html b/modules/chat/client/views/chat.client.view.html new file mode 100644 index 0000000000..7f072c9f22 --- /dev/null +++ b/modules/chat/client/views/chat.client.view.html @@ -0,0 +1,29 @@ + +
+ + +
+
+
+ + + + +
+
+
+
    + +
  • + + {{message.username}} +
    + +
    + +
    +
  • +
+
diff --git a/modules/chat/server/sockets/chat.server.socket.config.js b/modules/chat/server/sockets/chat.server.socket.config.js new file mode 100644 index 0000000000..8bb88db80f --- /dev/null +++ b/modules/chat/server/sockets/chat.server.socket.config.js @@ -0,0 +1,34 @@ +'use strict'; + +// Create the chat configuration +module.exports = function (io, socket) { + // Emit the status event when a new socket client is connected + io.emit('chatMessage', { + type: 'status', + text: 'Is now connected', + created: Date.now(), + profileImageURL: socket.request.user.profileImageURL, + username: socket.request.user.username + }); + + // Send a chat messages to all connected sockets when a message is received + socket.on('chatMessage', function (message) { + message.type = 'message'; + message.created = Date.now(); + message.profileImageURL = socket.request.user.profileImageURL; + message.username = socket.request.user.username; + + // Emit the 'chatMessage' event + io.emit('chatMessage', message); + }); + + // Emit the status event when a socket client is disconnected + socket.on('disconnect', function () { + io.emit('chatMessage', { + type: 'status', + text: 'disconnected', + created: Date.now(), + username: socket.request.user.username + }); + }); +}; diff --git a/modules/chat/tests/client/chat.client.controller.tests.js b/modules/chat/tests/client/chat.client.controller.tests.js new file mode 100644 index 0000000000..23c3e31f6d --- /dev/null +++ b/modules/chat/tests/client/chat.client.controller.tests.js @@ -0,0 +1,94 @@ +'use strict'; + +/** + * Chat client controller tests + */ +(function () { + describe('ChatController', function () { + //Initialize global variables + var scope, + Socket, + ChatController, + $timeout, + $location, + Authentication; + + // Load the main application module + beforeEach(module(ApplicationConfiguration.applicationModuleName)); + + beforeEach(inject(function ($controller, $rootScope, _Socket_, _Authentication_, _$timeout_, _$location_) { + scope = $rootScope.$new(); + Socket = _Socket_; + $timeout = _$timeout_; + $location = _$location_; + Authentication = _Authentication_; + })); + + describe('when user logged out', function () { + beforeEach(inject(function ($controller, $rootScope, _Socket_, _Authentication_, _$timeout_, _$location_) { + Authentication.user = undefined; + spyOn($location, 'path'); + ChatController = $controller('ChatController', { + $scope: scope, + }); + })); + + it('should redirect logged out user to /', function () { + expect($location.path).toHaveBeenCalledWith('/'); + }); + }); + + describe('when user logged in', function () { + beforeEach(inject(function ($controller, $rootScope, _Socket_, _Authentication_, _$timeout_, _$location_) { + Authentication.user = { + name: 'user', + roles: ['user'] + }; + + ChatController = $controller('ChatController', { + $scope: scope, + }); + })); + + it('should make sure socket is connected', function () { + expect(Socket.socket).toBeTruthy(); + }); + + it('should define messages array', function () { + expect(scope.messages).toBeDefined(); + expect(scope.messages.length).toBe(0); + }); + + describe('sendMessage', function () { + var text = 'hello world!'; + beforeEach(function () { + scope.messageText = text; + scope.sendMessage(); + $timeout.flush(); + }); + + it('should add message to messages', function () { + expect(scope.messages.length).toBe(1); + }); + + it('should add message with proper text attribute set', function () { + expect(scope.messages[0].text).toBe(text); + }); + + it('should clear messageText', function () { + expect(scope.messageText).toBe(''); + }); + }); + + describe('$destroy()', function () { + beforeEach(function () { + scope.$destroy(); + }); + + it('should remove chatMessage listener', function () { + expect(Socket.socket.cbs.chatMessage).toBeUndefined(); + }); + }); + }); + }); +}()); diff --git a/modules/chat/tests/e2e/chat.e2e.tests.js b/modules/chat/tests/e2e/chat.e2e.tests.js new file mode 100644 index 0000000000..3b117ac146 --- /dev/null +++ b/modules/chat/tests/e2e/chat.e2e.tests.js @@ -0,0 +1,8 @@ +'use strict'; + +/** + * Chat e2e tests + */ +describe('Chat E2E Tests:', function () { + // TODO: Add chat e2e tests +}); diff --git a/modules/chat/tests/server/chat.socket.tests.js b/modules/chat/tests/server/chat.socket.tests.js new file mode 100644 index 0000000000..21f94e4056 --- /dev/null +++ b/modules/chat/tests/server/chat.socket.tests.js @@ -0,0 +1,8 @@ +'use strict'; + +/** + * Chat socket tests + */ +describe('Chat Socket Tests:', function () { + // TODO: Add chat socket tests +}); diff --git a/modules/core/client/app/config.js b/modules/core/client/app/config.js new file mode 100644 index 0000000000..98b48bffff --- /dev/null +++ b/modules/core/client/app/config.js @@ -0,0 +1,23 @@ +'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', 'angularFileUpload']; + + // Add a new vertical module + var registerModule = function (moduleName, dependencies) { + // Create angular module + angular.module(moduleName, dependencies || []); + + // Add the module to the AngularJS configuration file + angular.module(applicationModuleName).requires.push(moduleName); + }; + + return { + applicationModuleName: applicationModuleName, + applicationModuleVendorDependencies: applicationModuleVendorDependencies, + registerModule: registerModule + }; +})(); diff --git a/modules/core/client/app/init.js b/modules/core/client/app/init.js new file mode 100644 index 0000000000..33470bae9a --- /dev/null +++ b/modules/core/client/app/init.js @@ -0,0 +1,55 @@ +'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.html5Mode(true).hashPrefix('!'); + } +]); + +angular.module(ApplicationConfiguration.applicationModuleName).run(function ($rootScope, $state, Authentication) { + // Check authentication before changing state + $rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) { + if (toState.data && toState.data.roles && toState.data.roles.length > 0) { + var allowed = false; + toState.data.roles.forEach(function (role) { + if (Authentication.user.roles !== undefined && Authentication.user.roles.indexOf(role) !== -1) { + allowed = true; + return true; + } + }); + + if (!allowed) { + event.preventDefault(); + $state.go('authentication.signin', {}, { + notify: false + }).then(function () { + $rootScope.$broadcast('$stateChangeSuccess', 'authentication.signin', {}, toState, toParams); + }); + } + } + }); + + // Record previous state + $rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) { + $state.previous = { + state: fromState, + params: fromParams, + href: $state.href(fromState, fromParams) + }; + }); +}); + +//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]); +}); diff --git a/modules/core/client/config/core-admin.client.menus.js b/modules/core/client/config/core-admin.client.menus.js new file mode 100644 index 0000000000..b048f6f020 --- /dev/null +++ b/modules/core/client/config/core-admin.client.menus.js @@ -0,0 +1,12 @@ +'use strict'; + +angular.module('core.admin').run(['Menus', + function (Menus) { + Menus.addMenuItem('topbar', { + title: 'Admin', + state: 'admin', + type: 'dropdown', + roles: ['admin'] + }); + } +]); diff --git a/modules/core/client/config/core-admin.client.routes.js b/modules/core/client/config/core-admin.client.routes.js new file mode 100644 index 0000000000..b7f5f45a6d --- /dev/null +++ b/modules/core/client/config/core-admin.client.routes.js @@ -0,0 +1,16 @@ +'use strict'; + +// Setting up route +angular.module('core.admin.routes').config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('admin', { + abstract: true, + url: '/admin', + template: '', + data: { + roles: ['admin'] + } + }); + } +]); diff --git a/modules/core/client/config/core.client.routes.js b/modules/core/client/config/core.client.routes.js new file mode 100644 index 0000000000..e9e64c82cf --- /dev/null +++ b/modules/core/client/config/core.client.routes.js @@ -0,0 +1,21 @@ +'use strict'; + +// Setting up route +angular.module('core').config(['$stateProvider', '$urlRouterProvider', + function ($stateProvider, $urlRouterProvider) { + + // Redirect to 404 when route not found + $urlRouterProvider.otherwise('not-found'); + + // Home state routing + $stateProvider + .state('home', { + url: '/', + templateUrl: 'modules/core/views/home.client.view.html' + }) + .state('not-found', { + url: '/not-found', + templateUrl: 'modules/core/views/404.client.view.html' + }); + } +]); diff --git a/modules/core/client/controllers/header.client.controller.js b/modules/core/client/controllers/header.client.controller.js new file mode 100644 index 0000000000..e688ad3c96 --- /dev/null +++ b/modules/core/client/controllers/header.client.controller.js @@ -0,0 +1,23 @@ +'use strict'; + +angular.module('core').controller('HeaderController', ['$scope', '$state', 'Authentication', 'Menus', + function ($scope, $state, Authentication, Menus) { + // Expose view variables + $scope.$state = $state; + $scope.authentication = Authentication; + + // Get the topbar menu + $scope.menu = Menus.getMenu('topbar'); + + // Toggle the menu items + $scope.isCollapsed = false; + $scope.toggleCollapsibleMenu = function () { + $scope.isCollapsed = !$scope.isCollapsed; + }; + + // Collapsing the menu after navigation + $scope.$on('$stateChangeSuccess', function () { + $scope.isCollapsed = false; + }); + } +]); diff --git a/modules/core/client/controllers/home.client.controller.js b/modules/core/client/controllers/home.client.controller.js new file mode 100644 index 0000000000..a76343c7f1 --- /dev/null +++ b/modules/core/client/controllers/home.client.controller.js @@ -0,0 +1,8 @@ +'use strict'; + +angular.module('core').controller('HomeController', ['$scope', 'Authentication', + function ($scope, Authentication) { + // This provides Authentication context. + $scope.authentication = Authentication; + } +]); diff --git a/modules/core/client/core.client.module.js b/modules/core/client/core.client.module.js new file mode 100644 index 0000000000..5850d6aec7 --- /dev/null +++ b/modules/core/client/core.client.module.js @@ -0,0 +1,6 @@ +'use strict'; + +// Use Applicaion configuration module to register a new module +ApplicationConfiguration.registerModule('core'); +ApplicationConfiguration.registerModule('core.admin', ['core']); +ApplicationConfiguration.registerModule('core.admin.routes', ['ui.router']); diff --git a/modules/core/client/css/core.css b/modules/core/client/css/core.css new file mode 100644 index 0000000000..861e085d37 --- /dev/null +++ b/modules/core/client/css/core.css @@ -0,0 +1,35 @@ +.content { + margin-top: 50px; +} +.undecorated-link:hover { + text-decoration: none; +} +[ng\:cloak], +[ng-cloak], +[data-ng-cloak], +[x-ng-cloak], +.ng-cloak, +.x-ng-cloak { + display: none !important; +} +.ng-invalid.ng-dirty { + border-color: #FA787E; +} +.ng-valid.ng-dirty { + border-color: #78FA89; +} +.header-profile-image { + opacity: 0.8; + height: 28px; + width: 28px; + border-radius: 50%; + margin-right: 5px; +} +.open .header-profile-image, +a:hover .header-profile-image { + opacity: 1; +} +.user-header-dropdown-toggle { + padding-top: 11px !important; + padding-bottom: 11px !important; +} diff --git a/public/modules/core/img/brand/favicon.ico b/modules/core/client/img/brand/favicon.ico similarity index 100% rename from public/modules/core/img/brand/favicon.ico rename to modules/core/client/img/brand/favicon.ico diff --git a/public/modules/core/img/brand/logo.png b/modules/core/client/img/brand/logo.png similarity index 100% rename from public/modules/core/img/brand/logo.png rename to modules/core/client/img/brand/logo.png diff --git a/public/modules/core/img/loaders/loader.gif b/modules/core/client/img/loaders/loader.gif similarity index 100% rename from public/modules/core/img/loaders/loader.gif rename to modules/core/client/img/loaders/loader.gif diff --git a/modules/core/client/services/menus.client.service.js b/modules/core/client/services/menus.client.service.js new file mode 100644 index 0000000000..df9b5eafe2 --- /dev/null +++ b/modules/core/client/services/menus.client.service.js @@ -0,0 +1,178 @@ +'use strict'; + +//Menu service used for managing menus +angular.module('core').service('Menus', [ + function () { + // Define a set of default roles + this.defaultRoles = ['*']; + + // Define the menus object + this.menus = {}; + + // A private function for rendering decision + var shouldRender = function (user) { + if (user) { + if (!!~this.roles.indexOf('*')) { + return true; + } else { + 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 exist'); + } + } 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, options) { + options = options || {}; + + // Create the new menu + this.menus[menuId] = { + isPublic: ((options.isPublic === null || typeof options.isPublic === 'undefined') ? true : options.isPublic), + roles: options.roles || this.defaultRoles, + items: options.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, options) { + options = options || {}; + + // Validate that the menu exists + this.validateMenuExistance(menuId); + + // Push new menu item + this.menus[menuId].items.push({ + title: options.title || '', + state: options.state || '', + type: options.type || 'item', + class: options.class, + isPublic: ((options.isPublic === null || typeof options.isPublic === 'undefined') ? this.menus[menuId].isPublic : options.isPublic), + roles: ((options.roles === null || typeof options.roles === 'undefined') ? this.menus[menuId].roles : options.roles), + position: options.position || 0, + items: [], + shouldRender: shouldRender + }); + + // Add submenu items + if (options.items) { + for (var i in options.items) { + this.addSubMenuItem(menuId, options.link, options.items[i]); + } + } + + // Return the menu object + return this.menus[menuId]; + }; + + // Add submenu item object + this.addSubMenuItem = function (menuId, parentItemState, options) { + options = options || {}; + + // 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].state === parentItemState) { + // Push new submenu item + this.menus[menuId].items[itemIndex].items.push({ + title: options.title || '', + state: options.state || '', + isPublic: ((options.isPublic === null || typeof options.isPublic === 'undefined') ? this.menus[menuId].items[itemIndex].isPublic : options.isPublic), + roles: ((options.roles === null || typeof options.roles === 'undefined') ? this.menus[menuId].items[itemIndex].roles : options.roles), + position: options.position || 0, + 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', { + isPublic: false + }); + } +]); diff --git a/modules/core/client/services/socket.io.client.service.js b/modules/core/client/services/socket.io.client.service.js new file mode 100644 index 0000000000..7031a0d2de --- /dev/null +++ b/modules/core/client/services/socket.io.client.service.js @@ -0,0 +1,40 @@ +'use strict'; + +// Create the Socket.io wrapper service +angular.module('core').service('Socket', ['Authentication', '$state', '$timeout', + function (Authentication, $state, $timeout) { + // Connect to Socket.io server + this.connect = function () { + // Connect only when authenticated + if (Authentication.user) { + this.socket = io(); + } + }; + this.connect(); + + // Wrap the Socket.io 'on' method + this.on = function (eventName, callback) { + if (this.socket) { + this.socket.on(eventName, function (data) { + $timeout(function () { + callback(data); + }); + }); + } + }; + + // Wrap the Socket.io 'emit' method + this.emit = function (eventName, data) { + if (this.socket) { + this.socket.emit(eventName, data); + } + }; + + // Wrap the Socket.io 'removeListener' method + this.removeListener = function (eventName) { + if (this.socket) { + this.socket.removeListener(eventName); + } + }; + } +]); diff --git a/modules/core/client/views/404.client.view.html b/modules/core/client/views/404.client.view.html new file mode 100644 index 0000000000..e8e6d82cc1 --- /dev/null +++ b/modules/core/client/views/404.client.view.html @@ -0,0 +1,5 @@ +

Page Not Found

+ diff --git a/modules/core/client/views/header.client.view.html b/modules/core/client/views/header.client.view.html new file mode 100644 index 0000000000..94d2dd9aee --- /dev/null +++ b/modules/core/client/views/header.client.view.html @@ -0,0 +1,59 @@ +
+ + +
diff --git a/modules/core/client/views/home.client.view.html b/modules/core/client/views/home.client.view.html new file mode 100644 index 0000000000..b34905cdfc --- /dev/null +++ b/modules/core/client/views/home.client.view.html @@ -0,0 +1,84 @@ +
+
+
+
+ MEAN.JS +
+
+
+
+

+ Open-Source Full-Stack Solution For MEAN Applications +

+
+
+

+ Learn more +

+
+
+
+

Congrats! You've configured and run the sample application.

+

MEAN.JS is a web application boilerplate, which means you should start changing everything :-)

+

This sample application tracks users and articles.

+
    +
  • + Click + Signup to get started. +
  • +
  • + Configure your app to work with your social accounts, by editing the + /config/env/*.js files. +
  • +
  • + Edit your users module. +
  • +
  • + Add new CRUD modules. +
  • +
  • + Have fun... +
  • +
+
+
+
+

MongoDB

+

MongoDB is a database. MongoDB's great manual is the place to get started with NoSQL and MongoDB.

+
+
+

Express

+

Express is an app server. Check out The ExpressJS API reference for more information or StackOverflow for more info.

+
+
+

AngularJS

+

AngularJS is web app framework. Angular's website offers a lot. The Thinkster Popular Guide and Egghead Videos are great resources.

+
+
+

Node.js

+

Node.js is a web server. Node's website and this stackOverflow thread offer excellent starting points to get to grasps with node.

+
+
+
+

MEAN.JS Documentation

+

+ Once you're familiar with the foundation technology, check out the MEAN.JS Documentation: +

+

+
+
Enjoy & Keep Us Updated, +
The MEAN.JS Team. +
diff --git a/modules/core/server/controllers/core.server.controller.js b/modules/core/server/controllers/core.server.controller.js new file mode 100644 index 0000000000..0f216b8063 --- /dev/null +++ b/modules/core/server/controllers/core.server.controller.js @@ -0,0 +1,42 @@ +'use strict'; + +/** + * Render the main application page + */ +exports.renderIndex = function (req, res) { + res.render('modules/core/server/views/index', { + user: req.user || null + }); +}; + +/** + * Render the server error page + */ +exports.renderServerError = function (req, res) { + res.status(500).render('modules/core/server/views/500', { + error: 'Oops! Something went wrong...' + }); +}; + +/** + * Render the server not found responses + * Performs content-negotiation on the Accept HTTP header + */ +exports.renderNotFound = function (req, res) { + + res.status(404).format({ + 'text/html': function () { + res.render('modules/core/server/views/404', { + url: req.originalUrl + }); + }, + 'application/json': function () { + res.json({ + error: 'Path not found' + }); + }, + 'default': function () { + res.send('Path not found'); + } + }); +}; diff --git a/modules/core/server/controllers/errors.server.controller.js b/modules/core/server/controllers/errors.server.controller.js new file mode 100644 index 0000000000..f0b96bdfb7 --- /dev/null +++ b/modules/core/server/controllers/errors.server.controller.js @@ -0,0 +1,44 @@ +'use strict'; + +/** + * Get unique error field name + */ +var getUniqueErrorMessage = function (err) { + var output; + + try { + var fieldName = err.errmsg.substring(err.errmsg.lastIndexOf('.$') + 2, err.errmsg.lastIndexOf('_1')); + output = fieldName.charAt(0).toUpperCase() + fieldName.slice(1) + ' already exists'; + + } catch (ex) { + output = 'Unique field already exists'; + } + + 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; +}; diff --git a/modules/core/server/routes/core.server.routes.js b/modules/core/server/routes/core.server.routes.js new file mode 100644 index 0000000000..d4e546131e --- /dev/null +++ b/modules/core/server/routes/core.server.routes.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = function (app) { + // Root routing + var core = require('../controllers/core.server.controller'); + + // Define error pages + app.route('/server-error').get(core.renderServerError); + + // Return a 404 for all undefined api, module or lib routes + app.route('/:url(api|modules|lib)/*').get(core.renderNotFound); + + // Define application route + app.route('/*').get(core.renderIndex); +}; diff --git a/modules/core/server/views/404.server.view.html b/modules/core/server/views/404.server.view.html new file mode 100644 index 0000000000..d5c785f63a --- /dev/null +++ b/modules/core/server/views/404.server.view.html @@ -0,0 +1,10 @@ +{% extends 'layout.server.view.html' %} + +{% block content %} +

Page Not Found

+ +{% endblock %} diff --git a/app/views/500.server.view.html b/modules/core/server/views/500.server.view.html similarity index 90% rename from app/views/500.server.view.html rename to modules/core/server/views/500.server.view.html index cc3b14785c..34359bfae5 100644 --- a/app/views/500.server.view.html +++ b/modules/core/server/views/500.server.view.html @@ -3,6 +3,6 @@ {% block content %}

Server Error

-	{{error}}
+  {{error}}
 
{% endblock %} diff --git a/app/views/index.server.view.html b/modules/core/server/views/index.server.view.html similarity index 68% rename from app/views/index.server.view.html rename to modules/core/server/views/index.server.view.html index 7e60893b1f..8a94dc81b1 100644 --- a/app/views/index.server.view.html +++ b/modules/core/server/views/index.server.view.html @@ -1,5 +1,5 @@ {% extends 'layout.server.view.html' %} {% block content %} -
+
{% endblock %} diff --git a/modules/core/server/views/layout.server.view.html b/modules/core/server/views/layout.server.view.html new file mode 100644 index 0000000000..7f8c83934f --- /dev/null +++ b/modules/core/server/views/layout.server.view.html @@ -0,0 +1,63 @@ + + + + + + + + {{title}} + + + + + + + + + + + + + + + + + + + + + + + + + + + {% for cssFile in cssFiles %}{% endfor %} + + + + +
+
+ {% block content %}{% endblock %} +
+
+ + + + + + + + + {% for jsFile in jsFiles %}{% endfor %} + + {% if livereload %} + + + {% endif %} + + + diff --git a/modules/core/tests/client/core.client.tests.js b/modules/core/tests/client/core.client.tests.js new file mode 100644 index 0000000000..655f5c68a1 --- /dev/null +++ b/modules/core/tests/client/core.client.tests.js @@ -0,0 +1,7 @@ +'use strict'; + +(function () { + beforeAll(function () { + angular.element(document.querySelector('head')).append(''); + }); +}()); diff --git a/modules/core/tests/client/header.client.controller.tests.js b/modules/core/tests/client/header.client.controller.tests.js new file mode 100644 index 0000000000..569e3c6e82 --- /dev/null +++ b/modules/core/tests/client/header.client.controller.tests.js @@ -0,0 +1,64 @@ +'use strict'; + +(function () { + describe('HeaderController', function () { + //Initialize global variables + var scope, + HeaderController, + $state, + Authentication; + + // Load the main application module + beforeEach(module(ApplicationConfiguration.applicationModuleName)); + + beforeEach(inject(function ($controller, $rootScope, _$state_, _Authentication_) { + scope = $rootScope.$new(); + $state = _$state_; + Authentication = _Authentication_; + + HeaderController = $controller('HeaderController', { + $scope: scope + }); + })); + + it('should expose the authentication service', function () { + expect(scope.authentication).toBe(Authentication); + }); + + it('should expose the $state service', function () { + expect(scope.$state).toBe($state); + }); + + it('should default menu to collapsed', function () { + expect(scope.isCollapsed).toBeFalsy(); + }); + + describe('when toggleCollapsibleMenu', function () { + var defaultCollapse; + beforeEach(function () { + defaultCollapse = scope.isCollapsed; + scope.toggleCollapsibleMenu(); + }); + + it('should toggle isCollapsed to non default value', function () { + expect(scope.isCollapsed).not.toBe(defaultCollapse); + }); + + it('should then toggle isCollapsed back to default value', function () { + scope.toggleCollapsibleMenu(); + expect(scope.isCollapsed).toBe(defaultCollapse); + }); + }); + + describe('when view state changes', function () { + beforeEach(function () { + scope.isCollapsed = true; + scope.$broadcast('$stateChangeSuccess'); + }); + + it('should set isCollapsed to false', function () { + expect(scope.isCollapsed).toBeFalsy(); + }); + }); + }); +})(); diff --git a/modules/core/tests/client/home.client.controller.tests.js b/modules/core/tests/client/home.client.controller.tests.js new file mode 100644 index 0000000000..b6db0cb919 --- /dev/null +++ b/modules/core/tests/client/home.client.controller.tests.js @@ -0,0 +1,24 @@ +'use strict'; + +(function () { + describe('HomeController', function () { + //Initialize global variables + var scope, + HomeController; + + // Load the main application module + beforeEach(module(ApplicationConfiguration.applicationModuleName)); + + beforeEach(inject(function ($controller, $rootScope) { + scope = $rootScope.$new(); + + HomeController = $controller('HomeController', { + $scope: scope + }); + })); + + it('should expose the authentication service', function () { + expect(scope.authentication).toBeTruthy(); + }); + }); +})(); diff --git a/modules/core/tests/client/menus.client.service.tests.js b/modules/core/tests/client/menus.client.service.tests.js new file mode 100644 index 0000000000..e693eae448 --- /dev/null +++ b/modules/core/tests/client/menus.client.service.tests.js @@ -0,0 +1,489 @@ +'use strict'; + +(function() { + describe('Menus', function() { + //Initialize global variables + var scope, + Menus; + + // Load the main application module + beforeEach(module(ApplicationConfiguration.applicationModuleName)); + + beforeEach(inject(function(_Menus_) { + Menus = _Menus_; + })); + + it('should have topbar added', function() { + expect(Menus.menus.topbar).toBeDefined(); + }); + + it('should have private topbar', function() { + expect(Menus.menus.topbar.isPublic).toBeFalsy(); + }); + + it('should have default roles to *', function() { + expect(Menus.defaultRoles).toEqual(['*']); + }); + + describe('addMenu', function() { + describe('with no options', function() { + var menuId = 'menu1', + menu; + beforeEach(function() { + menu = Menus.addMenu(menuId); + }); + + it('should return menu object', function() { + expect(menu).toBeDefined(); + }); + + it('should default roles', function() { + expect(menu.roles).toEqual(Menus.defaultRoles); + }); + + it('should have empty items', function() { + expect(menu.items).toEqual([]); + }); + + it('should be public by default', function() { + expect(menu.isPublic).toBeTruthy(); + }); + + it('should set shouldRender to shouldRender function handle', function() { + expect(menu.shouldRender()).toBeTruthy(); + }); + }); + + describe('with options', function() { + var menu, + options = { + roles: ['a', 'b', 'c'], + items: ['d', 'e', 'f'] + }; + beforeEach(function() { + menu = Menus.addMenu('menu1', options); + }); + + it('should set isPublic to true if options.isPublic equal to null', function() { + var menu = Menus.addMenu('menu1', { + isPublic: null + }); + expect(menu.isPublic).toBeTruthy(); + }); + + it('should set isPublic to true if options.isPublic equal to undefined', function() { + expect(menu.isPublic).toBeTruthy(); + }); + + it('should set items to options.items list', function() { + expect(menu.items).toBe(options.items); + }); + + it('should set roles to options.roles list', function() { + expect(menu.roles).toBe(options.roles); + }); + }); + }); + + describe('shouldRender', function() { + var menuOptions = { + roles: ['*', 'menurole'] + }, + menu; + beforeEach(function() { + menu = Menus.addMenu('menu1', menuOptions); + }); + + describe('when logged out', function() { + it('should render if menu is public', function() { + expect(menu.shouldRender()).toBeTruthy(); + }); + + it('should not render if menu is private', function() { + menu = Menus.addMenu('menu1', { + isPublic: false + }); + expect(menu.shouldRender()).toBeFalsy(); + }); + }); + + describe('when logged in', function() { + var user = { + roles: ['1', 'menurole', '2'] + }; + describe('menu with * role', function() { + it('should render', function() { + expect(menu.shouldRender(user)).toBeTruthy(); + }); + }); + + describe('menu without * role', function() { + beforeEach(function() { + menu = Menus.addMenu('menu1', { + roles: ['b', 'menurole', 'c'] + }); + }); + + it('should render if user has same role as menu', function() { + expect(menu.shouldRender(user)).toBeTruthy(); + }); + + it('should not render if user has different roles', function() { + user = { + roles: ['1', '2', '3'] + }; + expect(menu.shouldRender(user)).toBeFalsy(); + }); + }); + }); + }); + + describe('validateMenuExistance', function() { + describe('when menuId not provided', function() { + it('should throw menuId error', function() { + expect(Menus.validateMenuExistance).toThrowError('MenuId was not provided'); + }); + }); + + describe('when menu does not exist', function() { + it('should throw no menu error', function() { + var target = function() { + Menus.validateMenuExistance('noMenuId'); + }; + expect(target).toThrowError('Menu does not exist'); + }); + }); + + describe('when menu exists', function() { + var menuId = 'menuId'; + beforeEach(function() { + Menus.menus[menuId] = {}; + }); + + it('should return truthy', function() { + expect(Menus.validateMenuExistance(menuId)).toBeTruthy(); + }); + }); + }); + + describe('removeMenu', function() { + var menu = { + id: 'menuId' + }; + beforeEach(function() { + Menus.menus[menu.id] = menu; + Menus.validateMenuExistance = jasmine.createSpy(); + Menus.removeMenu(menu.id); + }); + + it('should remove existing menu from menus', function() { + expect(Menus.menus).not.toContain(menu.id); + }); + + it('validates menu existance before removing', function() { + expect(Menus.validateMenuExistance).toHaveBeenCalledWith(menu.id); + }); + }); + + describe('addMenuItem', function() { + var menuId = 'menu1', + subMenuItem1 = { + title: 'sub1' + }, + subMenuItem2 = { + title: 'sub2' + }, + menuItemOptions = { + title: 'title', + state: 'state', + type: 'type', + class: 'class', + isPublic: false, + roles: ['a', 'b'], + link: 'link', + position: 2, + items: [subMenuItem1, subMenuItem2] + }, + menu, + menuItem; + + beforeEach(function() { + Menus.validateMenuExistance = jasmine.createSpy(); + Menus.addSubMenuItem = jasmine.createSpy(); + Menus.addMenu(menuId, { + roles: ['a', 'b'] + }); + menu = Menus.addMenuItem(menuId, menuItemOptions); + menuItem = menu.items[0]; + }); + + it('should validate menu existance', function() { + expect(Menus.validateMenuExistance).toHaveBeenCalledWith(menuId); + }); + + it('should return the menu', function() { + expect(menu).toBeDefined(); + }); + + it('should set menu item shouldRender function', function() { + expect(menuItem.shouldRender).toBeDefined(); + }); + + describe('with options set', function() { + it('should add menu item to menu', function() { + expect(menu.items.length).toBe(1); + }); + + it('should set menu item title to options title', function() { + expect(menuItem.title).toBe(menuItemOptions.title); + }); + + it('should set menu item state to options state', function() { + expect(menuItem.state).toBe(menuItemOptions.state); + }); + + it('should set menu item type to options type', function() { + expect(menuItem.type).toBe(menuItemOptions.type); + }); + + it('should set menu item class to options class', function() { + expect(menuItem.class).toBe(menuItemOptions.class); + }); + + it('should set menu item isPublic to options isPublic', function() { + expect(menuItem.isPublic).toBe(menuItemOptions.isPublic); + }); + + it('should set menu item position to options position', function() { + expect(menuItem.position).toBe(menuItemOptions.position); + }); + + it('should call addSubMenuItem for each item in options', function() { + expect(Menus.addSubMenuItem).toHaveBeenCalledWith(menuId, menuItemOptions.link, subMenuItem1); + expect(Menus.addSubMenuItem).toHaveBeenCalledWith(menuId, menuItemOptions.link, subMenuItem2); + }); + }); + + describe('without options set', function() { + beforeEach(function() { + menu = Menus.addMenuItem(menuId); + menuItem = menu.items[1]; + }); + + it('should set menu item type to item', function() { + expect(menuItem.type).toBe('item'); + }); + + it('should set menu item title to empty', function() { + expect(menuItem.title).toBe(''); + }); + + it('should set menu item isPublic to menu.isPublic', function() { + expect(menuItem.isPublic).toBe(menu.isPublic); + }); + + it('should set menu item roles to menu roles', function() { + expect(menuItem.roles).toEqual(menu.roles); + }); + + it('should set menu item position to 0', function() { + expect(menuItem.position).toBe(0); + }); + }); + }); + + describe('removeMenuItem', function() { + var menuId = 'menuId', + menuItemURL = 'url', + menuItem1 = { + link: menuItemURL + }, + menuItem2 = { + link: '' + }, + newMenu = { + items: [menuItem1, menuItem2] + }, + menu = null; + + beforeEach(function() { + Menus.menus.menuId = newMenu; + Menus.validateMenuExistance = jasmine.createSpy(); + menu = Menus.removeMenuItem(menuId, menuItemURL); + }); + + it('should return menu object', function() { + expect(menu).not.toBeNull(); + }); + + it('should validate menu existance', function() { + expect(Menus.validateMenuExistance).toHaveBeenCalledWith(menuId); + }); + + it('should remove sub menu items with same link', function() { + expect(menu.items.length).toBe(1); + expect(menu.items[0]).toBe(menuItem2); + }); + }); + + describe('addSubMenuItem', function() { + var subItemOptions = { + title: 'title', + state: 'state', + isPublic: false, + roles: ['a', 'b'], + position: 4 + }; + var menuId = 'menu1', + menuItem1 = { + state: 'state', + items: [], + isPublic: false + }, + menuItem2 = { + state: 'state2', + items: [], + isPublic: true, + roles: ['a'] + }, + menuItem3 = { + state: 'state3', + items: [] + }, + newMenu = { + items: [menuItem1, menuItem2, menuItem3] + }, + menu; + + beforeEach(function() { + Menus.validateMenuExistance = jasmine.createSpy(); + Menus.menus[menuId] = newMenu; + Menus.addSubMenuItem(menuId, menuItem1.state, subItemOptions); + menu = Menus.addSubMenuItem(menuId, menuItem2.state); + }); + + afterEach(function() { + menuItem1.items = []; + menuItem2.items = []; + }); + + it('should return menu object', function() { + expect(menu).toEqual(newMenu); + }); + + it('should validate menu existance', function() { + expect(Menus.validateMenuExistance).toHaveBeenCalledWith(menuId); + }); + + it('should not add sub menu item to menu item of different state', function() { + expect(menuItem3.items.length).toBe(0); + }); + + it('should set shouldRender', function() { + expect(menuItem1.items[0].shouldRender).toBeDefined(); + }); + + describe('with options set', function() { + var subMenuItem; + beforeEach(function() { + subMenuItem = menuItem1.items[0]; + }); + + it('should add sub menu item to menu item', function() { + expect(menuItem1.items.length).toBe(1); + }); + + it('should set isPublic to options isPublic', function() { + expect(subMenuItem.isPublic).toBe(subItemOptions.isPublic); + }); + + it('should set title to options title', function() { + expect(subMenuItem.title).toBe(subItemOptions.title); + }); + + it('should set state to options state', function() { + expect(subMenuItem.state).toBe(subItemOptions.state); + }); + + it('should set roles to options roles', function() { + expect(subMenuItem.roles).toEqual(subItemOptions.roles); + }); + + it('should set position to options position', function() { + expect(subMenuItem.position).toEqual(subItemOptions.position); + }); + }); + + describe('without optoins set', function() { + var subMenuItem; + beforeEach(function() { + subMenuItem = menuItem2.items[0]; + }); + + it('should add sub menu item to menu item', function() { + expect(menuItem2.items.length).toBe(1); + }); + + it('should set isPublic to parent isPublic', function() { + expect(subMenuItem.isPublic).toBe(menuItem2.isPublic); + }); + + it('should set title to blank', function() { + expect(subMenuItem.title).toBe(''); + }); + + it('should set state to blank', function() { + expect(subMenuItem.state).toBe(''); + }); + + it('should set roles to parent roles', function() { + expect(subMenuItem.roles).toEqual(menuItem2.roles); + }); + + it('should set position to 0', function() { + expect(subMenuItem.position).toBe(0); + }); + }); + }); + + describe('removeSubMenuItem', function() { + var menuId = 'menu1', + subMenuItem1 = { + link: 'link1' + }, + subMenuItem2 = { + link: 'link2' + }, + menuItem1 = { + state: 'state', + items: [subMenuItem1, subMenuItem2], + }, + menuItem2 = { + state: 'state2', + items: [], + }, + newMenu = { + items: [menuItem1, menuItem2] + }, + menu; + beforeEach(function() { + Menus.validateMenuExistance = jasmine.createSpy(); + Menus.menus[menuId] = newMenu; + menu = Menus.removeSubMenuItem(menuId, subMenuItem1.link); + }); + + it('should validate menu existance', function() { + expect(Menus.validateMenuExistance).toHaveBeenCalledWith(menuId); + }); + + it('should return menu object', function() { + expect(menu).toEqual(newMenu); + }); + + it('should remove sub menu item', function() { + expect(menuItem1.items.length).toBe(1); + expect(menuItem1.items[0]).toEqual(subMenuItem2); + }); + }); + }); +})(); diff --git a/modules/core/tests/client/socket.io.client.service.tests.js b/modules/core/tests/client/socket.io.client.service.tests.js new file mode 100644 index 0000000000..732189f952 --- /dev/null +++ b/modules/core/tests/client/socket.io.client.service.tests.js @@ -0,0 +1,24 @@ +(function() { + 'use strict'; + + /* Creates a mock of socket.io for the browser. + * Functionality of the service is tested through + * the chat controller tests. + */ + window.io = function() { + this.cbs = {}; + this.on = function(msg, cb) { + this.cbs[msg] = cb; + }; + this.emit = function(msg, data) { + this.cbs[msg](data); + }; + this.removeListener = function(msg) { + delete this.cbs[msg]; + }; + this.connect = function() { + this.socket = {}; + }; + return this; + }; +})(); diff --git a/modules/users/client/config/users-admin.client.menus.js b/modules/users/client/config/users-admin.client.menus.js new file mode 100644 index 0000000000..eda2af52c0 --- /dev/null +++ b/modules/users/client/config/users-admin.client.menus.js @@ -0,0 +1,11 @@ +'use strict'; + +// Configuring the Articles module +angular.module('users.admin').run(['Menus', + function (Menus) { + Menus.addSubMenuItem('topbar', 'admin', { + title: 'Manage Users', + state: 'admin.users' + }); + } +]); diff --git a/modules/users/client/config/users-admin.client.routes.js b/modules/users/client/config/users-admin.client.routes.js new file mode 100644 index 0000000000..fc9cb42d23 --- /dev/null +++ b/modules/users/client/config/users-admin.client.routes.js @@ -0,0 +1,37 @@ +'use strict'; + +// Setting up route +angular.module('users.admin.routes').config(['$stateProvider', + function ($stateProvider) { + $stateProvider + .state('admin.users', { + url: '/users', + templateUrl: 'modules/users/views/admin/user-list.client.view.html', + controller: 'UserListController' + }) + .state('admin.user', { + url: '/users/:userId', + templateUrl: 'modules/users/views/admin/user.client.view.html', + controller: 'UserController', + resolve: { + userResolve: ['$stateParams', 'Admin', function ($stateParams, Admin) { + return Admin.get({ + userId: $stateParams.userId + }); + }] + } + }) + .state('admin.user-edit', { + url: '/users/:userId/edit', + templateUrl: 'modules/users/views/admin/user-edit.client.view.html', + controller: 'UserController', + resolve: { + userResolve: ['$stateParams', 'Admin', function ($stateParams, Admin) { + return Admin.get({ + userId: $stateParams.userId + }); + }] + } + }); + } +]); diff --git a/modules/users/client/config/users.client.config.js b/modules/users/client/config/users.client.config.js new file mode 100644 index 0000000000..33e950713d --- /dev/null +++ b/modules/users/client/config/users.client.config.js @@ -0,0 +1,30 @@ +'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); + } + }; + } + ]); + } +]); diff --git a/modules/users/client/config/users.client.routes.js b/modules/users/client/config/users.client.routes.js new file mode 100644 index 0000000000..b5c9efa9b9 --- /dev/null +++ b/modules/users/client/config/users.client.routes.js @@ -0,0 +1,72 @@ +'use strict'; + +// Setting up route +angular.module('users').config(['$stateProvider', + function ($stateProvider) { + // Users state routing + $stateProvider + .state('settings', { + abstract: true, + url: '/settings', + templateUrl: 'modules/users/views/settings/settings.client.view.html', + data: { + roles: ['user', 'admin'] + } + }) + .state('settings.profile', { + url: '/profile', + templateUrl: 'modules/users/views/settings/edit-profile.client.view.html' + }) + .state('settings.password', { + url: '/password', + templateUrl: 'modules/users/views/settings/change-password.client.view.html' + }) + .state('settings.accounts', { + url: '/accounts', + templateUrl: 'modules/users/views/settings/manage-social-accounts.client.view.html' + }) + .state('settings.picture', { + url: '/picture', + templateUrl: 'modules/users/views/settings/change-profile-picture.client.view.html' + }) + .state('authentication', { + abstract: true, + url: '/authentication', + templateUrl: 'modules/users/views/authentication/authentication.client.view.html' + }) + .state('authentication.signup', { + url: '/signup', + templateUrl: 'modules/users/views/authentication/signup.client.view.html' + }) + .state('authentication.signin', { + url: '/signin?err', + templateUrl: 'modules/users/views/authentication/signin.client.view.html' + }) + .state('password', { + abstract: true, + url: '/password', + template: '' + }) + .state('password.forgot', { + url: '/forgot', + templateUrl: 'modules/users/views/password/forgot-password.client.view.html' + }) + .state('password.reset', { + abstract: true, + url: '/reset', + template: '' + }) + .state('password.reset.invalid', { + url: '/invalid', + templateUrl: 'modules/users/views/password/reset-password-invalid.client.view.html' + }) + .state('password.reset.success', { + url: '/success', + templateUrl: 'modules/users/views/password/reset-password-success.client.view.html' + }) + .state('password.reset.form', { + url: '/:token', + templateUrl: 'modules/users/views/password/reset-password.client.view.html' + }); + } +]); diff --git a/modules/users/client/controllers/admin/user-list.client.controller.js b/modules/users/client/controllers/admin/user-list.client.controller.js new file mode 100644 index 0000000000..0e0114947c --- /dev/null +++ b/modules/users/client/controllers/admin/user-list.client.controller.js @@ -0,0 +1,31 @@ +'use strict'; + +angular.module('users.admin').controller('UserListController', ['$scope', '$filter', 'Admin', + function ($scope, $filter, Admin) { + Admin.query(function (data) { + $scope.users = data; + $scope.buildPager(); + }); + + $scope.buildPager = function () { + $scope.pagedItems = []; + $scope.itemsPerPage = 15; + $scope.currentPage = 1; + $scope.figureOutItemsToDisplay(); + }; + + $scope.figureOutItemsToDisplay = function () { + $scope.filteredItems = $filter('filter')($scope.users, { + $: $scope.search + }); + $scope.filterLength = $scope.filteredItems.length; + var begin = (($scope.currentPage - 1) * $scope.itemsPerPage); + var end = begin + $scope.itemsPerPage; + $scope.pagedItems = $scope.filteredItems.slice(begin, end); + }; + + $scope.pageChanged = function () { + $scope.figureOutItemsToDisplay(); + }; + } +]); diff --git a/modules/users/client/controllers/admin/user.client.controller.js b/modules/users/client/controllers/admin/user.client.controller.js new file mode 100644 index 0000000000..22a1dd4de4 --- /dev/null +++ b/modules/users/client/controllers/admin/user.client.controller.js @@ -0,0 +1,34 @@ +'use strict'; + +angular.module('users.admin').controller('UserController', ['$scope', '$state', 'Authentication', 'userResolve', + function ($scope, $state, Authentication, userResolve) { + $scope.authentication = Authentication; + $scope.user = userResolve; + + $scope.remove = function (user) { + if (confirm('Are you sure you want to delete this user?')) { + if (user) { + user.$remove(); + + $scope.users.splice($scope.users.indexOf(user), 1); + } else { + $scope.user.$remove(function () { + $state.go('admin.users'); + }); + } + } + }; + + $scope.update = function () { + var user = $scope.user; + + user.$update(function () { + $state.go('admin.user', { + userId: user._id + }); + }, function (errorResponse) { + $scope.error = errorResponse.data.message; + }); + }; + } +]); diff --git a/modules/users/client/controllers/authentication.client.controller.js b/modules/users/client/controllers/authentication.client.controller.js new file mode 100644 index 0000000000..e0e29e918e --- /dev/null +++ b/modules/users/client/controllers/authentication.client.controller.js @@ -0,0 +1,51 @@ +'use strict'; + +angular.module('users').controller('AuthenticationController', ['$scope', '$state', '$http', '$location', '$window', 'Authentication', + function ($scope, $state, $http, $location, $window, Authentication) { + $scope.authentication = Authentication; + + // Get an eventual error defined in the URL query string: + $scope.error = $location.search().err; + + // If user is signed in then redirect back home + if ($scope.authentication.user) { + $location.path('/'); + } + + $scope.signup = function () { + $http.post('/api/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 previous or home page + $state.go($state.previous.state.name || 'home', $state.previous.params); + }).error(function (response) { + $scope.error = response.message; + }); + }; + + $scope.signin = function () { + $http.post('/api/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 previous or home page + $state.go($state.previous.state.name || 'home', $state.previous.params); + }).error(function (response) { + $scope.error = response.message; + }); + }; + + // OAuth provider request + $scope.callOauthProvider = function (url) { + var redirect_to; + + if ($state.previous) { + redirect_to = $state.previous.href; + } + + // Effectively call OAuth authentication route: + $window.location.href = url + (redirect_to ? '?redirect_to=' + encodeURIComponent(redirect_to) : ''); + }; + } +]); diff --git a/modules/users/client/controllers/password.client.controller.js b/modules/users/client/controllers/password.client.controller.js new file mode 100644 index 0000000000..e24c556296 --- /dev/null +++ b/modules/users/client/controllers/password.client.controller.js @@ -0,0 +1,46 @@ +'use strict'; + +angular.module('users').controller('PasswordController', ['$scope', '$stateParams', '$http', '$location', 'Authentication', + function ($scope, $stateParams, $http, $location, Authentication) { + $scope.authentication = Authentication; + + //If user is signed in then redirect back home + if ($scope.authentication.user) { + $location.path('/'); + } + + // Submit forgotten password account id + $scope.askForPasswordReset = function () { + $scope.success = $scope.error = null; + + $http.post('/api/auth/forgot', $scope.credentials).success(function (response) { + // Show user success message and clear form + $scope.credentials = null; + $scope.success = response.message; + + }).error(function (response) { + // Show user error message and clear form + $scope.credentials = null; + $scope.error = response.message; + }); + }; + + // Change user password + $scope.resetUserPassword = function () { + $scope.success = $scope.error = null; + + $http.post('/api/auth/reset/' + $stateParams.token, $scope.passwordDetails).success(function (response) { + // If successful show success message and clear form + $scope.passwordDetails = null; + + // Attach user profile + Authentication.user = response; + + // And redirect to the index page + $location.path('/password/reset/success'); + }).error(function (response) { + $scope.error = response.message; + }); + }; + } +]); diff --git a/modules/users/client/controllers/settings/change-password.client.controller.js b/modules/users/client/controllers/settings/change-password.client.controller.js new file mode 100644 index 0000000000..d5ac3b2186 --- /dev/null +++ b/modules/users/client/controllers/settings/change-password.client.controller.js @@ -0,0 +1,20 @@ +'use strict'; + +angular.module('users').controller('ChangePasswordController', ['$scope', '$http', 'Authentication', + function ($scope, $http, Authentication) { + $scope.user = Authentication.user; + + // Change user password + $scope.changeUserPassword = function () { + $scope.success = $scope.error = null; + + $http.post('/api/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; + }); + }; + } +]); diff --git a/modules/users/client/controllers/settings/change-profile-picture.client.controller.js b/modules/users/client/controllers/settings/change-profile-picture.client.controller.js new file mode 100644 index 0000000000..36f5bb10b3 --- /dev/null +++ b/modules/users/client/controllers/settings/change-profile-picture.client.controller.js @@ -0,0 +1,72 @@ +'use strict'; + +angular.module('users').controller('ChangeProfilePictureController', ['$scope', '$timeout', '$window', 'Authentication', 'FileUploader', + function ($scope, $timeout, $window, Authentication, FileUploader) { + $scope.user = Authentication.user; + $scope.imageURL = $scope.user.profileImageURL; + + // Create file uploader instance + $scope.uploader = new FileUploader({ + url: 'api/users/picture' + }); + + // Set file uploader image filter + $scope.uploader.filters.push({ + name: 'imageFilter', + fn: function (item, options) { + var type = '|' + item.type.slice(item.type.lastIndexOf('/') + 1) + '|'; + return '|jpg|png|jpeg|bmp|gif|'.indexOf(type) !== -1; + } + }); + + // Called after the user selected a new picture file + $scope.uploader.onAfterAddingFile = function (fileItem) { + if ($window.FileReader) { + var fileReader = new FileReader(); + fileReader.readAsDataURL(fileItem._file); + + fileReader.onload = function (fileReaderEvent) { + $timeout(function () { + $scope.imageURL = fileReaderEvent.target.result; + }, 0); + }; + } + }; + + // Called after the user has successfully uploaded a new picture + $scope.uploader.onSuccessItem = function (fileItem, response, status, headers) { + // Show success message + $scope.success = true; + + // Populate user object + $scope.user = Authentication.user = response; + + // Clear upload buttons + $scope.cancelUpload(); + }; + + // Called after the user has failed to uploaded a new picture + $scope.uploader.onErrorItem = function (fileItem, response, status, headers) { + // Clear upload buttons + $scope.cancelUpload(); + + // Show error message + $scope.error = response.message; + }; + + // Change user profile picture + $scope.uploadProfilePicture = function () { + // Clear messages + $scope.success = $scope.error = null; + + // Start upload + $scope.uploader.uploadAll(); + }; + + // Cancel the upload process + $scope.cancelUpload = function () { + $scope.uploader.clearQueue(); + $scope.imageURL = $scope.user.profileImageURL; + }; + } +]); diff --git a/modules/users/client/controllers/settings/edit-profile.client.controller.js b/modules/users/client/controllers/settings/edit-profile.client.controller.js new file mode 100644 index 0000000000..cb985ad51b --- /dev/null +++ b/modules/users/client/controllers/settings/edit-profile.client.controller.js @@ -0,0 +1,24 @@ +'use strict'; + +angular.module('users').controller('EditProfileController', ['$scope', '$http', '$location', 'Users', 'Authentication', + function ($scope, $http, $location, Users, Authentication) { + $scope.user = Authentication.user; + + // Update a user profile + $scope.updateUserProfile = function (isValid) { + if (isValid) { + $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; + }); + } else { + $scope.submitted = true; + } + }; + } +]); diff --git a/modules/users/client/controllers/settings/manage-social-accounts.client.controller.js b/modules/users/client/controllers/settings/manage-social-accounts.client.controller.js new file mode 100644 index 0000000000..0cb16aea04 --- /dev/null +++ b/modules/users/client/controllers/settings/manage-social-accounts.client.controller.js @@ -0,0 +1,38 @@ +'use strict'; + +angular.module('users').controller('SocialAccountsController', ['$scope', '$http', 'Authentication', + function ($scope, $http, Authentication) { + $scope.user = Authentication.user; + + // 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('/api/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; + }); + }; + } +]); diff --git a/modules/users/client/controllers/settings/settings.client.controller.js b/modules/users/client/controllers/settings/settings.client.controller.js new file mode 100644 index 0000000000..475a6b057c --- /dev/null +++ b/modules/users/client/controllers/settings/settings.client.controller.js @@ -0,0 +1,7 @@ +'use strict'; + +angular.module('users').controller('SettingsController', ['$scope', 'Authentication', + function ($scope, Authentication) { + $scope.user = Authentication.user; + } +]); diff --git a/modules/users/client/css/users.css b/modules/users/client/css/users.css new file mode 100644 index 0000000000..9bbc8961b5 --- /dev/null +++ b/modules/users/client/css/users.css @@ -0,0 +1,36 @@ +@media (min-width: 992px) { + .nav-users { + position: fixed; + } +} +.social-account-container { + display: inline-block; + position: relative; +} +.btn-remove-account { + top: 10px; + right: 10px; + position: absolute; +} +.btn-file { + position: relative; + overflow: hidden; +} +.btn-file input[type=file] { + position: absolute; + top: 0; + right: 0; + min-width: 100%; + min-height: 100%; + font-size: 100px; + text-align: right; + filter: alpha(opacity=0); + opacity: 0; + background: white; + cursor: inherit; + display: block; +} +.user-profile-picture { + min-height: 150px; + max-height: 150px; +} diff --git a/public/modules/users/img/buttons/facebook.png b/modules/users/client/img/buttons/facebook.png similarity index 100% rename from public/modules/users/img/buttons/facebook.png rename to modules/users/client/img/buttons/facebook.png diff --git a/public/modules/users/img/buttons/github.png b/modules/users/client/img/buttons/github.png similarity index 100% rename from public/modules/users/img/buttons/github.png rename to modules/users/client/img/buttons/github.png diff --git a/public/modules/users/img/buttons/google.png b/modules/users/client/img/buttons/google.png similarity index 100% rename from public/modules/users/img/buttons/google.png rename to modules/users/client/img/buttons/google.png diff --git a/public/modules/users/img/buttons/linkedin.png b/modules/users/client/img/buttons/linkedin.png similarity index 100% rename from public/modules/users/img/buttons/linkedin.png rename to modules/users/client/img/buttons/linkedin.png diff --git a/modules/users/client/img/buttons/paypal.png b/modules/users/client/img/buttons/paypal.png new file mode 100644 index 0000000000..5468939563 Binary files /dev/null and b/modules/users/client/img/buttons/paypal.png differ diff --git a/public/modules/users/img/buttons/twitter.png b/modules/users/client/img/buttons/twitter.png similarity index 100% rename from public/modules/users/img/buttons/twitter.png rename to modules/users/client/img/buttons/twitter.png diff --git a/modules/users/client/img/profile/default.png b/modules/users/client/img/profile/default.png new file mode 100644 index 0000000000..edd013a68b Binary files /dev/null and b/modules/users/client/img/profile/default.png differ diff --git a/modules/users/client/services/authentication.client.service.js b/modules/users/client/services/authentication.client.service.js new file mode 100644 index 0000000000..b535d46515 --- /dev/null +++ b/modules/users/client/services/authentication.client.service.js @@ -0,0 +1,12 @@ +'use strict'; + +// Authentication service for user variables +angular.module('users').factory('Authentication', ['$window', + function ($window) { + var auth = { + user: $window.user + }; + + return auth; + } +]); diff --git a/modules/users/client/services/users.client.service.js b/modules/users/client/services/users.client.service.js new file mode 100644 index 0000000000..aaac9c3591 --- /dev/null +++ b/modules/users/client/services/users.client.service.js @@ -0,0 +1,25 @@ +'use strict'; + +// Users service used for communicating with the users REST endpoint +angular.module('users').factory('Users', ['$resource', + function ($resource) { + return $resource('api/users', {}, { + update: { + method: 'PUT' + } + }); + } +]); + +//TODO this should be Users service +angular.module('users.admin').factory('Admin', ['$resource', + function ($resource) { + return $resource('api/users/:userId', { + userId: '@_id' + }, { + update: { + method: 'PUT' + } + }); + } +]); diff --git a/modules/users/client/users.client.module.js b/modules/users/client/users.client.module.js new file mode 100644 index 0000000000..8a95d388ca --- /dev/null +++ b/modules/users/client/users.client.module.js @@ -0,0 +1,6 @@ +'use strict'; + +// Use Applicaion configuration module to register a new module +ApplicationConfiguration.registerModule('users', ['core']); +ApplicationConfiguration.registerModule('users.admin', ['core.admin']); +ApplicationConfiguration.registerModule('users.admin.routes', ['core.admin.routes']); diff --git a/modules/users/client/views/admin/user-edit.client.view.html b/modules/users/client/views/admin/user-edit.client.view.html new file mode 100644 index 0000000000..54f4a60ff8 --- /dev/null +++ b/modules/users/client/views/admin/user-edit.client.view.html @@ -0,0 +1,35 @@ +
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+ +
+
+
+
+
diff --git a/modules/users/client/views/admin/user-list.client.view.html b/modules/users/client/views/admin/user-list.client.view.html new file mode 100644 index 0000000000..47b146c776 --- /dev/null +++ b/modules/users/client/views/admin/user-list.client.view.html @@ -0,0 +1,20 @@ +
+ + + + +
diff --git a/modules/users/client/views/admin/user.client.view.html b/modules/users/client/views/admin/user.client.view.html new file mode 100644 index 0000000000..366f852f71 --- /dev/null +++ b/modules/users/client/views/admin/user.client.view.html @@ -0,0 +1,51 @@ +
+ + +
+
+
+
First Name
+
+
+
+
+
Last Name
+
+
+
+
+
Email
+
+
+
+
+
Provider
+
+
+
+
+
Created
+
+
+
+
+
Roles
+
+
+
+
+
diff --git a/modules/users/client/views/authentication/authentication.client.view.html b/modules/users/client/views/authentication/authentication.client.view.html new file mode 100644 index 0000000000..af052fad2d --- /dev/null +++ b/modules/users/client/views/authentication/authentication.client.view.html @@ -0,0 +1,12 @@ +
+

Sign in using your social accounts

+
+ + + + + + +
+
+
diff --git a/modules/users/client/views/authentication/signin.client.view.html b/modules/users/client/views/authentication/signin.client.view.html new file mode 100644 index 0000000000..cb2d908b3c --- /dev/null +++ b/modules/users/client/views/authentication/signin.client.view.html @@ -0,0 +1,30 @@ +
+

Or with your account

+
+ +
+
diff --git a/modules/users/client/views/authentication/signup.client.view.html b/modules/users/client/views/authentication/signup.client.view.html new file mode 100644 index 0000000000..2227fab432 --- /dev/null +++ b/modules/users/client/views/authentication/signup.client.view.html @@ -0,0 +1,42 @@ +
+

Or sign up using your email

+
+ +
+
diff --git a/modules/users/client/views/password/forgot-password.client.view.html b/modules/users/client/views/password/forgot-password.client.view.html new file mode 100644 index 0000000000..793b7a0f58 --- /dev/null +++ b/modules/users/client/views/password/forgot-password.client.view.html @@ -0,0 +1,22 @@ +
+

Restore your password

+

Enter your account username.

+
+
+
+
+ +
+
+ +
+
+ {{error}} +
+
+ {{success}} +
+
+
+
+
diff --git a/modules/users/client/views/password/reset-password-invalid.client.view.html b/modules/users/client/views/password/reset-password-invalid.client.view.html new file mode 100644 index 0000000000..2c44e89420 --- /dev/null +++ b/modules/users/client/views/password/reset-password-invalid.client.view.html @@ -0,0 +1,4 @@ +
+

Password reset is invalid

+ Ask for a new password reset +
diff --git a/modules/users/client/views/password/reset-password-success.client.view.html b/modules/users/client/views/password/reset-password-success.client.view.html new file mode 100644 index 0000000000..252a879448 --- /dev/null +++ b/modules/users/client/views/password/reset-password-success.client.view.html @@ -0,0 +1,4 @@ +
+

Password successfully reset

+ Continue to home page +
diff --git a/modules/users/client/views/password/reset-password.client.view.html b/modules/users/client/views/password/reset-password.client.view.html new file mode 100644 index 0000000000..bdd0611a73 --- /dev/null +++ b/modules/users/client/views/password/reset-password.client.view.html @@ -0,0 +1,26 @@ +
+

Reset your password

+
+ +
+
diff --git a/modules/users/client/views/settings/change-password.client.view.html b/modules/users/client/views/settings/change-password.client.view.html new file mode 100644 index 0000000000..28e72dddce --- /dev/null +++ b/modules/users/client/views/settings/change-password.client.view.html @@ -0,0 +1,29 @@ +
+
+ +
+
diff --git a/modules/users/client/views/settings/change-profile-picture.client.view.html b/modules/users/client/views/settings/change-profile-picture.client.view.html new file mode 100644 index 0000000000..17085f3d99 --- /dev/null +++ b/modules/users/client/views/settings/change-profile-picture.client.view.html @@ -0,0 +1,26 @@ +
+
+ +
+
diff --git a/modules/users/client/views/settings/edit-profile.client.view.html b/modules/users/client/views/settings/edit-profile.client.view.html new file mode 100644 index 0000000000..2f461127ad --- /dev/null +++ b/modules/users/client/views/settings/edit-profile.client.view.html @@ -0,0 +1,33 @@ +
+
+ +
+
diff --git a/modules/users/client/views/settings/manage-social-accounts.client.view.html b/modules/users/client/views/settings/manage-social-accounts.client.view.html new file mode 100644 index 0000000000..f07ad9ca1f --- /dev/null +++ b/modules/users/client/views/settings/manage-social-accounts.client.view.html @@ -0,0 +1,50 @@ +
+

Connected social accounts:

+
+ +
+

Unconnected social accounts:

+
+ + + + + + +
+
diff --git a/modules/users/client/views/settings/settings.client.view.html b/modules/users/client/views/settings/settings.client.view.html new file mode 100644 index 0000000000..8d5bd8c0b1 --- /dev/null +++ b/modules/users/client/views/settings/settings.client.view.html @@ -0,0 +1,26 @@ +
+ + +
diff --git a/modules/users/server/config/strategies/facebook.js b/modules/users/server/config/strategies/facebook.js new file mode 100644 index 0000000000..0f53a6a767 --- /dev/null +++ b/modules/users/server/config/strategies/facebook.js @@ -0,0 +1,41 @@ +'use strict'; + +/** + * Module dependencies. + */ +var passport = require('passport'), + FacebookStrategy = require('passport-facebook').Strategy, + users = require('../../controllers/users.server.controller'); + +module.exports = function (config) { + // Use facebook strategy + passport.use(new FacebookStrategy({ + clientID: config.facebook.clientID, + clientSecret: config.facebook.clientSecret, + callbackURL: config.facebook.callbackURL, + profileFields: ['id', 'name', 'displayName', 'emails', 'photos'], + passReqToCallback: true + }, + function (req, accessToken, refreshToken, profile, done) { + // Set the provider data and include tokens + var providerData = profile._json; + providerData.accessToken = accessToken; + providerData.refreshToken = refreshToken; + + // Create the user OAuth profile + var providerUserProfile = { + firstName: profile.name.givenName, + lastName: profile.name.familyName, + displayName: profile.displayName, + email: profile.emails[0].value, + profileImageURL: (profile.id) ? '//graph.facebook.com/' + profile.id + '/picture?type=large' : undefined, + provider: 'facebook', + providerIdentifierField: 'id', + providerData: providerData + }; + + // Save the user OAuth profile + users.saveOAuthUserProfile(req, providerUserProfile, done); + } + )); +}; diff --git a/modules/users/server/config/strategies/github.js b/modules/users/server/config/strategies/github.js new file mode 100644 index 0000000000..6e996e6306 --- /dev/null +++ b/modules/users/server/config/strategies/github.js @@ -0,0 +1,48 @@ +'use strict'; + +/** + * Module dependencies. + */ +var passport = require('passport'), + GithubStrategy = require('passport-github').Strategy, + users = require('../../controllers/users.server.controller'); + +module.exports = function (config) { + // Use github strategy + passport.use(new GithubStrategy({ + clientID: config.github.clientID, + clientSecret: config.github.clientSecret, + callbackURL: config.github.callbackURL, + passReqToCallback: true + }, + function (req, accessToken, refreshToken, profile, done) { + // Set the provider data and include tokens + var providerData = profile._json; + providerData.accessToken = accessToken; + providerData.refreshToken = refreshToken; + + // Create the user OAuth profile + var displayName = profile.displayName ? profile.displayName.trim() : profile.username.trim(); + var iSpace = displayName.indexOf(' '); // index of the whitespace following the firstName + var firstName = iSpace !== -1 ? displayName.substring(0, iSpace) : displayName; + var lastName = iSpace !== -1 ? displayName.substring(iSpace + 1) : ''; + + var providerUserProfile = { + firstName: firstName, + lastName: lastName, + displayName: displayName, + email: profile.emails[0].value, + username: profile.username, + // jscs:disable requireCamelCaseOrUpperCaseIdentifiers + profileImageURL: (providerData.avatar_url) ? providerData.avatar_url : undefined, + // jscs:enable + provider: 'github', + providerIdentifierField: 'id', + providerData: providerData + }; + + // Save the user OAuth profile + users.saveOAuthUserProfile(req, providerUserProfile, done); + } + )); +}; diff --git a/modules/users/server/config/strategies/google.js b/modules/users/server/config/strategies/google.js new file mode 100644 index 0000000000..b2e023bd28 --- /dev/null +++ b/modules/users/server/config/strategies/google.js @@ -0,0 +1,41 @@ +'use strict'; + +/** + * Module dependencies. + */ +var passport = require('passport'), + GoogleStrategy = require('passport-google-oauth').OAuth2Strategy, + users = require('../../controllers/users.server.controller'); + +module.exports = function (config) { + // Use google strategy + passport.use(new GoogleStrategy({ + clientID: config.google.clientID, + clientSecret: config.google.clientSecret, + callbackURL: config.google.callbackURL, + passReqToCallback: true + }, + function (req, accessToken, refreshToken, profile, done) { + // Set the provider data and include tokens + var providerData = profile._json; + providerData.accessToken = accessToken; + providerData.refreshToken = refreshToken; + + // Create the user OAuth profile + var providerUserProfile = { + firstName: profile.name.givenName, + lastName: profile.name.familyName, + displayName: profile.displayName, + email: profile.emails[0].value, + username: profile.username, + profileImageURL: (providerData.picture) ? providerData.picture : undefined, + provider: 'google', + providerIdentifierField: 'id', + providerData: providerData + }; + + // Save the user OAuth profile + users.saveOAuthUserProfile(req, providerUserProfile, done); + } + )); +}; diff --git a/modules/users/server/config/strategies/linkedin.js b/modules/users/server/config/strategies/linkedin.js new file mode 100644 index 0000000000..d6ae408d07 --- /dev/null +++ b/modules/users/server/config/strategies/linkedin.js @@ -0,0 +1,42 @@ +'use strict'; + +/** + * Module dependencies. + */ +var passport = require('passport'), + LinkedInStrategy = require('passport-linkedin').Strategy, + users = require('../../controllers/users.server.controller'); + +module.exports = function (config) { + // Use linkedin strategy + passport.use(new LinkedInStrategy({ + consumerKey: config.linkedin.clientID, + consumerSecret: config.linkedin.clientSecret, + callbackURL: config.linkedin.callbackURL, + passReqToCallback: true, + profileFields: ['id', 'first-name', 'last-name', 'email-address', 'picture-url'] + }, + function (req, accessToken, refreshToken, profile, done) { + // Set the provider data and include tokens + var providerData = profile._json; + providerData.accessToken = accessToken; + providerData.refreshToken = refreshToken; + + // Create the user OAuth profile + var providerUserProfile = { + firstName: profile.name.givenName, + lastName: profile.name.familyName, + displayName: profile.displayName, + email: profile.emails[0].value, + username: profile.username, + profileImageURL: (providerData.pictureUrl) ? providerData.pictureUrl : undefined, + provider: 'linkedin', + providerIdentifierField: 'id', + providerData: providerData + }; + + // Save the user OAuth profile + users.saveOAuthUserProfile(req, providerUserProfile, done); + } + )); +}; diff --git a/modules/users/server/config/strategies/local.js b/modules/users/server/config/strategies/local.js new file mode 100644 index 0000000000..684e7a8582 --- /dev/null +++ b/modules/users/server/config/strategies/local.js @@ -0,0 +1,33 @@ +'use strict'; + +/** + * Module dependencies. + */ +var passport = require('passport'), + LocalStrategy = require('passport-local').Strategy, + User = require('mongoose').model('User'); + +module.exports = function () { + // Use local strategy + passport.use(new LocalStrategy({ + usernameField: 'username', + passwordField: 'password' + }, + function (username, password, done) { + User.findOne({ + username: username + }, function (err, user) { + if (err) { + return done(err); + } + if (!user || !user.authenticate(password)) { + return done(null, false, { + message: 'Invalid username or password' + }); + } + + return done(null, user); + }); + } + )); +}; diff --git a/modules/users/server/config/strategies/paypal.js b/modules/users/server/config/strategies/paypal.js new file mode 100644 index 0000000000..c0d97302ee --- /dev/null +++ b/modules/users/server/config/strategies/paypal.js @@ -0,0 +1,42 @@ +'use strict'; + +/** + * Module dependencies. + */ +var passport = require('passport'), + PayPalStrategy = require('passport-paypal-openidconnect').Strategy, + users = require('../../controllers/users.server.controller'); + +module.exports = function (config) { + passport.use(new PayPalStrategy({ + clientID: config.paypal.clientID, + clientSecret: config.paypal.clientSecret, + callbackURL: config.paypal.callbackURL, + scope: 'openid profile email', + sandbox: config.paypal.sandbox, + passReqToCallback: true + + }, + function (req, accessToken, refreshToken, profile, done) { + // Set the provider data and include tokens + var providerData = profile._json; + providerData.accessToken = accessToken; + providerData.refreshToken = refreshToken; + + // Create the user OAuth profile + var providerUserProfile = { + firstName: profile.name.givenName, + lastName: profile.name.familyName, + displayName: profile.displayName, + email: profile._json.email, + username: profile.username, + provider: 'paypal', + providerIdentifierField: 'user_id', + providerData: providerData + }; + + // Save the user OAuth profile + users.saveOAuthUserProfile(req, providerUserProfile, done); + } + )); +}; diff --git a/modules/users/server/config/strategies/twitter.js b/modules/users/server/config/strategies/twitter.js new file mode 100644 index 0000000000..2f1d5abe40 --- /dev/null +++ b/modules/users/server/config/strategies/twitter.js @@ -0,0 +1,45 @@ +'use strict'; + +/** + * Module dependencies. + */ +var passport = require('passport'), + TwitterStrategy = require('passport-twitter').Strategy, + users = require('../../controllers/users.server.controller'); + +module.exports = function (config) { + // Use twitter strategy + passport.use(new TwitterStrategy({ + consumerKey: config.twitter.clientID, + consumerSecret: config.twitter.clientSecret, + callbackURL: config.twitter.callbackURL, + passReqToCallback: true + }, + function (req, token, tokenSecret, profile, done) { + // Set the provider data and include tokens + var providerData = profile._json; + providerData.token = token; + providerData.tokenSecret = tokenSecret; + + // Create the user OAuth profile + var displayName = profile.displayName.trim(); + var iSpace = displayName.indexOf(' '); // index of the whitespace following the firstName + var firstName = iSpace !== -1 ? displayName.substring(0, iSpace) : displayName; + var lastName = iSpace !== -1 ? displayName.substring(iSpace + 1) : ''; + + var providerUserProfile = { + firstName: firstName, + lastName: lastName, + displayName: displayName, + username: profile.username, + profileImageURL: profile.photos[0].value.replace('normal', 'bigger'), + provider: 'twitter', + providerIdentifierField: 'id_str', + providerData: providerData + }; + + // Save the user OAuth profile + users.saveOAuthUserProfile(req, providerUserProfile, done); + } + )); +}; diff --git a/modules/users/server/config/users.server.config.js b/modules/users/server/config/users.server.config.js new file mode 100644 index 0000000000..1f9fa19ed2 --- /dev/null +++ b/modules/users/server/config/users.server.config.js @@ -0,0 +1,37 @@ +'use strict'; + +/** + * Module dependencies. + */ +var passport = require('passport'), + User = require('mongoose').model('User'), + path = require('path'), + config = require(path.resolve('./config/config')); + +/** + * Module init function. + */ +module.exports = function (app, db) { + // Serialize sessions + passport.serializeUser(function (user, done) { + done(null, user.id); + }); + + // Deserialize sessions + passport.deserializeUser(function (id, done) { + User.findOne({ + _id: id + }, '-salt -password', function (err, user) { + done(err, user); + }); + }); + + // Initialize strategies + config.utils.getGlobbedPaths(path.join(__dirname, './strategies/**/*.js')).forEach(function (strategy) { + require(path.resolve(strategy))(config); + }); + + // Add passport's middleware + app.use(passport.initialize()); + app.use(passport.session()); +}; diff --git a/modules/users/server/controllers/admin.server.controller.js b/modules/users/server/controllers/admin.server.controller.js new file mode 100644 index 0000000000..ca3761d909 --- /dev/null +++ b/modules/users/server/controllers/admin.server.controller.js @@ -0,0 +1,93 @@ +'use strict'; + +/** + * Module dependencies. + */ +var path = require('path'), + mongoose = require('mongoose'), + User = mongoose.model('User'), + errorHandler = require(path.resolve('./modules/core/server/controllers/errors.server.controller')); + +/** + * Show the current user + */ +exports.read = function (req, res) { + res.json(req.model); +}; + +/** + * Update a User + */ +exports.update = function (req, res) { + var user = req.model; + + //For security purposes only merge these parameters + user.firstName = req.body.firstName; + user.lastName = req.body.lastName; + user.displayName = user.firstName + ' ' + user.lastName; + user.roles = req.body.roles; + + user.save(function (err) { + if (err) { + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); + } + + res.json(user); + }); +}; + +/** + * Delete a user + */ +exports.delete = function (req, res) { + var user = req.model; + + user.remove(function (err) { + if (err) { + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); + } + + res.json(user); + }); +}; + +/** + * List of Users + */ +exports.list = function (req, res) { + User.find({}, '-salt -password').sort('-created').populate('user', 'displayName').exec(function (err, users) { + if (err) { + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); + } + + res.json(users); + }); +}; + +/** + * User middleware + */ +exports.userByID = function (req, res, next, id) { + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).send({ + message: 'User is invalid' + }); + } + + User.findById(id, '-salt -password').exec(function (err, user) { + if (err) { + return next(err); + } else if (!user) { + return next(new Error('Failed to load user ' + id)); + } + + req.model = user; + next(); + }); +}; diff --git a/modules/users/server/controllers/users.server.controller.js b/modules/users/server/controllers/users.server.controller.js new file mode 100644 index 0000000000..37e0dcda6e --- /dev/null +++ b/modules/users/server/controllers/users.server.controller.js @@ -0,0 +1,16 @@ +'use strict'; + +/** + * Module dependencies. + */ +var _ = require('lodash'); + +/** + * Extend user's controller + */ +module.exports = _.extend( + require('./users/users.authentication.server.controller'), + require('./users/users.authorization.server.controller'), + require('./users/users.password.server.controller'), + require('./users/users.profile.server.controller') +); diff --git a/modules/users/server/controllers/users/users.authentication.server.controller.js b/modules/users/server/controllers/users/users.authentication.server.controller.js new file mode 100644 index 0000000000..40d8746f12 --- /dev/null +++ b/modules/users/server/controllers/users/users.authentication.server.controller.js @@ -0,0 +1,244 @@ +'use strict'; + +/** + * Module dependencies. + */ +var path = require('path'), + errorHandler = require(path.resolve('./modules/core/server/controllers/errors.server.controller')), + mongoose = require('mongoose'), + passport = require('passport'), + User = mongoose.model('User'); + +// URLs for which user can't be redirected on signin +var noReturnUrls = [ + '/authentication/signin', + '/authentication/signup' +]; + +/** + * 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.json(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.json(user); + } + }); + } + })(req, res, next); +}; + +/** + * Signout + */ +exports.signout = function (req, res) { + req.logout(); + res.redirect('/'); +}; + +/** + * OAuth provider call + */ +exports.oauthCall = function (strategy, scope) { + return function (req, res, next) { + // Set redirection path on session. + // Do not redirect to a signin or signup page + if (noReturnUrls.indexOf(req.query.redirect_to) === -1) { + req.session.redirect_to = req.query.redirect_to; + } + // Authenticate + passport.authenticate(strategy, scope)(req, res, next); + }; +}; + +/** + * OAuth callback + */ +exports.oauthCallback = function (strategy) { + return function (req, res, next) { + // Pop redirect URL from session + var sessionRedirectURL = req.session.redirect_to; + delete req.session.redirect_to; + + passport.authenticate(strategy, function (err, user, redirectURL) { + if (err) { + return res.redirect('/authentication/signin?err=' + encodeURIComponent(errorHandler.getErrorMessage(err))); + } + if (!user) { + return res.redirect('/authentication/signin'); + } + req.login(user, function (err) { + if (err) { + return res.redirect('/authentication/signin'); + } + + return res.redirect(redirectURL || sessionRedirectURL || '/'); + }); + })(req, res, next); + }; +}; + +/** + * Helper function to save or update a OAuth user profile + */ +exports.saveOAuthUserProfile = function (req, providerUserProfile, done) { + if (!req.user) { + // Define a search query fields + var searchMainProviderIdentifierField = 'providerData.' + providerUserProfile.providerIdentifierField; + var searchAdditionalProviderIdentifierField = 'additionalProvidersData.' + providerUserProfile.provider + '.' + providerUserProfile.providerIdentifierField; + + // Define main provider search query + var mainProviderSearchQuery = {}; + mainProviderSearchQuery.provider = providerUserProfile.provider; + mainProviderSearchQuery[searchMainProviderIdentifierField] = providerUserProfile.providerData[providerUserProfile.providerIdentifierField]; + + // Define additional provider search query + var additionalProviderSearchQuery = {}; + additionalProviderSearchQuery[searchAdditionalProviderIdentifierField] = providerUserProfile.providerData[providerUserProfile.providerIdentifierField]; + + // Define a search query to find existing user with current provider profile + var searchQuery = { + $or: [mainProviderSearchQuery, additionalProviderSearchQuery] + }; + + User.findOne(searchQuery, function (err, user) { + if (err) { + return done(err); + } else { + if (!user) { + var possibleUsername = providerUserProfile.username || ((providerUserProfile.email) ? providerUserProfile.email.split('@')[0] : ''); + + User.findUniqueUsername(possibleUsername, null, function (availableUsername) { + user = new User({ + firstName: providerUserProfile.firstName, + lastName: providerUserProfile.lastName, + username: availableUsername, + displayName: providerUserProfile.displayName, + email: providerUserProfile.email, + profileImageURL: providerUserProfile.profileImageURL, + 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.query.provider; + + if (!user) { + return res.status(401).json({ + message: 'User is not authenticated' + }); + } else if (!provider) { + return res.status(400).send(); + } + + // 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) { + return res.status(400).send(err); + } else { + return res.json(user); + } + }); + } + }); +}; diff --git a/modules/users/server/controllers/users/users.authorization.server.controller.js b/modules/users/server/controllers/users/users.authorization.server.controller.js new file mode 100644 index 0000000000..f7fa09a145 --- /dev/null +++ b/modules/users/server/controllers/users/users.authorization.server.controller.js @@ -0,0 +1,32 @@ +'use strict'; + +/** + * Module dependencies. + */ +var _ = require('lodash'), + mongoose = require('mongoose'), + User = mongoose.model('User'); + +/** + * User middleware + */ +exports.userByID = function (req, res, next, id) { + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).send({ + message: 'User is invalid' + }); + } + + User.findOne({ + _id: id + }).exec(function (err, user) { + if (err) { + return next(err); + } else if (!user) { + return next(new Error('Failed to load User ' + id)); + } + + req.profile = user; + next(); + }); +}; diff --git a/modules/users/server/controllers/users/users.password.server.controller.js b/modules/users/server/controllers/users/users.password.server.controller.js new file mode 100644 index 0000000000..d59c73975a --- /dev/null +++ b/modules/users/server/controllers/users/users.password.server.controller.js @@ -0,0 +1,254 @@ +'use strict'; + +/** + * Module dependencies. + */ +var path = require('path'), + config = require(path.resolve('./config/config')), + errorHandler = require(path.resolve('./modules/core/server/controllers/errors.server.controller')), + mongoose = require('mongoose'), + User = mongoose.model('User'), + nodemailer = require('nodemailer'), + async = require('async'), + crypto = require('crypto'); + +var smtpTransport = nodemailer.createTransport(config.mailer.options); + +/** + * 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(path.resolve('modules/users/server/templates/reset-password-email'), { + name: user.displayName, + appName: config.app.title, + url: 'http://' + req.headers.host + '/api/auth/reset/' + token + }, function (err, emailHTML) { + done(err, emailHTML, user); + }); + }, + // If valid email, send reset email using service + function (emailHTML, user, done) { + 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 the provided email with further instructions.' + }); + } else { + return res.status(400).send({ + message: 'Failure sending email' + }); + } + + done(err); + }); + } + ], 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.json(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('modules/users/server/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 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' + }); + } +}; diff --git a/modules/users/server/controllers/users/users.profile.server.controller.js b/modules/users/server/controllers/users/users.profile.server.controller.js new file mode 100644 index 0000000000..8dde7e6e55 --- /dev/null +++ b/modules/users/server/controllers/users/users.profile.server.controller.js @@ -0,0 +1,96 @@ +'use strict'; + +/** + * Module dependencies. + */ +var _ = require('lodash'), + fs = require('fs'), + path = require('path'), + errorHandler = require(path.resolve('./modules/core/server/controllers/errors.server.controller')), + mongoose = require('mongoose'), + User = mongoose.model('User'); + +/** + * Update user details + */ +exports.update = function (req, res) { + // Init Variables + var user = req.user; + + // 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.json(user); + } + }); + } + }); + } else { + res.status(400).send({ + message: 'User is not signed in' + }); + } +}; + +/** + * Update profile picture + */ +exports.changeProfilePicture = function (req, res) { + var user = req.user; + var message = null; + + if (user) { + fs.writeFile('./modules/users/client/img/profile/uploads/' + req.files.file.name, req.files.file.buffer, function (uploadError) { + if (uploadError) { + return res.status(400).send({ + message: 'Error occurred while uploading profile picture' + }); + } else { + user.profileImageURL = 'modules/users/img/profile/uploads/' + req.files.file.name; + + user.save(function (saveError) { + if (saveError) { + return res.status(400).send({ + message: errorHandler.getErrorMessage(saveError) + }); + } else { + req.login(user, function (err) { + if (err) { + res.status(400).send(err); + } else { + res.json(user); + } + }); + } + }); + } + }); + } else { + res.status(400).send({ + message: 'User is not signed in' + }); + } +}; + +/** + * Send User + */ +exports.me = function (req, res) { + res.json(req.user || null); +}; diff --git a/modules/users/server/models/user.server.model.js b/modules/users/server/models/user.server.model.js new file mode 100644 index 0000000000..8a4ff0e8da --- /dev/null +++ b/modules/users/server/models/user.server.model.js @@ -0,0 +1,158 @@ +'use strict'; + +/** + * Module dependencies. + */ +var mongoose = require('mongoose'), + Schema = mongoose.Schema, + crypto = require('crypto'), + validator = require('validator'); + +/** + * A Validation function for local strategy properties + */ +var validateLocalStrategyProperty = function (property) { + return ((this.provider !== 'local' && !this.updated) || property.length); +}; + +/** + * A Validation function for local strategy password + */ +var validateLocalStrategyPassword = function (password) { + return (this.provider !== 'local' || validator.isLength(password, 6)); +}; + +/** + * A Validation function for local strategy email + */ +var validateLocalStrategyEmail = function (email) { + return ((this.provider !== 'local' && !this.updated) || validator.isEmail(email)); +}; + +/** + * User Schema + */ +var UserSchema = new Schema({ + firstName: { + type: String, + trim: true, + default: '', + validate: [validateLocalStrategyProperty, 'Please fill in your first name'] + }, + lastName: { + type: String, + trim: true, + default: '', + validate: [validateLocalStrategyProperty, 'Please fill in your last name'] + }, + displayName: { + type: String, + trim: true + }, + email: { + type: String, + trim: true, + unique: true, + default: '', + validate: [validateLocalStrategyEmail, 'Please fill a valid email address'] + }, + username: { + type: String, + unique: 'Username already exists', + required: 'Please fill in a username', + trim: true + }, + password: { + type: String, + default: '', + validate: [validateLocalStrategyPassword, 'Password should be longer'] + }, + salt: { + type: String + }, + profileImageURL: { + type: String, + default: 'modules/users/img/profile/default.png' + }, + provider: { + type: String, + required: 'Provider is required' + }, + providerData: {}, + additionalProvidersData: {}, + roles: { + type: [{ + type: String, + enum: ['user', 'admin'] + }], + default: ['user'] + }, + updated: { + type: Date + }, + created: { + type: Date, + default: Date.now + }, + /* For reset password */ + resetPasswordToken: { + type: String + }, + resetPasswordExpires: { + type: Date + } +}); + +/** + * Hook a pre save method to hash the password + */ +UserSchema.pre('save', function (next) { + if (this.password && this.isModified('password') && this.password.length > 6) { + this.salt = crypto.randomBytes(16).toString('base64'); + this.password = this.hashPassword(this.password); + } + + next(); +}); + +/** + * Create instance method for hashing a password + */ +UserSchema.methods.hashPassword = function (password) { + if (this.salt && password) { + return crypto.pbkdf2Sync(password, new Buffer(this.salt, 'base64'), 10000, 64).toString('base64'); + } else { + return password; + } +}; + +/** + * Create instance method for authenticating user + */ +UserSchema.methods.authenticate = function (password) { + return this.password === this.hashPassword(password); +}; + +/** + * Find possible not used username + */ +UserSchema.statics.findUniqueUsername = function (username, suffix, callback) { + var _this = this; + var possibleUsername = username + (suffix || ''); + + _this.findOne({ + username: possibleUsername + }, function (err, user) { + if (!err) { + if (!user) { + callback(possibleUsername); + } else { + return _this.findUniqueUsername(username, (suffix || 0) + 1, callback); + } + } else { + callback(null); + } + }); +}; + +mongoose.model('User', UserSchema); diff --git a/modules/users/server/policies/admin.server.policy.js b/modules/users/server/policies/admin.server.policy.js new file mode 100644 index 0000000000..83dbc8957e --- /dev/null +++ b/modules/users/server/policies/admin.server.policy.js @@ -0,0 +1,49 @@ +'use strict'; + +/** + * Module dependencies. + */ +var acl = require('acl'); + +// Using the memory backend +acl = new acl(new acl.memoryBackend()); + +/** + * Invoke Admin Permissions + */ +exports.invokeRolesPolicies = function () { + acl.allow([{ + roles: ['admin'], + allows: [{ + resources: '/api/users', + permissions: '*' + }, { + resources: '/api/users/:userId', + permissions: '*' + }] + }]); +}; + +/** + * Check If Admin Policy Allows + */ +exports.isAllowed = function (req, res, next) { + var roles = (req.user) ? req.user.roles : ['guest']; + + // Check for user roles + acl.areAnyRolesAllowed(roles, req.route.path, req.method.toLowerCase(), function (err, isAllowed) { + if (err) { + // An authorization error occurred. + return res.status(500).send('Unexpected authorization error'); + } else { + if (isAllowed) { + // Access granted! Invoke next middleware + return next(); + } else { + return res.status(403).json({ + message: 'User is not authorized' + }); + } + } + }); +}; diff --git a/modules/users/server/routes/admin.server.routes.js b/modules/users/server/routes/admin.server.routes.js new file mode 100644 index 0000000000..1c3e5d2a46 --- /dev/null +++ b/modules/users/server/routes/admin.server.routes.js @@ -0,0 +1,25 @@ +'use strict'; + +/** + * Module dependencies. + */ +var adminPolicy = require('../policies/admin.server.policy'), + admin = require('../controllers/admin.server.controller'); + +module.exports = function (app) { + // User route registration first. Ref: #713 + require('./users.server.routes.js')(app); + + // Users collection routes + app.route('/api/users') + .get(adminPolicy.isAllowed, admin.list); + + // Single user routes + app.route('/api/users/:userId') + .get(adminPolicy.isAllowed, admin.read) + .put(adminPolicy.isAllowed, admin.update) + .delete(adminPolicy.isAllowed, admin.delete); + + // Finish by binding the user middleware + app.param('userId', admin.userByID); +}; diff --git a/modules/users/server/routes/auth.server.routes.js b/modules/users/server/routes/auth.server.routes.js new file mode 100644 index 0000000000..a3ff220581 --- /dev/null +++ b/modules/users/server/routes/auth.server.routes.js @@ -0,0 +1,57 @@ +'use strict'; + +/** + * Module dependencies. + */ +var passport = require('passport'); + +module.exports = function (app) { + // User Routes + var users = require('../controllers/users.server.controller'); + + // Setting up the users password api + app.route('/api/auth/forgot').post(users.forgot); + app.route('/api/auth/reset/:token').get(users.validateResetToken); + app.route('/api/auth/reset/:token').post(users.reset); + + // Setting up the users authentication api + app.route('/api/auth/signup').post(users.signup); + app.route('/api/auth/signin').post(users.signin); + app.route('/api/auth/signout').get(users.signout); + + // Setting the facebook oauth routes + app.route('/api/auth/facebook').get(users.oauthCall('facebook', { + scope: ['email'] + })); + app.route('/api/auth/facebook/callback').get(users.oauthCallback('facebook')); + + // Setting the twitter oauth routes + app.route('/api/auth/twitter').get(users.oauthCall('twitter')); + app.route('/api/auth/twitter/callback').get(users.oauthCallback('twitter')); + + // Setting the google oauth routes + app.route('/api/auth/google').get(users.oauthCall('google', { + scope: [ + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/userinfo.email' + ] + })); + app.route('/api/auth/google/callback').get(users.oauthCallback('google')); + + // Setting the linkedin oauth routes + app.route('/api/auth/linkedin').get(users.oauthCall('linkedin', { + scope: [ + 'r_basicprofile', + 'r_emailaddress' + ] + })); + app.route('/api/auth/linkedin/callback').get(users.oauthCallback('linkedin')); + + // Setting the github oauth routes + app.route('/api/auth/github').get(users.oauthCall('github')); + app.route('/api/auth/github/callback').get(users.oauthCallback('github')); + + // Setting the paypal oauth routes + app.route('/api/auth/paypal').get(users.oauthCall('paypal')); + app.route('/api/auth/paypal/callback').get(users.oauthCallback('paypal')); +}; diff --git a/modules/users/server/routes/users.server.routes.js b/modules/users/server/routes/users.server.routes.js new file mode 100644 index 0000000000..47cef3df49 --- /dev/null +++ b/modules/users/server/routes/users.server.routes.js @@ -0,0 +1,16 @@ +'use strict'; + +module.exports = function (app) { + // User Routes + var users = require('../controllers/users.server.controller'); + + // Setting up the users profile api + app.route('/api/users/me').get(users.me); + app.route('/api/users').put(users.update); + app.route('/api/users/accounts').delete(users.removeOAuthProvider); + app.route('/api/users/password').post(users.changePassword); + app.route('/api/users/picture').post(users.changeProfilePicture); + + // Finish by binding the user middleware + app.param('userId', users.userByID); +}; diff --git a/modules/users/server/templates/reset-password-confirm-email.server.view.html b/modules/users/server/templates/reset-password-confirm-email.server.view.html new file mode 100644 index 0000000000..a52c32615b --- /dev/null +++ b/modules/users/server/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

+ + + diff --git a/modules/users/server/templates/reset-password-email.server.view.html b/modules/users/server/templates/reset-password-email.server.view.html new file mode 100644 index 0000000000..590926fd25 --- /dev/null +++ b/modules/users/server/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

+ + + diff --git a/modules/users/tests/client/authentication.client.controller.tests.js b/modules/users/tests/client/authentication.client.controller.tests.js new file mode 100644 index 0000000000..9322d23a8a --- /dev/null +++ b/modules/users/tests/client/authentication.client.controller.tests.js @@ -0,0 +1,146 @@ +'use strict'; + +(function () { + // Authentication controller Spec + describe('AuthenticationController', function () { + // Initialize global variables + var AuthenticationController, + scope, + $httpBackend, + $stateParams, + $location; + + beforeEach(function () { + jasmine.addMatchers({ + toEqualData: function (util, customEqualityTesters) { + return { + compare: function (actual, expected) { + return { + pass: angular.equals(actual, expected) + }; + } + }; + } + }); + }); + + // Load the main application module + beforeEach(module(ApplicationConfiguration.applicationModuleName)); + + describe('Logged out user', function () { + // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). + // This allows us to inject a service but then attach it to a variable + // with the same name as the service. + beforeEach(inject(function ($controller, $rootScope, _$location_, _$stateParams_, _$httpBackend_) { + // Set a new global scope + scope = $rootScope.$new(); + + // Point global variables to injected services + $stateParams = _$stateParams_; + $httpBackend = _$httpBackend_; + $location = _$location_; + + // Initialize the Authentication controller + AuthenticationController = $controller('AuthenticationController', { + $scope: scope + }); + })); + + describe('$scope.signin()', function () { + it('should login with a correct user and password', function () { + // Test expected GET request + $httpBackend.when('POST', '/api/auth/signin').respond(200, 'Fred'); + + scope.signin(); + $httpBackend.flush(); + + // Test scope value + expect(scope.authentication.user).toEqual('Fred'); + expect($location.url()).toEqual('/'); + }); + + it('should fail to log in with nothing', function () { + // Test expected POST request + $httpBackend.expectPOST('/api/auth/signin').respond(400, { + 'message': 'Missing credentials' + }); + + scope.signin(); + $httpBackend.flush(); + + // Test scope value + expect(scope.error).toEqual('Missing credentials'); + }); + + it('should fail to log in with wrong credentials', function () { + // Foo/Bar combo assumed to not exist + scope.authentication.user = 'Foo'; + scope.credentials = 'Bar'; + + // Test expected POST request + $httpBackend.expectPOST('/api/auth/signin').respond(400, { + 'message': 'Unknown user' + }); + + scope.signin(); + $httpBackend.flush(); + + // Test scope value + expect(scope.error).toEqual('Unknown user'); + }); + }); + + describe('$scope.signup()', function () { + it('should register with correct data', function () { + // Test expected GET request + scope.authentication.user = 'Fred'; + $httpBackend.when('POST', '/api/auth/signup').respond(200, 'Fred'); + + scope.signup(); + $httpBackend.flush(); + + // test scope value + expect(scope.authentication.user).toBe('Fred'); + expect(scope.error).toEqual(undefined); + expect($location.url()).toBe('/'); + }); + + it('should fail to register with duplicate Username', function () { + // Test expected POST request + $httpBackend.when('POST', '/api/auth/signup').respond(400, { + 'message': 'Username already exists' + }); + + scope.signup(); + $httpBackend.flush(); + + // Test scope value + expect(scope.error).toBe('Username already exists'); + }); + }); + }); + + describe('Logged in user', function () { + beforeEach(inject(function ($controller, $rootScope, _$location_, _Authentication_) { + scope = $rootScope.$new(); + + $location = _$location_; + $location.path = jasmine.createSpy().and.returnValue(true); + + // Mock logged in user + _Authentication_.user = { + username: 'test', + roles: ['user'] + }; + + AuthenticationController = $controller('AuthenticationController', { + $scope: scope + }); + })); + + it('should be redirected to home', function () { + expect($location.path).toHaveBeenCalledWith('/'); + }); + }); + }); +}()); diff --git a/modules/users/tests/client/password.client.controller.tests.js b/modules/users/tests/client/password.client.controller.tests.js new file mode 100644 index 0000000000..86be25f575 --- /dev/null +++ b/modules/users/tests/client/password.client.controller.tests.js @@ -0,0 +1,198 @@ +'use strict'; + +(function() { + // Authentication controller Spec + describe('PasswordController', function() { + // Initialize global variables + var PasswordController, + scope, + $httpBackend, + $stateParams, + $location, + $window; + + beforeEach(function() { + jasmine.addMatchers({ + toEqualData: function(util, customEqualityTesters) { + return { + compare: function(actual, expected) { + return { + pass: angular.equals(actual, expected) + }; + } + }; + } + }); + }); + + // Load the main application module + beforeEach(module(ApplicationConfiguration.applicationModuleName)); + + describe('Logged in user', function() { + beforeEach(inject(function($controller, $rootScope, _Authentication_, _$stateParams_, _$httpBackend_, _$location_) { + // Set a new global scope + scope = $rootScope.$new(); + + // Point global variables to injected services + $stateParams = _$stateParams_; + $httpBackend = _$httpBackend_; + $location = _$location_; + $location.path = jasmine.createSpy().and.returnValue(true); + + // Mock logged in user + _Authentication_.user = { + username: 'test', + roles: ['user'] + }; + + // Initialize the Authentication controller + PasswordController = $controller('PasswordController', { + $scope: scope + }); + })); + + it('should redirect logged in user to home', function() { + expect($location.path).toHaveBeenCalledWith('/'); + }); + }); + + describe('Logged out user', function() { + beforeEach(inject(function($controller, $rootScope, _$window_, _$stateParams_, _$httpBackend_, _$location_) { + // Set a new global scope + scope = $rootScope.$new(); + + // Point global variables to injected services + $stateParams = _$stateParams_; + $httpBackend = _$httpBackend_; + $location = _$location_; + $location.path = jasmine.createSpy().and.returnValue(true); + $window = _$window_; + $window.user = null; + + // Initialize the Authentication controller + PasswordController = $controller('PasswordController', { + $scope: scope + }); + })); + + it('should not redirect to home', function() { + expect($location.path).not.toHaveBeenCalledWith('/'); + }); + + describe('askForPasswordReset', function() { + var credentials = { + username: 'test', + password: 'test' + }; + beforeEach(function() { + scope.credentials = credentials; + }); + + it('should clear scope.success and scope.error', function() { + scope.success = 'test'; + scope.error = 'test'; + scope.askForPasswordReset(); + + expect(scope.success).toBeNull(); + expect(scope.error).toBeNull(); + }); + + describe('POST error', function() { + var errorMessage = 'No account with that username has been found'; + beforeEach(function() { + $httpBackend.when('POST', '/api/auth/forgot', credentials).respond(400, { + 'message': errorMessage + }); + + scope.askForPasswordReset(); + $httpBackend.flush(); + }); + + it('should clear form', function() { + expect(scope.credentials).toBe(null); + }); + + it('should set error to response message', function() { + expect(scope.error).toBe(errorMessage); + }); + }); + + describe('POST success', function() { + var successMessage = 'An email has been sent to the provided email with further instructions.'; + beforeEach(function() { + $httpBackend.when('POST', '/api/auth/forgot', credentials).respond({ + 'message': successMessage + }); + + scope.askForPasswordReset(); + $httpBackend.flush(); + }); + + it('should clear form', function() { + expect(scope.credentials).toBe(null); + }); + + it('should set success to response message', function() { + expect(scope.success).toBe(successMessage); + }); + }); + }); + + describe('resetUserPassword', function() { + var token = 'testToken'; + var passwordDetails = { + password: 'test' + }; + beforeEach(function() { + $stateParams.token = token; + scope.passwordDetails = passwordDetails; + }); + + it('should clear scope.success and scope.error', function() { + scope.success = 'test'; + scope.error = 'test'; + scope.resetUserPassword(); + + expect(scope.success).toBeNull(); + expect(scope.error).toBeNull(); + }); + + it('POST error should set scope.error to response message', function() { + var errorMessage = 'Passwords do not match'; + $httpBackend.when('POST', '/api/auth/reset/' + token, passwordDetails).respond(400, { + 'message': errorMessage + }); + + scope.resetUserPassword(); + $httpBackend.flush(); + + expect(scope.error).toBe(errorMessage); + }); + + describe('POST success', function() { + var user = { + username: 'test' + }; + beforeEach(function() { + $httpBackend.when('POST', '/api/auth/reset/' + token, passwordDetails).respond(user); + + scope.resetUserPassword(); + $httpBackend.flush(); + }); + + it('should clear password form', function() { + expect(scope.passwordDetails).toBe(null); + }); + + it('should attach user profile', function() { + expect(scope.authentication.user).toEqual(user); + }); + + it('should redirect to password reset success view', function() { + expect($location.path).toHaveBeenCalledWith('/password/reset/success'); + }); + }); + }); + }); + }); +}()); diff --git a/modules/users/tests/e2e/users.e2e.tests.js b/modules/users/tests/e2e/users.e2e.tests.js new file mode 100644 index 0000000000..d7b30c0411 --- /dev/null +++ b/modules/users/tests/e2e/users.e2e.tests.js @@ -0,0 +1,13 @@ +'use strict'; + +describe('Users E2E Tests:', function () { + describe('Signin Validation', function () { + it('Should report missing credentials', function () { + browser.get('http://localhost:3000/authentication/signin'); + element(by.css('button[type=submit]')).click(); + element(by.binding('error')).getText().then(function (errorText) { + expect(errorText).toBe('Missing credentials'); + }); + }); + }); +}); diff --git a/modules/users/tests/server/user.server.model.tests.js b/modules/users/tests/server/user.server.model.tests.js new file mode 100644 index 0000000000..d43320cf99 --- /dev/null +++ b/modules/users/tests/server/user.server.model.tests.js @@ -0,0 +1,125 @@ +'use strict'; + +/** + * Module dependencies. + */ +var should = require('should'), + mongoose = require('mongoose'), + User = mongoose.model('User'); + +/** + * Globals + */ +var user, user2, user3; + +/** + * Unit tests + */ +describe('User Model Unit Tests:', function () { + before(function (done) { + user = new User({ + firstName: 'Full', + lastName: 'Name', + displayName: 'Full Name', + email: 'test@test.com', + username: 'username', + password: 'password', + provider: 'local' + }); + user2 = new User({ + firstName: 'Full', + lastName: 'Name', + displayName: 'Full Name', + email: 'test@test.com', + username: 'username', + password: 'password', + provider: 'local' + }); + user3 = new User({ + firstName: 'Different', + lastName: 'User', + displayName: 'Full Different Name', + email: 'test3@test.com', + username: 'different_username', + password: 'different_password', + provider: 'local' + }); + + done(); + }); + + describe('Method Save', function () { + it('should begin with no users', function (done) { + User.find({}, function (err, users) { + users.should.have.length(0); + done(); + }); + }); + + it('should be able to save without problems', function (done) { + user.save(done); + }); + + it('should fail to save an existing user again', function (done) { + user.save(function () { + user2.save(function (err) { + should.exist(err); + done(); + }); + }); + }); + + it('should be able to show an error when try to save without first name', function (done) { + user.firstName = ''; + return user.save(function (err) { + should.exist(err); + done(); + }); + }); + + it('should confirm that saving user model doesnt change the password', function (done) { + user.firstName = 'test'; + var passwordBefore = user.password; + return user.save(function (err) { + var passwordAfter = user.password; + passwordBefore.should.equal(passwordAfter); + done(); + }); + }); + + it('should be able to save 2 different users', function (done) { + user.remove(function (err) { + should.not.exist(err); + user.save(function (err) { + user3.save(function (err) { + should.not.exist(err); + user3.remove(function (err) { + should.not.exist(err); + done(); + }); + + }); + }); + }); + }); + + it('should not be able to save different user with the same email address', function (done) { + user.remove(function (err) { + should.not.exist(err); + user.save(function (err) { + user3.email = user.email; + user3.save(function (err) { + should.exist(err); + done(); + }); + }); + }); + + }); + + }); + + after(function (done) { + User.remove().exec(done); + }); +}); diff --git a/modules/users/tests/server/user.server.routes.tests.js b/modules/users/tests/server/user.server.routes.tests.js new file mode 100644 index 0000000000..1bce3ee03d --- /dev/null +++ b/modules/users/tests/server/user.server.routes.tests.js @@ -0,0 +1,107 @@ +'use strict'; + +var should = require('should'), + request = require('supertest'), + path = require('path'), + mongoose = require('mongoose'), + User = mongoose.model('User'), + express = require(path.resolve('./config/lib/express')); + +/** + * Globals + */ +var app, agent, credentials, user, admin; + +/** + * User routes tests + */ +describe('User CRUD tests', function () { + before(function (done) { + // Get application + app = express.init(mongoose); + agent = request.agent(app); + + done(); + }); + + beforeEach(function (done) { + // Create user credentials + credentials = { + username: 'username', + password: 'password' + }; + + // Create a new user + user = new User({ + firstName: 'Full', + lastName: 'Name', + displayName: 'Full Name', + email: 'test@test.com', + username: credentials.username, + password: credentials.password, + provider: 'local' + }); + + // Save a user to the test db and create new article + user.save(function () { + done(); + }); + }); + + it('should not be able to retrieve a list of users if not admin', function (done) { + agent.post('/api/auth/signin') + .send(credentials) + .expect(200) + .end(function (signinErr, signinRes) { + // Handle signin error + if (signinErr) { + return done(signinErr); + } + + // Save a new article + agent.get('/api/users') + .expect(403) + .end(function (usersGetErr, usersGetRes) { + if (usersGetErr) { + return done(usersGetErr); + } + + return done(); + }); + }); + }); + + it('should be able to retrieve a list of users if admin', function (done) { + user.roles = ['user', 'admin']; + + user.save(function () { + agent.post('/api/auth/signin') + .send(credentials) + .expect(200) + .end(function (signinErr, signinRes) { + // Handle signin error + if (signinErr) { + return done(signinErr); + } + + // Save a new article + agent.get('/api/users') + .expect(200) + .end(function (usersGetErr, usersGetRes) { + if (usersGetErr) { + return done(usersGetErr); + } + + usersGetRes.body.should.be.instanceof(Array).and.have.lengthOf(1); + + // Call the assertion callback + done(); + }); + }); + }); + }); + + afterEach(function (done) { + User.remove().exec(done); + }); +}); diff --git a/package.json b/package.json old mode 100755 new mode 100644 index 091f2e121f..24ecb6a4dc --- a/package.json +++ b/package.json @@ -1,74 +1,107 @@ { - "name": "meanjs", - "description": "Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js.", - "version": "0.3.3", - "private": false, - "author": "https://github.com/meanjs/mean/graphs/contributors", - "repository": { - "type": "git", - "url": "https://github.com/meanjs/mean.git" - }, - "engines": { - "node": ">=0.10.28", - "npm": ">=1.4.28" - }, - "scripts": { - "start": "grunt", - "test": "grunt test", - "postinstall": "bower install --config.interactive=false" - }, - "dependencies": { - "express": "~4.10.1", - "express-session": "~1.9.1", - "body-parser": "~1.9.0", - "cookie-parser": "~1.3.2", - "compression": "~1.2.0", - "method-override": "~2.3.0", - "morgan": "~1.4.1", - "connect-mongo": "~0.4.1", - "connect-flash": "~0.1.1", - "helmet": "~0.5.0", - "consolidate": "~0.10.0", - "swig": "~1.4.1", - "mongoose": "~3.8.8", - "passport": "~0.2.0", - "passport-local": "~1.0.0", - "passport-facebook": "~1.0.2", - "passport-twitter": "~1.0.2", - "passport-linkedin": "~0.1.3", - "passport-google-oauth": "~0.1.5", - "passport-github": "~0.1.5", - "lodash": "~2.4.1", - "forever": "~0.11.0", - "bower": "~1.3.8", - "grunt-cli": "~0.1.13", - "glob": "~4.0.5", - "async": "~0.9.0", - "nodemailer": "~1.3.0", - "chalk": "~1.0.0" - }, - "devDependencies": { - "supertest": "~0.14.0", - "should": "~4.1.0", - "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.3.1", - "grunt-ng-annotate": "~0.4.0", - "grunt-contrib-uglify": "~0.6.0", - "grunt-contrib-cssmin": "~0.10.0", - "grunt-nodemon": "~0.3.0", - "grunt-concurrent": "~1.0.0", - "grunt-mocha-test": "~0.12.1", - "grunt-karma": "~0.9.0", - "load-grunt-tasks": "~1.0.0", - "grunt-contrib-copy": "0.8", - "karma": "~0.12.0", - "karma-jasmine": "~0.2.1", - "karma-coverage": "~0.2.0", - "karma-chrome-launcher": "~0.1.2", - "karma-firefox-launcher": "~0.1.3", - "karma-phantomjs-launcher": "~0.1.2" - } + "name": "meanjs", + "description": "Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js.", + "version": "0.4.0", + "private": false, + "author": "https://github.com/meanjs/mean/graphs/contributors", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/meanjs/mean.git" + }, + "engines": { + "node": ">=0.10.28", + "npm": ">=1.4.28" + }, + "scripts": { + "start": "grunt", + "test": "grunt test", + "postinstall": "bower install --config.interactive=false" + }, + "dependencies": { + "acl": "~0.4.4", + "async": "^1.3.0", + "body-parser": "^1.13.1", + "bower": "^1.4.1", + "chalk": "^1.1.0", + "compression": "^1.5.0", + "connect-flash": "~0.1.1", + "connect-mongo": "~0.8.1", + "consolidate": "~0.13.1", + "cookie-parser": "^1.3.2", + "express": "^4.13.1", + "express-session": "^1.11.3", + "forever": "~0.14.2", + "glob": "^5.0.13", + "grunt-cli": "~0.1.13", + "grunt": "0.4.5", + "helmet": "~0.9.1", + "jasmine-core": "^2.3.4", + "lodash": "^3.10.0", + "method-override": "^2.3.3", + "mocha": "~1.20.0", + "mongoose": "^4.0.6", + "morgan": "^1.6.1", + "multer": "0.1.8", + "node-pre-gyp": "0.6.4", + "nodemailer": "^1.4.0", + "passport": "~0.2.2", + "passport-facebook": "^2.0.0", + "passport-github": "~0.1.5", + "passport-google-oauth": "~0.2.0", + "passport-linkedin": "~0.1.3", + "passport-local": "^1.0.0", + "passport-paypal-openidconnect": "^0.1.1", + "passport-twitter": "^1.0.2", + "phantomjs": ">=1.9.0", + "serve-favicon": "^2.3.0", + "socket.io": "^1.3.5", + "swig": "^1.4.2", + "validator": "^3.41.2" + }, + "devDependencies": { + "grunt-concurrent": "^2.0.0", + "grunt-contrib-copy": "~0.8.0", + "grunt-contrib-csslint": "~0.4.0", + "grunt-contrib-cssmin": "~0.12.3", + "grunt-contrib-jshint": "~0.11.2", + "grunt-contrib-less": "^1.0.1", + "grunt-contrib-sass": "~0.9.2", + "grunt-contrib-uglify": "~0.9.1", + "grunt-contrib-watch": "~0.6.1", + "grunt-env": "~0.4.4", + "grunt-karma": "~0.11.2", + "grunt-mocha-test": "~0.12.7", + "grunt-ng-annotate": "^1.0.1", + "grunt-node-inspector": "~0.2.0", + "grunt-nodemon": "~0.4.0", + "grunt-protractor-runner": "^2.0.0", + "gulp": "^3.9.0", + "gulp-concat": "^2.6.0", + "gulp-csslint": "~0.1.5", + "gulp-cssmin": "~0.1.7", + "gulp-jshint": "^1.11.2", + "gulp-karma": "~0.0.4", + "gulp-less": "^3.0.3", + "gulp-livereload": "^3.8.0", + "gulp-load-plugins": "^1.0.0-rc.1", + "gulp-mocha": "^2.1.2", + "gulp-ng-annotate": "^1.0.0", + "gulp-nodemon": "^2.0.3", + "gulp-protractor": "^1.0.0", + "gulp-rename": "^1.2.2", + "gulp-sass": "^2.0.3", + "gulp-uglify": "^1.2.0", + "karma": "~0.12.37", + "karma-chrome-launcher": "~0.2.0", + "karma-coverage": "~0.4.2", + "karma-firefox-launcher": "~0.1.6", + "karma-jasmine": "~0.3.6", + "karma-ng-html2js-preprocessor": "^0.1.2", + "karma-phantomjs-launcher": "~0.2.0", + "load-grunt-tasks": "^3.2.0", + "run-sequence": "^1.1.1", + "should": "^7.0.1", + "supertest": "^1.0.1" + } } diff --git a/protractor.conf.js b/protractor.conf.js new file mode 100644 index 0000000000..cf92640b55 --- /dev/null +++ b/protractor.conf.js @@ -0,0 +1,6 @@ +'use strict'; + +// Protractor configuration +exports.config = { + specs: ['modules/*/tests/e2e/*.js'] +}; diff --git a/public/application.js b/public/application.js deleted file mode 100644 index 7aaf1c5df4..0000000000 --- a/public/application.js +++ /dev/null @@ -1,27 +0,0 @@ -'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 = '#!'; - - // Fixing google bug with redirect - if (window.location.href[window.location.href.length - 1] === '#' && - // for just the error url (origin + /#) - (window.location.href.length - window.location.origin.length) === 2) { - window.location.href = window.location.origin + '/#!'; - } - - //Then init the app - angular.bootstrap(document, [ApplicationConfiguration.applicationModuleName]); -}); diff --git a/public/config.js b/public/config.js deleted file mode 100644 index 75de1c4bce..0000000000 --- a/public/config.js +++ /dev/null @@ -1,23 +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, dependencies) { - // Create angular module - angular.module(moduleName, dependencies || []); - - // Add the module to the AngularJS configuration file - angular.module(applicationModuleName).requires.push(moduleName); - }; - - return { - applicationModuleName: applicationModuleName, - applicationModuleVendorDependencies: applicationModuleVendorDependencies, - registerModule: registerModule - }; -})(); \ No newline at end of file diff --git a/public/humans.txt b/public/humans.txt old mode 100755 new mode 100644 diff --git a/public/modules/articles/config/articles.client.config.js b/public/modules/articles/config/articles.client.config.js deleted file mode 100644 index 7e1b0ffd27..0000000000 --- a/public/modules/articles/config/articles.client.config.js +++ /dev/null @@ -1,11 +0,0 @@ -'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'); - } -]); \ No newline at end of file diff --git a/public/modules/articles/config/articles.client.routes.js b/public/modules/articles/config/articles.client.routes.js deleted file mode 100755 index 1531a9a57c..0000000000 --- a/public/modules/articles/config/articles.client.routes.js +++ /dev/null @@ -1,25 +0,0 @@ -'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' - }); - } -]); \ 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 deleted file mode 100644 index d90ec3dc5d..0000000000 --- a/public/modules/articles/controllers/articles.client.controller.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict'; - -// Articles controller -angular.module('articles').controller('ArticlesController', ['$scope', '$stateParams', '$location', 'Authentication', 'Articles', - function($scope, $stateParams, $location, Authentication, Articles) { - $scope.authentication = Authentication; - - // Create new Article - $scope.create = function() { - // Create new Article object - var article = new Articles({ - title: this.title, - content: this.content - }); - - // Redirect after save - article.$save(function(response) { - $location.path('articles/' + response._id); - - // Clear form fields - $scope.title = ''; - $scope.content = ''; - }, function(errorResponse) { - $scope.error = errorResponse.data.message; - }); - }; - - // Remove existing Article - $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'); - }); - } - }; - - // Update existing Article - $scope.update = function() { - var article = $scope.article; - - article.$update(function() { - $location.path('articles/' + article._id); - }, function(errorResponse) { - $scope.error = errorResponse.data.message; - }); - }; - - // Find a list of Articles - $scope.find = function() { - $scope.articles = Articles.query(); - }; - - // Find existing Article - $scope.findOne = function() { - $scope.article = Articles.get({ - articleId: $stateParams.articleId - }); - }; - } -]); \ No newline at end of file diff --git a/public/modules/articles/services/articles.client.service.js b/public/modules/articles/services/articles.client.service.js deleted file mode 100644 index deeb7da58c..0000000000 --- a/public/modules/articles/services/articles.client.service.js +++ /dev/null @@ -1,14 +0,0 @@ -'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' - } - }); - } -]); \ No newline at end of file diff --git a/public/modules/articles/tests/articles.client.controller.test.js b/public/modules/articles/tests/articles.client.controller.test.js deleted file mode 100644 index 677cb99431..0000000000 --- a/public/modules/articles/tests/articles.client.controller.test.js +++ /dev/null @@ -1,170 +0,0 @@ -'use strict'; - -(function() { - // Articles Controller Spec - describe('Articles Controller Tests', function() { - // Initialize global variables - var ArticlesController, - scope, - $httpBackend, - $stateParams, - $location; - - // The $resource service augments the response object with methods for updating and deleting the resource. - // If we were to use the standard toEqual matcher, our tests would fail because the test values would not match - // the responses exactly. To solve the problem, we define a new toEqualData Jasmine matcher. - // When the toEqualData matcher compares two objects, it takes only object properties into - // account and ignores methods. - beforeEach(function() { - jasmine.addMatchers({ - toEqualData: function(util, customEqualityTesters) { - return { - compare: function(actual, expected) { - return { - pass: angular.equals(actual, expected) - }; - } - }; - } - }); - }); - - // Then we can start by loading the main application module - beforeEach(module(ApplicationConfiguration.applicationModuleName)); - - // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). - // This allows us to inject a service but then attach it to a variable - // with the same name as the service. - beforeEach(inject(function($controller, $rootScope, _$location_, _$stateParams_, _$httpBackend_) { - // Set a new global scope - scope = $rootScope.$new(); - - // Point global variables to injected services - $stateParams = _$stateParams_; - $httpBackend = _$httpBackend_; - $location = _$location_; - - // Initialize the Articles controller. - ArticlesController = $controller('ArticlesController', { - $scope: scope - }); - })); - - it('$scope.find() should create an array with at least one article object fetched from XHR', inject(function(Articles) { - // Create sample article using the Articles service - var sampleArticle = new Articles({ - title: 'An Article about MEAN', - content: 'MEAN rocks!' - }); - - // Create a sample articles array that includes the new article - var sampleArticles = [sampleArticle]; - - // Set GET response - $httpBackend.expectGET('articles').respond(sampleArticles); - - // Run controller functionality - scope.find(); - $httpBackend.flush(); - - // Test scope value - expect(scope.articles).toEqualData(sampleArticles); - })); - - it('$scope.findOne() should create an array with one article object fetched from XHR using a articleId URL parameter', inject(function(Articles) { - // Define a sample article object - var sampleArticle = new Articles({ - title: 'An Article about MEAN', - content: 'MEAN rocks!' - }); - - // Set the URL parameter - $stateParams.articleId = '525a8422f6d0f87f0e407a33'; - - // Set GET response - $httpBackend.expectGET(/articles\/([0-9a-fA-F]{24})$/).respond(sampleArticle); - - // Run controller functionality - scope.findOne(); - $httpBackend.flush(); - - // Test scope value - expect(scope.article).toEqualData(sampleArticle); - })); - - it('$scope.create() with valid form data should send a POST request with the form input values and then locate to new object URL', inject(function(Articles) { - // Create a sample article object - var sampleArticlePostData = new Articles({ - title: 'An Article about MEAN', - content: 'MEAN rocks!' - }); - - // Create a sample article response - var sampleArticleResponse = new Articles({ - _id: '525cf20451979dea2c000001', - title: 'An Article about MEAN', - content: 'MEAN rocks!' - }); - - // Fixture mock form input values - scope.title = 'An Article about MEAN'; - scope.content = 'MEAN rocks!'; - - // Set POST response - $httpBackend.expectPOST('articles', sampleArticlePostData).respond(sampleArticleResponse); - - // Run controller functionality - scope.create(); - $httpBackend.flush(); - - // Test form inputs are reset - expect(scope.title).toEqual(''); - expect(scope.content).toEqual(''); - - // Test URL redirection after the article was created - expect($location.path()).toBe('/articles/' + sampleArticleResponse._id); - })); - - it('$scope.update() should update a valid article', inject(function(Articles) { - // Define a sample article put data - var sampleArticlePutData = new Articles({ - _id: '525cf20451979dea2c000001', - title: 'An Article about MEAN', - content: 'MEAN Rocks!' - }); - - // Mock article in scope - scope.article = sampleArticlePutData; - - // Set PUT response - $httpBackend.expectPUT(/articles\/([0-9a-fA-F]{24})$/).respond(); - - // Run controller functionality - scope.update(); - $httpBackend.flush(); - - // Test URL location to new object - expect($location.path()).toBe('/articles/' + sampleArticlePutData._id); - })); - - it('$scope.remove() should send a DELETE request with a valid articleId and remove the article from the scope', inject(function(Articles) { - // Create new article object - var sampleArticle = new Articles({ - _id: '525a8422f6d0f87f0e407a33' - }); - - // Create new articles array and include the article - scope.articles = [sampleArticle]; - - // Set expected DELETE response - $httpBackend.expectDELETE(/articles\/([0-9a-fA-F]{24})$/).respond(204); - - // Run controller functionality - scope.remove(sampleArticle); - $httpBackend.flush(); - - // Test array after successful delete - expect(scope.articles.length).toBe(0); - })); - }); -}()); \ No newline at end of file diff --git a/public/modules/articles/views/create-article.client.view.html b/public/modules/articles/views/create-article.client.view.html deleted file mode 100644 index ab8db8ef61..0000000000 --- a/public/modules/articles/views/create-article.client.view.html +++ /dev/null @@ -1,29 +0,0 @@ -
- -
-
-
-
- -
- -
-
-
- -
- -
-
-
- -
-
- -
-
-
-
-
\ No newline at end of file diff --git a/public/modules/articles/views/edit-article.client.view.html b/public/modules/articles/views/edit-article.client.view.html deleted file mode 100644 index 353cb8e666..0000000000 --- a/public/modules/articles/views/edit-article.client.view.html +++ /dev/null @@ -1,35 +0,0 @@ -
- -
-
-
-
- -
- -
-
-

Title is required

-
-
-
- -
- -
-
-

Content is required

-
-
-
- -
-
- -
-
-
-
-
\ No newline at end of file diff --git a/public/modules/articles/views/list-articles.client.view.html b/public/modules/articles/views/list-articles.client.view.html deleted file mode 100644 index 861ae5b6ba..0000000000 --- a/public/modules/articles/views/list-articles.client.view.html +++ /dev/null @@ -1,20 +0,0 @@ -
- - -
- No articles yet, why don't you create one? -
-
\ No newline at end of file diff --git a/public/modules/articles/views/view-article.client.view.html b/public/modules/articles/views/view-article.client.view.html deleted file mode 100644 index 312d25c84e..0000000000 --- a/public/modules/articles/views/view-article.client.view.html +++ /dev/null @@ -1,22 +0,0 @@ -
- - - - - Posted on - - by - - - -

-
\ No newline at end of file diff --git a/public/modules/core/config/core.client.routes.js b/public/modules/core/config/core.client.routes.js deleted file mode 100755 index 894e3a6caf..0000000000 --- a/public/modules/core/config/core.client.routes.js +++ /dev/null @@ -1,16 +0,0 @@ -'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' - }); - } -]); \ No newline at end of file diff --git a/public/modules/core/controllers/header.client.controller.js b/public/modules/core/controllers/header.client.controller.js deleted file mode 100644 index 1b8c2b7bec..0000000000 --- a/public/modules/core/controllers/header.client.controller.js +++ /dev/null @@ -1,18 +0,0 @@ -'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; - }); - } -]); \ No newline at end of file diff --git a/public/modules/core/controllers/home.client.controller.js b/public/modules/core/controllers/home.client.controller.js deleted file mode 100644 index 63d0f297ae..0000000000 --- a/public/modules/core/controllers/home.client.controller.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - - -angular.module('core').controller('HomeController', ['$scope', 'Authentication', - function($scope, Authentication) { - // This provides Authentication context. - $scope.authentication = Authentication; - } -]); \ No newline at end of file diff --git a/public/modules/core/core.client.module.js b/public/modules/core/core.client.module.js deleted file mode 100755 index 01c9d321fe..0000000000 --- a/public/modules/core/core.client.module.js +++ /dev/null @@ -1,4 +0,0 @@ -'use strict'; - -// Use Application configuration module to register a new module -ApplicationConfiguration.registerModule('core'); diff --git a/public/modules/core/css/core.css b/public/modules/core/css/core.css deleted file mode 100644 index f20a04c936..0000000000 --- a/public/modules/core/css/core.css +++ /dev/null @@ -1,20 +0,0 @@ -.content { - margin-top: 50px; -} -.undecorated-link:hover { - text-decoration: none; -} -[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { - display: none !important; -} -.ng-invalid.ng-dirty { - border-color: #FA787E; -} -.ng-valid.ng-dirty { - border-color: #78FA89; -} -.browsehappy.jumbotron.hide, -body.ng-cloak -{ - display: block; -} diff --git a/public/modules/core/services/menus.client.service.js b/public/modules/core/services/menus.client.service.js deleted file mode 100644 index d2366d1de3..0000000000 --- a/public/modules/core/services/menus.client.service.js +++ /dev/null @@ -1,166 +0,0 @@ -'use strict'; - -//Menu service used for managing menus -angular.module('core').service('Menus', [ - - function() { - // Define a set of default roles - this.defaultRoles = ['*']; - - // Define the menus object - this.menus = {}; - - // A private function for rendering decision - var shouldRender = function(user) { - if (user) { - if (!!~this.roles.indexOf('*')) { - return true; - } else { - 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, position) { - // 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 === null || typeof isPublic === 'undefined') ? this.menus[menuId].isPublic : isPublic), - roles: ((roles === null || typeof roles === 'undefined') ? this.menus[menuId].roles : roles), - position: position || 0, - 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, position) { - // 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 === null || typeof isPublic === 'undefined') ? this.menus[menuId].items[itemIndex].isPublic : isPublic), - roles: ((roles === null || typeof roles === 'undefined') ? this.menus[menuId].items[itemIndex].roles : roles), - position: position || 0, - 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'); - } -]); \ No newline at end of file diff --git a/public/modules/core/tests/header.client.controller.test.js b/public/modules/core/tests/header.client.controller.test.js deleted file mode 100644 index 76ee4fb4e7..0000000000 --- a/public/modules/core/tests/header.client.controller.test.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -(function() { - describe('HeaderController', function() { - //Initialize global variables - var scope, - HeaderController; - - // Load the main application module - beforeEach(module(ApplicationConfiguration.applicationModuleName)); - - beforeEach(inject(function($controller, $rootScope) { - scope = $rootScope.$new(); - - HeaderController = $controller('HeaderController', { - $scope: scope - }); - })); - - it('should expose the authentication service', function() { - expect(scope.authentication).toBeTruthy(); - }); - }); -})(); \ No newline at end of file diff --git a/public/modules/core/tests/home.client.controller.test.js b/public/modules/core/tests/home.client.controller.test.js deleted file mode 100644 index a5b1a566d5..0000000000 --- a/public/modules/core/tests/home.client.controller.test.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -(function() { - describe('HomeController', function() { - //Initialize global variables - var scope, - HomeController; - - // Load the main application module - beforeEach(module(ApplicationConfiguration.applicationModuleName)); - - beforeEach(inject(function($controller, $rootScope) { - scope = $rootScope.$new(); - - HomeController = $controller('HomeController', { - $scope: scope - }); - })); - - it('should expose the authentication service', function() { - expect(scope.authentication).toBeTruthy(); - }); - }); -})(); \ No newline at end of file diff --git a/public/modules/core/views/header.client.view.html b/public/modules/core/views/header.client.view.html deleted file mode 100644 index 541aa3a8d4..0000000000 --- a/public/modules/core/views/header.client.view.html +++ /dev/null @@ -1,58 +0,0 @@ -
- - -
\ No newline at end of file diff --git a/public/modules/core/views/home.client.view.html b/public/modules/core/views/home.client.view.html deleted file mode 100644 index 2625a9845e..0000000000 --- a/public/modules/core/views/home.client.view.html +++ /dev/null @@ -1,94 +0,0 @@ -
-
-
-
- MEAN.JS -
-
-
-
-

- Open-Source Full-Stack Solution For MEAN Applications -

-
-
-

- Learn more -

-
-
-
-

Congrats! You've configured and ran the sample application successfully.

-

MEAN.JS is a web application boilerplate, which means you should start changing everything :-)

-

This sample application tracks users and articles.

-
    -
  • - Click - Signup - to get started. -
  • -
  • - Configure your app to work with your social accounts, by editing the - /config/env/*.js - files. -
  • -
  • - Edit your users module. -
  • -
  • - Add new CRUD modules. -
  • -
  • - Have fun... -
  • -
-
-
-
-

- MongoDB -

-

MongoDB is a database. MongoDB's great manual is the place to get started with NoSQL and MongoDB.

-
-
-

- Express -

-

Express is an app server. Check out The ExpressJS API reference for more information or StackOverflow for more info.

-
-
-

- AngularJS -

-

AngularJS is web app framework. Angular's website offers a lot. The Thinkster Popular Guide and Egghead Videos are great resources.

-
-
-

- Node.js -

-

Node.js is a web server. Node's website and this stackOverflow thread offer excellent starting points to get to grasps with node.

-
-
-
-

MEAN.JS Documentation

-

- Once you're familiar with the foundation technology, check out the MEAN.JS Documentation: -

-

-
-
Enjoy & Keep Us Updated, -
The MEAN.JS Team. -
diff --git a/public/modules/users/config/users.client.config.js b/public/modules/users/config/users.client.config.js deleted file mode 100644 index 0bfc8b640b..0000000000 --- a/public/modules/users/config/users.client.config.js +++ /dev/null @@ -1,30 +0,0 @@ -'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); - } - }; - } - ]); - } -]); \ No newline at end of file diff --git a/public/modules/users/config/users.client.routes.js b/public/modules/users/config/users.client.routes.js deleted file mode 100755 index 879c2c47b8..0000000000 --- a/public/modules/users/config/users.client.routes.js +++ /dev/null @@ -1,45 +0,0 @@ -'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/authentication/signup.client.view.html' - }). - state('signin', { - url: '/signin', - templateUrl: 'modules/users/views/authentication/signin.client.view.html' - }). - state('forgot', { - url: '/password/forgot', - templateUrl: 'modules/users/views/password/forgot-password.client.view.html' - }). - state('reset-invalid', { - url: '/password/reset/invalid', - templateUrl: 'modules/users/views/password/reset-password-invalid.client.view.html' - }). - state('reset-success', { - url: '/password/reset/success', - templateUrl: 'modules/users/views/password/reset-password-success.client.view.html' - }). - state('reset', { - url: '/password/reset/:token', - templateUrl: 'modules/users/views/password/reset-password.client.view.html' - }); - } -]); \ No newline at end of file diff --git a/public/modules/users/controllers/authentication.client.controller.js b/public/modules/users/controllers/authentication.client.controller.js deleted file mode 100644 index 3e27cc3b88..0000000000 --- a/public/modules/users/controllers/authentication.client.controller.js +++ /dev/null @@ -1,34 +0,0 @@ -'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; - }); - }; - } -]); \ No newline at end of file diff --git a/public/modules/users/controllers/password.client.controller.js b/public/modules/users/controllers/password.client.controller.js deleted file mode 100644 index dbc9e92977..0000000000 --- a/public/modules/users/controllers/password.client.controller.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - -angular.module('users').controller('PasswordController', ['$scope', '$stateParams', '$http', '$location', 'Authentication', - function($scope, $stateParams, $http, $location, Authentication) { - $scope.authentication = Authentication; - - //If user is signed in then redirect back home - if ($scope.authentication.user) $location.path('/'); - - // Submit forgotten password account id - $scope.askForPasswordReset = function() { - $scope.success = $scope.error = null; - - $http.post('/auth/forgot', $scope.credentials).success(function(response) { - // Show user success message and clear form - $scope.credentials = null; - $scope.success = response.message; - - }).error(function(response) { - // Show user error message and clear form - $scope.credentials = null; - $scope.error = response.message; - }); - }; - - // Change user password - $scope.resetUserPassword = function() { - $scope.success = $scope.error = null; - - $http.post('/auth/reset/' + $stateParams.token, $scope.passwordDetails).success(function(response) { - // If successful show success message and clear form - $scope.passwordDetails = null; - - // Attach user profile - Authentication.user = response; - - // And redirect to the index page - $location.path('/password/reset/success'); - }).error(function(response) { - $scope.error = response.message; - }); - }; - } -]); \ No newline at end of file diff --git a/public/modules/users/controllers/settings.client.controller.js b/public/modules/users/controllers/settings.client.controller.js deleted file mode 100644 index 8616fc9463..0000000000 --- a/public/modules/users/controllers/settings.client.controller.js +++ /dev/null @@ -1,71 +0,0 @@ -'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(isValid) { - if (isValid) { - $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; - }); - } else { - $scope.submitted = true; - } - }; - - // 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; - }); - }; - } -]); \ No newline at end of file diff --git a/public/modules/users/css/users.css b/public/modules/users/css/users.css deleted file mode 100644 index de67bf94f5..0000000000 --- a/public/modules/users/css/users.css +++ /dev/null @@ -1,14 +0,0 @@ -@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 diff --git a/public/modules/users/services/authentication.client.service.js b/public/modules/users/services/authentication.client.service.js deleted file mode 100644 index 56225dba39..0000000000 --- a/public/modules/users/services/authentication.client.service.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -// Authentication service for user variables -angular.module('users').factory('Authentication', ['$window', function($window) { - var auth = { - user: $window.user - }; - - return auth; -}]); diff --git a/public/modules/users/services/users.client.service.js b/public/modules/users/services/users.client.service.js deleted file mode 100644 index 664828f0a5..0000000000 --- a/public/modules/users/services/users.client.service.js +++ /dev/null @@ -1,12 +0,0 @@ -'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/modules/users/tests/authentication.client.controller.test.js b/public/modules/users/tests/authentication.client.controller.test.js deleted file mode 100644 index 4c95d686ae..0000000000 --- a/public/modules/users/tests/authentication.client.controller.test.js +++ /dev/null @@ -1,118 +0,0 @@ -'use strict'; - -(function() { - // Authentication controller Spec - describe('AuthenticationController', function() { - // Initialize global variables - var AuthenticationController, - scope, - $httpBackend, - $stateParams, - $location; - - beforeEach(function() { - jasmine.addMatchers({ - toEqualData: function(util, customEqualityTesters) { - return { - compare: function(actual, expected) { - return { - pass: angular.equals(actual, expected) - }; - } - }; - } - }); - }); - - // Load the main application module - beforeEach(module(ApplicationConfiguration.applicationModuleName)); - - // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). - // This allows us to inject a service but then attach it to a variable - // with the same name as the service. - beforeEach(inject(function($controller, $rootScope, _$location_, _$stateParams_, _$httpBackend_) { - // Set a new global scope - scope = $rootScope.$new(); - - // Point global variables to injected services - $stateParams = _$stateParams_; - $httpBackend = _$httpBackend_; - $location = _$location_; - - // Initialize the Authentication controller - AuthenticationController = $controller('AuthenticationController', { - $scope: scope - }); - })); - - - it('$scope.signin() should login with a correct user and password', function() { - // Test expected GET request - $httpBackend.when('POST', '/auth/signin').respond(200, 'Fred'); - - scope.signin(); - $httpBackend.flush(); - - // Test scope value - expect(scope.authentication.user).toEqual('Fred'); - expect($location.url()).toEqual('/'); - }); - - it('$scope.signin() should fail to log in with nothing', function() { - // Test expected POST request - $httpBackend.expectPOST('/auth/signin').respond(400, { - 'message': 'Missing credentials' - }); - - scope.signin(); - $httpBackend.flush(); - - // Test scope value - expect(scope.error).toEqual('Missing credentials'); - }); - - it('$scope.signin() should fail to log in with wrong credentials', function() { - // Foo/Bar combo assumed to not exist - scope.authentication.user = 'Foo'; - scope.credentials = 'Bar'; - - // Test expected POST request - $httpBackend.expectPOST('/auth/signin').respond(400, { - 'message': 'Unknown user' - }); - - scope.signin(); - $httpBackend.flush(); - - // Test scope value - expect(scope.error).toEqual('Unknown user'); - }); - - it('$scope.signup() should register with correct data', function() { - // Test expected GET request - scope.authentication.user = 'Fred'; - $httpBackend.when('POST', '/auth/signup').respond(200, 'Fred'); - - scope.signup(); - $httpBackend.flush(); - - // test scope value - expect(scope.authentication.user).toBe('Fred'); - expect(scope.error).toEqual(undefined); - expect($location.url()).toBe('/'); - }); - - it('$scope.signup() should fail to register with duplicate Username', function() { - // Test expected POST request - $httpBackend.when('POST', '/auth/signup').respond(400, { - 'message': 'Username already exists' - }); - - scope.signup(); - $httpBackend.flush(); - - // Test scope value - expect(scope.error).toBe('Username already exists'); - }); - }); -}()); \ No newline at end of file diff --git a/public/modules/users/users.client.module.js b/public/modules/users/users.client.module.js deleted file mode 100755 index 7b2f6465cb..0000000000 --- a/public/modules/users/users.client.module.js +++ /dev/null @@ -1,4 +0,0 @@ -'use strict'; - -// Use Application configuration module to register a new module -ApplicationConfiguration.registerModule('users'); \ No newline at end of file diff --git a/public/modules/users/views/authentication/signin.client.view.html b/public/modules/users/views/authentication/signin.client.view.html deleted file mode 100644 index 91e256eff2..0000000000 --- a/public/modules/users/views/authentication/signin.client.view.html +++ /dev/null @@ -1,45 +0,0 @@ -
-

Sign in using your social accounts

- -

Or with your account

-
- -
-
\ No newline at end of file diff --git a/public/modules/users/views/authentication/signup.client.view.html b/public/modules/users/views/authentication/signup.client.view.html deleted file mode 100644 index e2051760a0..0000000000 --- a/public/modules/users/views/authentication/signup.client.view.html +++ /dev/null @@ -1,54 +0,0 @@ -
-

Sign up using your social accounts

- -

Or with your email

-
- -
-
\ No newline at end of file diff --git a/public/modules/users/views/password/forgot-password.client.view.html b/public/modules/users/views/password/forgot-password.client.view.html deleted file mode 100644 index e6275f941f..0000000000 --- a/public/modules/users/views/password/forgot-password.client.view.html +++ /dev/null @@ -1,22 +0,0 @@ -
-

Restore your password

-

Enter your account username.

-
- -
-
\ No newline at end of file diff --git a/public/modules/users/views/password/reset-password-invalid.client.view.html b/public/modules/users/views/password/reset-password-invalid.client.view.html deleted file mode 100644 index d5fc23733d..0000000000 --- a/public/modules/users/views/password/reset-password-invalid.client.view.html +++ /dev/null @@ -1,4 +0,0 @@ -
-

Password reset is invalid

- Ask for a new password reset -
\ No newline at end of file diff --git a/public/modules/users/views/password/reset-password-success.client.view.html b/public/modules/users/views/password/reset-password-success.client.view.html deleted file mode 100644 index 4de46c4b22..0000000000 --- a/public/modules/users/views/password/reset-password-success.client.view.html +++ /dev/null @@ -1,4 +0,0 @@ -
-

Password successfully reset

- Continue to home page -
\ No newline at end of file diff --git a/public/modules/users/views/password/reset-password.client.view.html b/public/modules/users/views/password/reset-password.client.view.html deleted file mode 100644 index dc8b2ea0c4..0000000000 --- a/public/modules/users/views/password/reset-password.client.view.html +++ /dev/null @@ -1,26 +0,0 @@ -
-

Reset your password

-
- -
-
\ No newline at end of file diff --git a/public/modules/users/views/settings/change-password.client.view.html b/public/modules/users/views/settings/change-password.client.view.html deleted file mode 100644 index 9811011a53..0000000000 --- a/public/modules/users/views/settings/change-password.client.view.html +++ /dev/null @@ -1,30 +0,0 @@ -
-

Change your password

-
- -
-
\ No newline at end of file diff --git a/public/modules/users/views/settings/edit-profile.client.view.html b/public/modules/users/views/settings/edit-profile.client.view.html deleted file mode 100644 index a4be680f41..0000000000 --- a/public/modules/users/views/settings/edit-profile.client.view.html +++ /dev/null @@ -1,34 +0,0 @@ -
-

Edit your profile

-
- -
-
\ No newline at end of file diff --git a/public/modules/users/views/settings/social-accounts.client.view.html b/public/modules/users/views/settings/social-accounts.client.view.html deleted file mode 100644 index 4712ee093b..0000000000 --- a/public/modules/users/views/settings/social-accounts.client.view.html +++ /dev/null @@ -1,29 +0,0 @@ -
-

Connected social accounts:

-
- -
-

Connect other social accounts:

- -
\ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt old mode 100755 new mode 100644 diff --git a/scripts/generate-ssl-certs.sh b/scripts/generate-ssl-certs.sh old mode 100644 new mode 100755 index f09002c00f..b9fd595829 --- a/scripts/generate-ssl-certs.sh +++ b/scripts/generate-ssl-certs.sh @@ -10,7 +10,7 @@ fi echo "Generating self-signed certificates..." mkdir -p ./config/sslcerts -openssl genrsa -out ./config/sslcerts/key.pem -aes256 1024 +openssl genrsa -out ./config/sslcerts/key.pem 1024 openssl req -new -key ./config/sslcerts/key.pem -out ./config/sslcerts/csr.pem openssl x509 -req -days 9999 -in ./config/sslcerts/csr.pem -signkey ./config/sslcerts/key.pem -out ./config/sslcerts/cert.pem rm ./config/sslcerts/csr.pem diff --git a/scripts/reset-password.js b/scripts/reset-password.js new file mode 100755 index 0000000000..8a268567fa --- /dev/null +++ b/scripts/reset-password.js @@ -0,0 +1,50 @@ +var nodemailer = require('nodemailer'), + mongoose = require('mongoose'), + config = require('../config/config'), + mg = require('../config/lib/mongoose'); + +var transporter = nodemailer.createTransport(config.mailer.options); +var link = 'reset link here'; // PUT reset link here + +mg.connect(function(db) { + var User = mongoose.model('User'); + + User.find().exec(function(err, users) { + if (err) { + throw err; + } + + var email = { + from: 'noreply@xyz.com', + subject: 'Security update' + }; + + for (var i = 0; i < users.length; i++) { + var text = [ + 'Dear ' + users[i].displayName, + '\n', + 'We have updated our password storage systems to be more secure and more efficient, please click the link below to reset your password so you can login in the future.', + link, + '\n', + 'Thanks,', + 'The Team' + ].join('\n'); + + email.to = users[i].email; + email.text = text; + email.html = text; + + transporter.sendMail(email, function(err, info) { + if (err) { + console.log('Error: ', err); + console.log('Could not send email for ', users[i].displayName); + } else { + console.log('Sent reset password email for ', users[i].displayName); + } + }); + } + + console.log('Sent all emails'); + process.exit(0); + }); +}); diff --git a/server.js b/server.js old mode 100755 new mode 100644 index 6d5f4c2d30..8fa7b2e044 --- a/server.js +++ b/server.js @@ -1,49 +1,7 @@ 'use strict'; -/** - * Module dependencies. - */ -var init = require('./config/init')(), - config = require('./config/config'), - mongoose = require('mongoose'), - chalk = require('chalk'); /** - * Main application entry file. - * Please note that the order of loading is important. + * Module dependencies. */ - -// Bootstrap db connection -var db = mongoose.connect(config.db.uri, config.db.options, function(err) { - if (err) { - console.error(chalk.red('Could not connect to MongoDB!')); - console.log(chalk.red(err)); - } -}); -mongoose.connection.on('error', function(err) { - console.error(chalk.red('MongoDB connection error: ' + err)); - process.exit(-1); - } -); - -// Init the express application -var app = require('./config/express')(db); - -// Bootstrap passport config -require('./config/passport')(); - -// Start the app by listening on -app.listen(config.port); - -// Expose app -exports = module.exports = app; - -// Logging initialization -console.log('--'); -console.log(chalk.green(config.app.title + ' application started')); -console.log(chalk.green('Environment:\t\t\t' + process.env.NODE_ENV)); -console.log(chalk.green('Port:\t\t\t\t' + config.port)); -console.log(chalk.green('Database:\t\t\t' + config.db.uri)); -if (process.env.NODE_ENV === 'secure') { - console.log(chalk.green('HTTPs:\t\t\t\ton')); -} -console.log('--'); +var app = require('./config/lib/app'); +var server = app.start();