diff --git a/.editorconfig b/.editorconfig index 636ceea4..06dd60bf 100644 --- a/.editorconfig +++ b/.editorconfig @@ -33,4 +33,4 @@ indent_style = tab # Standard at: [Makefile] -indent_style = tab \ No newline at end of file +indent_style = tab diff --git a/.gitignore b/.gitignore index 79f6cf28..721705ea 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,5 @@ npm-debug.log node_modules/ public/lib -app/tests/coverage/ .bower-*/ .idea/ -config/sslcert/*.pem diff --git a/.jshintrc b/.jshintrc index 4cd07cdc..b3a00dc3 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/README.md b/README.md index 6f7d5f4f..dec285d1 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,20 @@ [![Build Status](https://travis-ci.org/meanjs/mean.svg?branch=master)](https://travis-ci.org/meanjs/mean) [![Dependencies Status](https://david-dm.org/meanjs/mean.svg)](https://david-dm.org/meanjs/mean) -[![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/meanjs/mean?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) MEAN.JS is a full-stack JavaScript open-source solution, which provides a solid starting point for [MongoDB](http://www.mongodb.org/), [Node.js](http://www.nodejs.org/), [Express](http://expressjs.com/), and [AngularJS](http://angularjs.org/) based applications. The idea is to solve the common issues with connecting those frameworks, build a robust framework to support daily development needs, and help developers use better practices while working with popular JavaScript components. ## Before You Begin Before you begin we recommend you read about the basic building blocks that assemble a MEAN.JS application: * MongoDB - Go through [MongoDB Official Website](http://mongodb.org/) and proceed to their [Official Manual](http://docs.mongodb.org/manual/), which should help you understand NoSQL and MongoDB better. -* Express - The best way to understand express is through its [Official Website](http://expressjs.com/), which has a [Getting Started](http://expressjs.com/starter/installing.html) guide, as well as an [ExpressJS Guide](http://expressjs.com/guide/error-handling.html) guide for general express topics. You can also go through this [StackOverflow Thread](http://stackoverflow.com/questions/8144214/learning-express-for-node-js) for more resources. +* Express - The best way to understand express is through its [Official Website](http://expressjs.com/), particularly [The Express Guide](http://expressjs.com/guide.html); you can also go through this [StackOverflow Thread](http://stackoverflow.com/questions/8144214/learning-express-for-node-js) for more resources. * AngularJS - Angular's [Official Website](http://angularjs.org/) is a great starting point. You can also use [Thinkster Popular Guide](http://www.thinkster.io/), and the [Egghead Videos](https://egghead.io/). * Node.js - Start by going through [Node.js Official Website](http://nodejs.org/) and this [StackOverflow Thread](http://stackoverflow.com/questions/2353818/how-do-i-get-started-with-node-js), which should get you going with the Node.js platform in no time. ## Prerequisites Make sure you have installed all these prerequisites on your development machine. -* Node.js - [Download & Install Node.js](http://www.nodejs.org/download/) and the npm package manager, if you encounter any problems, you can also use this [GitHub Gist](https://gist.github.com/isaacs/579814) to install Node.js. +* Node.js - [Download & Install Node.js](http://www.nodejs.org/download/) and the npm package manager, if you encounter any problems, you can also use this [Github Gist](https://gist.github.com/isaacs/579814) to install Node.js. * MongoDB - [Download & Install MongoDB](http://www.mongodb.org/downloads), and make sure it's running on the default port (27017). * Bower - You're going to use the [Bower Package Manager](http://bower.io/) to manage your front-end packages, in order to install it make sure you've installed Node.js and npm, then install bower globally using npm: @@ -44,7 +43,7 @@ $ git clone https://github.com/meanjs/mean.git meanjs This will clone the latest version of the MEAN.JS repository to a **meanjs** folder. ### Downloading The Repository Zip File -Another way to use the MEAN.JS boilerplate is to download a zip copy from the [master branch on GitHub](https://github.com/meanjs/mean/archive/master.zip). You can also do this using `wget` command: +Another way to use the MEAN.JS boilerplate is to download a zip copy from the [master branch on github](https://github.com/meanjs/mean/archive/master.zip). You can also do this using `wget` command: ``` $ wget https://github.com/meanjs/mean/archive/master.zip -O meanjs.zip; unzip meanjs.zip; rm meanjs.zip ``` @@ -64,7 +63,7 @@ $ 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 installcommand 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: @@ -98,20 +97,12 @@ $ * To enable live reload forward 35729 port and mount /app and /public as volumes: ```bash -$ docker run -p 3000:3000 -p 35729:35729 -v /Users/mdl/workspace/mean-stack/mean/public:/home/mean/public -v /Users/mdl/workspace/mean-stack/mean/app:/home/mean/app --link db:db_1 mean +$ docker run -p 3000:3000 -p 35729:35729 -v /Users/mdl/workspace/mean-stack/mean/public:/home/mean/public -v /Users/mdl/workspa/mean-stack/mean/app:/home/mean/app --link db:db_1 mean ``` -## Running in a secure environment -To run your application in a secure manner you'll need to use OpenSSL and generate a set of self-signed certificates. Unix-based users can use the following commnad: -``` -$ sh generate-ssl-certs.sh -``` -Windows users can follow instructions found [here](http://www.websense.com/support/article/kbarticle/How-to-use-OpenSSL-and-Microsoft-Certification-Authority) -To generate the key and certificate and place them in the *config/sslcert* folder. - ## Getting Started With MEAN.JS -You have your application running but there are a lot of stuff to understand, we recommend you'll go over the [Official Documentation](http://meanjs.org/docs.html). -In the docs we'll try to explain both general concepts of MEAN components and give you some guidelines to help you improve your development process. We tried covering as many aspects as possible, and will keep update it by your request, you can also help us develop the documentation better by checking out the *gh-pages* branch of this repository. +You have your application running but there are a lot of stuff to understand, we recommend you'll go over the [Offical Documentation](http://meanjs.org/docs.html). +In the docs we'll try to explain both general concepts of MEAN components and give you some guidelines to help you improve your development procees. We tried covering as many aspects as possible, and will keep update it by your request, you can also help us develop the documentation better by checking out the *gh-pages* branch of this repository. ## Community * Use to [Offical Website](http://meanjs.org) to learn about changes and the roadmap. diff --git a/app/controllers/core.server.controller.js b/app/controllers/core.server.controller.js deleted file mode 100644 index f2af8e1f..00000000 --- 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 - }); -}; \ No newline at end of file diff --git a/app/controllers/users/users.profile.server.controller.js b/app/controllers/users/users.profile.server.controller.js deleted file mode 100644 index dd38936f..00000000 --- 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); -}; \ No newline at end of file diff --git a/app/routes/articles.server.routes.js b/app/routes/articles.server.routes.js deleted file mode 100644 index 7d840e6f..00000000 --- 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); -}; \ No newline at end of file diff --git a/app/routes/core.server.routes.js b/app/routes/core.server.routes.js deleted file mode 100644 index 1db9d400..00000000 --- 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); -}; \ No newline at end of file diff --git a/app/routes/users.server.routes.js b/app/routes/users.server.routes.js deleted file mode 100644 index 3120e9ab..00000000 --- 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); -}; \ No newline at end of file diff --git a/bower.json b/bower.json index 3d7a7451..951e48aa 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", @@ -8,8 +8,9 @@ "angular-resource": "~1.2", "angular-animate": "~1.2", "angular-mocks": "~1.2", - "angular-bootstrap": "~0.11.2", + "angular-bootstrap": "~0.11.0", "angular-ui-utils": "~0.1.1", - "angular-ui-router": "~0.2.11" + "angular-ui-router": "~0.2.10", + "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 00000000..8d614fd4 --- /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 00000000..47a2c6d2 --- /dev/null +++ b/config/assets/development.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + // Development assets +}; \ No newline at end of file diff --git a/config/assets/production.js b/config/assets/production.js new file mode 100644 index 00000000..9a664c45 --- /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 00000000..ecc4cac9 --- /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'] + } +}; \ No newline at end of file diff --git a/config/config.js b/config/config.js index 3baa02e5..a966dde2 100644 --- a/config/config.js +++ b/config/config.js @@ -4,73 +4,169 @@ * Module dependencies. */ var _ = require('lodash'), - glob = require('glob'); - -/** - * Load app configurations - */ -module.exports = _.extend( - require('./env/all'), - require('./env/' + process.env.NODE_ENV) || {} -); + chalk = require('chalk'), + glob = require('glob'), + path = require('path'); /** * Get files by glob patterns */ -module.exports.getGlobbedFiles = function(globPatterns, removeRoot) { - // For context switching - var _this = this; +var getGlobbedPaths = function(globPatterns, excludes) { + // URL paths regex + var urlRegex = new RegExp('^(?:[a-z]+:)?\/\/', 'i'); - // URL paths regex - var urlRegex = new RegExp('^(?:[a-z]+:)?\/\/', 'i'); + // The output array + var output = []; - // 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, getGlobbedPaths(globPattern, excludes)); + }); + } else if (_.isString(globPatterns)) { + if (urlRegex.test(globPatterns)) { + output.push(globPatterns); + } else { + glob(globPatterns, { + sync: true + }, function(err, files) { + 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, ''); + } - // 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, ''); - }); - } + return file; + }); + } - output = _.union(output, files); - }); - } - } + output = _.union(output, files); + }); + } + } - return output; + return output; }; /** - * Get the modules JavaScript files + * Validate NODE_ENV existance */ -module.exports.getJavaScriptAssets = function(includeTests) { - var output = this.getGlobbedFiles(this.assets.lib.js.concat(this.assets.js), 'public/'); +var validateEnvironmentVariable = function() { + glob('./config/env/' + process.env.NODE_ENV + '.js', { + sync: true + }, function(err, environmentFiles) { + console.log(); - // To include tests - if (includeTests) { - output = _.union(output, this.getGlobbedFiles(this.assets.tests)); - } + 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')); + } - return output; + 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 the modules CSS files + * Initialize global configuration files */ -module.exports.getCSSAssets = function() { - var output = this.getGlobbedFiles(this.assets.lib.css.concat(this.assets.css), 'public/'); - return output; -}; \ No newline at end of file +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()); +}; + +/** + * Initialize global configuration files + */ +var initGlobalConfigFiles = function(config, assets) { + // Appending files + config.files = { + server: {}, + client: {} + }; + + // Setting Globbed model files + config.files.server.models = getGlobbedPaths(assets.server.models); + + // Setting Globbed route files + config.files.server.routes = getGlobbedPaths(assets.server.routes); + + // Setting Globbed config files + config.files.server.configs = getGlobbedPaths(assets.server.config); + + // Setting Globbed socket files + config.files.server.sockets = getGlobbedPaths(assets.server.sockets); + + // Setting Globbed policies files + config.files.server.policies = getGlobbedPaths(assets.server.policies); + + // 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); +}; + +/** + * Initialize global configuration + */ +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 config = _.extend(defaultConfig, environmentConfig); + + // 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 b4e1749e..00000000 --- a/config/env/all.js +++ /dev/null @@ -1,51 +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', - sessionSecret: 'MEAN', - sessionCollection: 'sessions', - 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 00000000..8acd131a --- /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 2b895b8b..31018945 100644 --- a/config/env/development.js +++ b/config/env/development.js @@ -2,43 +2,34 @@ module.exports = { db: 'mongodb://localhost/mean-dev', - log: { - // Can specify one of 'combined', 'common', 'dev', 'short', 'tiny' - format: 'dev', - // Stream defaults to process.stdout - // Uncomment to enable logging to a log on the file system - options: { - //stream: 'access.log' - } - }, app: { title: 'MEAN.JS - Development Environment' }, facebook: { - clientID: process.env.FACEBOOK_ID || 'APP_ID', - clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET', - callbackURL: '/auth/facebook/callback' - }, - twitter: { - clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY', - clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET', - callbackURL: '/auth/twitter/callback' - }, - google: { - clientID: process.env.GOOGLE_ID || 'APP_ID', - clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET', - callbackURL: '/auth/google/callback' - }, - linkedin: { - clientID: process.env.LINKEDIN_ID || 'APP_ID', - clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', - callbackURL: '/auth/linkedin/callback' - }, - github: { - clientID: process.env.GITHUB_ID || 'APP_ID', - clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET', - callbackURL: '/auth/github/callback' - }, + 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 c9f28173..3e875547 100644 --- a/config/env/production.js +++ b/config/env/production.js @@ -1,67 +1,40 @@ 'use strict'; module.exports = { - db: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://' + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + '/mean', - log: { - // Can specify one of 'combined', 'common', 'dev', 'short', 'tiny' - format: 'combined', - // Stream defaults to process.stdout - // Uncomment to enable logging to a log on the file system - options: { - stream: 'access.log' - } - }, - assets: { - lib: { - css: [ - 'public/lib/bootstrap/dist/css/bootstrap.min.css', - 'public/lib/bootstrap/dist/css/bootstrap-theme.min.css', - ], - js: [ - 'public/lib/angular/angular.min.js', - 'public/lib/angular-resource/angular-resource.min.js', - 'public/lib/angular-animate/angular-animate.min.js', - 'public/lib/angular-ui-router/release/angular-ui-router.min.js', - 'public/lib/angular-ui-utils/ui-utils.min.js', - 'public/lib/angular-bootstrap/ui-bootstrap-tpls.min.js' - ] - }, - css: 'public/dist/application.min.css', - js: 'public/dist/application.min.js' - }, - facebook: { - clientID: process.env.FACEBOOK_ID || 'APP_ID', - clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET', - callbackURL: '/auth/facebook/callback' - }, - twitter: { - clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY', - clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET', - callbackURL: '/auth/twitter/callback' - }, - google: { - clientID: process.env.GOOGLE_ID || 'APP_ID', - clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET', - callbackURL: '/auth/google/callback' - }, - linkedin: { - clientID: process.env.LINKEDIN_ID || 'APP_ID', - clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', - callbackURL: '/auth/linkedin/callback' - }, - github: { - clientID: process.env.GITHUB_ID || 'APP_ID', - clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET', - callbackURL: '/auth/github/callback' - }, - mailer: { - from: process.env.MAILER_FROM || 'MAILER_FROM', - options: { - service: process.env.MAILER_SERVICE_PROVIDER || 'MAILER_SERVICE_PROVIDER', - auth: { - user: process.env.MAILER_EMAIL_ID || 'MAILER_EMAIL_ID', - pass: process.env.MAILER_PASSWORD || 'MAILER_PASSWORD' - } - } - } -}; + db: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://' + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + '/mean', + facebook: { + clientID: process.env.FACEBOOK_ID || 'APP_ID', + clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET', + callbackURL: '/api/auth/facebook/callback' + }, + twitter: { + clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY', + clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET', + callbackURL: '/api/auth/twitter/callback' + }, + google: { + clientID: process.env.GOOGLE_ID || 'APP_ID', + clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET', + callbackURL: '/api/auth/google/callback' + }, + linkedin: { + clientID: process.env.LINKEDIN_ID || 'APP_ID', + clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', + callbackURL: '/api/auth/linkedin/callback' + }, + github: { + clientID: process.env.GITHUB_ID || 'APP_ID', + clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET', + callbackURL: '/api/auth/github/callback' + }, + mailer: { + from: process.env.MAILER_FROM || 'MAILER_FROM', + options: { + service: process.env.MAILER_SERVICE_PROVIDER || 'MAILER_SERVICE_PROVIDER', + auth: { + user: process.env.MAILER_EMAIL_ID || 'MAILER_EMAIL_ID', + pass: process.env.MAILER_PASSWORD || 'MAILER_PASSWORD' + } + } + } +}; \ No newline at end of file diff --git a/config/env/secure.js b/config/env/secure.js deleted file mode 100644 index ee2b270a..00000000 --- a/config/env/secure.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict'; - -module.exports = { - port: 443, - db: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://localhost/mean', - 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' - } - } - } -}; \ No newline at end of file diff --git a/config/env/test.js b/config/env/test.js index 22c47c9f..6f95fdd3 100644 --- a/config/env/test.js +++ b/config/env/test.js @@ -3,42 +3,33 @@ module.exports = { db: 'mongodb://localhost/mean-test', 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 f19ba586..00000000 --- a/config/express.js +++ /dev/null @@ -1,164 +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'), - compress = 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(compress({ - filter: function(req, res) { - return (/json|text|javascript|css/).test(res.getHeader('Content-Type')); - }, - level: 9 - })); - - // 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()); - - // 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 - }) - })); - - // use passport session - app.use(passport.initialize()); - app.use(passport.session()); - - // connect flash for flash messages - app.use(flash()); - - // 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'))); - - // 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') { - // Log SSL usage - console.log('Securely using https protocol'); - - // 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; -}; \ No newline at end of file diff --git a/config/init.js b/config/init.js deleted file mode 100644 index 6facd9d0..00000000 --- a/config/init.js +++ /dev/null @@ -1,33 +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'; - } else { - console.log(chalk.black.bgWhite('Application loaded using the "' + process.env.NODE_ENV + '" environment configuration')); - } - }); - -}; \ No newline at end of file diff --git a/config/lib/express.js b/config/lib/express.js new file mode 100755 index 00000000..2ad68145 --- /dev/null +++ b/config/lib/express.js @@ -0,0 +1,251 @@ +'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.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/).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({ + db: db.connection.db, + 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 00000000..9bb0b74a --- /dev/null +++ b/config/lib/mongoose.js @@ -0,0 +1,36 @@ +'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); + } + }); +}; diff --git a/config/lib/socket.io.js b/config/lib/socket.io.js new file mode 100644 index 00000000..4e89bdee --- /dev/null +++ b/config/lib/socket.io.js @@ -0,0 +1,61 @@ +'use strict'; + +// Load the module dependencies +var config = require('../config'), + path = require('path'), + cookieParser = require('cookie-parser'), + passport = require('passport'), + socketio = require('socket.io'), + session = require('express-session'), + MongoStore = require('connect-mongo')(session), + http = require('http'); + +// Define the Socket.io configuration method +module.exports = function(app, db) { + // Create a new HTTP server + var server = http.createServer(app); + + // Create a new Socket.io server + var io = socketio.listen(server); + + // Create a MongoDB storage object + var mongoStore = new MongoStore({ + db: db.connection.db, + 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; +}; \ No newline at end of file diff --git a/config/logger.js b/config/logger.js deleted file mode 100644 index 98b3a4c5..00000000 --- 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; - } - -}; \ No newline at end of file diff --git a/gruntfile.js b/gruntfile.js index 45554857..ea4eb0d8 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -1,68 +1,117 @@ 'use strict'; -module.exports = function(grunt) { - // Unified Watch Object - var watchFiles = { - serverViews: ['app/views/**/*.*'], - serverJS: ['gruntfile.js', 'server.js', 'config/**/*.js', 'app/**/*.js'], - 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'); +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, + 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 } + }, + 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 } } }, csslint: { options: { - csslintrc: '.csslintrc', + csslintrc: '.csslintrc' }, all: { - src: watchFiles.clientCSS + src: defaultAssets.client.css + } + }, + ngAnnotate: { + production: { + files: { + 'public/dist/application.js': defaultAssets.client.js + } } }, uglify: { @@ -78,18 +127,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': { @@ -105,73 +168,66 @@ 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: { unit: { configFile: 'karma.conf.js' } + }, + protractor: { + options: { + configFile: 'protractor.conf.js', + keepAlive: true, + noColor: false + }, + e2e: { + options: { + args: {} // Target-specific arguments + } + } } }); - // 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(); - grunt.config.set('applicationJavaScriptFiles', config.assets.js); - grunt.config.set('applicationCSSFiles', config.assets.css); + // Use mongoose configuration + var mongoose = require('./config/lib/mongoose.js'); + + // Connect to database + mongoose.connect(function(db) { + done(); + }); }); - // Default task(s). - grunt.registerTask('default', ['lint', 'concurrent:default']); + // Lint CSS and JavaScript files. + grunt.registerTask('lint', ['sass', 'less', 'jshint', 'csslint']); - // Debug task. - grunt.registerTask('debug', ['lint', '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', 'concurrent:default']); + // Run the project tests + grunt.registerTask('test', ['env:test', 'mongoose', 'mochaTest', 'karma:unit']); - // Lint task(s). - grunt.registerTask('lint', ['jshint', 'csslint']); + // Run the project in development mode + grunt.registerTask('default', ['env:dev', 'lint', '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', 'concurrent:debug']); - // Test task. - grunt.registerTask('test', ['env:test', 'mochaTest', 'karma:unit']); -}; \ No newline at end of file + // Run the project in production mode + grunt.registerTask('prod', ['build', 'env:prod', 'concurrent:default']); +}; diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 00000000..b0a7ceda --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,187 @@ +'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/')); +}); + +// Connect to MongoDB using the mongoose module +gulp.task('mongoose', function (done) { + var mongoose = require('./config/lib/mongoose.js'); + + mongoose.connect(function(db) { + done(); + }); +}); + +// Mocha tests task +gulp.task('mocha', function () { + return gulp.src(testAssets.tests.server) + .pipe(plugins.mocha({ + reporter: 'spec' + })); +}); + +// Karma test runner task +gulp.task('karma', function (done) { + return gulp.src([]) + .pipe(plugins.karma({ + configFile: 'karma.conf.js', + action: 'run', + singleRun: true + })) + .on('error', function (err) { + // Make sure failed tests cause gulp to exit non-zero + throw err; + }); +}); + +// 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', 'mongoose', ['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 e85f39b7..0b6ead26 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' @@ -26,8 +28,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, @@ -49,4 +51,4 @@ module.exports = function(config) { // If true, it capture browsers, run tests and exit singleRun: true }); -}; \ No newline at end of file +}; diff --git a/public/modules/articles/articles.client.module.js b/modules/articles/client/articles.client.module.js similarity index 100% rename from public/modules/articles/articles.client.module.js rename to modules/articles/client/articles.client.module.js 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 00000000..d43e0893 --- /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 similarity index 64% rename from public/modules/articles/config/articles.client.routes.js rename to modules/articles/client/config/articles.client.routes.js index 1531a9a5..c6890be3 100755 --- 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 100% rename from public/modules/articles/controllers/articles.client.controller.js rename to modules/articles/client/controllers/articles.client.controller.js 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 deeb7da5..5c8967f5 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 ab8db8ef..79ea510e 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 @@
-
+
- +
@@ -26,4 +26,4 @@
- \ 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 353cb8e6..7a4d4ca0 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 @@
- \ 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 861ae5b6..0d8c3b79 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 312d25c8..a298412b 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 81% rename from app/controllers/articles.server.controller.js rename to modules/articles/server/controllers/articles.server.controller.js index 0a24e817..d9c9b45e 100644 --- a/app/controllers/articles.server.controller.js +++ b/modules/articles/server/controllers/articles.server.controller.js @@ -3,10 +3,11 @@ /** * Module dependencies. */ -var mongoose = require('mongoose'), - errorHandler = require('./errors.server.controller'), +var _ = require('lodash'), + 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 +40,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) { @@ -95,15 +97,3 @@ exports.articleByID = function(req, res, next, id) { 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 00000000..a8572d9e --- /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 00000000..49e3697f --- /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 7e25c699..859a967e 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); 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 00000000..2c6e1dd3 --- /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 99% rename from app/tests/article.server.model.test.js rename to modules/articles/tests/server/article.server.model.tests.js index e3dd60c8..ee5fd067 100644 --- a/app/tests/article.server.model.test.js +++ b/modules/articles/tests/server/article.server.model.tests.js @@ -61,4 +61,4 @@ describe('Article Model Unit Tests:', function() { User.remove().exec(); done(); }); -}); \ No newline at end of file +}); 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 2576095a..04874135 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,13 +118,13 @@ 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) { // Set message assertion (articleSaveRes.body.message).should.match('Title cannot be blank'); - + // Handle article save error done(articleSaveErr); }); @@ -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); @@ -202,7 +210,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) { @@ -213,7 +221,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) { @@ -221,7 +229,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) { @@ -248,11 +256,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); @@ -266,4 +274,4 @@ describe('Article CRUD tests', function() { Article.remove().exec(); done(); }); -}); \ No newline at end of file +}); diff --git a/public/modules/users/users.client.module.js b/modules/chat/client/chat.client.module.js old mode 100755 new mode 100644 similarity index 61% rename from public/modules/users/users.client.module.js rename to modules/chat/client/chat.client.module.js index b1199860..80ef9c29 --- a/public/modules/users/users.client.module.js +++ b/modules/chat/client/chat.client.module.js @@ -1,4 +1,4 @@ 'use strict'; // Use Applicaion configuration module to register a new module -ApplicationConfiguration.registerModule('users'); \ No newline at end of file +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 00000000..67a4719f --- /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 00000000..a86defc1 --- /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' + }); + } +]); \ No newline at end of file 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 00000000..ffb83330 --- /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 00000000..d04a8689 --- /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 00000000..c3cd6019 --- /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 00000000..d48791d6 --- /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 00000000..e8423228 --- /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 + }); +}()); \ No newline at end of file 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 00000000..06f5fc62 --- /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 +}); \ No newline at end of file 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 00000000..8a82c74e --- /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 +}); \ No newline at end of file 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 75de1c4b..b4b5947c 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 19bb411e..5e144afc 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 similarity index 100% rename from public/modules/core/config/core.client.routes.js rename to modules/core/client/config/core.client.routes.js 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 1b8c2b7b..64c30197 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 99% rename from public/modules/core/controllers/home.client.controller.js rename to modules/core/client/controllers/home.client.controller.js index 63d0f297..086632ed 100644 --- a/public/modules/core/controllers/home.client.controller.js +++ b/modules/core/client/controllers/home.client.controller.js @@ -1,6 +1,5 @@ 'use strict'; - angular.module('core').controller('HomeController', ['$scope', 'Authentication', function($scope, Authentication) { // This provides Authentication context. diff --git a/public/modules/core/core.client.module.js b/modules/core/client/core.client.module.js similarity index 100% rename from public/modules/core/core.client.module.js rename to modules/core/client/core.client.module.js diff --git a/modules/core/client/css/core.css b/modules/core/client/css/core.css new file mode 100644 index 00000000..e89af839 --- /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 00000000..020d7cf3 --- /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 00000000..e66224cf --- /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/public/modules/core/views/header.client.view.html b/modules/core/client/views/header.client.view.html similarity index 50% rename from public/modules/core/views/header.client.view.html rename to modules/core/client/views/header.client.view.html index eb6ac6df..754e9bc2 100644 --- a/public/modules/core/views/header.client.view.html +++ b/modules/core/client/views/header.client.view.html @@ -6,53 +6,57 @@ -
MEAN.JS + MEAN.JS
- \ 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 90% rename from public/modules/core/views/home.client.view.html rename to modules/core/client/views/home.client.view.html index 0fa572df..3f8242f2 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.

@@ -91,4 +91,4 @@


Enjoy & Keep Us Updated,
The MEAN.JS Team. - \ No newline at end of file + 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 00000000..3b427c38 --- /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 41078b4d..e4604f8c 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 00000000..33a43664 --- /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 75% rename from app/views/layout.server.view.html rename to modules/core/server/views/layout.server.view.html index 9f47509e..4a55542a 100644 --- a/app/views/layout.server.view.html +++ b/modules/core/server/views/layout.server.view.html @@ -8,6 +8,10 @@ + + + + @@ -55,14 +59,28 @@ + + + {% 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 100% rename from public/modules/core/tests/header.client.controller.test.js rename to modules/core/tests/client/header.client.controller.tests.js diff --git a/public/modules/core/tests/home.client.controller.test.js b/modules/core/tests/client/home.client.controller.tests.js similarity index 100% rename from public/modules/core/tests/home.client.controller.test.js rename to modules/core/tests/client/home.client.controller.tests.js 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 0bfc8b64..19d2c115 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 100755 index 00000000..9b054636 --- /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 3e27cc3b..dbc1cc0e 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; 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 dbc9e929..f5bc915c 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; 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 8616fc94..70b16c51 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 00000000..26575a81 --- /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 00000000..d131c720 --- /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 00000000..8e1d4238 --- /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 00000000..74774f06 --- /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 00000000..25af5843 --- /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 00000000..e1727b21 --- /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 00000000..edd013a6 Binary files /dev/null and b/modules/users/client/img/profile/default.png differ diff --git a/modules/users/client/img/profile/uploads/117bf261c0152c79a428db522715cdd7.png b/modules/users/client/img/profile/uploads/117bf261c0152c79a428db522715cdd7.png new file mode 100644 index 00000000..65c6f11e Binary files /dev/null and b/modules/users/client/img/profile/uploads/117bf261c0152c79a428db522715cdd7.png differ diff --git a/modules/users/client/img/profile/uploads/5323300de0498510f42b6bbb57800b77.png b/modules/users/client/img/profile/uploads/5323300de0498510f42b6bbb57800b77.png new file mode 100644 index 00000000..65c6f11e Binary files /dev/null and b/modules/users/client/img/profile/uploads/5323300de0498510f42b6bbb57800b77.png differ diff --git a/modules/users/client/img/profile/uploads/97ddbe2719b2f5da90c24b8194cca2c0.png b/modules/users/client/img/profile/uploads/97ddbe2719b2f5da90c24b8194cca2c0.png new file mode 100644 index 00000000..65c6f11e Binary files /dev/null and b/modules/users/client/img/profile/uploads/97ddbe2719b2f5da90c24b8194cca2c0.png differ diff --git a/modules/users/client/img/profile/uploads/edb5fdc047848ce1089c0e78693f7652.png b/modules/users/client/img/profile/uploads/edb5fdc047848ce1089c0e78693f7652.png new file mode 100644 index 00000000..2ed647ce Binary files /dev/null and b/modules/users/client/img/profile/uploads/edb5fdc047848ce1089c0e78693f7652.png differ diff --git a/public/modules/users/services/authentication.client.service.js b/modules/users/client/services/authentication.client.service.js similarity index 99% rename from public/modules/users/services/authentication.client.service.js rename to modules/users/client/services/authentication.client.service.js index 4418b2d7..2cbe0d87 100644 --- a/public/modules/users/services/authentication.client.service.js +++ b/modules/users/client/services/authentication.client.service.js @@ -2,6 +2,7 @@ // Authentication service for user variables angular.module('users').factory('Authentication', [ + function() { var _this = this; diff --git a/public/modules/users/services/users.client.service.js b/modules/users/client/services/users.client.service.js similarity index 84% rename from public/modules/users/services/users.client.service.js rename to modules/users/client/services/users.client.service.js index 664828f0..eaa6909a 100644 --- a/public/modules/users/services/users.client.service.js +++ b/modules/users/client/services/users.client.service.js @@ -3,7 +3,7 @@ // 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' } diff --git a/modules/users/client/users.client.module.js b/modules/users/client/users.client.module.js new file mode 100755 index 00000000..569aba8c --- /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 00000000..44675189 --- /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 00000000..5345a5dc --- /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 00000000..bb711b3e --- /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 00000000..02feebfc --- /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 00000000..a9b8512b --- /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 4de46c4b..a15df192 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 00000000..69d1f346 --- /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 9811011a..7f965801 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 00000000..e0e4f925 --- /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 a4be680f..ae8cd303 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 00000000..b40b203e --- /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 00000000..9a05049f --- /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 79% rename from config/strategies/facebook.js rename to modules/users/server/config/strategies/facebook.js index 11bd3686..4329c4d0 100644 --- a/config/strategies/facebook.js +++ b/modules/users/server/config/strategies/facebook.js @@ -6,15 +6,15 @@ var passport = require('passport'), url = require('url'), 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', 'email', 'username', 'photos'], passReqToCallback: true }, function(req, accessToken, refreshToken, profile, done) { @@ -30,6 +30,7 @@ module.exports = function() { displayName: profile.displayName, email: profile.emails[0].value, username: profile.username, + profileImageURL: (profile.photos && profile.photos.length) ? profile.photos[0].value : undefined, provider: 'facebook', providerIdentifierField: 'id', providerData: providerData @@ -39,4 +40,4 @@ module.exports = function() { users.saveOAuthUserProfile(req, providerUserProfile, done); } )); -}; \ No newline at end of file +}; diff --git a/config/strategies/github.js b/modules/users/server/config/strategies/github.js similarity index 84% rename from config/strategies/github.js rename to modules/users/server/config/strategies/github.js index 799574f5..228fdd69 100644 --- a/config/strategies/github.js +++ b/modules/users/server/config/strategies/github.js @@ -6,10 +6,9 @@ var passport = require('passport'), url = require('url'), 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, @@ -28,6 +27,7 @@ module.exports = function() { displayName: profile.displayName, email: profile.emails[0].value, username: profile.username, + profileImageURL: (providerData.avatar_url) ? providerData.avatar_url : undefined, provider: 'github', providerIdentifierField: 'id', providerData: providerData @@ -37,4 +37,4 @@ module.exports = function() { users.saveOAuthUserProfile(req, providerUserProfile, done); } )); -}; \ No newline at end of file +}; 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 80caaa2c..1c095763 100644 --- a/config/strategies/google.js +++ b/modules/users/server/config/strategies/google.js @@ -6,10 +6,9 @@ var passport = require('passport'), url = require('url'), 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, @@ -30,6 +29,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 @@ -39,4 +39,4 @@ module.exports = function() { users.saveOAuthUserProfile(req, providerUserProfile, done); } )); -}; \ No newline at end of file +}; 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 cfc173ff..a3c97d24 100644 --- a/config/strategies/linkedin.js +++ b/modules/users/server/config/strategies/linkedin.js @@ -6,17 +6,16 @@ var passport = require('passport'), url = require('url'), 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 @@ -31,6 +30,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 @@ -40,4 +40,4 @@ module.exports = function() { users.saveOAuthUserProfile(req, providerUserProfile, done); } )); -}; \ No newline at end of file +}; diff --git a/config/strategies/local.js b/modules/users/server/config/strategies/local.js similarity index 87% rename from config/strategies/local.js rename to modules/users/server/config/strategies/local.js index 7101cac2..5471473d 100644 --- a/config/strategies/local.js +++ b/modules/users/server/config/strategies/local.js @@ -22,12 +22,12 @@ module.exports = function() { } if (!user) { return done(null, false, { - message: 'Unknown user or invalid password' + message: 'Unknown user' }); } if (!user.authenticate(password)) { return done(null, false, { - message: 'Unknown user or invalid password' + message: 'Invalid password' }); } diff --git a/config/strategies/twitter.js b/modules/users/server/config/strategies/twitter.js similarity index 82% rename from config/strategies/twitter.js rename to modules/users/server/config/strategies/twitter.js index 3ffb08ba..f58de392 100644 --- a/config/strategies/twitter.js +++ b/modules/users/server/config/strategies/twitter.js @@ -6,10 +6,9 @@ var passport = require('passport'), url = require('url'), 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, @@ -27,6 +26,7 @@ module.exports = function() { var providerUserProfile = { displayName: profile.displayName, username: profile.username, + profileImageURL: (profile.photos && profile.photos.length) ? profile.photos[0].value : undefined, provider: 'twitter', providerIdentifierField: 'id_str', providerData: providerData @@ -36,4 +36,4 @@ module.exports = function() { users.saveOAuthUserProfile(req, providerUserProfile, done); } )); -}; \ No newline at end of file +}; diff --git a/config/passport.js b/modules/users/server/config/users.server.config.js old mode 100755 new mode 100644 similarity index 57% rename from config/passport.js rename to modules/users/server/config/users.server.config.js index fabbf77c..86ff6090 --- a/config/passport.js +++ b/modules/users/server/config/users.server.config.js @@ -6,12 +6,9 @@ var passport = require('passport'), User = require('mongoose').model('User'), path = require('path'), - config = require('./config'); - -/** - * Module init function. - */ -module.exports = function() { + config = require(path.resolve('./config/config')); + +module.exports = function(app, db) { // Serialize sessions passport.serializeUser(function(user, done) { done(null, user.id); @@ -27,7 +24,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); }); -}; \ No newline at end of file + + // Add passport's middleware + app.use(passport.initialize()); + app.use(passport.session()); +}; diff --git a/app/controllers/users.server.controller.js b/modules/users/server/controllers/users.server.controller.js 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 c15c8a11..2998a836 100644 --- a/app/controllers/users/users.authentication.server.controller.js +++ b/modules/users/server/controllers/users/users.authentication.server.controller.js @@ -4,7 +4,8 @@ * Module dependencies. */ var _ = require('lodash'), - errorHandler = require('../errors.server.controller'), + 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 +135,7 @@ exports.saveOAuthUserProfile = function(req, providerUserProfile, done) { username: availableUsername, displayName: providerUserProfile.displayName, email: providerUserProfile.email, + profileImageURL: providerUserProfile.profileImageURL, provider: providerUserProfile.provider, providerData: providerUserProfile.providerData }); @@ -203,4 +205,4 @@ exports.removeOAuthProvider = function(req, res, next) { } }); } -}; \ No newline at end of file +}; diff --git a/app/controllers/users/users.authorization.server.controller.js b/modules/users/server/controllers/users/users.authorization.server.controller.js similarity index 100% rename from app/controllers/users/users.authorization.server.controller.js rename to modules/users/server/controllers/users/users.authorization.server.controller.js diff --git a/app/controllers/users/users.password.server.controller.js b/modules/users/server/controllers/users/users.password.server.controller.js similarity index 91% rename from app/controllers/users/users.password.server.controller.js rename to modules/users/server/controllers/users/users.password.server.controller.js index 1d4ae65e..9b4ee838 100644 --- a/app/controllers/users/users.password.server.controller.js +++ b/modules/users/server/controllers/users/users.password.server.controller.js @@ -4,12 +4,14 @@ * Module dependencies. */ var _ = require('lodash'), - errorHandler = require('../errors.server.controller'), + 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'), + crypto = require('crypto'), async = require('async'), crypto = require('crypto'); @@ -55,10 +57,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); }); @@ -111,6 +113,7 @@ exports.validateResetToken = function(req, res) { exports.reset = function(req, res, next) { // Init Variables var passwordDetails = req.body; + var message = null; async.waterfall([ @@ -158,7 +161,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) { @@ -174,7 +177,7 @@ exports.reset = function(req, res, next) { subject: 'Your password has been changed', html: emailHTML }; - + smtpTransport.sendMail(mailOptions, function(err) { done(err, 'done'); }); @@ -187,9 +190,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) { @@ -242,4 +246,4 @@ exports.changePassword = function(req, res) { message: 'User is not signed in' }); } -}; \ No newline at end of file +}; 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 00000000..ec2ad9c9 --- /dev/null +++ b/modules/users/server/controllers/users/users.profile.server.controller.js @@ -0,0 +1,97 @@ +'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'), + passport = require('passport'), + 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 similarity index 94% rename from app/models/user.server.model.js rename to modules/users/server/models/user.server.model.js index 9370da01..d92d29af 100755 --- 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 + } }); /** @@ -143,4 +147,4 @@ UserSchema.statics.findUniqueUsername = function(username, suffix, callback) { }); }; -mongoose.model('User', UserSchema); \ No newline at end of file +mongoose.model('User', UserSchema); 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 00000000..6390beed --- /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 00000000..80e52832 --- /dev/null +++ b/modules/users/server/routes/users.server.routes.js @@ -0,0 +1,21 @@ +'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 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 626ddc3f..eec61a67 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

+ \ No newline at end of file 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 262edf00..eb73cb83 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

+ \ No newline at end of file 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 4c95d686..5f2d6626 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' }); 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 00000000..ef761097 --- /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 99% rename from app/tests/user.server.model.test.js rename to modules/users/tests/server/user.server.model.tests.js index 7369d414..63983d1a 100644 --- a/app/tests/user.server.model.test.js +++ b/modules/users/tests/server/user.server.model.tests.js @@ -72,4 +72,4 @@ describe('User Model Unit Tests:', function() { User.remove().exec(); done(); }); -}); \ No newline at end of file +}); diff --git a/package.json b/package.json index 80638c37..b114c282 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "meanjs", "description": "Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js.", - "version": "0.3.3", + "version": "0.4.0", "private": false, "author": "https://github.com/meanjs/mean/graphs/contributors", "repository": { @@ -18,16 +18,18 @@ "postinstall": "bower install --config.interactive=false" }, "dependencies": { - "express": "~4.10.1", + "express": "~4.10.0", "express-session": "~1.9.1", + "serve-favicon": "~2.1.6", "body-parser": "~1.9.0", "cookie-parser": "~1.3.2", "compression": "~1.2.0", "method-override": "~2.3.0", "morgan": "~1.4.1", + "multer": "0.1.6", "connect-mongo": "~0.4.1", "connect-flash": "~0.1.1", - "helmet": "~0.5.0", + "helmet": "~0.4.0", "consolidate": "~0.10.0", "swig": "~1.4.1", "mongoose": "~3.8.8", @@ -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", + "chalk": "~0.5.1", "glob": "~4.0.5", "async": "~0.9.0", - "nodemailer": "~1.3.0", - "chalk": "~0.5" + "nodemailer": "~1.3.0" }, "devDependencies": { "supertest": "~0.14.0", @@ -62,7 +66,28 @@ "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", + "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-watch": "~1.1.0", + "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.2.2", + "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", @@ -70,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 00000000..a2308500 --- /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/dist/application.js b/public/dist/application.js deleted file mode 100644 index 4d0efe1e..00000000 --- a/public/dist/application.js +++ /dev/null @@ -1,622 +0,0 @@ -'use strict'; - -// Init the application configuration module for AngularJS application -var ApplicationConfiguration = (function() { - // Init module configuration options - var applicationModuleName = 'mean'; - var applicationModuleVendorDependencies = ['ngResource', 'ngAnimate', 'ui.router', 'ui.bootstrap', 'ui.utils']; - - // Add a new vertical module - var registerModule = function(moduleName, dependencies) { - // Create angular module - angular.module(moduleName, dependencies || []); - - // Add the module to the AngularJS configuration file - angular.module(applicationModuleName).requires.push(moduleName); - }; - - return { - applicationModuleName: applicationModuleName, - applicationModuleVendorDependencies: applicationModuleVendorDependencies, - registerModule: registerModule - }; -})(); -'use strict'; - -//Start by defining the main module and adding the module dependencies -angular.module(ApplicationConfiguration.applicationModuleName, ApplicationConfiguration.applicationModuleVendorDependencies); - -// Setting HTML5 Location Mode -angular.module(ApplicationConfiguration.applicationModuleName).config(['$locationProvider', - function($locationProvider) { - $locationProvider.hashPrefix('!'); - } -]); - -//Then define the init function for starting up the application -angular.element(document).ready(function() { - //Fixing facebook bug with redirect - if (window.location.hash === '#_=_') window.location.hash = '#!'; - - //Then init the app - angular.bootstrap(document, [ApplicationConfiguration.applicationModuleName]); -}); -'use strict'; - -// Use Applicaion configuration module to register a new module -ApplicationConfiguration.registerModule('articles'); -'use strict'; - -// Use Applicaion configuration module to register a new module -ApplicationConfiguration.registerModule('core'); -'use strict'; - -// Use Applicaion configuration module to register a new module -ApplicationConfiguration.registerModule('users'); -'use strict'; - -// Configuring the Articles module -angular.module('articles').run(['Menus', - function(Menus) { - // Set top bar menu items - Menus.addMenuItem('topbar', 'Articles', 'articles', 'dropdown', '/articles(/create)?'); - Menus.addSubMenuItem('topbar', 'articles', 'List Articles', 'articles'); - Menus.addSubMenuItem('topbar', 'articles', 'New Article', 'articles/create'); - } -]); -'use strict'; - -// Setting up route -angular.module('articles').config(['$stateProvider', - function($stateProvider) { - // Articles state routing - $stateProvider. - state('listArticles', { - url: '/articles', - templateUrl: 'modules/articles/views/list-articles.client.view.html' - }). - state('createArticle', { - url: '/articles/create', - templateUrl: 'modules/articles/views/create-article.client.view.html' - }). - state('viewArticle', { - url: '/articles/:articleId', - templateUrl: 'modules/articles/views/view-article.client.view.html' - }). - state('editArticle', { - url: '/articles/:articleId/edit', - templateUrl: 'modules/articles/views/edit-article.client.view.html' - }); - } -]); -'use strict'; - -angular.module('articles').controller('ArticlesController', ['$scope', '$stateParams', '$location', 'Authentication', 'Articles', - function($scope, $stateParams, $location, Authentication, Articles) { - $scope.authentication = Authentication; - - $scope.create = function() { - var article = new Articles({ - title: this.title, - content: this.content - }); - article.$save(function(response) { - $location.path('articles/' + response._id); - - $scope.title = ''; - $scope.content = ''; - }, function(errorResponse) { - $scope.error = errorResponse.data.message; - }); - }; - - $scope.remove = function(article) { - if (article) { - article.$remove(); - - for (var i in $scope.articles) { - if ($scope.articles[i] === article) { - $scope.articles.splice(i, 1); - } - } - } else { - $scope.article.$remove(function() { - $location.path('articles'); - }); - } - }; - - $scope.update = function() { - var article = $scope.article; - - article.$update(function() { - $location.path('articles/' + article._id); - }, function(errorResponse) { - $scope.error = errorResponse.data.message; - }); - }; - - $scope.find = function() { - $scope.articles = Articles.query(); - }; - - $scope.findOne = function() { - $scope.article = Articles.get({ - articleId: $stateParams.articleId - }); - }; - } -]); -'use strict'; - -//Articles service used for communicating with the articles REST endpoints -angular.module('articles').factory('Articles', ['$resource', - function($resource) { - return $resource('articles/:articleId', { - articleId: '@_id' - }, { - update: { - method: 'PUT' - } - }); - } -]); -'use strict'; - -// Setting up route -angular.module('core').config(['$stateProvider', '$urlRouterProvider', - function($stateProvider, $urlRouterProvider) { - // Redirect to home view when route not found - $urlRouterProvider.otherwise('/'); - - // Home state routing - $stateProvider. - state('home', { - url: '/', - templateUrl: 'modules/core/views/home.client.view.html' - }); - } -]); -'use strict'; - -angular.module('core').controller('HeaderController', ['$scope', 'Authentication', 'Menus', - function($scope, Authentication, Menus) { - $scope.authentication = Authentication; - $scope.isCollapsed = false; - $scope.menu = Menus.getMenu('topbar'); - - $scope.toggleCollapsibleMenu = function() { - $scope.isCollapsed = !$scope.isCollapsed; - }; - - // Collapsing the menu after navigation - $scope.$on('$stateChangeSuccess', function() { - $scope.isCollapsed = false; - }); - } -]); -'use strict'; - - -angular.module('core').controller('HomeController', ['$scope', 'Authentication', - function($scope, Authentication) { - // This provides Authentication context. - $scope.authentication = Authentication; - } -]); -'use strict'; - -//Menu service used for managing menus -angular.module('core').service('Menus', [ - - function() { - // Define a set of default roles - this.defaultRoles = ['*']; - - // 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'); - } -]); -'use strict'; - -// Config HTTP Error Handling -angular.module('users').config(['$httpProvider', - function($httpProvider) { - // Set the httpProvider "not authorized" interceptor - $httpProvider.interceptors.push(['$q', '$location', 'Authentication', - function($q, $location, Authentication) { - return { - responseError: function(rejection) { - switch (rejection.status) { - case 401: - // Deauthenticate the global user - Authentication.user = null; - - // Redirect to signin page - $location.path('signin'); - break; - case 403: - // Add unauthorized behaviour - break; - } - - return $q.reject(rejection); - } - }; - } - ]); - } -]); -'use strict'; - -// Setting up route -angular.module('users').config(['$stateProvider', - function($stateProvider) { - // Users state routing - $stateProvider. - state('profile', { - url: '/settings/profile', - templateUrl: 'modules/users/views/settings/edit-profile.client.view.html' - }). - state('password', { - url: '/settings/password', - templateUrl: 'modules/users/views/settings/change-password.client.view.html' - }). - state('accounts', { - url: '/settings/accounts', - templateUrl: 'modules/users/views/settings/social-accounts.client.view.html' - }). - state('signup', { - url: '/signup', - templateUrl: 'modules/users/views/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' - }); - } -]); -'use strict'; - -angular.module('users').controller('AuthenticationController', ['$scope', '$http', '$location', 'Authentication', - function($scope, $http, $location, Authentication) { - $scope.authentication = Authentication; - - // If user is signed in then redirect back home - if ($scope.authentication.user) $location.path('/'); - - $scope.signup = function() { - $http.post('/auth/signup', $scope.credentials).success(function(response) { - // If successful we assign the response to the global user model - $scope.authentication.user = response; - - // And redirect to the index page - $location.path('/'); - }).error(function(response) { - $scope.error = response.message; - }); - }; - - $scope.signin = function() { - $http.post('/auth/signin', $scope.credentials).success(function(response) { - // If successful we assign the response to the global user model - $scope.authentication.user = response; - - // And redirect to the index page - $location.path('/'); - }).error(function(response) { - $scope.error = response.message; - }); - }; - } -]); -'use strict'; - -angular.module('users').controller('PasswordController', ['$scope', '$stateParams', '$http', '$location', 'Authentication', - function($scope, $stateParams, $http, $location, Authentication) { - $scope.authentication = Authentication; - - //If user is signed in then redirect back home - if ($scope.authentication.user) $location.path('/'); - - // Submit forgotten password account id - $scope.askForPasswordReset = function() { - $scope.success = $scope.error = null; - - $http.post('/auth/forgot', $scope.credentials).success(function(response) { - // Show user success message and clear form - $scope.credentials = null; - $scope.success = response.message; - - }).error(function(response) { - // Show user error message and clear form - $scope.credentials = null; - $scope.error = response.message; - }); - }; - - // Change user password - $scope.resetUserPassword = function() { - $scope.success = $scope.error = null; - - $http.post('/auth/reset/' + $stateParams.token, $scope.passwordDetails).success(function(response) { - // If successful show success message and clear form - $scope.passwordDetails = null; - - // Attach user profile - Authentication.user = response; - - // And redirect to the index page - $location.path('/password/reset/success'); - }).error(function(response) { - $scope.error = response.message; - }); - }; - } -]); -'use strict'; - -angular.module('users').controller('SettingsController', ['$scope', '$http', '$location', 'Users', 'Authentication', - function($scope, $http, $location, Users, Authentication) { - $scope.user = Authentication.user; - - // If user is not signed in then redirect back home - if (!$scope.user) $location.path('/'); - - // Check if there are additional accounts - $scope.hasConnectedAdditionalSocialAccounts = function(provider) { - for (var i in $scope.user.additionalProvidersData) { - return true; - } - - return false; - }; - - // Check if provider is already in use with current user - $scope.isConnectedSocialAccount = function(provider) { - return $scope.user.provider === provider || ($scope.user.additionalProvidersData && $scope.user.additionalProvidersData[provider]); - }; - - // Remove a user social account - $scope.removeUserSocialAccount = function(provider) { - $scope.success = $scope.error = null; - - $http.delete('/users/accounts', { - params: { - provider: provider - } - }).success(function(response) { - // If successful show success message and clear form - $scope.success = true; - $scope.user = Authentication.user = response; - }).error(function(response) { - $scope.error = response.message; - }); - }; - - // Update a user profile - $scope.updateUserProfile = function(isValid) { - if (isValid) { - $scope.success = $scope.error = null; - var user = new Users($scope.user); - - user.$update(function(response) { - $scope.success = true; - Authentication.user = response; - }, function(response) { - $scope.error = response.data.message; - }); - } else { - $scope.submitted = true; - } - }; - - // Change user password - $scope.changeUserPassword = function() { - $scope.success = $scope.error = null; - - $http.post('/users/password', $scope.passwordDetails).success(function(response) { - // If successful show success message and clear form - $scope.success = true; - $scope.passwordDetails = null; - }).error(function(response) { - $scope.error = response.message; - }); - }; - } -]); -'use strict'; - -// Authentication service for user variables -angular.module('users').factory('Authentication', [ - function() { - var _this = this; - - _this._data = { - user: window.user - }; - - return _this._data; - } -]); -'use strict'; - -// Users service used for communicating with the users REST endpoint -angular.module('users').factory('Users', ['$resource', - function($resource) { - return $resource('users', {}, { - update: { - method: 'PUT' - } - }); - } -]); \ No newline at end of file diff --git a/public/dist/application.min.css b/public/dist/application.min.css index 323ffcca..034f618c 100644 --- a/public/dist/application.min.css +++ b/public/dist/application.min.css @@ -1 +1,5 @@ -.content{margin-top:50px}.undecorated-link:hover{text-decoration:none}.ng-cloak,.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}.ng-invalid.ng-dirty{border-color:#FA787E}.ng-valid.ng-dirty{border-color:#78FA89}@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 +.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%} + +.content-navigation{border-color:#fff;color:#e8e8e8}.border{padding:8px;margin:8px;border-color:#fff} +.content{margin-top:50px}.undecorated-link:hover{text-decoration:none}.ng-cloak,.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}.ng-invalid.ng-dirty{border-color:#FA787E}.ng-valid.ng-dirty{border-color:#78FA89}.header-profile-image{opacity:.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} +@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:#fff;cursor:inherit;display:block}.user-profile-picture{min-height:150px;max-height:150px} \ No newline at end of file diff --git a/public/dist/application.min.js b/public/dist/application.min.js index 2b2b9efa..b8414130 100644 --- a/public/dist/application.min.js +++ b/public/dist/application.min.js @@ -1 +1,25 @@ -"use strict";var ApplicationConfiguration=function(){var applicationModuleName="mean",applicationModuleVendorDependencies=["ngResource","ngAnimate","ui.router","ui.bootstrap","ui.utils"],registerModule=function(moduleName,dependencies){angular.module(moduleName,dependencies||[]),angular.module(applicationModuleName).requires.push(moduleName)};return{applicationModuleName:applicationModuleName,applicationModuleVendorDependencies:applicationModuleVendorDependencies,registerModule:registerModule}}();angular.module(ApplicationConfiguration.applicationModuleName,ApplicationConfiguration.applicationModuleVendorDependencies),angular.module(ApplicationConfiguration.applicationModuleName).config(["$locationProvider",function($locationProvider){$locationProvider.hashPrefix("!")}]),angular.element(document).ready(function(){"#_=_"===window.location.hash&&(window.location.hash="#!"),angular.bootstrap(document,[ApplicationConfiguration.applicationModuleName])}),ApplicationConfiguration.registerModule("articles"),ApplicationConfiguration.registerModule("core"),ApplicationConfiguration.registerModule("users"),angular.module("articles").run(["Menus",function(Menus){Menus.addMenuItem("topbar","Articles","articles","dropdown","/articles(/create)?"),Menus.addSubMenuItem("topbar","articles","List Articles","articles"),Menus.addSubMenuItem("topbar","articles","New Article","articles/create")}]),angular.module("articles").config(["$stateProvider",function($stateProvider){$stateProvider.state("listArticles",{url:"/articles",templateUrl:"modules/articles/views/list-articles.client.view.html"}).state("createArticle",{url:"/articles/create",templateUrl:"modules/articles/views/create-article.client.view.html"}).state("viewArticle",{url:"/articles/:articleId",templateUrl:"modules/articles/views/view-article.client.view.html"}).state("editArticle",{url:"/articles/:articleId/edit",templateUrl:"modules/articles/views/edit-article.client.view.html"})}]),angular.module("articles").controller("ArticlesController",["$scope","$stateParams","$location","Authentication","Articles",function($scope,$stateParams,$location,Authentication,Articles){$scope.authentication=Authentication,$scope.create=function(){var article=new Articles({title:this.title,content:this.content});article.$save(function(response){$location.path("articles/"+response._id),$scope.title="",$scope.content=""},function(errorResponse){$scope.error=errorResponse.data.message})},$scope.remove=function(article){if(article){article.$remove();for(var i in $scope.articles)$scope.articles[i]===article&&$scope.articles.splice(i,1)}else $scope.article.$remove(function(){$location.path("articles")})},$scope.update=function(){var article=$scope.article;article.$update(function(){$location.path("articles/"+article._id)},function(errorResponse){$scope.error=errorResponse.data.message})},$scope.find=function(){$scope.articles=Articles.query()},$scope.findOne=function(){$scope.article=Articles.get({articleId:$stateParams.articleId})}}]),angular.module("articles").factory("Articles",["$resource",function($resource){return $resource("articles/:articleId",{articleId:"@_id"},{update:{method:"PUT"}})}]),angular.module("core").config(["$stateProvider","$urlRouterProvider",function($stateProvider,$urlRouterProvider){$urlRouterProvider.otherwise("/"),$stateProvider.state("home",{url:"/",templateUrl:"modules/core/views/home.client.view.html"})}]),angular.module("core").controller("HeaderController",["$scope","Authentication","Menus",function($scope,Authentication,Menus){$scope.authentication=Authentication,$scope.isCollapsed=!1,$scope.menu=Menus.getMenu("topbar"),$scope.toggleCollapsibleMenu=function(){$scope.isCollapsed=!$scope.isCollapsed},$scope.$on("$stateChangeSuccess",function(){$scope.isCollapsed=!1})}]),angular.module("core").controller("HomeController",["$scope","Authentication",function($scope,Authentication){$scope.authentication=Authentication}]),angular.module("core").service("Menus",[function(){this.defaultRoles=["*"],this.menus={};var shouldRender=function(user){if(!user)return this.isPublic;if(~this.roles.indexOf("*"))return!0;for(var userRoleIndex in user.roles)for(var roleIndex in this.roles)if(this.roles[roleIndex]===user.roles[userRoleIndex])return!0;return!1};this.validateMenuExistance=function(menuId){if(menuId&&menuId.length){if(this.menus[menuId])return!0;throw new Error("Menu does not exists")}throw new Error("MenuId was not provided")},this.getMenu=function(menuId){return this.validateMenuExistance(menuId),this.menus[menuId]},this.addMenu=function(menuId,isPublic,roles){return this.menus[menuId]={isPublic:isPublic||!1,roles:roles||this.defaultRoles,items:[],shouldRender:shouldRender},this.menus[menuId]},this.removeMenu=function(menuId){this.validateMenuExistance(menuId),delete this.menus[menuId]},this.addMenuItem=function(menuId,menuItemTitle,menuItemURL,menuItemType,menuItemUIRoute,isPublic,roles,position){return this.validateMenuExistance(menuId),this.menus[menuId].items.push({title:menuItemTitle,link:menuItemURL,menuItemType:menuItemType||"item",menuItemClass:menuItemType,uiRoute:menuItemUIRoute||"/"+menuItemURL,isPublic:null===isPublic||"undefined"==typeof isPublic?this.menus[menuId].isPublic:isPublic,roles:null===roles||"undefined"==typeof roles?this.menus[menuId].roles:roles,position:position||0,items:[],shouldRender:shouldRender}),this.menus[menuId]},this.addSubMenuItem=function(menuId,rootMenuItemURL,menuItemTitle,menuItemURL,menuItemUIRoute,isPublic,roles,position){this.validateMenuExistance(menuId);for(var itemIndex in this.menus[menuId].items)this.menus[menuId].items[itemIndex].link===rootMenuItemURL&&this.menus[menuId].items[itemIndex].items.push({title:menuItemTitle,link:menuItemURL,uiRoute:menuItemUIRoute||"/"+menuItemURL,isPublic:null===isPublic||"undefined"==typeof isPublic?this.menus[menuId].items[itemIndex].isPublic:isPublic,roles:null===roles||"undefined"==typeof roles?this.menus[menuId].items[itemIndex].roles:roles,position:position||0,shouldRender:shouldRender});return this.menus[menuId]},this.removeMenuItem=function(menuId,menuItemURL){this.validateMenuExistance(menuId);for(var itemIndex in this.menus[menuId].items)this.menus[menuId].items[itemIndex].link===menuItemURL&&this.menus[menuId].items.splice(itemIndex,1);return this.menus[menuId]},this.removeSubMenuItem=function(menuId,submenuItemURL){this.validateMenuExistance(menuId);for(var itemIndex in this.menus[menuId].items)for(var subitemIndex in this.menus[menuId].items[itemIndex].items)this.menus[menuId].items[itemIndex].items[subitemIndex].link===submenuItemURL&&this.menus[menuId].items[itemIndex].items.splice(subitemIndex,1);return this.menus[menuId]},this.addMenu("topbar")}]),angular.module("users").config(["$httpProvider",function($httpProvider){$httpProvider.interceptors.push(["$q","$location","Authentication",function($q,$location,Authentication){return{responseError:function(rejection){switch(rejection.status){case 401:Authentication.user=null,$location.path("signin");break;case 403:}return $q.reject(rejection)}}}])}]),angular.module("users").config(["$stateProvider",function($stateProvider){$stateProvider.state("profile",{url:"/settings/profile",templateUrl:"modules/users/views/settings/edit-profile.client.view.html"}).state("password",{url:"/settings/password",templateUrl:"modules/users/views/settings/change-password.client.view.html"}).state("accounts",{url:"/settings/accounts",templateUrl:"modules/users/views/settings/social-accounts.client.view.html"}).state("signup",{url:"/signup",templateUrl:"modules/users/views/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"})}]),angular.module("users").controller("AuthenticationController",["$scope","$http","$location","Authentication",function($scope,$http,$location,Authentication){$scope.authentication=Authentication,$scope.authentication.user&&$location.path("/"),$scope.signup=function(){$http.post("/auth/signup",$scope.credentials).success(function(response){$scope.authentication.user=response,$location.path("/")}).error(function(response){$scope.error=response.message})},$scope.signin=function(){$http.post("/auth/signin",$scope.credentials).success(function(response){$scope.authentication.user=response,$location.path("/")}).error(function(response){$scope.error=response.message})}}]),angular.module("users").controller("PasswordController",["$scope","$stateParams","$http","$location","Authentication",function($scope,$stateParams,$http,$location,Authentication){$scope.authentication=Authentication,$scope.authentication.user&&$location.path("/"),$scope.askForPasswordReset=function(){$scope.success=$scope.error=null,$http.post("/auth/forgot",$scope.credentials).success(function(response){$scope.credentials=null,$scope.success=response.message}).error(function(response){$scope.credentials=null,$scope.error=response.message})},$scope.resetUserPassword=function(){$scope.success=$scope.error=null,$http.post("/auth/reset/"+$stateParams.token,$scope.passwordDetails).success(function(response){$scope.passwordDetails=null,Authentication.user=response,$location.path("/password/reset/success")}).error(function(response){$scope.error=response.message})}}]),angular.module("users").controller("SettingsController",["$scope","$http","$location","Users","Authentication",function($scope,$http,$location,Users,Authentication){$scope.user=Authentication.user,$scope.user||$location.path("/"),$scope.hasConnectedAdditionalSocialAccounts=function(){for(var i in $scope.user.additionalProvidersData)return!0;return!1},$scope.isConnectedSocialAccount=function(provider){return $scope.user.provider===provider||$scope.user.additionalProvidersData&&$scope.user.additionalProvidersData[provider]},$scope.removeUserSocialAccount=function(provider){$scope.success=$scope.error=null,$http.delete("/users/accounts",{params:{provider:provider}}).success(function(response){$scope.success=!0,$scope.user=Authentication.user=response}).error(function(response){$scope.error=response.message})},$scope.updateUserProfile=function(isValid){if(isValid){$scope.success=$scope.error=null;var user=new Users($scope.user);user.$update(function(response){$scope.success=!0,Authentication.user=response},function(response){$scope.error=response.data.message})}else $scope.submitted=!0},$scope.changeUserPassword=function(){$scope.success=$scope.error=null,$http.post("/users/password",$scope.passwordDetails).success(function(){$scope.success=!0,$scope.passwordDetails=null}).error(function(response){$scope.error=response.message})}}]),angular.module("users").factory("Authentication",[function(){var _this=this;return _this._data={user:window.user},_this._data}]),angular.module("users").factory("Users",["$resource",function($resource){return $resource("users",{},{update:{method:"PUT"}})}]); \ No newline at end of file +"use strict";var ApplicationConfiguration=function(){var applicationModuleName="mean",applicationModuleVendorDependencies=["ngResource","ngAnimate","ui.router","ui.bootstrap","ui.utils","angularFileUpload"],registerModule=function(moduleName,dependencies){angular.module(moduleName,dependencies||[]),angular.module(applicationModuleName).requires.push(moduleName)};return{applicationModuleName:applicationModuleName,applicationModuleVendorDependencies:applicationModuleVendorDependencies,registerModule:registerModule}}(); +"use strict";angular.module(ApplicationConfiguration.applicationModuleName,ApplicationConfiguration.applicationModuleVendorDependencies),angular.module(ApplicationConfiguration.applicationModuleName).config(["$locationProvider",function($locationProvider){$locationProvider.hashPrefix("!")}]),angular.element(document).ready(function(){"#_=_"===window.location.hash&&(window.location.hash="#!"),angular.bootstrap(document,[ApplicationConfiguration.applicationModuleName])}); +"use strict";ApplicationConfiguration.registerModule("chat"); +"use strict";ApplicationConfiguration.registerModule("core"); +"use strict";ApplicationConfiguration.registerModule("users"); +"use strict";angular.module("chat").run(["Menus",function(Menus){Menus.addMenuItem("topbar",{title:"Chat",state:"chat"})}]); +"use strict";angular.module("chat").config(["$stateProvider",function($stateProvider){$stateProvider.state("chat",{url:"/chat",templateUrl:"modules/chat/views/chat.client.view.html"})}]); +"use strict";angular.module("chat").controller("ChatController",["$scope","Socket",function($scope,Socket){$scope.messages=[],Socket.on("chatMessage",function(message){$scope.messages.unshift(message)}),$scope.sendMessage=function(){var message={text:this.messageText};Socket.emit("chatMessage",message),this.messageText=""},$scope.$on("$destroy",function(){Socket.removeListener("chatMessage")})}]); +"use strict";angular.module("core").config(["$stateProvider","$urlRouterProvider",function($stateProvider,$urlRouterProvider){$urlRouterProvider.otherwise("/"),$stateProvider.state("home",{url:"/",templateUrl:"modules/core/views/home.client.view.html"})}]); +"use strict";angular.module("core").controller("HeaderController",["$scope","Authentication","Menus",function($scope,Authentication,Menus){$scope.authentication=Authentication,$scope.isCollapsed=!1,$scope.menu=Menus.getMenu("topbar"),$scope.toggleCollapsibleMenu=function(){$scope.isCollapsed=!$scope.isCollapsed},$scope.$on("$stateChangeSuccess",function(){$scope.isCollapsed=!1})}]); +"use strict";angular.module("core").controller("HomeController",["$scope","Authentication",function($scope,Authentication){$scope.authentication=Authentication}]); +"use strict";angular.module("core").service("Menus",[function(){this.defaultRoles=["*"],this.menus={};var shouldRender=function(user){if(!user)return this.isPublic;if(~this.roles.indexOf("*"))return!0;for(var userRoleIndex in user.roles)for(var roleIndex in this.roles)if(this.roles[roleIndex]===user.roles[userRoleIndex])return!0;return!1};this.validateMenuExistance=function(menuId){if(menuId&&menuId.length){if(this.menus[menuId])return!0;throw new Error("Menu does not exists")}throw new Error("MenuId was not provided")},this.getMenu=function(menuId){return this.validateMenuExistance(menuId),this.menus[menuId]},this.addMenu=function(menuId,options){return options=options||{},this.menus[menuId]={isPublic:null===options.isPublic||"undefined"==typeof options.isPublic?!0:options.isPublic,roles:options.roles||this.defaultRoles,items:options.items||[],shouldRender:shouldRender},this.menus[menuId]},this.removeMenu=function(menuId){this.validateMenuExistance(menuId),delete this.menus[menuId]},this.addMenuItem=function(menuId,options){if(options=options||{},this.validateMenuExistance(menuId),this.menus[menuId].items.push({title:options.title||"",state:options.state||"",type:options.type||"item","class":options.class,isPublic:null===options.isPublic||"undefined"==typeof options.isPublic?this.menus[menuId].isPublic:options.isPublic,roles:null===options.roles||"undefined"==typeof options.roles?this.menus[menuId].roles:options.roles,position:options.position||0,items:[],shouldRender:shouldRender}),options.items)for(var i in options.items)this.addSubMenuItem(menuId,options.link,options.items[i]);return this.menus[menuId]},this.addSubMenuItem=function(menuId,rootMenuItemURL,options){options=options||{},this.validateMenuExistance(menuId);for(var itemIndex in this.menus[menuId].items)this.menus[menuId].items[itemIndex].link===rootMenuItemURL&&this.menus[menuId].items[itemIndex].items.push({title:options.title||"",state:options.state||"",isPublic:null===options.isPublic||"undefined"==typeof options.isPublic?this.menus[menuId].items[itemIndex].isPublic:options.isPublic,roles:null===options.roles||"undefined"==typeof options.roles?this.menus[menuId].items[itemIndex].roles:options.roles,position:options.position||0,shouldRender:shouldRender});return this.menus[menuId]},this.removeMenuItem=function(menuId,menuItemURL){this.validateMenuExistance(menuId);for(var itemIndex in this.menus[menuId].items)this.menus[menuId].items[itemIndex].link===menuItemURL&&this.menus[menuId].items.splice(itemIndex,1);return this.menus[menuId]},this.removeSubMenuItem=function(menuId,submenuItemURL){this.validateMenuExistance(menuId);for(var itemIndex in this.menus[menuId].items)for(var subitemIndex in this.menus[menuId].items[itemIndex].items)this.menus[menuId].items[itemIndex].items[subitemIndex].link===submenuItemURL&&this.menus[menuId].items[itemIndex].items.splice(subitemIndex,1);return this.menus[menuId]},this.addMenu("topbar",{isPublic:!1})}]); +"use strict";angular.module("core").service("Socket",["Authentication","$state","$timeout",function(Authentication,$state,$timeout){Authentication.user?this.socket=io():$state.go("home"),this.on=function(eventName,callback){this.socket&&this.socket.on(eventName,function(data){$timeout(function(){callback(data)})})},this.emit=function(eventName,data){this.socket&&this.socket.emit(eventName,data)},this.removeListener=function(eventName){this.socket&&this.socket.removeListener(eventName)}}]); +"use strict";angular.module("users").config(["$httpProvider",function($httpProvider){$httpProvider.interceptors.push(["$q","$location","Authentication",function($q,$location,Authentication){return{responseError:function(rejection){switch(rejection.status){case 401:Authentication.user=null,$location.path("signin");break;case 403:}return $q.reject(rejection)}}}])}]); +"use strict";angular.module("users").config(["$stateProvider",function($stateProvider){$stateProvider.state("settings",{"abstract":!0,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":!0,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":!0,url:"/password",template:""}).state("password.forgot",{url:"/forgot",templateUrl:"modules/users/views/password/forgot-password.client.view.html"}).state("password.reset",{"abstract":!0,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"})}]); +"use strict";angular.module("users").controller("AuthenticationController",["$scope","$http","$location","Authentication",function($scope,$http,$location,Authentication){$scope.authentication=Authentication,$scope.authentication.user&&$location.path("/"),$scope.signup=function(){$http.post("/api/auth/signup",$scope.credentials).success(function(response){$scope.authentication.user=response,$location.path("/")}).error(function(response){$scope.error=response.message})},$scope.signin=function(){$http.post("/api/auth/signin",$scope.credentials).success(function(response){$scope.authentication.user=response,$location.path("/")}).error(function(response){$scope.error=response.message})}}]); +"use strict";angular.module("users").controller("PasswordController",["$scope","$stateParams","$http","$location","Authentication",function($scope,$stateParams,$http,$location,Authentication){$scope.authentication=Authentication,$scope.authentication.user&&$location.path("/"),$scope.askForPasswordReset=function(){$scope.success=$scope.error=null,$http.post("/api/auth/forgot",$scope.credentials).success(function(response){$scope.credentials=null,$scope.success=response.message}).error(function(response){$scope.credentials=null,$scope.error=response.message})},$scope.resetUserPassword=function(){$scope.success=$scope.error=null,$http.post("/api/auth/reset/"+$stateParams.token,$scope.passwordDetails).success(function(response){$scope.passwordDetails=null,Authentication.user=response,$location.path("/password/reset/success")}).error(function(response){$scope.error=response.message})}}]); +"use strict";angular.module("users").controller("SettingsController",["$scope","$http","$location","Users","Authentication",function($scope,$http,$location,Users,Authentication){$scope.user=Authentication.user,$scope.user||$location.path("/"),$scope.hasConnectedAdditionalSocialAccounts=function(){for(var i in $scope.user.additionalProvidersData)return!0;return!1},$scope.isConnectedSocialAccount=function(provider){return $scope.user.provider===provider||$scope.user.additionalProvidersData&&$scope.user.additionalProvidersData[provider]},$scope.removeUserSocialAccount=function(provider){$scope.success=$scope.error=null,$http.delete("/api/users/accounts",{params:{provider:provider}}).success(function(response){$scope.success=!0,$scope.user=Authentication.user=response}).error(function(response){$scope.error=response.message})},$scope.updateUserProfile=function(isValid){if(isValid){$scope.success=$scope.error=null;var user=new Users($scope.user);user.$update(function(response){$scope.success=!0,Authentication.user=response},function(response){$scope.error=response.data.message})}else $scope.submitted=!0},$scope.changeUserPassword=function(){$scope.success=$scope.error=null,$http.post("/api/users/password",$scope.passwordDetails).success(function(){$scope.success=!0,$scope.passwordDetails=null}).error(function(response){$scope.error=response.message})}}]); +"use strict";angular.module("users").factory("Authentication",[function(){var _this=this;return _this._data={user:window.user},_this._data}]); +"use strict";angular.module("users").factory("Users",["$resource",function($resource){return $resource("api/users",{},{update:{method:"PUT"}})}]); +"use strict";angular.module("users").controller("ChangePasswordController",["$scope","$http","$location","Users","Authentication",function($scope,$http,$location,Users,Authentication){$scope.user=Authentication.user,$scope.changeUserPassword=function(){$scope.success=$scope.error=null,$http.post("/api/users/password",$scope.passwordDetails).success(function(){$scope.success=!0,$scope.passwordDetails=null}).error(function(response){$scope.error=response.message})}}]); +"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,$scope.uploader=new FileUploader({url:"api/users/picture"}),$scope.uploader.filters.push({name:"imageFilter",fn:function(item){var type="|"+item.type.slice(item.type.lastIndexOf("/")+1)+"|";return-1!=="|jpg|png|jpeg|bmp|gif|".indexOf(type)}}),$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)}}},$scope.uploader.onSuccessItem=function(fileItem,response){$scope.success=!0,$scope.user=Authentication.user=response,$scope.cancelUpload()},$scope.uploader.onErrorItem=function(fileItem,response){$scope.cancelUpload(),$scope.error=response.message},$scope.uploadProfilePicture=function(){$scope.success=$scope.error=null,$scope.uploader.uploadAll()},$scope.cancelUpload=function(){$scope.uploader.clearQueue(),$scope.imageURL=$scope.user.profileImageURL}}]); +"use strict";angular.module("users").controller("EditProfileController",["$scope","$http","$location","Users","Authentication",function($scope,$http,$location,Users,Authentication){$scope.user=Authentication.user,$scope.updateUserProfile=function(isValid){if(isValid){$scope.success=$scope.error=null;var user=new Users($scope.user);user.$update(function(response){$scope.success=!0,Authentication.user=response},function(response){$scope.error=response.data.message})}else $scope.submitted=!0}}]); +"use strict";angular.module("users").controller("SocialAccountsController",["$scope","$http","$location","Users","Authentication",function($scope,$http,$location,Users,Authentication){$scope.user=Authentication.user,$scope.hasConnectedAdditionalSocialAccounts=function(){for(var i in $scope.user.additionalProvidersData)return!0;return!1},$scope.isConnectedSocialAccount=function(provider){return $scope.user.provider===provider||$scope.user.additionalProvidersData&&$scope.user.additionalProvidersData[provider]},$scope.removeUserSocialAccount=function(provider){$scope.success=$scope.error=null,$http.delete("/api/users/accounts",{params:{provider:provider}}).success(function(response){$scope.success=!0,$scope.user=Authentication.user=response}).error(function(response){$scope.error=response.message})}}]); +"use strict";angular.module("users").controller("SettingsController",["$scope","$http","$location","Users","Authentication",function($scope,$http,$location,Users,Authentication){$scope.user=Authentication.user,$scope.user||$location.path("/")}]); \ No newline at end of file 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 7e1b0ffd..00000000 --- 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/css/core.css b/public/modules/core/css/core.css deleted file mode 100644 index c5108914..00000000 --- a/public/modules/core/css/core.css +++ /dev/null @@ -1,15 +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; -} \ No newline at end of file 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 d2366d1d..00000000 --- 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/users/config/users.client.routes.js b/public/modules/users/config/users.client.routes.js deleted file mode 100755 index 879c2c47..00000000 --- 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 de67bf94..00000000 --- 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/views/authentication/signin.client.view.html b/public/modules/users/views/authentication/signin.client.view.html deleted file mode 100644 index 91e256ef..00000000 --- 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 e2051760..00000000 --- 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 e6275f94..00000000 --- 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 d5fc2373..00000000 --- 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 dc8b2ea0..00000000 --- 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 4712ee09..00000000 --- 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/server.js b/server.js index 98d108bb..af09ff32 100755 --- a/server.js +++ b/server.js @@ -1,36 +1,20 @@ 'use strict'; + /** * Module dependencies. */ -var init = require('./config/init')(), - config = require('./config/config'), - mongoose = require('mongoose'), - chalk = require('chalk'); +var config = require('./config/config'), + mongoose = require('./config/lib/mongoose'), + express = require('./config/lib/express'); -/** - * Main application entry file. - * Please note that the order of loading is important. - */ +// Initialize mongoose +mongoose.connect(function (db) { + // Initialize express + var app = express.init(db); -// Bootstrap db connection -var db = mongoose.connect(config.db, function(err) { - if (err) { - console.error(chalk.red('Could not connect to MongoDB!')); - console.log(chalk.red(err)); - } + // Start the app by listening on + app.listen(config.port); + + // Logging initialization + console.log('MEAN.JS application started on port ' + config.port); }); - -// Init the express application -var app = require('./config/express')(db); - -// Bootstrap passport config -require('./config/passport')(); - -// Start the app by listening on -app.listen(config.port); - -// Expose app -exports = module.exports = app; - -// Logging initialization -console.log('MEAN.JS application started on port ' + config.port); \ No newline at end of file