diff --git a/.editorconfig b/.editorconfig index ed90a06898..5f5f16c70c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,25 +1,36 @@ # 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 = tab quote_type = single curly_bracket_next_line = false spaces_around_operators = true space_after_control_statements = true space_after_anonymous_functions = false spaces_in_brackets = false + +# 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 \ No newline at end of file diff --git a/.gitignore b/.gitignore index c519f47471..8eb8d6fd26 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ app/tests/coverage/ config/sslcerts/*.pem access.log public/dist/ +uploads +modules/users/client/img/profile/uploads +*.pem # Sublime editor # ============== diff --git a/.jshintrc b/.jshintrc index 4cd07cdcab..b3a00dc361 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,5 +1,7 @@ { "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.). @@ -18,25 +20,17 @@ "trailing": true, // Prohibit trailing whitespaces. "smarttabs": false, // Suppresses warnings about mixed tabs and spaces "globals": { // Globals variables. - "jasmine": true, "angular": true, + "io": true, "ApplicationConfiguration": true }, "predef": [ // Extra globals. - "define", - "require", - "exports", - "module", - "describe", - "before", - "beforeEach", - "after", - "afterEach", - "it", "inject", - "expect" + "by", + "browser", + "element" ], "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 +} 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/Procfile b/Procfile old mode 100755 new mode 100644 diff --git a/README.md b/README.md index 4b1a5accbe..276db76565 100644 --- a/README.md +++ b/README.md @@ -64,12 +64,12 @@ $ 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 ``` 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/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.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/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/bower.json b/bower.json index 51e865536a..3a959bd1b0 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "meanjs", - "version": "0.3.2", + "version": "0.4.0", "description": "Fullstack JavaScript with MongoDB, Express, AngularJS, and Node.js.", "dependencies": { "bootstrap": "~3", @@ -9,8 +9,8 @@ "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" + "angular-ui-router": "~0.2.11", + "angular-file-upload": "~1.1.5" } -} +} \ No newline at end of file diff --git a/config/assets/default.js b/config/assets/default.js new file mode 100644 index 0000000000..42f1b1062a --- /dev/null +++ b/config/assets/default.js @@ -0,0 +1,47 @@ +'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: { + allJS: ['gruntfile.js', '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..b521b6577d --- /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..9a664c455b --- /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..29ffa89c9e --- /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..85458853b9 100644 --- a/config/config.js +++ b/config/config.js @@ -4,87 +4,161 @@ * 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('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'; + } else { + console.log(chalk.bold('Application loaded using the "' + process.env.NODE_ENV + '" environment configuration')); + } + // Reset console color + console.log(chalk.white('')); +}; /** - * Get files by glob patterns + * Initialize global configuration files */ -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 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),'/')); }; /** - * 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 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); - // To include tests - if (includeTests) { - output = _.union(output, this.getGlobbedFiles(this.assets.tests)); - } + // Setting Globbed config files + config.files.server.configs = getGlobbedPaths(assets.server.config); - return output; + // Setting Globbed socket files + config.files.server.sockets = getGlobbedPaths(assets.server.sockets); + + // Setting Globbed policies files + config.files.server.policies = getGlobbedPaths(assets.server.policies); + + // Setting Globbed js files + config.files.client.js = getGlobbedPaths(assets.client.lib.js, 'public/').concat(getGlobbedPaths(assets.client.js, ['client/', 'public/'])); + + // 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 = _.extend(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 = _.extend(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); + + // 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..8acd131a0b --- /dev/null +++ b/config/env/default.js @@ -0,0 +1,14 @@ +'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' +}; diff --git a/config/env/development.js b/config/env/development.js index 35c08b7e8e..9b31b3b04f 100644 --- a/config/env/development.js +++ b/config/env/development.js @@ -1,13 +1,7 @@ 'use strict'; module.exports = { - db: { - uri: 'mongodb://localhost/mean-dev', - options: { - user: '', - pass: '' - } - }, + db: 'mongodb://localhost/mean-dev', log: { // Can specify one of 'combined', 'common', 'dev', 'short', 'tiny' format: 'dev', @@ -21,30 +15,30 @@ module.exports = { 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' - }, + 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' + }, mailer: { from: process.env.MAILER_FROM || 'MAILER_FROM', options: { diff --git a/config/env/production.js b/config/env/production.js index a56a48f0e5..275be8084d 100644 --- a/config/env/production.js +++ b/config/env/production.js @@ -1,6 +1,8 @@ 'use strict'; module.exports = { + 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: { @@ -38,27 +40,27 @@ module.exports = { facebook: { clientID: process.env.FACEBOOK_ID || 'APP_ID', clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET', - callbackURL: '/auth/facebook/callback' + callbackURL: '/api/auth/facebook/callback' }, twitter: { clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY', clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET', - callbackURL: '/auth/twitter/callback' + callbackURL: '/api/auth/twitter/callback' }, google: { clientID: process.env.GOOGLE_ID || 'APP_ID', clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET', - callbackURL: '/auth/google/callback' + callbackURL: '/api/auth/google/callback' }, linkedin: { clientID: process.env.LINKEDIN_ID || 'APP_ID', clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', - callbackURL: '/auth/linkedin/callback' + callbackURL: '/api/auth/linkedin/callback' }, github: { clientID: process.env.GITHUB_ID || 'APP_ID', clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET', - callbackURL: '/auth/github/callback' + callbackURL: '/api/auth/github/callback' }, mailer: { from: process.env.MAILER_FROM || 'MAILER_FROM', 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..3959a5bfc6 100644 --- a/config/env/test.js +++ b/config/env/test.js @@ -9,42 +9,33 @@ module.exports = { } }, 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' + callbackURL: '/api/auth/facebook/callback' }, twitter: { clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY', clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET', - callbackURL: '/auth/twitter/callback' + callbackURL: '/api/auth/twitter/callback' }, google: { clientID: process.env.GOOGLE_ID || 'APP_ID', clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET', - callbackURL: '/auth/google/callback' + callbackURL: '/api/auth/google/callback' }, linkedin: { clientID: process.env.LINKEDIN_ID || 'APP_ID', clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', - callbackURL: '/auth/linkedin/callback' + callbackURL: '/api/auth/linkedin/callback' }, github: { clientID: process.env.GITHUB_ID || 'APP_ID', clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET', - callbackURL: '/auth/github/callback' + callbackURL: '/api/auth/github/callback' }, mailer: { from: process.env.MAILER_FROM || 'MAILER_FROM', 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/express.js b/config/lib/express.js new file mode 100644 index 0000000000..a208695df4 --- /dev/null +++ b/config/lib/express.js @@ -0,0 +1,252 @@ +'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'), + passport = require('passport'), + 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; + + // 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) { + // 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); + + // Redirect to error page + res.redirect('/server-error'); + }); + + // Assume 404 since no middleware responded + app.use(function (req, res) { + // Redirect to not found page + res.redirect('/not-found'); + }); +}; + +/** + * 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..51eb37f76a --- /dev/null +++ b/config/lib/mongoose.js @@ -0,0 +1,43 @@ +'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, function (err) { + // Log Error + if (err) { + console.error(chalk.red('Could not connect to MongoDB!')); + console.log(err); + } else { + // Load modules + _this.loadModels(); + + // 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..18adffd0d0 --- /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/gruntfile.js b/gruntfile.js index c028ff7751..e8abfe71ab 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -1,65 +1,102 @@ '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'] - }; +/** + * Module dependencies. + */ +var _ = require('lodash'), + defaultAssets = require('./config/assets/default'), + testAssets = require('./config/assets/test'), + fs = require('fs'); +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: watchFiles.serverViews, + files: defaultAssets.server.views, options: { livereload: true } }, serverJS: { - files: watchFiles.serverJS, + files: defaultAssets.server.allJS, tasks: ['jshint'], options: { livereload: true } }, clientViews: { - files: watchFiles.clientViews, + files: defaultAssets.client.views, options: { livereload: true } }, clientJS: { - files: watchFiles.clientJS, + files: defaultAssets.client.js, tasks: ['jshint'], options: { livereload: true } }, clientCSS: { - files: watchFiles.clientCSS, + files: defaultAssets.client.css, tasks: ['csslint'], options: { livereload: true } }, - mochaTests: { - files: watchFiles.mochaTests, - tasks: ['test:server'], + 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.views, defaultAssets.server.allJS, defaultAssets.server.config) + } + } + }, + concurrent: { + default: ['nodemon', 'watch'], + debug: ['nodemon', 'watch', 'node-inspector'], + options: { + logConcurrentOutput: true } }, jshint: { all: { - src: watchFiles.clientJS.concat(watchFiles.serverJS), + src: _.union(defaultAssets.server.allJS, defaultAssets.client.js, testAssets.tests.server, testAssets.tests.client, testAssets.tests.e2e), options: { - jshintrc: true + jshintrc: true, + node: true, + mocha: true, + jasmine: true } } }, @@ -68,7 +105,14 @@ module.exports = function(grunt) { csslintrc: '.csslintrc' }, all: { - src: watchFiles.clientCSS + src: defaultAssets.client.css + } + }, + ngAnnotate: { + production: { + files: { + 'public/dist/application.js': defaultAssets.client.js + } } }, uglify: { @@ -84,18 +128,32 @@ module.exports = function(grunt) { cssmin: { combine: { files: { - 'public/dist/application.min.css': '<%= applicationCSSFiles %>' + 'public/dist/application.min.css': defaultAssets.client.css } } }, - nodemon: { - dev: { - script: 'server.js', - options: { - nodeArgs: ['--debug'], - ext: 'js,html', - watch: watchFiles.serverViews.concat(watchFiles.serverJS) - } + 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': { @@ -111,34 +169,10 @@ module.exports = function(grunt) { } } }, - 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, + src: testAssets.tests.server, options: { - reporter: 'spec', - require: 'server.js' + reporter: 'spec' } }, karma: { @@ -146,6 +180,18 @@ module.exports = function(grunt) { 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', @@ -153,42 +199,45 @@ module.exports = function(grunt) { filter: function() { return !fs.existsSync('config/env/local.js'); } - } + } } }); - // Load NPM tasks + // 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'); + // 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'); - grunt.config.set('applicationJavaScriptFiles', config.assets.js); - grunt.config.set('applicationCSSFiles', config.assets.css); + // Connect to database + mongoose.connect(function(db) { + done(); + }); }); - // Default task(s). - grunt.registerTask('default', ['lint', 'copy:localConfig', 'concurrent:default']); + // Lint CSS and JavaScript files. + grunt.registerTask('lint', ['sass', 'less', 'jshint', 'csslint']); - // Debug task. - grunt.registerTask('debug', ['lint', 'copy:localConfig', 'concurrent:debug']); + // Lint project files and minify them into two production files. + grunt.registerTask('build', ['env:dev', 'lint', 'ngAnnotate', 'uglify', 'cssmin']); - // Secure task(s). - grunt.registerTask('secure', ['env:secure', 'lint', 'copy:localConfig', 'concurrent:default']); + // Run the project tests + grunt.registerTask('test', ['env:test', 'copy:localConfig', 'mongoose', 'mochaTest', 'karma:unit']); - // Lint task(s). - grunt.registerTask('lint', ['jshint', 'csslint']); + // Run the project in development mode + grunt.registerTask('default', ['env:dev', 'lint', 'copy:localConfig', 'concurrent:default']); - // Build task(s). - grunt.registerTask('build', ['lint', 'loadConfig', 'ngAnnotate', 'uglify', 'cssmin']); + // Run the project in debug mode + grunt.registerTask('debug', ['env:dev', 'lint', 'copy:localConfig', 'concurrent:debug']); - // 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']); + // Run the project in production mode + grunt.registerTask('prod', ['build', 'env:prod', 'copy:localConfig', 'concurrent:default']); }; diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000000..c9b0fe6c12 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,193 @@ +'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(); + +// 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.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.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 (path) { + path.dirname = path.dirname.replace('/scss', '/css'); + })) + .pipe(gulp.dest('./modules/')); +}); + +// Less task +gulp.task('less', function () { + return gulp.src(defaultAssets.client.less) + .pipe(plugins.less()) + .pipe(plugins.rename(function (path) { + path.dirname = path.dirname.replace('/less', '/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..2e926ad5f3 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -3,16 +3,18 @@ /** * 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({ +module.exports = function(karmaConfig) { + karmaConfig.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), + files: _.union(defaultAssets.client.lib.js, defaultAssets.client.lib.tests, defaultAssets.client.js, testAssets.tests.client), // Test results reporter to use // Possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' @@ -25,8 +27,8 @@ module.exports = function(config) { 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, + // 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, 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..d43e0893f4 --- /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/public/modules/articles/config/articles.client.routes.js b/modules/articles/client/config/articles.client.routes.js old mode 100755 new mode 100644 similarity index 64% rename from public/modules/articles/config/articles.client.routes.js rename to modules/articles/client/config/articles.client.routes.js index 1531a9a57c..c6890be3ef --- a/public/modules/articles/config/articles.client.routes.js +++ b/modules/articles/client/config/articles.client.routes.js @@ -5,21 +5,26 @@ angular.module('articles').config(['$stateProvider', function($stateProvider) { // Articles state routing $stateProvider. - state('listArticles', { + state('articles', { + abstract: true, url: '/articles', + template: '' + }). + state('articles.list', { + url: '', templateUrl: 'modules/articles/views/list-articles.client.view.html' }). - state('createArticle', { - url: '/articles/create', + state('articles.create', { + url: '/create', templateUrl: 'modules/articles/views/create-article.client.view.html' }). - state('viewArticle', { - url: '/articles/:articleId', + state('articles.view', { + url: '/:articleId', templateUrl: 'modules/articles/views/view-article.client.view.html' }). - state('editArticle', { - url: '/articles/:articleId/edit', + state('articles.edit', { + url: '/: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/modules/articles/client/controllers/articles.client.controller.js similarity index 99% rename from public/modules/articles/controllers/articles.client.controller.js rename to modules/articles/client/controllers/articles.client.controller.js index d90ec3dc5d..a54e32ea7e 100644 --- a/public/modules/articles/controllers/articles.client.controller.js +++ b/modules/articles/client/controllers/articles.client.controller.js @@ -65,4 +65,4 @@ angular.module('articles').controller('ArticlesController', ['$scope', '$statePa }); }; } -]); \ No newline at end of file +]); diff --git a/public/modules/articles/services/articles.client.service.js b/modules/articles/client/services/articles.client.service.js similarity index 82% rename from public/modules/articles/services/articles.client.service.js rename to modules/articles/client/services/articles.client.service.js index deeb7da58c..5c8967f5a0 100644 --- a/public/modules/articles/services/articles.client.service.js +++ b/modules/articles/client/services/articles.client.service.js @@ -3,7 +3,7 @@ //Articles service used for communicating with the articles REST endpoints angular.module('articles').factory('Articles', ['$resource', function($resource) { - return $resource('articles/:articleId', { + return $resource('api/articles/:articleId', { articleId: '@_id' }, { update: { @@ -11,4 +11,4 @@ angular.module('articles').factory('Articles', ['$resource', } }); } -]); \ No newline at end of file +]); diff --git a/public/modules/articles/views/create-article.client.view.html b/modules/articles/client/views/create-article.client.view.html similarity index 83% rename from public/modules/articles/views/create-article.client.view.html rename to modules/articles/client/views/create-article.client.view.html index ab8db8ef61..79ea510e65 100644 --- a/public/modules/articles/views/create-article.client.view.html +++ b/modules/articles/client/views/create-article.client.view.html @@ -5,10 +5,10 @@

New Article

-
+
- +
@@ -26,4 +26,4 @@

New Article

- \ No newline at end of file + diff --git a/public/modules/articles/views/edit-article.client.view.html b/modules/articles/client/views/edit-article.client.view.html similarity index 59% rename from public/modules/articles/views/edit-article.client.view.html rename to modules/articles/client/views/edit-article.client.view.html index 353cb8e666..7a4d4ca01a 100644 --- a/public/modules/articles/views/edit-article.client.view.html +++ b/modules/articles/client/views/edit-article.client.view.html @@ -3,24 +3,18 @@

Edit Article

-
+
-
+
-
-

Title is required

-
-
+
- -
-
-

Content is required

+
@@ -32,4 +26,4 @@

Edit Article

- \ No newline at end of file + diff --git a/public/modules/articles/views/list-articles.client.view.html b/modules/articles/client/views/list-articles.client.view.html similarity index 74% rename from public/modules/articles/views/list-articles.client.view.html rename to modules/articles/client/views/list-articles.client.view.html index 861ae5b6ba..0d8c3b7900 100644 --- a/public/modules/articles/views/list-articles.client.view.html +++ b/modules/articles/client/views/list-articles.client.view.html @@ -3,7 +3,7 @@

Articles

- + Posted on @@ -15,6 +15,6 @@

- No articles yet, why don't you create one? + 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/modules/articles/client/views/view-article.client.view.html similarity index 87% rename from public/modules/articles/views/view-article.client.view.html rename to modules/articles/client/views/view-article.client.view.html index 312d25c84e..a298412b4e 100644 --- a/public/modules/articles/views/view-article.client.view.html +++ b/modules/articles/client/views/view-article.client.view.html @@ -3,7 +3,7 @@

- + @@ -19,4 +19,4 @@

- \ No newline at end of file + diff --git a/app/controllers/articles.server.controller.js b/modules/articles/server/controllers/articles.server.controller.js similarity index 79% rename from app/controllers/articles.server.controller.js rename to modules/articles/server/controllers/articles.server.controller.js index f5b4d27f12..d720196d45 100644 --- a/app/controllers/articles.server.controller.js +++ b/modules/articles/server/controllers/articles.server.controller.js @@ -3,10 +3,10 @@ /** * Module dependencies. */ -var mongoose = require('mongoose'), - errorHandler = require('./errors.server.controller'), +var path = require('path'), + mongoose = require('mongoose'), Article = mongoose.model('Article'), - _ = require('lodash'); + errorHandler = require(path.resolve('./modules/core/server/controllers/errors.server.controller')); /** * Create a article @@ -39,7 +39,8 @@ exports.read = function(req, res) { exports.update = function(req, res) { var article = req.article; - article = _.extend(article, req.body); + article.title = req.body.title; + article.content = req.body.content; article.save(function(err) { if (err) { @@ -91,7 +92,7 @@ exports.articleByID = function(req, res, next, id) { if (!mongoose.Types.ObjectId.isValid(id)) { return res.status(400).send({ - message: 'Article is invalid' + message: errorHandler.getErrorMessage(err) }); } @@ -99,22 +100,10 @@ exports.articleByID = function(req, res, next, id) { if (err) return next(err); if (!article) { return res.status(404).send({ - message: 'Article not found' + message: errorHandler.getErrorMessage(err) }); } 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(); -}; +}; \ No newline at end of file diff --git a/app/models/article.server.model.js b/modules/articles/server/models/article.server.model.js similarity index 100% rename from app/models/article.server.model.js rename to modules/articles/server/models/article.server.model.js 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..a8572d9e92 --- /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..49e3697f9f --- /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/public/modules/articles/tests/articles.client.controller.test.js b/modules/articles/tests/client/articles.client.controller.tests.js similarity index 92% rename from public/modules/articles/tests/articles.client.controller.test.js rename to modules/articles/tests/client/articles.client.controller.tests.js index 677cb99431..04c0918437 100644 --- a/public/modules/articles/tests/articles.client.controller.test.js +++ b/modules/articles/tests/client/articles.client.controller.tests.js @@ -61,7 +61,7 @@ var sampleArticles = [sampleArticle]; // Set GET response - $httpBackend.expectGET('articles').respond(sampleArticles); + $httpBackend.expectGET('api/articles').respond(sampleArticles); // Run controller functionality scope.find(); @@ -82,7 +82,7 @@ $stateParams.articleId = '525a8422f6d0f87f0e407a33'; // Set GET response - $httpBackend.expectGET(/articles\/([0-9a-fA-F]{24})$/).respond(sampleArticle); + $httpBackend.expectGET(/api\/articles\/([0-9a-fA-F]{24})$/).respond(sampleArticle); // Run controller functionality scope.findOne(); @@ -111,7 +111,7 @@ scope.content = 'MEAN rocks!'; // Set POST response - $httpBackend.expectPOST('articles', sampleArticlePostData).respond(sampleArticleResponse); + $httpBackend.expectPOST('api/articles', sampleArticlePostData).respond(sampleArticleResponse); // Run controller functionality scope.create(); @@ -137,7 +137,7 @@ scope.article = sampleArticlePutData; // Set PUT response - $httpBackend.expectPUT(/articles\/([0-9a-fA-F]{24})$/).respond(); + $httpBackend.expectPUT(/api\/articles\/([0-9a-fA-F]{24})$/).respond(); // Run controller functionality scope.update(); @@ -157,7 +157,7 @@ scope.articles = [sampleArticle]; // Set expected DELETE response - $httpBackend.expectDELETE(/articles\/([0-9a-fA-F]{24})$/).respond(204); + $httpBackend.expectDELETE(/api\/articles\/([0-9a-fA-F]{24})$/).respond(204); // Run controller functionality scope.remove(sampleArticle); @@ -167,4 +167,4 @@ expect(scope.articles.length).toBe(0); })); }); -}()); \ No newline at end of file +}()); 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..2c6e1dd3fd --- /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/app/tests/article.server.model.test.js b/modules/articles/tests/server/article.server.model.tests.js similarity index 100% rename from app/tests/article.server.model.test.js rename to modules/articles/tests/server/article.server.model.tests.js diff --git a/app/tests/article.server.routes.test.js b/modules/articles/tests/server/article.server.routes.tests.js similarity index 86% rename from app/tests/article.server.routes.test.js rename to modules/articles/tests/server/article.server.routes.tests.js index c583c3f3af..1f078861fa 100644 --- a/app/tests/article.server.routes.test.js +++ b/modules/articles/tests/server/article.server.routes.tests.js @@ -2,21 +2,29 @@ var should = require('should'), request = require('supertest'), - app = require('../../server'), + path = require('path'), mongoose = require('mongoose'), User = mongoose.model('User'), Article = mongoose.model('Article'), - agent = request.agent(app); + express = require(path.resolve('./config/lib/express')); /** * Globals */ -var credentials, user, article; +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 = { @@ -47,7 +55,7 @@ describe('Article CRUD tests', function() { }); it('should be able to save an article if logged in', function(done) { - agent.post('/auth/signin') + agent.post('/api/auth/signin') .send(credentials) .expect(200) .end(function(signinErr, signinRes) { @@ -58,7 +66,7 @@ describe('Article CRUD tests', function() { var userId = user.id; // Save a new article - agent.post('/articles') + agent.post('/api/articles') .send(article) .expect(200) .end(function(articleSaveErr, articleSaveRes) { @@ -66,7 +74,7 @@ describe('Article CRUD tests', function() { if (articleSaveErr) done(articleSaveErr); // Get a list of articles - agent.get('/articles') + agent.get('/api/articles') .end(function(articlesGetErr, articlesGetRes) { // Handle article save error if (articlesGetErr) done(articlesGetErr); @@ -86,9 +94,9 @@ describe('Article CRUD tests', function() { }); it('should not be able to save an article if not logged in', function(done) { - agent.post('/articles') + agent.post('/api/articles') .send(article) - .expect(401) + .expect(403) .end(function(articleSaveErr, articleSaveRes) { // Call the assertion callback done(articleSaveErr); @@ -99,7 +107,7 @@ describe('Article CRUD tests', function() { // Invalidate title field article.title = ''; - agent.post('/auth/signin') + agent.post('/api/auth/signin') .send(credentials) .expect(200) .end(function(signinErr, signinRes) { @@ -110,7 +118,7 @@ describe('Article CRUD tests', function() { var userId = user.id; // Save a new article - agent.post('/articles') + agent.post('/api/articles') .send(article) .expect(400) .end(function(articleSaveErr, articleSaveRes) { @@ -124,7 +132,7 @@ describe('Article CRUD tests', function() { }); it('should be able to update an article if signed in', function(done) { - agent.post('/auth/signin') + agent.post('/api/auth/signin') .send(credentials) .expect(200) .end(function(signinErr, signinRes) { @@ -135,7 +143,7 @@ describe('Article CRUD tests', function() { var userId = user.id; // Save a new article - agent.post('/articles') + agent.post('/api/articles') .send(article) .expect(200) .end(function(articleSaveErr, articleSaveRes) { @@ -146,7 +154,7 @@ describe('Article CRUD tests', function() { article.title = 'WHY YOU GOTTA BE SO MEAN?'; // Update an existing article - agent.put('/articles/' + articleSaveRes.body._id) + agent.put('/api/articles/' + articleSaveRes.body._id) .send(article) .expect(200) .end(function(articleUpdateErr, articleUpdateRes) { @@ -171,7 +179,7 @@ describe('Article CRUD tests', function() { // Save the article articleObj.save(function() { // Request articles - request(app).get('/articles') + request(app).get('/api/articles') .end(function(req, res) { // Set assertion res.body.should.be.an.Array.with.lengthOf(1); @@ -190,7 +198,7 @@ describe('Article CRUD tests', function() { // Save the article articleObj.save(function() { - request(app).get('/articles/' + articleObj._id) + request(app).get('/api/articles/' + articleObj._id) .end(function(req, res) { // Set assertion res.body.should.be.an.Object.with.property('title', article.title); @@ -213,7 +221,7 @@ describe('Article CRUD tests', function() { }); it('should be able to delete an article if signed in', function(done) { - agent.post('/auth/signin') + agent.post('/api/auth/signin') .send(credentials) .expect(200) .end(function(signinErr, signinRes) { @@ -224,7 +232,7 @@ describe('Article CRUD tests', function() { var userId = user.id; // Save a new article - agent.post('/articles') + agent.post('/api/articles') .send(article) .expect(200) .end(function(articleSaveErr, articleSaveRes) { @@ -232,7 +240,7 @@ describe('Article CRUD tests', function() { if (articleSaveErr) done(articleSaveErr); // Delete an existing article - agent.delete('/articles/' + articleSaveRes.body._id) + agent.delete('/api/articles/' + articleSaveRes.body._id) .send(article) .expect(200) .end(function(articleDeleteErr, articleDeleteRes) { @@ -259,11 +267,11 @@ describe('Article CRUD tests', function() { // Save the article articleObj.save(function() { // Try deleting article - request(app).delete('/articles/' + articleObj._id) - .expect(401) + request(app).delete('/api/articles/' + articleObj._id) + .expect(403) .end(function(articleDeleteErr, articleDeleteRes) { // Set message assertion - (articleDeleteRes.body.message).should.match('User is not logged in'); + (articleDeleteRes.body.message).should.match('User is not authorized'); // Handle article error error done(articleDeleteErr); 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..67a4719f4a --- /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..c688ac096f --- /dev/null +++ b/modules/chat/client/config/chat.client.routes.js @@ -0,0 +1,12 @@ +'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' + }); + } +]); 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..ffb83330ad --- /dev/null +++ b/modules/chat/client/controllers/chat.client.controller.js @@ -0,0 +1,34 @@ +'use strict'; + +// Create the 'chat' controller +angular.module('chat').controller('ChatController', ['$scope', 'Socket', + function($scope, Socket) { + // Create a messages array + $scope.messages = []; + + // 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..d04a8689e3 --- /dev/null +++ b/modules/chat/client/css/chat.css @@ -0,0 +1,18 @@ +.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..c3cd60190b --- /dev/null +++ b/modules/chat/client/views/chat.client.view.html @@ -0,0 +1,28 @@ + +
+ + +
+
+
+ + + + +
+
+
+
    + +
  • + + {{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..d48791d66d --- /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..8a31f1930f --- /dev/null +++ b/modules/chat/tests/client/chat.client.controller.tests.js @@ -0,0 +1,10 @@ +'use strict'; + +/** + * Chat client controller tests + */ +(function() { + describe('ChatController', function() { + // TODO: Add chat client controller tests + }); +}()); 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..d3fd973c29 --- /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..f900e2f626 --- /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/public/config.js b/modules/core/client/app/config.js similarity index 90% rename from public/config.js rename to modules/core/client/app/config.js index 75de1c4bce..b4b5947cd3 100644 --- a/public/config.js +++ b/modules/core/client/app/config.js @@ -4,7 +4,7 @@ var ApplicationConfiguration = (function() { // Init module configuration options var applicationModuleName = 'mean'; - var applicationModuleVendorDependencies = ['ngResource', 'ngAnimate', 'ui.router', 'ui.bootstrap', 'ui.utils']; + var applicationModuleVendorDependencies = ['ngResource', 'ngAnimate', 'ui.router', 'ui.bootstrap', 'ui.utils', 'angularFileUpload']; // Add a new vertical module var registerModule = function(moduleName, dependencies) { @@ -20,4 +20,4 @@ var ApplicationConfiguration = (function() { applicationModuleVendorDependencies: applicationModuleVendorDependencies, registerModule: registerModule }; -})(); \ No newline at end of file +})(); diff --git a/public/application.js b/modules/core/client/app/init.js similarity index 92% rename from public/application.js rename to modules/core/client/app/init.js index 19bb411ef2..5e144afce5 100644 --- a/public/application.js +++ b/modules/core/client/app/init.js @@ -6,7 +6,7 @@ angular.module(ApplicationConfiguration.applicationModuleName, ApplicationConfig // Setting HTML5 Location Mode angular.module(ApplicationConfiguration.applicationModuleName).config(['$locationProvider', function($locationProvider) { - $locationProvider.hashPrefix('!'); + $locationProvider.html5Mode(true).hashPrefix('!'); } ]); @@ -17,4 +17,4 @@ angular.element(document).ready(function() { //Then init the app angular.bootstrap(document, [ApplicationConfiguration.applicationModuleName]); -}); \ No newline at end of file +}); diff --git a/public/modules/core/config/core.client.routes.js b/modules/core/client/config/core.client.routes.js old mode 100755 new mode 100644 similarity index 98% rename from public/modules/core/config/core.client.routes.js rename to modules/core/client/config/core.client.routes.js index 894e3a6caf..7328d0d213 --- a/public/modules/core/config/core.client.routes.js +++ b/modules/core/client/config/core.client.routes.js @@ -13,4 +13,4 @@ angular.module('core').config(['$stateProvider', '$urlRouterProvider', 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/modules/core/client/controllers/header.client.controller.js similarity index 67% rename from public/modules/core/controllers/header.client.controller.js rename to modules/core/client/controllers/header.client.controller.js index 1b8c2b7bec..64c3019724 100644 --- a/public/modules/core/controllers/header.client.controller.js +++ b/modules/core/client/controllers/header.client.controller.js @@ -1,11 +1,16 @@ 'use strict'; -angular.module('core').controller('HeaderController', ['$scope', 'Authentication', 'Menus', - function($scope, Authentication, Menus) { +angular.module('core').controller('HeaderController', ['$scope', '$state', 'Authentication', 'Menus', + function($scope, $state, Authentication, Menus) { + // Expose view variables + $scope.$state = $state; $scope.authentication = Authentication; - $scope.isCollapsed = false; + + // Get the topbar menu $scope.menu = Menus.getMenu('topbar'); + // Toggle the menu items + $scope.isCollapsed = false; $scope.toggleCollapsibleMenu = function() { $scope.isCollapsed = !$scope.isCollapsed; }; @@ -15,4 +20,4 @@ angular.module('core').controller('HeaderController', ['$scope', 'Authentication $scope.isCollapsed = false; }); } -]); \ No newline at end of file +]); diff --git a/public/modules/core/controllers/home.client.controller.js b/modules/core/client/controllers/home.client.controller.js similarity index 98% rename from public/modules/core/controllers/home.client.controller.js rename to modules/core/client/controllers/home.client.controller.js index 63d0f297ae..ea80b59e60 100644 --- a/public/modules/core/controllers/home.client.controller.js +++ b/modules/core/client/controllers/home.client.controller.js @@ -1,9 +1,8 @@ '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/modules/core/client/core.client.module.js b/modules/core/client/core.client.module.js new file mode 100644 index 0000000000..b2658634cb --- /dev/null +++ b/modules/core/client/core.client.module.js @@ -0,0 +1,4 @@ +'use strict'; + +// Use Applicaion configuration module to register a new module +ApplicationConfiguration.registerModule('core'); diff --git a/modules/core/client/css/core.css b/modules/core/client/css/core.css new file mode 100644 index 0000000000..e89af83986 --- /dev/null +++ b/modules/core/client/css/core.css @@ -0,0 +1,33 @@ +.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..020d7cf32b --- /dev/null +++ b/modules/core/client/services/menus.client.service.js @@ -0,0 +1,179 @@ +'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, 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..e66224cf5c --- /dev/null +++ b/modules/core/client/services/socket.io.client.service.js @@ -0,0 +1,38 @@ +'use strict'; + +// Create the Socket.io wrapper service +angular.module('core').service('Socket', ['Authentication', '$state', '$timeout', + function(Authentication, $state, $timeout) { + // Connect to the Socket.io server only when authenticated + if (Authentication.user) { + this.socket = io(); + } else { + $state.go('home'); + } + + // 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/header.client.view.html b/modules/core/client/views/header.client.view.html new file mode 100644 index 0000000000..1279b2a1a9 --- /dev/null +++ b/modules/core/client/views/header.client.view.html @@ -0,0 +1,62 @@ +
\ No newline at end of file diff --git a/public/modules/core/views/home.client.view.html b/modules/core/client/views/home.client.view.html similarity index 86% rename from public/modules/core/views/home.client.view.html rename to modules/core/client/views/home.client.view.html index 2625a9845e..55e6eb5a55 100644 --- a/public/modules/core/views/home.client.view.html +++ b/modules/core/client/views/home.client.view.html @@ -18,7 +18,7 @@
-

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

+

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.

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..3b427c38f7 --- /dev/null +++ b/modules/core/server/controllers/core.server.controller.js @@ -0,0 +1,28 @@ +'use strict'; + +/** + * Render the main applicaion 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 page + */ +exports.renderNotFound = function(req, res) { + res.status(404).render('modules/core/server/views/404', { + url: req.originalUrl + }); +}; diff --git a/app/controllers/errors.server.controller.js b/modules/core/server/controllers/errors.server.controller.js similarity index 90% rename from app/controllers/errors.server.controller.js rename to modules/core/server/controllers/errors.server.controller.js index 5944d786af..db81a8ec86 100644 --- a/app/controllers/errors.server.controller.js +++ b/modules/core/server/controllers/errors.server.controller.js @@ -8,10 +8,10 @@ var getUniqueErrorMessage = function(err) { try { var fieldName = err.err.substring(err.err.lastIndexOf('.$') + 2, err.err.lastIndexOf('_1')); - output = fieldName.charAt(0).toUpperCase() + fieldName.slice(1) + ' already exists'; + output = fieldName.charAt(0).toUpperCase() + fieldName.slice(1) + ' already exist'; - } catch (ex) { - output = 'Unique field already exists'; + } catch(ex) { + output = 'Unique field already exist'; } return output; @@ -22,7 +22,7 @@ var getUniqueErrorMessage = function(err) { */ exports.getErrorMessage = function(err) { var message = ''; - + if (err.code) { switch (err.code) { case 11000: 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..33a436641c --- /dev/null +++ b/modules/core/server/routes/core.server.routes.js @@ -0,0 +1,13 @@ +'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); + app.route('/not-found').get(core.renderNotFound); + + // Define application route + app.route('/*').get(core.renderIndex); +}; diff --git a/app/views/404.server.view.html b/modules/core/server/views/404.server.view.html similarity index 100% rename from app/views/404.server.view.html rename to modules/core/server/views/404.server.view.html diff --git a/app/views/500.server.view.html b/modules/core/server/views/500.server.view.html similarity index 100% rename from app/views/500.server.view.html rename to modules/core/server/views/500.server.view.html diff --git a/app/views/index.server.view.html b/modules/core/server/views/index.server.view.html similarity index 100% rename from app/views/index.server.view.html rename to modules/core/server/views/index.server.view.html diff --git a/app/views/layout.server.view.html b/modules/core/server/views/layout.server.view.html similarity index 68% rename from app/views/layout.server.view.html rename to modules/core/server/views/layout.server.view.html index 9e9cebd41d..c5b350467c 100644 --- a/app/views/layout.server.view.html +++ b/modules/core/server/views/layout.server.view.html @@ -1,5 +1,5 @@ - + {{title}} @@ -8,6 +8,10 @@ + + + + @@ -22,22 +26,20 @@ - + - + - {% for cssFile in cssFiles %} - - {% endfor %} + {% for cssFile in cssFiles %}{% endfor %} + - {% for jsFile in jsFiles %} - - {% endfor %} + {% for jsFile in jsFiles %}{% endfor %} {% if process.env.NODE_ENV === 'development' %} - + {% endif %} - - - + \ No newline at end of file diff --git a/public/modules/core/tests/header.client.controller.test.js b/modules/core/tests/client/header.client.controller.tests.js similarity index 98% rename from public/modules/core/tests/header.client.controller.test.js rename to modules/core/tests/client/header.client.controller.tests.js index 76ee4fb4e7..b45cc26b37 100644 --- a/public/modules/core/tests/header.client.controller.test.js +++ b/modules/core/tests/client/header.client.controller.tests.js @@ -21,4 +21,4 @@ expect(scope.authentication).toBeTruthy(); }); }); -})(); \ No newline at end of file +})(); diff --git a/public/modules/core/tests/home.client.controller.test.js b/modules/core/tests/client/home.client.controller.tests.js similarity index 98% rename from public/modules/core/tests/home.client.controller.test.js rename to modules/core/tests/client/home.client.controller.tests.js index a5b1a566d5..b83b12d736 100644 --- a/public/modules/core/tests/home.client.controller.test.js +++ b/modules/core/tests/client/home.client.controller.tests.js @@ -21,4 +21,4 @@ expect(scope.authentication).toBeTruthy(); }); }); -})(); \ No newline at end of file +})(); diff --git a/public/modules/users/config/users.client.config.js b/modules/users/client/config/users.client.config.js similarity index 77% rename from public/modules/users/config/users.client.config.js rename to modules/users/client/config/users.client.config.js index 0bfc8b640b..19d2c115e1 100644 --- a/public/modules/users/config/users.client.config.js +++ b/modules/users/client/config/users.client.config.js @@ -2,12 +2,12 @@ // Config HTTP Error Handling angular.module('users').config(['$httpProvider', - function($httpProvider) { + function ($httpProvider) { // Set the httpProvider "not authorized" interceptor $httpProvider.interceptors.push(['$q', '$location', 'Authentication', - function($q, $location, Authentication) { + function ($q, $location, Authentication) { return { - responseError: function(rejection) { + responseError: function (rejection) { switch (rejection.status) { case 401: // Deauthenticate the global user @@ -17,7 +17,7 @@ angular.module('users').config(['$httpProvider', $location.path('signin'); break; case 403: - // Add unauthorized behaviour + // Add unauthorized behaviour break; } @@ -27,4 +27,4 @@ angular.module('users').config(['$httpProvider', } ]); } -]); \ No newline at end of file +]); 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..9b0546360e --- /dev/null +++ b/modules/users/client/config/users.client.routes.js @@ -0,0 +1,69 @@ +'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' + }). + 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', + 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/public/modules/users/controllers/authentication.client.controller.js b/modules/users/client/controllers/authentication.client.controller.js similarity index 84% rename from public/modules/users/controllers/authentication.client.controller.js rename to modules/users/client/controllers/authentication.client.controller.js index 3e27cc3b88..a1b349f4f1 100644 --- a/public/modules/users/controllers/authentication.client.controller.js +++ b/modules/users/client/controllers/authentication.client.controller.js @@ -8,7 +8,7 @@ angular.module('users').controller('AuthenticationController', ['$scope', '$http if ($scope.authentication.user) $location.path('/'); $scope.signup = function() { - $http.post('/auth/signup', $scope.credentials).success(function(response) { + $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; @@ -20,7 +20,7 @@ angular.module('users').controller('AuthenticationController', ['$scope', '$http }; $scope.signin = function() { - $http.post('/auth/signin', $scope.credentials).success(function(response) { + $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; @@ -31,4 +31,4 @@ angular.module('users').controller('AuthenticationController', ['$scope', '$http }); }; } -]); \ No newline at end of file +]); diff --git a/public/modules/users/controllers/password.client.controller.js b/modules/users/client/controllers/password.client.controller.js similarity index 86% rename from public/modules/users/controllers/password.client.controller.js rename to modules/users/client/controllers/password.client.controller.js index dbc9e92977..6c95125750 100644 --- a/public/modules/users/controllers/password.client.controller.js +++ b/modules/users/client/controllers/password.client.controller.js @@ -11,7 +11,7 @@ angular.module('users').controller('PasswordController', ['$scope', '$stateParam $scope.askForPasswordReset = function() { $scope.success = $scope.error = null; - $http.post('/auth/forgot', $scope.credentials).success(function(response) { + $http.post('/api/auth/forgot', $scope.credentials).success(function(response) { // Show user success message and clear form $scope.credentials = null; $scope.success = response.message; @@ -27,7 +27,7 @@ angular.module('users').controller('PasswordController', ['$scope', '$stateParam $scope.resetUserPassword = function() { $scope.success = $scope.error = null; - $http.post('/auth/reset/' + $stateParams.token, $scope.passwordDetails).success(function(response) { + $http.post('/api/auth/reset/' + $stateParams.token, $scope.passwordDetails).success(function(response) { // If successful show success message and clear form $scope.passwordDetails = null; @@ -41,4 +41,4 @@ angular.module('users').controller('PasswordController', ['$scope', '$stateParam }); }; } -]); \ No newline at end of file +]); diff --git a/public/modules/users/controllers/settings.client.controller.js b/modules/users/client/controllers/settings.client.controller.js similarity index 92% rename from public/modules/users/controllers/settings.client.controller.js rename to modules/users/client/controllers/settings.client.controller.js index 8616fc9463..70b16c5151 100644 --- a/public/modules/users/controllers/settings.client.controller.js +++ b/modules/users/client/controllers/settings.client.controller.js @@ -25,7 +25,7 @@ angular.module('users').controller('SettingsController', ['$scope', '$http', '$l $scope.removeUserSocialAccount = function(provider) { $scope.success = $scope.error = null; - $http.delete('/users/accounts', { + $http.delete('/api/users/accounts', { params: { provider: provider } @@ -40,10 +40,10 @@ angular.module('users').controller('SettingsController', ['$scope', '$http', '$l // Update a user profile $scope.updateUserProfile = function(isValid) { - if (isValid) { + if (isValid){ $scope.success = $scope.error = null; var user = new Users($scope.user); - + user.$update(function(response) { $scope.success = true; Authentication.user = response; @@ -59,7 +59,7 @@ angular.module('users').controller('SettingsController', ['$scope', '$http', '$l $scope.changeUserPassword = function() { $scope.success = $scope.error = null; - $http.post('/users/password', $scope.passwordDetails).success(function(response) { + $http.post('/api/users/password', $scope.passwordDetails).success(function(response) { // If successful show success message and clear form $scope.success = true; $scope.passwordDetails = null; @@ -68,4 +68,4 @@ angular.module('users').controller('SettingsController', ['$scope', '$http', '$l }); }; } -]); \ No newline at end of file +]); 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..26575a81bb --- /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', '$location', 'Users', 'Authentication', + function($scope, $http, $location, Users, 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..d131c720fe --- /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..8e1d423819 --- /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..74774f06d1 --- /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', '$location', 'Users', 'Authentication', + function($scope, $http, $location, Users, 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..25af58435f --- /dev/null +++ b/modules/users/client/controllers/settings/settings.client.controller.js @@ -0,0 +1,10 @@ +'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('/'); + } +]); diff --git a/modules/users/client/css/users.css b/modules/users/client/css/users.css new file mode 100644 index 0000000000..e1727b2153 --- /dev/null +++ b/modules/users/client/css/users.css @@ -0,0 +1,41 @@ +@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/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..8eadf2c06f --- /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/public/modules/users/services/users.client.service.js b/modules/users/client/services/users.client.service.js similarity index 83% rename from public/modules/users/services/users.client.service.js rename to modules/users/client/services/users.client.service.js index 664828f0a5..f8a68eeac2 100644 --- a/public/modules/users/services/users.client.service.js +++ b/modules/users/client/services/users.client.service.js @@ -3,10 +3,10 @@ // Users service used for communicating with the users REST endpoint angular.module('users').factory('Users', ['$resource', function($resource) { - return $resource('users', {}, { + return $resource('api/users', {}, { update: { method: 'PUT' } }); } -]); \ No newline at end of file +]); diff --git a/modules/users/client/users.client.module.js b/modules/users/client/users.client.module.js new file mode 100644 index 0000000000..569aba8c16 --- /dev/null +++ b/modules/users/client/users.client.module.js @@ -0,0 +1,4 @@ +'use strict'; + +// Use Applicaion configuration module to register a new module +ApplicationConfiguration.registerModule('users'); 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..3f67964d3c --- /dev/null +++ b/modules/users/client/views/authentication/authentication.client.view.html @@ -0,0 +1,21 @@ +
+

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..5345a5dc6c --- /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..bb711b3e05 --- /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..02feebfc2d --- /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..a9b8512b15 --- /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/public/modules/users/views/password/reset-password-success.client.view.html b/modules/users/client/views/password/reset-password-success.client.view.html similarity index 53% rename from public/modules/users/views/password/reset-password-success.client.view.html rename to modules/users/client/views/password/reset-password-success.client.view.html index 4de46c4b22..a15df19248 100644 --- a/public/modules/users/views/password/reset-password-success.client.view.html +++ b/modules/users/client/views/password/reset-password-success.client.view.html @@ -1,4 +1,4 @@

Password successfully reset

- Continue to home page -
\ No newline at end of file + 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..69d1f346d2 --- /dev/null +++ b/modules/users/client/views/password/reset-password.client.view.html @@ -0,0 +1,26 @@ +
+

Reset your password

+
+ +
+
diff --git a/public/modules/users/views/settings/change-password.client.view.html b/modules/users/client/views/settings/change-password.client.view.html similarity index 86% rename from public/modules/users/views/settings/change-password.client.view.html rename to modules/users/client/views/settings/change-password.client.view.html index 9811011a53..7f965801bb 100644 --- a/public/modules/users/views/settings/change-password.client.view.html +++ b/modules/users/client/views/settings/change-password.client.view.html @@ -1,6 +1,5 @@ -
-

Change your password

-
+
+
-
\ No newline at end of file +
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..e0e4f92572 --- /dev/null +++ b/modules/users/client/views/settings/change-profile-picture.client.view.html @@ -0,0 +1,26 @@ +
+
+ +
+
diff --git a/public/modules/users/views/settings/edit-profile.client.view.html b/modules/users/client/views/settings/edit-profile.client.view.html similarity index 87% rename from public/modules/users/views/settings/edit-profile.client.view.html rename to modules/users/client/views/settings/edit-profile.client.view.html index a4be680f41..ae8cd30327 100644 --- a/public/modules/users/views/settings/edit-profile.client.view.html +++ b/modules/users/client/views/settings/edit-profile.client.view.html @@ -1,6 +1,5 @@ -
-

Edit your profile

-
+
+
-
\ No newline at end of file +
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..b40b203e6c --- /dev/null +++ b/modules/users/client/views/settings/manage-social-accounts.client.view.html @@ -0,0 +1,44 @@ +
+

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..9a05049fcd --- /dev/null +++ b/modules/users/client/views/settings/settings.client.view.html @@ -0,0 +1,26 @@ +
+ + +
diff --git a/config/strategies/facebook.js b/modules/users/server/config/strategies/facebook.js similarity index 78% rename from config/strategies/facebook.js rename to modules/users/server/config/strategies/facebook.js index 34ddc68f16..ed03ce267a 100644 --- a/config/strategies/facebook.js +++ b/modules/users/server/config/strategies/facebook.js @@ -5,15 +5,15 @@ */ var passport = require('passport'), FacebookStrategy = require('passport-facebook').Strategy, - config = require('../config'), - users = require('../../app/controllers/users.server.controller'); + users = require('../../controllers/users.server.controller'); -module.exports = function() { +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) { @@ -28,7 +28,7 @@ module.exports = function() { lastName: profile.name.familyName, displayName: profile.displayName, email: profile.emails[0].value, - username: profile.username, + profileImageURL: (profile.id) ? '//graph.facebook.com/' + profile.id + '/picture?type=large' : undefined, provider: 'facebook', providerIdentifierField: 'id', providerData: providerData diff --git a/config/strategies/github.js b/modules/users/server/config/strategies/github.js similarity index 87% rename from config/strategies/github.js rename to modules/users/server/config/strategies/github.js index f10a413e20..60771e6615 100644 --- a/config/strategies/github.js +++ b/modules/users/server/config/strategies/github.js @@ -5,10 +5,9 @@ */ var passport = require('passport'), GithubStrategy = require('passport-github').Strategy, - config = require('../config'), - users = require('../../app/controllers/users.server.controller'); + users = require('../../controllers/users.server.controller'); -module.exports = function() { +module.exports = function(config) { // Use github strategy passport.use(new GithubStrategy({ clientID: config.github.clientID, @@ -34,6 +33,7 @@ module.exports = function() { displayName: displayName, email: profile.emails[0].value, username: profile.username, + profileImageURL: (providerData.avatar_url) ? providerData.avatar_url : undefined, provider: 'github', providerIdentifierField: 'id', providerData: providerData diff --git a/config/strategies/google.js b/modules/users/server/config/strategies/google.js similarity index 85% rename from config/strategies/google.js rename to modules/users/server/config/strategies/google.js index 8044ed4eec..bd4df63f87 100644 --- a/config/strategies/google.js +++ b/modules/users/server/config/strategies/google.js @@ -5,10 +5,9 @@ */ var passport = require('passport'), GoogleStrategy = require('passport-google-oauth').OAuth2Strategy, - config = require('../config'), - users = require('../../app/controllers/users.server.controller'); + users = require('../../controllers/users.server.controller'); -module.exports = function() { +module.exports = function(config) { // Use google strategy passport.use(new GoogleStrategy({ clientID: config.google.clientID, @@ -29,6 +28,7 @@ module.exports = function() { displayName: profile.displayName, email: profile.emails[0].value, username: profile.username, + profileImageURL: (providerData.picture) ? providerData.picture : undefined, provider: 'google', providerIdentifierField: 'id', providerData: providerData diff --git a/config/strategies/linkedin.js b/modules/users/server/config/strategies/linkedin.js similarity index 84% rename from config/strategies/linkedin.js rename to modules/users/server/config/strategies/linkedin.js index 1ee5b3f5bc..220269cf48 100644 --- a/config/strategies/linkedin.js +++ b/modules/users/server/config/strategies/linkedin.js @@ -5,17 +5,16 @@ */ var passport = require('passport'), LinkedInStrategy = require('passport-linkedin').Strategy, - config = require('../config'), - users = require('../../app/controllers/users.server.controller'); + users = require('../../controllers/users.server.controller'); -module.exports = function() { +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'] + profileFields: ['id', 'first-name', 'last-name', 'email-address', 'picture-url'] }, function(req, accessToken, refreshToken, profile, done) { // Set the provider data and include tokens @@ -30,6 +29,7 @@ module.exports = function() { displayName: profile.displayName, email: profile.emails[0].value, username: profile.username, + profileImageURL: (providerData.pictureUrl) ? providerData.pictureUrl : undefined, provider: 'linkedin', providerIdentifierField: 'id', providerData: providerData diff --git a/config/strategies/local.js b/modules/users/server/config/strategies/local.js similarity index 73% rename from config/strategies/local.js rename to modules/users/server/config/strategies/local.js index ad56052422..50ad14f142 100644 --- a/config/strategies/local.js +++ b/modules/users/server/config/strategies/local.js @@ -20,14 +20,9 @@ module.exports = function() { if (err) { return done(err); } - if (!user) { + if (!user || !user.authenticate(password)) { return done(null, false, { - message: 'Unknown user or invalid password' - }); - } - if (!user.authenticate(password)) { - return done(null, false, { - message: 'Unknown user or invalid password' + message: 'Invalid username or password' }); } diff --git a/config/strategies/twitter.js b/modules/users/server/config/strategies/twitter.js similarity index 88% rename from config/strategies/twitter.js rename to modules/users/server/config/strategies/twitter.js index 5dcc93f4ed..393a2bd9fe 100644 --- a/config/strategies/twitter.js +++ b/modules/users/server/config/strategies/twitter.js @@ -5,10 +5,9 @@ */ var passport = require('passport'), TwitterStrategy = require('passport-twitter').Strategy, - config = require('../config'), - users = require('../../app/controllers/users.server.controller'); + users = require('../../controllers/users.server.controller'); -module.exports = function() { +module.exports = function(config) { // Use twitter strategy passport.use(new TwitterStrategy({ consumerKey: config.twitter.clientID, @@ -33,6 +32,7 @@ module.exports = function() { lastName: lastName, displayName: displayName, username: profile.username, + profileImageURL: profile.photos[0].value.replace('normal', 'bigger'), provider: 'twitter', providerIdentifierField: 'id_str', providerData: providerData diff --git a/config/passport.js b/modules/users/server/config/users.server.config.js old mode 100755 new mode 100644 similarity index 59% rename from config/passport.js rename to modules/users/server/config/users.server.config.js index 5abfae766e..84d20558c9 --- a/config/passport.js +++ b/modules/users/server/config/users.server.config.js @@ -6,12 +6,12 @@ var passport = require('passport'), User = require('mongoose').model('User'), path = require('path'), - config = require('./config'); + config = require(path.resolve('./config/config')); /** * Module init function. */ -module.exports = function() { +module.exports = function(app, db) { // Serialize sessions passport.serializeUser(function(user, done) { done(null, user.id); @@ -27,7 +27,11 @@ module.exports = function() { }); // Initialize strategies - config.getGlobbedFiles('./config/strategies/**/*.js').forEach(function(strategy) { - require(path.resolve(strategy))(); + 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()); +}; \ No newline at end of file diff --git a/app/controllers/users.server.controller.js b/modules/users/server/controllers/users.server.controller.js old mode 100755 new mode 100644 similarity index 100% rename from app/controllers/users.server.controller.js rename to modules/users/server/controllers/users.server.controller.js diff --git a/app/controllers/users/users.authentication.server.controller.js b/modules/users/server/controllers/users/users.authentication.server.controller.js similarity index 96% rename from app/controllers/users/users.authentication.server.controller.js rename to modules/users/server/controllers/users/users.authentication.server.controller.js index d34642b5b6..b31776e3fd 100644 --- a/app/controllers/users/users.authentication.server.controller.js +++ b/modules/users/server/controllers/users/users.authentication.server.controller.js @@ -3,8 +3,8 @@ /** * Module dependencies. */ -var _ = require('lodash'), - errorHandler = require('../errors.server.controller'), +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'); @@ -134,6 +134,7 @@ exports.saveOAuthUserProfile = function(req, providerUserProfile, done) { username: availableUsername, displayName: providerUserProfile.displayName, email: providerUserProfile.email, + profileImageURL: providerUserProfile.profileImageURL, provider: providerUserProfile.provider, providerData: providerUserProfile.providerData }); @@ -176,7 +177,7 @@ exports.saveOAuthUserProfile = function(req, providerUserProfile, done) { */ exports.removeOAuthProvider = function(req, res, next) { var user = req.user; - var provider = req.param('provider'); + var provider = req.params.provider; if (user && provider) { // Delete the additional provider 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..9ad24fc3ef --- /dev/null +++ b/modules/users/server/controllers/users/users.authorization.server.controller.js @@ -0,0 +1,22 @@ +'use strict'; + +/** + * Module dependencies. + */ +var _ = require('lodash'), + mongoose = require('mongoose'), + User = mongoose.model('User'); + +/** + * User middleware + */ +exports.userByID = function(req, res, next, id) { + User.findOne({ + _id: id + }).exec(function(err, user) { + if (err) return next(err); + if (!user) return next(new Error('Failed to load User ' + id)); + req.profile = user; + next(); + }); +}; diff --git a/app/controllers/users/users.password.server.controller.js b/modules/users/server/controllers/users/users.password.server.controller.js similarity index 90% rename from app/controllers/users/users.password.server.controller.js rename to modules/users/server/controllers/users/users.password.server.controller.js index e246baed8f..98d865fb7a 100644 --- a/app/controllers/users/users.password.server.controller.js +++ b/modules/users/server/controllers/users/users.password.server.controller.js @@ -3,12 +3,11 @@ /** * Module dependencies. */ -var _ = require('lodash'), - errorHandler = require('../errors.server.controller'), +var path = require('path'), + config = require(path.resolve('./config/config')), + errorHandler = require(path.resolve('./modules/core/server/controllers/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'); @@ -57,10 +56,10 @@ exports.forgot = function(req, res, next) { } }, function(token, user, done) { - res.render('templates/reset-password-email', { + res.render(path.resolve('modules/users/server/templates/reset-password-email'), { name: user.displayName, appName: config.app.title, - url: 'http://' + req.headers.host + '/auth/reset/' + token + url: 'http://' + req.headers.host + '/api/auth/reset/' + token }, function(err, emailHTML) { done(err, emailHTML, user); }); @@ -76,7 +75,7 @@ exports.forgot = function(req, res, next) { smtpTransport.sendMail(mailOptions, function(err) { if (!err) { res.send({ - message: 'An email has been sent to ' + user.email + ' with further instructions.' + message: 'An email has been sent to the provided email with further instructions.' }); } else { return res.status(400).send({ @@ -116,6 +115,7 @@ exports.validateResetToken = function(req, res) { exports.reset = function(req, res, next) { // Init Variables var passwordDetails = req.body; + var message = null; async.waterfall([ @@ -163,7 +163,7 @@ exports.reset = function(req, res, next) { }); }, function(user, done) { - res.render('templates/reset-password-confirm-email', { + res.render('modules/users/server/templates/reset-password-confirm-email', { name: user.displayName, appName: config.app.title }, function(err, emailHTML) { @@ -191,9 +191,10 @@ exports.reset = function(req, res, next) { /** * Change Password */ -exports.changePassword = function(req, res) { +exports.changePassword = function(req, res, next) { // Init Variables var passwordDetails = req.body; + var message = null; if (req.user) { if (passwordDetails.newPassword) { 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..63e18e8705 --- /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/app/models/user.server.model.js b/modules/users/server/models/user.server.model.js old mode 100755 new mode 100644 similarity index 95% rename from app/models/user.server.model.js rename to modules/users/server/models/user.server.model.js index 39dff15a89..ac86e3031e --- a/app/models/user.server.model.js +++ b/modules/users/server/models/user.server.model.js @@ -62,6 +62,10 @@ var UserSchema = new Schema({ salt: { type: String }, + profileImageURL: { + type: String, + default: 'modules/users/img/profile/default.png' + }, provider: { type: String, required: 'Provider is required' @@ -86,9 +90,9 @@ var UserSchema = new Schema({ resetPasswordToken: { type: String }, - resetPasswordExpires: { - type: Date - } + resetPasswordExpires: { + type: Date + } }); /** 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..6390beed5f --- /dev/null +++ b/modules/users/server/routes/auth.server.routes.js @@ -0,0 +1,53 @@ +'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(passport.authenticate('facebook', { + scope: ['email'] + })); + app.route('/api/auth/facebook/callback').get(users.oauthCallback('facebook')); + + // Setting the twitter oauth routes + app.route('/api/auth/twitter').get(passport.authenticate('twitter')); + app.route('/api/auth/twitter/callback').get(users.oauthCallback('twitter')); + + // Setting the google oauth routes + app.route('/api/auth/google').get(passport.authenticate('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(passport.authenticate('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(passport.authenticate('github')); + app.route('/api/auth/github/callback').get(users.oauthCallback('github')); +}; 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..d74a428190 --- /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/app/views/templates/reset-password-confirm-email.server.view.html b/modules/users/server/templates/reset-password-confirm-email.server.view.html similarity index 98% rename from app/views/templates/reset-password-confirm-email.server.view.html rename to modules/users/server/templates/reset-password-confirm-email.server.view.html index bfbcb157fc..baeddbf957 100644 --- a/app/views/templates/reset-password-confirm-email.server.view.html +++ b/modules/users/server/templates/reset-password-confirm-email.server.view.html @@ -1,7 +1,9 @@ + +

Dear {{name}},

@@ -10,4 +12,5 @@

The {{appName}} Support Team

+ diff --git a/app/views/templates/reset-password-email.server.view.html b/modules/users/server/templates/reset-password-email.server.view.html similarity index 99% rename from app/views/templates/reset-password-email.server.view.html rename to modules/users/server/templates/reset-password-email.server.view.html index 4869dfd859..e5934a95af 100644 --- a/app/views/templates/reset-password-email.server.view.html +++ b/modules/users/server/templates/reset-password-email.server.view.html @@ -1,8 +1,11 @@ + + +

Dear {{name}},


@@ -15,4 +18,5 @@

The {{appName}} Support Team

+ diff --git a/public/modules/users/tests/authentication.client.controller.test.js b/modules/users/tests/client/authentication.client.controller.tests.js similarity index 89% rename from public/modules/users/tests/authentication.client.controller.test.js rename to modules/users/tests/client/authentication.client.controller.tests.js index 4c95d686ae..348ec474c6 100644 --- a/public/modules/users/tests/authentication.client.controller.test.js +++ b/modules/users/tests/client/authentication.client.controller.tests.js @@ -48,7 +48,7 @@ it('$scope.signin() should login with a correct user and password', function() { // Test expected GET request - $httpBackend.when('POST', '/auth/signin').respond(200, 'Fred'); + $httpBackend.when('POST', '/api/auth/signin').respond(200, 'Fred'); scope.signin(); $httpBackend.flush(); @@ -60,7 +60,7 @@ it('$scope.signin() should fail to log in with nothing', function() { // Test expected POST request - $httpBackend.expectPOST('/auth/signin').respond(400, { + $httpBackend.expectPOST('/api/auth/signin').respond(400, { 'message': 'Missing credentials' }); @@ -77,7 +77,7 @@ scope.credentials = 'Bar'; // Test expected POST request - $httpBackend.expectPOST('/auth/signin').respond(400, { + $httpBackend.expectPOST('/api/auth/signin').respond(400, { 'message': 'Unknown user' }); @@ -91,7 +91,7 @@ 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'); + $httpBackend.when('POST', '/api/auth/signup').respond(200, 'Fred'); scope.signup(); $httpBackend.flush(); @@ -104,7 +104,7 @@ it('$scope.signup() should fail to register with duplicate Username', function() { // Test expected POST request - $httpBackend.when('POST', '/auth/signup').respond(400, { + $httpBackend.when('POST', '/api/auth/signup').respond(400, { 'message': 'Username already exists' }); @@ -115,4 +115,4 @@ expect(scope.error).toBe('Username already exists'); }); }); -}()); \ No newline at end of file +}()); 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..ef761097bc --- /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/app/tests/user.server.model.test.js b/modules/users/tests/server/user.server.model.tests.js similarity index 100% rename from app/tests/user.server.model.test.js rename to modules/users/tests/server/user.server.model.tests.js diff --git a/package.json b/package.json old mode 100755 new mode 100644 index 091f2e121f..407d9a5d66 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "meanjs", "description": "Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js.", - "version": "0.3.3", + "version": "0.4.0", "private": false, "author": "https://github.com/meanjs/mean/graphs/contributors", "repository": { @@ -18,17 +18,19 @@ "postinstall": "bower install --config.interactive=false" }, "dependencies": { - "express": "~4.10.1", - "express-session": "~1.9.1", - "body-parser": "~1.9.0", + "express": "~4.12.3", + "express-session": "~1.10.4", + "serve-favicon": "~2.1.6", + "body-parser": "~1.12.2", "cookie-parser": "~1.3.2", - "compression": "~1.2.0", + "compression": "~1.4.3", "method-override": "~2.3.0", - "morgan": "~1.4.1", - "connect-mongo": "~0.4.1", + "morgan": "~1.5.2", + "multer": "0.1.6", + "connect-mongo": "~0.8.0", "connect-flash": "~0.1.1", - "helmet": "~0.5.0", - "consolidate": "~0.10.0", + "helmet": "~0.7.1", + "consolidate": "~0.11.0", "swig": "~1.4.1", "mongoose": "~3.8.8", "passport": "~0.2.0", @@ -38,14 +40,16 @@ "passport-linkedin": "~0.1.3", "passport-google-oauth": "~0.1.5", "passport-github": "~0.1.5", + "acl": "~0.4.4", + "socket.io": "~1.1.0", "lodash": "~2.4.1", "forever": "~0.11.0", "bower": "~1.3.8", "grunt-cli": "~0.1.13", - "glob": "~4.0.5", + "chalk": "~0.5.1", + "glob": "~5.0.0", "async": "~0.9.0", - "nodemailer": "~1.3.0", - "chalk": "~1.0.0" + "nodemailer": "~1.3.0" }, "devDependencies": { "supertest": "~0.14.0", @@ -59,11 +63,31 @@ "grunt-contrib-uglify": "~0.6.0", "grunt-contrib-cssmin": "~0.10.0", "grunt-nodemon": "~0.3.0", + "grunt-contrib-copy": "0.8", "grunt-concurrent": "~1.0.0", "grunt-mocha-test": "~0.12.1", "grunt-karma": "~0.9.0", + "grunt-protractor-runner": "1.1.4", + "grunt-contrib-sass": "~0.8.1", + "grunt-contrib-less": "~0.12.0", "load-grunt-tasks": "~1.0.0", - "grunt-contrib-copy": "0.8", + "gulp": "~3.8.9", + "run-sequence": "~1.0.1", + "gulp-rename": "~1.2.0", + "gulp-concat": "~2.4.1", + "gulp-nodemon": "~1.0.4", + "gulp-livereload": "~2.1.1", + "gulp-jshint": "~1.8.6", + "gulp-csslint": "~0.1.5", + "gulp-ng-annotate": "~0.3.3", + "gulp-uglify": "~1.0.1", + "gulp-cssmin": "~0.1.6", + "gulp-mocha": "~1.1.1", + "gulp-karma": "~0.0.4", + "gulp-protractor": "~0.0.11", + "gulp-sass": "~1.3.3", + "gulp-less": "~1.3.6", + "gulp-load-plugins": "~0.7.0", "karma": "~0.12.0", "karma-jasmine": "~0.2.1", "karma-coverage": "~0.2.0", @@ -71,4 +95,4 @@ "karma-firefox-launcher": "~0.1.3", "karma-phantomjs-launcher": "~0.1.2" } -} +} \ No newline at end of file diff --git a/protractor.conf.js b/protractor.conf.js new file mode 100644 index 0000000000..a2308500e5 --- /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/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/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/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/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/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/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.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/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/reset-password.js b/scripts/reset-password.js new file mode 100644 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..4e697a2b8e --- a/server.js +++ b/server.js @@ -1,10 +1,11 @@ 'use strict'; + /** * Module dependencies. */ -var init = require('./config/init')(), - config = require('./config/config'), - mongoose = require('mongoose'), +var config = require('./config/config'), + mongoose = require('./config/lib/mongoose'), + express = require('./config/lib/express'), chalk = require('chalk'); /** @@ -12,38 +13,22 @@ var init = require('./config/init')(), * Please note that the order of loading is important. */ -// 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); +// Initialize mongoose +mongoose.connect(function (db) { + // Initialize express + var app = express.init(db); -// Bootstrap passport config -require('./config/passport')(); + // Start the app by listening on + app.listen(config.port); -// 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('--'); + // 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('--'); +}); \ No newline at end of file