diff --git a/.csslintrc b/.csslintrc new file mode 100644 index 0000000000..0dab227ebb --- /dev/null +++ b/.csslintrc @@ -0,0 +1,15 @@ +{ + "adjoining-classes": false, + "box-model": false, + "box-sizing": false, + "floats": false, + "font-sizes": false, + "important": false, + "known-properties": false, + "overqualified-elements": false, + "qualified-headings": false, + "regex-selectors": false, + "unique-headings": false, + "universal-selector": false, + "unqualified-attributes": false +} diff --git a/README.md b/README.md index 1241cf2ba8..05ac4b3b0e 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ Browse the live MEAN.JS example on [http://meanjs.herokuapp.com](http://meanjs.h ## Credits Inspired by the great work of [Madhusudhan Srinivasa](https://github.com/madhums/) +The MEAN name was coined by [Valeri Karpov](http://blog.mongodb.org/post/49262866911/the-mean-stack-mongodb-expressjs-angularjs-and) ## License (The MIT License) diff --git a/app/controllers/articles.js b/app/controllers/articles.server.controller.js similarity index 68% rename from app/controllers/articles.js rename to app/controllers/articles.server.controller.js index 6cdc331873..c60e93035b 100644 --- a/app/controllers/articles.js +++ b/app/controllers/articles.server.controller.js @@ -7,6 +7,30 @@ var mongoose = require('mongoose'), Article = mongoose.model('Article'), _ = require('lodash'); +/** + * Get the error message from error object + */ +var getErrorMessage = function(err) { + var message = ''; + + if (err.code) { + switch (err.code) { + case 11000: + case 11001: + message = 'Article already exists'; + break; + default: + message = 'Something went wrong'; + } + } else { + for (var errName in err.errors) { + if (err.errors[errName].message) message = err.errors[errName].message; + } + } + + return message; +}; + /** * Create a article */ @@ -16,9 +40,8 @@ exports.create = function(req, res) { article.save(function(err) { if (err) { - return res.send('users/signup', { - errors: err.errors, - article: article + return res.send(400, { + message: getErrorMessage(err) }); } else { res.jsonp(article); @@ -43,8 +66,8 @@ exports.update = function(req, res) { article.save(function(err) { if (err) { - res.render('error', { - status: 500 + return res.send(400, { + message: getErrorMessage(err) }); } else { res.jsonp(article); @@ -60,8 +83,8 @@ exports.delete = function(req, res) { article.remove(function(err) { if (err) { - res.render('error', { - status: 500 + return res.send(400, { + message: getErrorMessage(err) }); } else { res.jsonp(article); @@ -75,8 +98,8 @@ exports.delete = function(req, res) { exports.list = function(req, res) { Article.find().sort('-created').populate('user', 'displayName').exec(function(err, articles) { if (err) { - res.render('error', { - status: 500 + return res.send(400, { + message: getErrorMessage(err) }); } else { res.jsonp(articles); @@ -101,7 +124,9 @@ exports.articleByID = function(req, res, next, id) { */ exports.hasAuthorization = function(req, res, next) { if (req.article.user.id !== req.user.id) { - return res.send(403, 'User is not authorized'); + return res.send(403, { + message: 'User is not authorized' + }); } next(); }; \ No newline at end of file diff --git a/app/controllers/core.js b/app/controllers/core.server.controller.js similarity index 80% rename from app/controllers/core.js rename to app/controllers/core.server.controller.js index 7073572639..db02fb5ec7 100644 --- a/app/controllers/core.js +++ b/app/controllers/core.server.controller.js @@ -4,7 +4,7 @@ * Module dependencies. */ exports.index = function(req, res) { - res.render('index.html', { + res.render('index', { user: req.user || null }); }; \ No newline at end of file diff --git a/app/controllers/users.js b/app/controllers/users.server.controller.js similarity index 93% rename from app/controllers/users.js rename to app/controllers/users.server.controller.js index 9faaca577a..b8b4c76f6f 100755 --- a/app/controllers/users.js +++ b/app/controllers/users.server.controller.js @@ -36,6 +36,9 @@ var getErrorMessage = function(err) { * Signup */ exports.signup = function(req, res) { + // For security measurement we remove the roles from the req.body object + delete req.body.roles; + // Init Variables var user = new User(req.body); var message = null; @@ -44,6 +47,7 @@ exports.signup = function(req, res) { user.provider = 'local'; user.displayName = user.firstName + ' ' + user.lastName; + // Then save the user user.save(function(err) { if (err) { return res.send(400, { @@ -96,6 +100,9 @@ exports.update = function(req, res) { 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); @@ -233,7 +240,9 @@ exports.userByID = function(req, res, next, id) { */ exports.requiresLogin = function(req, res, next) { if (!req.isAuthenticated()) { - return res.send(401, 'User is not logged in'); + return res.send(401, { + message: 'User is not logged in' + }); } next(); @@ -242,12 +251,20 @@ exports.requiresLogin = function(req, res, next) { /** * User authorizations routing middleware */ -exports.hasAuthorization = function(req, res, next) { - if (req.profile.id !== req.user.id) { - return res.send(403, 'User is not authorized'); - } +exports.hasAuthorization = function(roles) { + var _this = this; - next(); + return function(req, res, next) { + _this.requiresLogin(req, res, function() { + if (_.intersection(req.user.roles, roles).length) { + return next(); + } else { + return res.send(403, { + message: 'User is not authorized' + }); + } + }); + }; }; /** @@ -339,7 +356,7 @@ exports.removeOAuthProvider = function(req, res, next) { // Delete the additional provider if (user.additionalProvidersData[provider]) { delete user.additionalProvidersData[provider]; - + // Then tell mongoose that we've updated the additionalProvidersData field user.markModified('additionalProvidersData'); } diff --git a/app/models/article.js b/app/models/article.server.model.js similarity index 100% rename from app/models/article.js rename to app/models/article.server.model.js diff --git a/app/models/user.js b/app/models/user.server.model.js similarity index 93% rename from app/models/user.js rename to app/models/user.server.model.js index b5cfdff313..6cfe08c231 100755 --- a/app/models/user.js +++ b/app/models/user.server.model.js @@ -68,6 +68,13 @@ var UserSchema = new Schema({ }, providerData: {}, additionalProvidersData: {}, + roles: { + type: [{ + type: String, + enum: ['user', 'admin'] + }], + default: ['user'] + }, updated: { type: Date }, @@ -114,8 +121,10 @@ UserSchema.statics.findUniqueUsername = function(username, suffix, callback) { var _this = this; var possibleUsername = username + (suffix || ''); - _this.findOne({username: possibleUsername}, function(err, user) { - if(!err) { + _this.findOne({ + username: possibleUsername + }, function(err, user) { + if (!err) { if (!user) { callback(possibleUsername); } else { diff --git a/app/routes/articles.js b/app/routes/articles.js deleted file mode 100644 index c0d39095a9..0000000000 --- a/app/routes/articles.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var users = require('../../app/controllers/users'), - articles = require('../../app/controllers/articles'); - -module.exports = function(app) { - // Article Routes - app.get('/articles', articles.list); - app.post('/articles', users.requiresLogin, articles.create); - app.get('/articles/:articleId', articles.read); - app.put('/articles/:articleId', users.requiresLogin, articles.hasAuthorization, articles.update); - app.del('/articles/:articleId', 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/articles.server.routes.js b/app/routes/articles.server.routes.js new file mode 100644 index 0000000000..e393414177 --- /dev/null +++ b/app/routes/articles.server.routes.js @@ -0,0 +1,22 @@ +'use strict'; + +/** + * Module dependencies. + */ +var users = require('../../app/controllers/users'), + articles = require('../../app/controllers/articles'); + +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.js b/app/routes/core.server.routes.js similarity index 78% rename from app/routes/core.js rename to app/routes/core.server.routes.js index ba232004ee..4cd9616aa9 100644 --- a/app/routes/core.js +++ b/app/routes/core.server.routes.js @@ -3,5 +3,5 @@ module.exports = function(app) { // Root routing var core = require('../../app/controllers/core'); - app.get('/', core.index); + app.route('/').get(core.index); }; \ No newline at end of file diff --git a/app/routes/users.js b/app/routes/users.js deleted file mode 100644 index 23f8126b52..0000000000 --- a/app/routes/users.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var passport = require('passport'); - -module.exports = function(app) { - // User Routes - var users = require('../../app/controllers/users'); - app.get('/users/me', users.me); - app.put('/users', users.update); - app.post('/users/password', users.changePassword); - app.del('/users/accounts', users.removeOAuthProvider); - - // Setting up the users api - app.post('/auth/signup', users.signup); - app.post('/auth/signin', users.signin); - app.get('/auth/signout', users.signout); - - // Setting the facebook oauth routes - app.get('/auth/facebook', passport.authenticate('facebook', { - scope: ['email'] - })); - app.get('/auth/facebook/callback', users.oauthCallback('facebook')); - - // Setting the twitter oauth routes - app.get('/auth/twitter', passport.authenticate('twitter')); - app.get('/auth/twitter/callback', users.oauthCallback('twitter')); - - // Setting the google oauth routes - app.get('/auth/google', passport.authenticate('google', { - scope: [ - 'https://www.googleapis.com/auth/userinfo.profile', - 'https://www.googleapis.com/auth/userinfo.email' - ] - })); - app.get('/auth/google/callback', users.oauthCallback('google')); - - // Setting the linkedin oauth routes - app.get('/auth/linkedin', passport.authenticate('linkedin')); - app.get('/auth/linkedin/callback', users.oauthCallback('linkedin')); - - // Finish by binding the user middleware - app.param('userId', users.userByID); -}; diff --git a/app/routes/users.server.routes.js b/app/routes/users.server.routes.js new file mode 100644 index 0000000000..f925eb0618 --- /dev/null +++ b/app/routes/users.server.routes.js @@ -0,0 +1,46 @@ +'use strict'; + +/** + * Module dependencies. + */ +var passport = require('passport'); + +module.exports = function(app) { + // User Routes + var users = require('../../app/controllers/users'); + app.route('/users/me').get(users.me); + app.route('/users').put(users.update); + app.route('/users/password').post(users.changePassword); + app.route('/users/accounts').delete(users.removeOAuthProvider); + + // Setting up the users api + 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')); + + // Finish by binding the user middleware + app.param('userId', users.userByID); +}; diff --git a/app/tests/articles.js b/app/tests/article.server.model.test.js similarity index 100% rename from app/tests/articles.js rename to app/tests/article.server.model.test.js diff --git a/app/tests/users.js b/app/tests/user.server.model.test.js similarity index 100% rename from app/tests/users.js rename to app/tests/user.server.model.test.js diff --git a/app/views/404.html b/app/views/404.server.view.html similarity index 71% rename from app/views/404.html rename to app/views/404.server.view.html index c993001842..0074fa456d 100644 --- a/app/views/404.html +++ b/app/views/404.server.view.html @@ -1,4 +1,4 @@ -{% extends 'layout.html' %} +{% extends 'layout.server.view.html' %} {% block content %}

Page Not Found

diff --git a/app/views/500.html b/app/views/500.server.view.html similarity index 66% rename from app/views/500.html rename to app/views/500.server.view.html index 7bbded9e38..8e6711b72a 100644 --- a/app/views/500.html +++ b/app/views/500.server.view.html @@ -1,4 +1,4 @@ -{% extends 'layout.html' %} +{% extends 'layout.server.view.html' %} {% block content %}

Server Error

diff --git a/app/views/index.html b/app/views/index.server.view.html similarity index 63% rename from app/views/index.html rename to app/views/index.server.view.html index 832522f4d0..7e60893b1f 100644 --- a/app/views/index.html +++ b/app/views/index.server.view.html @@ -1,4 +1,4 @@ -{% extends 'layout.html' %} +{% extends 'layout.server.view.html' %} {% block content %}
diff --git a/app/views/layout.html b/app/views/layout.html deleted file mode 100644 index 62848d038b..0000000000 --- a/app/views/layout.html +++ /dev/null @@ -1,91 +0,0 @@ - - - - - {{title}} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {% for modulesCSSFile in modulesCSSFiles %} - {% endfor %} - - - - - - - -
-
- {% block content %}{% endblock %} -
-
- - - - - - - - - - - - - - - - - - - - - {% for modulesJSFile in modulesJSFiles %} - - {% endfor %} - - {% if process.env.NODE_ENV === 'development' %} - - - {% endif %} - - - \ No newline at end of file diff --git a/app/views/layout.server.view.html b/app/views/layout.server.view.html new file mode 100644 index 0000000000..e13028aed1 --- /dev/null +++ b/app/views/layout.server.view.html @@ -0,0 +1,68 @@ + + + + + {{title}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% for cssFile in cssFiles %} + {% endfor %} + + + + + + + +
+
+ {% block content %}{% endblock %} +
+
+ + + + + + {% for jsFile in jsFiles %} + {% endfor %} + + {% if process.env.NODE_ENV === 'development' %} + + + {% endif %} + + + \ No newline at end of file diff --git a/bower.json b/bower.json index 5a6215eb31..e0c9250ba8 100644 --- a/bower.json +++ b/bower.json @@ -1,11 +1,10 @@ { "name": "meanjs", - "version": "0.2.2", + "version": "0.3.0", "description": "Fullstack JavaScript with MongoDB, Express, AngularJS, and Node.js.", "dependencies": { "bootstrap": "~3", "angular": "~1.2", - "angular-cookies": "~1.2", "angular-resource": "~1.2", "angular-animate": "~1.2", "angular-mocks": "~1.2", diff --git a/config/config.js b/config/config.js index afe7eb09c0..e694bf4715 100644 --- a/config/config.js +++ b/config/config.js @@ -1,15 +1,76 @@ 'use strict'; +/** + * Module dependencies. + */ var _ = require('lodash'), - utilities = require('./utilities'); + glob = require('glob'); -// Look for a valid NODE_ENV variable and if one cannot be found load the development NODE_ENV -process.env.NODE_ENV = ~utilities.walk('./config/env', /(.*)\.js$/).map(function(file) { - return file.split('/').pop().slice(0, -3); -}).indexOf(process.env.NODE_ENV) ? process.env.NODE_ENV : 'development'; - -// Load app configurations +/** + * Load app configurations + */ module.exports = _.extend( - require('./env/all'), - require('./env/' + process.env.NODE_ENV) || {} -); \ No newline at end of file + require('./env/all'), + require('./env/' + process.env.NODE_ENV) || {} +); + +/** + * Get files by glob patterns + */ +module.exports.getGlobbedFiles = function(globPatterns, removeRoot) { + // For context switching + var _this = this; + + // URL paths regex + var urlRegex = new RegExp('^(?:[a-z]+:)?//', 'i'); + + // The output array + var output = []; + + // If glob pattern is array so we use each pattern in a recursive way, otherwise we use glob + if (_.isArray(globPatterns)) { + globPatterns.forEach(function(globPattern) { + output = _.union(output, _this.getGlobbedFiles(globPattern, removeRoot)); + }); + } else if (_.isString(globPatterns)) { + if (urlRegex.test(globPatterns)) { + output.push(globPatterns); + } else { + glob(globPatterns, { + sync: true + }, function(err, files) { + if (removeRoot) { + files = files.map(function(file) { + return file.replace(removeRoot, ''); + }); + } + + output = _.union(output, files); + }); + } + } + + return output; +}; + +/** + * Get the modules JavaScript files + */ +module.exports.getJavaScriptAssets = function(includeTests) { + var output = this.getGlobbedFiles(this.assets.lib.js.concat(this.assets.js), 'public/'); + + // To include tests + if (includeTests) { + output = _.union(output, this.getGlobbedFiles(this.assets.tests)); + } + + return output; +}; + +/** + * Get the modules CSS 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 diff --git a/config/env/all.js b/config/env/all.js index 46815116cb..aaf9262cf7 100644 --- a/config/env/all.js +++ b/config/env/all.js @@ -1,17 +1,42 @@ 'use strict'; -var path = require('path'), - rootPath = path.normalize(__dirname + '/../..'); - 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' }, - root: rootPath, port: process.env.PORT || 3000, templateEngine: 'swig', sessionSecret: 'MEAN', - sessionCollection: 'sessions' -}; \ No newline at end of file + sessionCollection: 'sessions', + 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/development.js b/config/env/development.js index dc5837249b..43dc47d03e 100644 --- a/config/env/development.js +++ b/config/env/development.js @@ -6,23 +6,23 @@ module.exports = { title: 'MEAN.JS - Development Environment' }, facebook: { - clientID: 'APP_ID', - clientSecret: 'APP_SECRET', - callbackURL: 'http://localhost:3000/auth/facebook/callback' + clientID: process.env.FACEBOOK_ID || 'APP_ID', + clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET', + callbackPath: '/auth/facebook/callback' }, twitter: { - clientID: 'CONSUMER_KEY', - clientSecret: 'CONSUMER_SECRET', - callbackURL: 'http://localhost:3000/auth/twitter/callback' + clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY', + clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET', + callbackPath: '/auth/twitter/callback' }, google: { - clientID: 'APP_ID', - clientSecret: 'APP_SECRET', - callbackURL: 'http://localhost:3000/auth/google/callback' + clientID: process.env.GOOGLE_ID || 'APP_ID', + clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET', + callbackPath: '/auth/google/callback' }, linkedin: { - clientID: 'APP_ID', - clientSecret: 'APP_SECRET', - callbackURL: 'http://localhost:3000/auth/linkedin/callback' + clientID: process.env.LINKEDIN_ID || 'APP_ID', + clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', + callbackPath: '/auth/linkedin/callback' } -}; \ No newline at end of file +}; diff --git a/config/env/production.js b/config/env/production.js index 32f970d69c..876efd1141 100644 --- a/config/env/production.js +++ b/config/env/production.js @@ -1,25 +1,43 @@ 'use strict'; module.exports = { - db: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://localhost/mean', + db: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://localhost/mean', + 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: 'APP_ID', - clientSecret: 'APP_SECRET', - callbackURL: 'http://localhost:3000/auth/facebook/callback' + clientID: process.env.FACEBOOK_ID || 'APP_ID', + clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET', + callbackPath: '/auth/facebook/callback' }, twitter: { - clientID: 'CONSUMER_KEY', - clientSecret: 'CONSUMER_SECRET', - callbackURL: 'http://localhost:3000/auth/twitter/callback' + clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY', + clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET', + callbackPath: '/auth/twitter/callback' }, google: { - clientID: 'APP_ID', - clientSecret: 'APP_SECRET', - callbackURL: 'http://localhost:3000/auth/google/callback' + clientID: process.env.GOOGLE_ID || 'APP_ID', + clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET', + callbackPath: '/auth/google/callback' }, linkedin: { - clientID: 'APP_ID', - clientSecret: 'APP_SECRET', - callbackURL: 'http://localhost:3000/auth/linkedin/callback' + clientID: process.env.LINKEDIN_ID || 'APP_ID', + clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', + callbackPath: '/auth/linkedin/callback' } -}; \ No newline at end of file +}; diff --git a/config/env/test.js b/config/env/test.js index a5a01a2998..a0f72fcb3a 100644 --- a/config/env/test.js +++ b/config/env/test.js @@ -7,23 +7,23 @@ module.exports = { title: 'MEAN.JS - Test Environment' }, facebook: { - clientID: 'APP_ID', - clientSecret: 'APP_SECRET', - callbackURL: 'http://localhost:3000/auth/facebook/callback' + clientID: process.env.FACEBOOK_ID || 'APP_ID', + clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET', + callbackPath: '/auth/facebook/callback' }, twitter: { - clientID: 'CONSUMER_KEY', - clientSecret: 'CONSUMER_SECRET', - callbackURL: 'http://localhost:3000/auth/twitter/callback' + clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY', + clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET', + callbackPath: '/auth/twitter/callback' }, google: { - clientID: 'APP_ID', - clientSecret: 'APP_SECRET', - callbackURL: 'http://localhost:3000/auth/google/callback' + clientID: process.env.GOOGLE_ID || 'APP_ID', + clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET', + callbackPath: '/auth/google/callback' }, linkedin: { - clientID: 'APP_ID', - clientSecret: 'APP_SECRET', - callbackURL: 'http://localhost:3000/auth/linkedin/callback' + clientID: process.env.LINKEDIN_ID || 'APP_ID', + clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', + callbackPath: '/auth/linkedin/callback' } -}; \ No newline at end of file +}; diff --git a/config/env/travis.js b/config/env/travis.js deleted file mode 100644 index cc03946045..0000000000 --- a/config/env/travis.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict'; - -module.exports = { - db: 'mongodb://localhost/mean-travis', - port: 3001, - app: { - title: 'MEAN.JS - Travis Environment' - }, - facebook: { - clientID: 'APP_ID', - clientSecret: 'APP_SECRET', - callbackURL: 'http://localhost:3000/auth/facebook/callback' - }, - twitter: { - clientID: 'CONSUMER_KEY', - clientSecret: 'CONSUMER_SECRET', - callbackURL: 'http://localhost:3000/auth/twitter/callback' - }, - google: { - clientID: 'APP_ID', - clientSecret: 'APP_SECRET', - callbackURL: 'http://localhost:3000/auth/google/callback' - }, - linkedin: { - clientID: 'APP_ID', - clientSecret: 'APP_SECRET', - callbackURL: 'http://localhost:3000/auth/linkedin/callback' - } -}; \ No newline at end of file diff --git a/config/express.js b/config/express.js index d83ee8fb92..89fa4337d6 100755 --- a/config/express.js +++ b/config/express.js @@ -4,32 +4,38 @@ * Module dependencies. */ var express = require('express'), + morgan = require('morgan'), + 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')(express), + mongoStore = require('connect-mongo')({ + session: session + }), flash = require('connect-flash'), config = require('./config'), consolidate = require('consolidate'), - path = require('path'), - utilities = require('./utilities'); + path = require('path'); module.exports = function(db) { // Initialize express app var app = express(); - // Initialize models - utilities.walk('./app/models').forEach(function(modelPath) { + // Globbing model files + config.getGlobbedFiles('./app/models/**/*.js').forEach(function(modelPath) { require(path.resolve(modelPath)); }); - // Setting the environment locals - app.locals({ - title: config.app.title, - description: config.app.description, - keywords: config.app.keywords, - facebookAppId: config.facebook.clientID, - modulesJSFiles: utilities.walk('./public/modules', /(.*)\.(js)/, /(.*)\.(spec.js)/, './public'), - modulesCSSFiles: utilities.walk('./public/modules', /(.*)\.(css)/, null, './public') - }); + // 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) { @@ -38,7 +44,7 @@ module.exports = function(db) { }); // Should be placed before express.static - app.use(express.compress({ + app.use(compress({ filter: function(req, res) { return (/json|text|javascript|css/).test(res.getHeader('Content-Type')); }, @@ -49,41 +55,36 @@ module.exports = function(db) { app.set('showStackError', true); // Set swig as the template engine - app.engine('html', consolidate[config.templateEngine]); + app.engine('server.view.html', consolidate[config.templateEngine]); // Set views path and view engine - app.set('view engine', 'html'); - app.set('views', config.root + '/app/views'); + app.set('view engine', 'server.view.html'); + app.set('views', './app/views'); - // Application Configuration for development environment - app.configure('development', function() { - // Enable logger - app.use(express.logger('dev')); + // Environment dependent middleware + if (process.env.NODE_ENV === 'development') { + // Enable logger (morgan) + app.use(morgan('dev')); // Disable views cache app.set('view cache', false); - }); - - // Application Configuration for production environment - app.configure('production', function() { - app.locals({ - cache: 'memory' // To solve SWIG Cache Issues - }); - }); + } else if (process.env.NODE_ENV === 'production') { + app.locals.cache = 'memory'; + } - // request body parsing middleware should be above methodOverride - app.use(express.urlencoded()); - app.use(express.json()); - app.use(express.methodOverride()); + // Request body parsing middleware should be above methodOverride + app.use(bodyParser.urlencoded()); + app.use(bodyParser.json()); + app.use(methodOverride()); // Enable jsonp app.enable('jsonp callback'); - // cookieParser should be above session - app.use(express.cookieParser()); + // CookieParser should be above session + app.use(cookieParser()); - // express/mongo session storage - app.use(express.session({ + // Express MongoDB session storage + app.use(session({ secret: config.sessionSecret, store: new mongoStore({ db: db.connection.db, @@ -98,14 +99,18 @@ module.exports = function(db) { // connect flash for flash messages app.use(flash()); - // routes should be at the last - app.use(app.router); - + // Use helmet to secure Express headers + app.use(helmet.xframe()); + app.use(helmet.iexss()); + app.use(helmet.contentTypeOptions()); + app.use(helmet.ienoopen()); + app.disable('x-powered-by'); + // Setting the app router and static folder - app.use(express.static(config.root + '/public')); + app.use(express.static(path.resolve('./public'))); - // Load Routes - utilities.walk('./app/routes').forEach(function(routePath) { + // Globbing routing files + config.getGlobbedFiles('./app/routes/**/*.js').forEach(function(routePath) { require(path.resolve(routePath))(app); }); @@ -118,14 +123,14 @@ module.exports = function(db) { console.error(err.stack); // Error page - res.status(500).render('500.html', { + res.status(500).render('500', { error: err.stack }); }); // Assume 404 since no middleware responded app.use(function(req, res) { - res.status(404).render('404.html', { + res.status(404).render('404', { url: req.originalUrl, error: 'Not Found' }); diff --git a/config/init.js b/config/init.js new file mode 100644 index 0000000000..9a2ae984e9 --- /dev/null +++ b/config/init.js @@ -0,0 +1,40 @@ +'use strict'; + +/** + * Module dependencies. + */ +var glob = require('glob'); + +/** + * Module init function. + */ +module.exports = function() { + /** + * Before we begin, lets set the envrionment 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) { + console.log(); + if (!environmentFiles.length) { + if(process.env.NODE_ENV) { + console.log('\x1b[31m', 'No configuration file found for "' + process.env.NODE_ENV + '" envrionment using develpoment instead'); + } else { + console.log('\x1b[31m', 'NODE_ENV is not defined! Using default develpoment envrionment'); + } + + process.env.NODE_ENV = 'development'; + } else { + console.log('\x1b[7m', 'Application loaded using the "' + process.env.NODE_ENV + '" envrionment configuration'); + } + console.log('\x1b[0m'); + }); + + /** + * Add our server node extensions + */ + require.extensions['.server.controller.js'] = require.extensions['.js']; + require.extensions['.server.model.js'] = require.extensions['.js']; + require.extensions['.server.routes.js'] = require.extensions['.js']; +}; \ No newline at end of file diff --git a/config/passport.js b/config/passport.js index cbd25e6a90..9e3a31ae5a 100755 --- a/config/passport.js +++ b/config/passport.js @@ -3,7 +3,7 @@ var passport = require('passport'), User = require('mongoose').model('User'), path = require('path'), - utilities = require('./utilities'); + config = require('./config'); module.exports = function() { // Serialize sessions @@ -21,7 +21,7 @@ module.exports = function() { }); // Initialize strategies - utilities.walk('./config/strategies').forEach(function(strategyPath) { - require(path.resolve(strategyPath))(); + config.getGlobbedFiles('./config/strategies/**/*.js').forEach(function(strategy) { + require(path.resolve(strategy))(); }); -}; +}; \ No newline at end of file diff --git a/config/strategies/facebook.js b/config/strategies/facebook.js index 113cb74b25..51d5f1725e 100644 --- a/config/strategies/facebook.js +++ b/config/strategies/facebook.js @@ -1,6 +1,10 @@ 'use strict'; +/** + * Module dependencies. + */ var passport = require('passport'), + url = require('url'), FacebookStrategy = require('passport-facebook').Strategy, config = require('../config'), users = require('../../app/controllers/users'); @@ -10,7 +14,7 @@ module.exports = function() { passport.use(new FacebookStrategy({ clientID: config.facebook.clientID, clientSecret: config.facebook.clientSecret, - callbackURL: config.facebook.callbackURL, + callbackURL: config.facebook.callbackPath, passReqToCallback: true }, function(req, accessToken, refreshToken, profile, done) { @@ -35,4 +39,4 @@ module.exports = function() { users.saveOAuthUserProfile(req, providerUserProfile, done); } )); -}; \ No newline at end of file +}; diff --git a/config/strategies/google.js b/config/strategies/google.js index f06c0998fd..dceb96c708 100644 --- a/config/strategies/google.js +++ b/config/strategies/google.js @@ -1,38 +1,42 @@ 'use strict'; +/** + * Module dependencies. + */ var passport = require('passport'), - GoogleStrategy = require('passport-google-oauth').OAuth2Strategy, - config = require('../config'), - users = require('../../app/controllers/users'); + url = require('url'), + GoogleStrategy = require('passport-google-oauth').OAuth2Strategy, + config = require('../config'), + users = require('../../app/controllers/users'); module.exports = function() { - // Use google strategy - passport.use(new GoogleStrategy({ - clientID: config.google.clientID, - clientSecret: config.google.clientSecret, - callbackURL: config.google.callbackURL, - passReqToCallback: true - }, - function(req, accessToken, refreshToken, profile, done) { - // Set the provider data and include tokens - var providerData = profile._json; - providerData.accessToken = accessToken; - providerData.refreshToken = refreshToken; - - // Create the user OAuth profile - var providerUserProfile = { - firstName: profile.name.givenName, - lastName: profile.name.familyName, - displayName: profile.displayName, - email: profile.emails[0].value, - username: profile.username, - provider: 'google', - providerIdentifierField: 'id', - providerData: providerData - }; + // Use google strategy + passport.use(new GoogleStrategy({ + clientID: config.google.clientID, + clientSecret: config.google.clientSecret, + callbackURL: config.google.callbackPath, + passReqToCallback: true + }, + function(req, accessToken, refreshToken, profile, done) { + // Set the provider data and include tokens + var providerData = profile._json; + providerData.accessToken = accessToken; + providerData.refreshToken = refreshToken; - // Save the user OAuth profile - users.saveOAuthUserProfile(req, providerUserProfile, done); - } - )); + // Create the user OAuth profile + var providerUserProfile = { + firstName: profile.name.givenName, + lastName: profile.name.familyName, + displayName: profile.displayName, + email: profile.emails[0].value, + username: profile.username, + provider: 'google', + providerIdentifierField: 'id', + providerData: providerData + }; + + // Save the user OAuth profile + users.saveOAuthUserProfile(req, providerUserProfile, done); + } + )); }; \ No newline at end of file diff --git a/config/strategies/linkedin.js b/config/strategies/linkedin.js index 2ba5736d06..e29d5d433d 100644 --- a/config/strategies/linkedin.js +++ b/config/strategies/linkedin.js @@ -1,6 +1,10 @@ 'use strict'; +/** + * Module dependencies. + */ var passport = require('passport'), + url = require('url'), LinkedInStrategy = require('passport-linkedin').Strategy, config = require('../config'), users = require('../../app/controllers/users'); @@ -10,7 +14,7 @@ module.exports = function() { passport.use(new LinkedInStrategy({ consumerKey: config.linkedin.clientID, consumerSecret: config.linkedin.clientSecret, - callbackURL: config.linkedin.callbackURL, + callbackURL: config.linkedin.callbackPath, passReqToCallback: true, profileFields: ['id', 'first-name', 'last-name', 'email-address'] }, @@ -36,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/config/strategies/local.js index c4850a6e5d..97f8d43098 100644 --- a/config/strategies/local.js +++ b/config/strategies/local.js @@ -1,10 +1,12 @@ 'use strict'; +/** + * Module dependencies. + */ var passport = require('passport'), LocalStrategy = require('passport-local').Strategy, User = require('mongoose').model('User'); - module.exports = function() { // Use local strategy passport.use(new LocalStrategy({ diff --git a/config/strategies/twitter.js b/config/strategies/twitter.js index cf3e6cc2b1..fa62fc9bf1 100644 --- a/config/strategies/twitter.js +++ b/config/strategies/twitter.js @@ -1,6 +1,10 @@ 'use strict'; +/** + * Module dependencies. + */ var passport = require('passport'), + url = require('url'), TwitterStrategy = require('passport-twitter').Strategy, config = require('../config'), users = require('../../app/controllers/users'); @@ -10,7 +14,7 @@ module.exports = function() { passport.use(new TwitterStrategy({ consumerKey: config.twitter.clientID, consumerSecret: config.twitter.clientSecret, - callbackURL: config.twitter.callbackURL, + callbackURL: config.twitter.callbackPath, passReqToCallback: true }, function(req, token, tokenSecret, profile, done) { @@ -32,4 +36,4 @@ module.exports = function() { users.saveOAuthUserProfile(req, providerUserProfile, done); } )); -}; \ No newline at end of file +}; diff --git a/config/utilities.js b/config/utilities.js deleted file mode 100644 index cb6da59221..0000000000 --- a/config/utilities.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var fs = require('fs'); - -// Walk function to recursively get files -var _walk = function(root, includeRegex, excludeRegex, removePath) { - var output = []; - var directories = []; - includeRegex = includeRegex || /(.*)\.(js|coffee)$/; - - // First read through files - fs.readdirSync(root).forEach(function(file) { - var newPath = root + '/' + file; - var stat = fs.statSync(newPath); - - if (stat.isFile()) { - if (includeRegex.test(file) && (!excludeRegex || !excludeRegex.test(file))) { - output.push(newPath.replace(removePath, '')); - } - } else if (stat.isDirectory()) { - directories.push(newPath); - } - }); - - // Then recursively add directories - directories.forEach(function(directory) { - output = output.concat(_walk(directory, includeRegex, excludeRegex, removePath)); - }); - - return output; -}; - -/** - * Exposing the walk function - */ -exports.walk = _walk; diff --git a/gruntfile.js b/gruntfile.js index 3778d02f2e..9dff768f99 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -1,99 +1,133 @@ 'use strict'; module.exports = function(grunt) { - // Project Configuration - grunt.initConfig({ - pkg: grunt.file.readJSON('package.json'), - watch: { - serverViews: { - files: ['app/views/**'], - options: { - livereload: true, - } - }, - serverJS: { - files: ['gruntfile.js', 'server.js', 'config/**/*.js', 'app/**/*.js'], - tasks: ['jshint'], - options: { - livereload: true, - } - }, - clientViews: { - files: ['public/modules/**/views/*.html'], - options: { - livereload: true, - } - }, - clientJS: { - files: ['public/js/**/*.js', 'public/modules/**/*.js'], - tasks: ['jshint'], - options: { - livereload: true, - } - }, - clientCSS: { - files: ['public/**/css/*.css'], - options: { - livereload: true, - } - } - }, - jshint: { - all: { - src: ['gruntfile.js', 'server.js', 'config/**/*.js', 'app/**/*.js', 'public/js/**/*.js', 'public/modules/**/*.js'], - options: { - jshintrc: true - } - } - }, - nodemon: { - dev: { - script: 'server.js', - options: { - nodeArgs: ['--debug'] - } - } - }, - concurrent: { - tasks: ['nodemon', 'watch'], - options: { - logConcurrentOutput: true - } - }, - env: { - test: { - NODE_ENV: 'test' - } - }, - mochaTest: { - src: ['app/tests/**/*.js'], - options: { - reporter: 'spec', - require: 'server.js' - } - }, - karma: { - unit: { - configFile: 'karma.conf.js' - } - } - }); + // Project Configuration + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + watch: { + serverViews: { + files: ['app/views/**'], + options: { + livereload: true, + } + }, + serverJS: { + files: ['gruntfile.js', 'server.js', 'config/**/*.js', 'app/**/*.js'], + tasks: ['jshint'], + options: { + livereload: true, + } + }, + clientViews: { + files: ['public/modules/**/views/*.html'], + options: { + livereload: true, + } + }, + clientJS: { + files: ['public/js/**/*.js', 'public/modules/**/*.js'], + tasks: ['jshint'], + options: { + livereload: true, + } + }, + clientCSS: { + files: ['public/**/css/*.css'], + tasks: ['csslint'], + options: { + livereload: true, + } + } + }, + jshint: { + all: { + src: ['gruntfile.js', 'server.js', 'config/**/*.js', 'app/**/*.js', 'public/js/**/*.js', 'public/modules/**/*.js'], + options: { + jshintrc: true + } + } + }, + csslint: { + options: { + csslintrc: '.csslintrc', + }, + all: { + src: ['public/modules/**/css/*.css'] + } + }, + uglify: { + production: { + options: { + mangle: false + }, + files: { + 'public/dist/application.min.js': '<%= applicationJavaScriptFiles %>' + } + } + }, + cssmin: { + combine: { + files: { + 'public/dist/application.min.css': '<%= applicationCSSFiles %>' + } + } + }, + nodemon: { + dev: { + script: 'server.js', + options: { + nodeArgs: ['--debug'] + } + } + }, + concurrent: { + tasks: ['nodemon', 'watch'], + options: { + logConcurrentOutput: true + } + }, + env: { + test: { + NODE_ENV: 'test' + } + }, + mochaTest: { + src: ['app/tests/**/*.js'], + options: { + reporter: 'spec', + require: 'server.js' + } + }, + karma: { + unit: { + configFile: 'karma.conf.js' + } + } + }); - //Load NPM tasks - grunt.loadNpmTasks('grunt-contrib-watch'); - grunt.loadNpmTasks('grunt-contrib-jshint'); - grunt.loadNpmTasks('grunt-mocha-test'); - grunt.loadNpmTasks('grunt-karma'); - grunt.loadNpmTasks('grunt-nodemon'); - grunt.loadNpmTasks('grunt-concurrent'); - grunt.loadNpmTasks('grunt-env'); + // Load NPM tasks + require('load-grunt-tasks')(grunt); - //Making grunt default to force in order not to break the project. - grunt.option('force', true); + // Making grunt default to force in order not to break the project. + grunt.option('force', true); - //Default task(s). - grunt.registerTask('default', ['jshint', 'concurrent']); + // A Task for loading the configuration object + grunt.task.registerTask('loadConfig', 'Task that loads the config into a grunt option.', function() { + var config = require('./config/config'); - //Test task. - grunt.registerTask('test', ['env:test', 'mochaTest', 'karma:unit']); + grunt.config.set('applicationJavaScriptFiles', config.assets.js); + grunt.config.set('applicationCSSFiles', config.assets.css); + }); + + // Default task(s). + grunt.registerTask('default', ['jshint', 'csslint', 'concurrent']); + + // Lint task(s). + grunt.registerTask('lint', ['jshint', 'csslint']); + + // Build task(s). + grunt.registerTask('build', ['jshint', 'csslint', 'loadConfig' ,'uglify', 'cssmin']); + + // Test task. + grunt.registerTask('test', ['env:test', 'mochaTest', 'karma:unit']); }; \ No newline at end of file diff --git a/karma.conf.js b/karma.conf.js index e4c06df54a..99103482ff 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -3,10 +3,7 @@ /** * Module dependencies. */ -var utilities = require('./config/utilities'); - -// Grabbing module files using the walk function -var modulesJSFiles = utilities.walk('./public/modules', /(.*)\.js$/); +var applicationConfiguration = require('./config/config'); // Karma configuration module.exports = function(config) { @@ -15,18 +12,7 @@ module.exports = function(config) { frameworks: ['jasmine'], // List of files / patterns to load in the browser - files: [ - 'public/lib/angular/angular.js', - 'public/lib/angular-animate/angular-animate.js', - 'public/lib/angular-mocks/angular-mocks.js', - 'public/lib/angular-cookies/angular-cookies.js', - 'public/lib/angular-resource/angular-resource.js', - 'public/lib/angular-bootstrap/ui-bootstrap.js', - 'public/lib/angular-ui-utils/ui-utils.js', - 'public/lib/angular-ui-router/release/angular-ui-router.js', - 'public/js/config.js', - 'public/js/application.js', - ].concat(modulesJSFiles), + files: applicationConfiguration.assets.lib.js.concat(applicationConfiguration.assets.js, applicationConfiguration.assets.tests), // Test results reporter to use // Possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' diff --git a/package.json b/package.json index 2261bfaa38..bf7aed985c 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.2.3", + "version": "0.3.0", "private": false, "author": "https://github.com/meanjs/mean/graphs/contributors", "repository": { @@ -18,12 +18,19 @@ "postinstall": "bower install --config.interactive=false" }, "dependencies": { - "express": "~3.5.1", + "express": "~4.1.0", + "express-session": "~1.0.2", + "body-parser": "~1.0.1", + "cookie-parser": "~1.0.1", + "compression": "~1.0.1", + "method-override": "~1.0.0", + "morgan": "~1.0.0", + "connect-mongo": "~0.4.0", + "connect-flash": "~0.1.1", + "helmet": "~0.2.1", "consolidate": "~0.10.0", "swig": "~1.3.2", "mongoose": "~3.8.8", - "connect-mongo": "~0.4.0", - "connect-flash": "~0.1.1", "passport": "~0.2.0", "passport-local": "~1.0.0", "passport-facebook": "~1.0.2", @@ -31,9 +38,10 @@ "passport-linkedin": "~0.1.3", "passport-google-oauth": "~0.1.5", "lodash": "~2.4.1", - "forever": "~0.11.00", + "forever": "~0.11.0", "bower": "~1.3.1", - "grunt-cli": "~0.1.13" + "grunt-cli": "~0.1.13", + "glob": "~3.2.9" }, "devDependencies": { "supertest": "~0.10.0", @@ -42,10 +50,14 @@ "grunt-node-inspector": "~0.1.3", "grunt-contrib-watch": "~0.6.1", "grunt-contrib-jshint": "~0.10.0", + "grunt-contrib-csslint": "^0.2.0", + "grunt-contrib-uglify": "~0.4.0", + "grunt-contrib-cssmin": "~0.9.0", "grunt-nodemon": "~0.2.0", "grunt-concurrent": "~0.5.0", "grunt-mocha-test": "~0.10.0", "grunt-karma": "~0.8.2", + "load-grunt-tasks": "~0.4.0", "karma": "~0.12.0", "karma-jasmine": "~0.2.1", "karma-coverage": "~0.2.0", diff --git a/public/js/application.js b/public/application.js similarity index 100% rename from public/js/application.js rename to public/application.js diff --git a/public/js/config.js b/public/config.js similarity index 83% rename from public/js/config.js rename to public/config.js index e429f257f1..f61ff77ac9 100644 --- a/public/js/config.js +++ b/public/config.js @@ -4,7 +4,7 @@ var ApplicationConfiguration = (function() { // Init module configuration options var applicationModuleName = 'mean'; - var applicationModuleVendorDependencies = ['ngResource', 'ngCookies', 'ngAnimate', 'ui.router', 'ui.bootstrap', 'ui.utils']; + var applicationModuleVendorDependencies = ['ngResource', 'ngAnimate', 'ui.router', 'ui.bootstrap', 'ui.utils']; // Add a new vertical module var registerModule = function(moduleName) { diff --git a/public/dist/application.min.css b/public/dist/application.min.css new file mode 100644 index 0000000000..7769639b99 --- /dev/null +++ b/public/dist/application.min.css @@ -0,0 +1 @@ +.content{margin-top:50px}.undecorated-link:hover{text-decoration:none}.ng-cloak,.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}@media (min-width:992px){.nav-users{position:fixed}}.remove-account-container{display:inline-block;position:relative}.btn-remove-account{top:10px;right:10px;position:absolute} \ No newline at end of file diff --git a/public/dist/application.min.js b/public/dist/application.min.js new file mode 100644 index 0000000000..62a5f946a5 --- /dev/null +++ b/public/dist/application.min.js @@ -0,0 +1 @@ +"use strict";var ApplicationConfiguration=function(){return{applicationModuleName:"mean",applicationModuleVendorDependencies:["ngResource","ngAnimate","ui.router","ui.bootstrap","ui.utils"],registerModule:function(moduleName){angular.module(moduleName,[]),angular.module(this.applicationModuleName).requires.push(moduleName)}}}();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"),Menus.addMenuItem("topbar","New Article","articles/create")}]),angular.module("articles").config(["$stateProvider",function($stateProvider){$stateProvider.state("listArticles",{url:"/articles",templateUrl:"modules/articles/views/list-articles.client.view.html"}).state("createArticle",{url:"/articles/create",templateUrl:"modules/articles/views/create-article.client.view.html"}).state("viewArticle",{url:"/articles/:articleId",templateUrl:"modules/articles/views/view-article.client.view.html"}).state("editArticle",{url:"/articles/:articleId/edit",templateUrl:"modules/articles/views/edit-article.client.view.html"})}]),angular.module("articles").controller("ArticlesController",["$scope","$stateParams","$location","Authentication","Articles",function($scope,$stateParams,$location,Authentication,Articles){$scope.authentication=Authentication,$scope.create=function(){var article=new Articles({title:this.title,content:this.content});article.$save(function(response){$location.path("articles/"+response._id)},function(errorResponse){$scope.error=errorResponse.data.message}),this.title="",this.content=""},$scope.remove=function(article){if(article){article.$remove();for(var i in $scope.articles)$scope.articles[i]===article&&$scope.articles.splice(i,1)}else $scope.article.$remove(function(){$location.path("articles")})},$scope.update=function(){var article=$scope.article;article.updated||(article.updated=[]),article.updated.push((new Date).getTime()),article.$update(function(){$location.path("articles/"+article._id)},function(errorResponse){$scope.error=errorResponse.data.message})},$scope.find=function(){Articles.query(function(articles){$scope.articles=articles})},$scope.findOne=function(){Articles.get({articleId:$stateParams.articleId},function(article){$scope.article=article})}}]),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}}]),angular.module("core").controller("HomeController",["$scope","Authentication",function($scope,Authentication){$scope.authentication=Authentication}]),angular.module("core").service("Menus",[function(){this.defaultRoles=["user"],this.menus={};var shouldRender=function(user){if(!user)return!this.requiresAuthentication;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,requiresAuthentication,roles){return this.menus[menuId]={requiresAuthentication:requiresAuthentication||!0,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,menuItemUIRoute,requiresAuthentication,roles){return this.validateMenuExistance(menuId),this.menus[menuId].items.push({title:menuItemTitle,link:menuItemURL,uiRoute:menuItemUIRoute||"/"+menuItemURL,requiresAuthentication:requiresAuthentication||!1,roles:roles||this.defaultRoles,shouldRender:shouldRender}),this.menus[menuId]},this.removeMenuItem=function(menuId,menuItemURL){this.validateMenuExistance(menuId);for(var itemIndex in this.menus[menuId].items)this.menus[menuId].items[itemIndex].menuItemURL===menuItemURL&&this.menus[menuId].items.splice(itemIndex,1);return this.menus[menuId]},this.addMenu("topbar")}]),angular.module("users").config(["$httpProvider",function($httpProvider){$httpProvider.interceptors.push(["$q","$location","Authentication",function($q,$location,Authentication){return{responseError:function(rejection){switch(rejection.status){case 401:Authentication.user=null,$location.path("signin");break;case 403:}return $q.reject(rejection)}}}])}]),angular.module("users").config(["$stateProvider",function($stateProvider){$stateProvider.state("profile",{url:"/settings/profile",templateUrl:"modules/users/views/settings/edit-profile.client.view.html"}).state("password",{url:"/settings/password",templateUrl:"modules/users/views/settings/change-password.client.view.html"}).state("accounts",{url:"/settings/accounts",templateUrl:"modules/users/views/settings/social-accounts.client.view.html"}).state("signup",{url:"/signup",templateUrl:"modules/users/views/signup.client.view.html"}).state("signin",{url:"/signin",templateUrl:"modules/users/views/signin.client.view.html"})}]),angular.module("users").controller("AuthenticationController",["$scope","$http","$location","Authentication",function($scope,$http,$location,Authentication){$scope.authentication=Authentication,$scope.authentication.user&&$location.path("/"),$scope.signup=function(){$http.post("/auth/signup",$scope.credentials).success(function(response){$scope.authentication.user=response,$location.path("/")}).error(function(response){$scope.error=response.message})},$scope.signin=function(){$http.post("/auth/signin",$scope.credentials).success(function(response){$scope.authentication.user=response,$location.path("/")}).error(function(response){$scope.error=response.message})}}]),angular.module("users").controller("SettingsController",["$scope","$http","$location","Users","Authentication",function($scope,$http,$location,Users,Authentication){$scope.user=Authentication.user,$scope.user||$location.path("/"),$scope.hasConnectedAdditionalSocialAccounts=function(){for(var i in $scope.user.additionalProvidersData)return!0;return!1},$scope.isConnectedSocialAccount=function(provider){return $scope.user.provider===provider||$scope.user.additionalProvidersData&&$scope.user.additionalProvidersData[provider]},$scope.removeUserSocialAccount=function(provider){$scope.success=$scope.error=null,$http.delete("/users/accounts",{params:{provider:provider}}).success(function(response){$scope.success=!0,$scope.user=Authentication.user=response}).error(function(response){$scope.error=response.message})},$scope.updateUserProfile=function(){$scope.success=$scope.error=null;var user=new Users($scope.user);user.$update(function(response){$scope.success=!0,Authentication.user=response},function(response){$scope.error=response.data.message})},$scope.changeUserPassword=function(){$scope.success=$scope.error=null,$http.post("/users/password",$scope.passwordDetails).success(function(){$scope.success=!0,$scope.passwordDetails=null}).error(function(response){$scope.error=response.message})}}]),angular.module("users").factory("Authentication",[function(){var _this=this;return _this._data={user:window.user},_this._data}]),angular.module("users").factory("Users",["$resource",function($resource){return $resource("users",{},{update:{method:"PUT"}})}]); \ No newline at end of file diff --git a/public/img/.gitignore b/public/img/.gitignore deleted file mode 100755 index e69de29bb2..0000000000 diff --git a/public/modules/articles/articles.js b/public/modules/articles/articles.client.module.js similarity index 100% rename from public/modules/articles/articles.js rename to public/modules/articles/articles.client.module.js diff --git a/public/modules/articles/config/articles.client.config.js b/public/modules/articles/config/articles.client.config.js new file mode 100644 index 0000000000..b69ee99354 --- /dev/null +++ b/public/modules/articles/config/articles.client.config.js @@ -0,0 +1,10 @@ +'use strict'; + +// Configuring the Articles module +angular.module('articles').run(['Menus', + function(Menus) { + // Set top bar menu items + Menus.addMenuItem('topbar', 'Articles', 'articles'); + Menus.addMenuItem('topbar', 'New Article', 'articles/create'); + } +]); \ No newline at end of file diff --git a/public/modules/articles/config/routes.js b/public/modules/articles/config/articles.client.routes.js similarity index 59% rename from public/modules/articles/config/routes.js rename to public/modules/articles/config/articles.client.routes.js index 5d5b8dad4e..1531a9a57c 100755 --- a/public/modules/articles/config/routes.js +++ b/public/modules/articles/config/articles.client.routes.js @@ -7,19 +7,19 @@ angular.module('articles').config(['$stateProvider', $stateProvider. state('listArticles', { url: '/articles', - templateUrl: 'modules/articles/views/list.html' + templateUrl: 'modules/articles/views/list-articles.client.view.html' }). state('createArticle', { url: '/articles/create', - templateUrl: 'modules/articles/views/create.html' + templateUrl: 'modules/articles/views/create-article.client.view.html' }). state('viewArticle', { url: '/articles/:articleId', - templateUrl: 'modules/articles/views/view.html' + templateUrl: 'modules/articles/views/view-article.client.view.html' }). state('editArticle', { url: '/articles/:articleId/edit', - templateUrl: 'modules/articles/views/edit.html' + templateUrl: 'modules/articles/views/edit-article.client.view.html' }); } ]); \ No newline at end of file diff --git a/public/modules/articles/controllers/articles.client.controller.js b/public/modules/articles/controllers/articles.client.controller.js new file mode 100644 index 0000000000..02ab64c908 --- /dev/null +++ b/public/modules/articles/controllers/articles.client.controller.js @@ -0,0 +1,58 @@ +'use strict'; + +angular.module('articles').controller('ArticlesController', ['$scope', '$stateParams', '$location', 'Authentication', 'Articles', + function($scope, $stateParams, $location, Authentication, Articles) { + $scope.authentication = Authentication; + + $scope.create = function() { + var article = new Articles({ + title: this.title, + content: this.content + }); + article.$save(function(response) { + $location.path('articles/' + response._id); + }, function(errorResponse) { + $scope.error = errorResponse.data.message; + }); + + this.title = ''; + this.content = ''; + }; + + $scope.remove = function(article) { + if (article) { + article.$remove(); + + for (var i in $scope.articles) { + if ($scope.articles[i] === article) { + $scope.articles.splice(i, 1); + } + } + } else { + $scope.article.$remove(function() { + $location.path('articles'); + }); + } + }; + + $scope.update = function() { + var article = $scope.article; + + article.$update(function() { + $location.path('articles/' + article._id); + }, function(errorResponse) { + $scope.error = errorResponse.data.message; + }); + }; + + $scope.find = function() { + $scope.articles = Articles.query(); + }; + + $scope.findOne = function() { + $scope.article = Articles.get({ + articleId: $stateParams.articleId + }); + }; + } +]); \ No newline at end of file diff --git a/public/modules/articles/controllers/articles.js b/public/modules/articles/controllers/articles.js deleted file mode 100644 index 27eee82fdc..0000000000 --- a/public/modules/articles/controllers/articles.js +++ /dev/null @@ -1,62 +0,0 @@ -'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); - }); - - this.title = ''; - this.content = ''; - }; - - $scope.remove = function(article) { - if (article) { - article.$remove(); - - for (var i in $scope.articles) { - if ($scope.articles[i] === article) { - $scope.articles.splice(i, 1); - } - } - } else { - $scope.article.$remove(function() { - $location.path('articles'); - }); - } - }; - - $scope.update = function() { - var article = $scope.article; - if (!article.updated) { - article.updated = []; - } - article.updated.push(new Date().getTime()); - - article.$update(function() { - $location.path('articles/' + article._id); - }); - }; - - $scope.find = function() { - Articles.query(function(articles) { - $scope.articles = articles; - }); - }; - - $scope.findOne = function() { - Articles.get({ - articleId: $stateParams.articleId - }, function(article) { - $scope.article = article; - }); - }; - } -]); \ No newline at end of file diff --git a/public/modules/articles/services/articles.js b/public/modules/articles/services/articles.client.service.js similarity index 100% rename from public/modules/articles/services/articles.js rename to public/modules/articles/services/articles.client.service.js diff --git a/public/modules/articles/tests/articles.spec.js b/public/modules/articles/tests/articles.client.controller.test.js similarity index 100% rename from public/modules/articles/tests/articles.spec.js rename to public/modules/articles/tests/articles.client.controller.test.js diff --git a/public/modules/articles/views/create.html b/public/modules/articles/views/create-article.client.view.html similarity index 86% rename from public/modules/articles/views/create.html rename to public/modules/articles/views/create-article.client.view.html index ed0bf7aa20..fa2331374c 100644 --- a/public/modules/articles/views/create.html +++ b/public/modules/articles/views/create-article.client.view.html @@ -3,7 +3,7 @@

New Article

-
+
@@ -20,6 +20,9 @@

New Article

+
+ {{error}} +
diff --git a/public/modules/articles/views/edit.html b/public/modules/articles/views/edit-article.client.view.html similarity index 86% rename from public/modules/articles/views/edit.html rename to public/modules/articles/views/edit-article.client.view.html index 790eafad21..4d9bcbf243 100644 --- a/public/modules/articles/views/edit.html +++ b/public/modules/articles/views/edit-article.client.view.html @@ -3,7 +3,7 @@

Edit Article

-
+
@@ -20,6 +20,9 @@

Edit Article

+
+ {{error}} +
diff --git a/public/modules/articles/views/list.html b/public/modules/articles/views/list-articles.client.view.html similarity index 94% rename from public/modules/articles/views/list.html rename to public/modules/articles/views/list-articles.client.view.html index d033e492c9..9b7f014181 100644 --- a/public/modules/articles/views/list.html +++ b/public/modules/articles/views/list-articles.client.view.html @@ -9,7 +9,7 @@

{{article.title}}

{{article.content}}

-
+
No articles yet, why don't you create one?
\ No newline at end of file diff --git a/public/modules/articles/views/view.html b/public/modules/articles/views/view-article.client.view.html similarity index 100% rename from public/modules/articles/views/view.html rename to public/modules/articles/views/view-article.client.view.html diff --git a/public/modules/core/config/routes.js b/public/modules/core/config/core.client.routes.js similarity index 84% rename from public/modules/core/config/routes.js rename to public/modules/core/config/core.client.routes.js index 270503b413..894e3a6caf 100755 --- a/public/modules/core/config/routes.js +++ b/public/modules/core/config/core.client.routes.js @@ -10,7 +10,7 @@ angular.module('core').config(['$stateProvider', '$urlRouterProvider', $stateProvider. state('home', { url: '/', - templateUrl: 'modules/core/views/home.html' + templateUrl: 'modules/core/views/home.client.view.html' }); } ]); \ No newline at end of file diff --git a/public/modules/core/controllers/header.js b/public/modules/core/controllers/header.client.controller.js similarity index 51% rename from public/modules/core/controllers/header.js rename to public/modules/core/controllers/header.client.controller.js index c603172914..4fe41f2af0 100644 --- a/public/modules/core/controllers/header.js +++ b/public/modules/core/controllers/header.client.controller.js @@ -1,19 +1,10 @@ 'use strict'; -angular.module('core').controller('HeaderController', ['$scope', 'Authentication', - function($scope, Authentication) { +angular.module('core').controller('HeaderController', ['$scope', 'Authentication', 'Menus', + function($scope, Authentication, Menus) { $scope.authentication = Authentication; $scope.isCollapsed = false; - - $scope.menu = [{ - title: 'Articles', - link: 'articles', - uiRoute: '/articles' - }, { - title: 'New Article', - link: 'articles/create', - uiRoute: '/articles/create' - }]; + $scope.menu = Menus.getMenu('topbar'); $scope.toggleCollapsibleMenu = function() { $scope.isCollapsed = !$scope.isCollapsed; diff --git a/public/modules/core/controllers/home.js b/public/modules/core/controllers/home.client.controller.js similarity index 100% rename from public/modules/core/controllers/home.js rename to public/modules/core/controllers/home.client.controller.js diff --git a/public/modules/core/core.js b/public/modules/core/core.client.module.js similarity index 100% rename from public/modules/core/core.js rename to public/modules/core/core.client.module.js diff --git a/public/css/common.css b/public/modules/core/css/core.css similarity index 86% rename from public/css/common.css rename to public/modules/core/css/core.css index a1a2546a61..30aebaaf0e 100644 --- a/public/css/common.css +++ b/public/modules/core/css/core.css @@ -1,7 +1,7 @@ .content { margin-top: 50px; } -a.undecorated-link:hover { +.undecorated-link:hover { text-decoration: none; } [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { diff --git a/public/img/brand/favicon.ico b/public/modules/core/img/brand/favicon.ico similarity index 100% rename from public/img/brand/favicon.ico rename to public/modules/core/img/brand/favicon.ico diff --git a/public/img/brand/logo.png b/public/modules/core/img/brand/logo.png similarity index 100% rename from public/img/brand/logo.png rename to public/modules/core/img/brand/logo.png diff --git a/public/img/loaders/loader.gif b/public/modules/core/img/loaders/loader.gif similarity index 100% rename from public/img/loaders/loader.gif rename to public/modules/core/img/loaders/loader.gif diff --git a/public/modules/core/services/menus.client.service.js b/public/modules/core/services/menus.client.service.js new file mode 100644 index 0000000000..66d6bac912 --- /dev/null +++ b/public/modules/core/services/menus.client.service.js @@ -0,0 +1,114 @@ +'use strict'; + +//Menu service used for managing menus +angular.module('core').service('Menus', [ + function() { + // Define a set of default roles + this.defaultRoles = ['user']; + + // Define the menus object + this.menus = {}; + + // A private function for rendering decision + var shouldRender = function(user) { + if(user) { + for (var userRoleIndex in user.roles) { + for (var roleIndex in this.roles) { + if(this.roles[roleIndex] === user.roles[userRoleIndex]) { + return true; + } + } + } + } else { + return this.isPublic; + } + + return false; + }; + + // Validate menu existance + this.validateMenuExistance = function(menuId) { + if (menuId && menuId.length) { + if (this.menus[menuId]) { + return true; + } else { + throw new Error('Menu does not exists'); + } + } else { + throw new Error('MenuId was not provided'); + } + + return false; + }; + + // Get the menu object by menu id + this.getMenu = function(menuId) { + // Validate that the menu exists + this.validateMenuExistance(menuId); + + // Return the menu object + return this.menus[menuId]; + }; + + // Add new menu object by menu id + this.addMenu = function(menuId, isPublic, roles) { + // Create the new menu + this.menus[menuId] = { + isPublic: isPublic || false, + roles: roles || this.defaultRoles, + items: [], + shouldRender: shouldRender + }; + + // Return the menu object + return this.menus[menuId]; + }; + + // Remove existing menu object by menu id + this.removeMenu = function(menuId) { + // Validate that the menu exists + this.validateMenuExistance(menuId); + + // Return the menu object + delete this.menus[menuId]; + }; + + // Add menu item object + this.addMenuItem = function(menuId, menuItemTitle, menuItemURL, menuItemUIRoute, isPublic, roles) { + // Validate that the menu exists + this.validateMenuExistance(menuId); + + // Push new menu item + this.menus[menuId].items.push({ + title: menuItemTitle, + link: menuItemURL, + uiRoute: menuItemUIRoute || ('/' + menuItemURL), + isPublic: isPublic || this.menus[menuId].isPublic, + roles: roles || this.defaultRoles, + shouldRender: shouldRender + }); + + // Return the menu object + return this.menus[menuId]; + }; + + // Remove existing menu object by menu id + this.removeMenuItem = function(menuId, menuItemURL) { + // Validate that the menu exists + this.validateMenuExistance(menuId); + + // Search for menu item to remove + for (var itemIndex in this.menus[menuId].items) { + if (this.menus[menuId].items[itemIndex].menuItemURL === menuItemURL) { + this.menus[menuId].items.splice(itemIndex, 1); + } + } + + // Return the menu object + return this.menus[menuId]; + }; + + //Adding the topbar menu + this.addMenu('topbar'); + } +]); \ No newline at end of file diff --git a/public/modules/core/tests/header.spec.js b/public/modules/core/tests/header.client.controller.test.js similarity index 100% rename from public/modules/core/tests/header.spec.js rename to public/modules/core/tests/header.client.controller.test.js diff --git a/public/modules/core/tests/home.spec.js b/public/modules/core/tests/home.client.controller.test.js similarity index 100% rename from public/modules/core/tests/home.spec.js rename to public/modules/core/tests/home.client.controller.test.js diff --git a/public/modules/core/views/header.html b/public/modules/core/views/header.client.view.html similarity index 87% rename from public/modules/core/views/header.html rename to public/modules/core/views/header.client.view.html index 918c882be5..9c93c6e717 100644 --- a/public/modules/core/views/header.html +++ b/public/modules/core/views/header.client.view.html @@ -9,8 +9,8 @@ MEAN.JS