New 0.4 version

This commit is contained in:
Amos Haviv
2014-11-10 23:12:33 +02:00
parent ad870299c6
commit ab81d61bd3
153 changed files with 2603 additions and 2063 deletions

View File

@@ -33,4 +33,4 @@ indent_style = tab
# Standard at:
[Makefile]
indent_style = tab
indent_style = tab

2
.gitignore vendored
View File

@@ -4,7 +4,5 @@
npm-debug.log
node_modules/
public/lib
app/tests/coverage/
.bower-*/
.idea/
config/sslcert/*.pem

View File

@@ -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.
}
}

View File

@@ -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.

View File

@@ -1,11 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
exports.index = function(req, res) {
res.render('index', {
user: req.user || null,
request: req
});
};

View File

@@ -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);
};

View File

@@ -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);
};

View File

@@ -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);
};

View File

@@ -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);
};

View File

@@ -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"
}
}
}

47
config/assets/default.js Normal file
View File

@@ -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'
}
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = {
// Development assets
};

View File

@@ -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'
}
};

9
config/assets/test.js Normal file
View File

@@ -0,0 +1,9 @@
'use strict';
module.exports = {
tests: {
client: ['modules/*/tests/client/**/*.js'],
server: ['modules/*/tests/server/**/*.js'],
e2e: ['modules/*/tests/e2e/**/*.js']
}
};

View File

@@ -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;
};
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();

51
config/env/all.js vendored
View File

@@ -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'
]
}
};

14
config/env/default.js vendored Normal file
View File

@@ -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'
};

View File

@@ -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: {

View File

@@ -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'
}
}
}
};

68
config/env/secure.js vendored
View File

@@ -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'
}
}
}
};

19
config/env/test.js vendored
View File

@@ -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',

View File

@@ -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;
};

View File

@@ -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'));
}
});
};

251
config/lib/express.js Executable file
View File

@@ -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;
};

36
config/lib/mongoose.js Normal file
View File

@@ -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);
}
});
};

61
config/lib/socket.io.js Normal file
View File

@@ -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;
};

View File

@@ -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;
}
};

View File

@@ -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']);
};
// Run the project in production mode
grunt.registerTask('prod', ['build', 'env:prod', 'concurrent:default']);
};

187
gulpfile.js Normal file
View File

@@ -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);
});

View File

@@ -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
});
};
};

View File

@@ -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'
});
}
]);

View File

@@ -5,21 +5,26 @@ angular.module('articles').config(['$stateProvider',
function($stateProvider) {
// Articles state routing
$stateProvider.
state('listArticles', {
state('articles', {
abstract: true,
url: '/articles',
template: '<ui-view/>'
}).
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'
});
}
]);
]);

View File

@@ -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',
}
});
}
]);
]);

View File

@@ -5,10 +5,10 @@
<div class="col-md-12">
<form name="articleForm" class="form-horizontal" data-ng-submit="create()" novalidate>
<fieldset>
<div class="form-group" ng-class="{ 'has-error': articleForm.title.$dirty && articleForm.title.$invalid }">
<div class="form-group">
<label class="control-label" for="title">Title</label>
<div class="controls">
<input name="title" type="text" data-ng-model="title" id="title" class="form-control" placeholder="Title" required>
<input name="title" type="text" data-ng-model="title" id="title" class="form-control" placeholder="Title">
</div>
</div>
<div class="form-group">
@@ -26,4 +26,4 @@
</fieldset>
</form>
</div>
</section>
</section>

View File

@@ -3,24 +3,18 @@
<h1>Edit Article</h1>
</div>
<div class="col-md-12">
<form name="articleForm" class="form-horizontal" data-ng-submit="update(articleForm.$valid)" novalidate>
<form name="articleForm" class="form-horizontal" data-ng-submit="update()" novalidate>
<fieldset>
<div class="form-group" ng-class="{ 'has-error' : submitted && articleForm.title.$invalid}">
<div class="form-group">
<label class="control-label" for="title">Title</label>
<div class="controls">
<input name="title" type="text" data-ng-model="article.title" id="title" class="form-control" placeholder="Title" required>
</div>
<div ng-show="submitted && articleForm.title.$invalid" class="help-block">
<p ng-show="articleForm.title.$error.required" class="text-danger">Title is required</p>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error' : submitted && articleForm.content.$invalid}">
<div class="form-group">
<label class="control-label" for="content">Content</label>
<div class="controls">
<textarea name="content" data-ng-model="article.content" id="content" class="form-control" cols="30" rows="10" placeholder="Content" required></textarea>
</div>
<div ng-show="submitted && articleForm.content.$invalid" class="help-block">
<p ng-show="articleForm.content.$error.required" class="text-danger">Content is required</p>
<textarea name="content" data-ng-model="article.content" id="content" class="form-control" cols="30" rows="10" placeholder="Content"></textarea>
</div>
</div>
<div class="form-group">
@@ -32,4 +26,4 @@
</fieldset>
</form>
</div>
</section>
</section>

View File

@@ -3,7 +3,7 @@
<h1>Articles</h1>
</div>
<div class="list-group">
<a data-ng-repeat="article in articles" data-ng-href="#!/articles/{{article._id}}" class="list-group-item">
<a data-ng-repeat="article in articles" data-ui-sref="articles.view({articleId: article._id})" class="list-group-item">
<small class="list-group-item-text">
Posted on
<span data-ng-bind="article.created | date:'mediumDate'"></span>
@@ -15,6 +15,6 @@
</a>
</div>
<div class="alert alert-warning text-center" data-ng-if="articles.$resolved && !articles.length">
No articles yet, why don't you <a href="/#!/articles/create">create one</a>?
No articles yet, why don't you <a data-ui-sref="articles.create">create one</a>?
</div>
</section>
</section>

View File

@@ -3,7 +3,7 @@
<h1 data-ng-bind="article.title"></h1>
</div>
<div class="pull-right" data-ng-show="authentication.user._id == article.user._id">
<a class="btn btn-primary" href="/#!/articles/{{article._id}}/edit">
<a class="btn btn-primary" data-ui-sref="articles.edit({articleId: article._id})">
<i class="glyphicon glyphicon-edit"></i>
</a>
<a class="btn btn-primary" data-ng-click="remove();">
@@ -19,4 +19,4 @@
</em>
</small>
<p class="lead" data-ng-bind="article.content"></p>
</section>
</section>

View File

@@ -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();
};

View File

@@ -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'
});
}
}
});
};

View File

@@ -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);
};

View File

@@ -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);

View File

@@ -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);
});
});
});

View File

@@ -61,4 +61,4 @@ describe('Article Model Unit Tests:', function() {
User.remove().exec();
done();
});
});
});

View File

@@ -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();
});
});
});

View File

@@ -1,4 +1,4 @@
'use strict';
// Use Applicaion configuration module to register a new module
ApplicationConfiguration.registerModule('users');
ApplicationConfiguration.registerModule('chat');

View File

@@ -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'
});
}
]);

View File

@@ -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'
});
}
]);

View File

@@ -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');
});
}
]);

View File

@@ -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%;
}

View File

@@ -0,0 +1,28 @@
<!-- The chat view -->
<section class="container" data-ng-controller="ChatController">
<div class="page-header">
<h1>Chat Example</h1>
</div>
<!-- The message form -->
<form class="col-xs-12 col-md-offset-4 col-md-4" ng-submit="sendMessage();">
<fieldset class="row">
<div class="input-group">
<input type="text" id="messageText" name="messageText" class="form-control" data-ng-model="messageText" placeholder="Enter new message">
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">Submit</button>
</span>
</div>
</fieldset>
</form>
<ul class="list-unstyled">
<!-- List all messages -->
<li class="col-xs-12 col-md-offset-4 col-md-4 chat-message" data-ng-repeat="message in messages">
<small class="pull-right text-muted" data-ng-bind="message.created | date:'mediumTime'"></small>
<img data-ng-src="{{message.profileImageURL}}" alt="{{message.username}}" class="pull-left chat-profile-image"/>
<div class="pull-left chat-message-details">
<strong data-ng-bind="message.username"></strong><br>
<span data-ng-bind="message.text"></span>
</div>
</li>
</ul>
</section>

View File

@@ -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
});
});
};

View File

@@ -0,0 +1,10 @@
'use strict';
/**
* Chat client controller tests
*/
(function() {
describe('ChatController', function() {
// TODO: Add chat client controller tests
});
}());

View File

@@ -0,0 +1,8 @@
'use strict';
/**
* Chat e2e tests
*/
describe('Chat E2E Tests:', function() {
// TODO: Add chat e2e tests
});

View File

@@ -0,0 +1,8 @@
'use strict';
/**
* Chat socket tests
*/
describe('Chat Socket Tests:', function() {
// TODO: Add chat socket tests
});

View File

@@ -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
};
})();
})();

View File

@@ -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]);
});
});

View File

@@ -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;
});
}
]);
]);

View File

@@ -1,6 +1,5 @@
'use strict';
angular.module('core').controller('HomeController', ['$scope', 'Authentication',
function($scope, Authentication) {
// This provides Authentication context.

View File

@@ -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;
}

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -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
});
}
]);

View File

@@ -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);
}
};
}
]);

View File

@@ -6,53 +6,57 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="/#!/" class="navbar-brand">MEAN.JS</a>
<a data-ui-sref="home" class="navbar-brand">MEAN.JS</a>
</div>
<nav class="collapse navbar-collapse" collapse="!isCollapsed" role="navigation">
<ul class="nav navbar-nav" data-ng-if="menu.shouldRender(authentication.user);">
<li data-ng-repeat="item in menu.items | orderBy: 'position'" data-ng-if="item.shouldRender(authentication.user);" ng-switch="item.menuItemType" ui-route="{{item.uiRoute}}" class="{{item.menuItemClass}}" ng-class="{active: ($uiRoute)}" dropdown="item.menuItemType === 'dropdown'">
<li data-ng-repeat="item in menu.items | orderBy: 'position'" data-ng-if="item.shouldRender(authentication.user);" ng-switch="item.type" data-ng-class="{active: $state.includes(item.state)}" class="{{item.class}}" dropdown="item.type === 'dropdown'">
<a ng-switch-when="dropdown" class="dropdown-toggle">
<span data-ng-bind="item.title"></span>
<b class="caret"></b>
</a>
<ul ng-switch-when="dropdown" class="dropdown-menu">
<li data-ng-repeat="subitem in item.items | orderBy: 'position'" data-ng-if="subitem.shouldRender(authentication.user);" ui-route="{{subitem.uiRoute}}" ng-class="{active: $uiRoute}">
<a href="/#!/{{subitem.link}}" data-ng-bind="subitem.title"></a>
<li data-ng-repeat="subitem in item.items | orderBy: 'position'" data-ng-if="subitem.shouldRender(authentication.user);" data-ui-sref-active="active">
<a data-ui-sref="{{subitem.state}}" data-ng-bind="subitem.title"></a>
</li>
</ul>
<a ng-switch-default href="/#!/{{item.link}}" data-ng-bind="item.title"></a>
<a ng-switch-default data-ui-sref="{{item.state}}" data-ng-bind="item.title"></a>
</li>
</ul>
<ul class="nav navbar-nav navbar-right" data-ng-hide="authentication.user">
<li ui-route="/signup" ng-class="{active: $uiRoute}">
<a href="/#!/signup">Sign Up</a>
<li data-ui-sref-active="active">
<a data-ui-sref="authentication.signup" target="_self">Sign Up</a>
</li>
<li class="divider-vertical"></li>
<li ui-route="/signin" ng-class="{active: $uiRoute}">
<a href="/#!/signin">Sign In</a>
<li data-ui-sref-active="active">
<a data-ui-sref="authentication.signin" target="_self">Sign In</a>
</li>
</ul>
<ul class="nav navbar-nav navbar-right" data-ng-show="authentication.user">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<a href="#" class="dropdown-toggle user-header-dropdown-toggle" data-toggle="dropdown">
<img data-ng-src="{{authentication.user.profileImageURL}}" alt="{{authentication.user.displayName}}" class="header-profile-image"/>
<span data-ng-bind="authentication.user.displayName"></span> <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li>
<a href="/#!/settings/profile">Edit Profile</a>
<li data-ui-sref-active="active">
<a data-ui-sref="settings.profile">Edit Profile</a>
</li>
<li>
<a href="/#!/settings/accounts">Manage Social Accounts</a>
<li data-ui-sref-active="active">
<a data-ui-sref="settings.picture">Change Profile Picture</a>
</li>
<li data-ng-show="authentication.user.provider === 'local'">
<a href="/#!/settings/password">Change Password</a>
<li data-ui-sref-active="active" data-ng-show="authentication.user.provider === 'local'">
<a data-ui-sref="settings.password">Change Password</a>
</li>
<li data-ui-sref-active="active">
<a data-ui-sref="settings.accounts">Manage Social Accounts</a>
</li>
<li class="divider"></li>
<li>
<a href="/auth/signout">Signout</a>
<a href="/api/auth/signout" target="_self">Signout</a>
</li>
</ul>
</li>
</ul>
</nav>
</div>
</div>

View File

@@ -18,7 +18,7 @@
</div>
</div>
<div>
<h2>Congrats! You've configured and ran the sample application successfully.</h2>
<h2>Congrats! You've configured and run the sample application.</h2>
<p>MEAN.JS is a web application boilerplate, which means you should start changing everything :-)</p>
<p>This sample application tracks users and articles.</p>
<ul>
@@ -54,7 +54,7 @@
<h2>
<strong>E</strong>xpress
</h2>
<p><a target="_blank" href="http://expressjs.com/"> Express</a> is an app server. Check out <a target="_blank" href="http://expressjs.com/guide/error-handling.html">The ExpressJS guide for general topics</a> or <a target="_blank" href="http://stackoverflow.com/questions/8144214/learning-express-for-node-js">StackOverflow</a> for more info.</p>
<p><a target="_blank" href="http://expressjs.com/"> Express</a> is an app server. Check out <a target="_blank" href="http://expressjs.com/guide.html">The Express Guide</a> or <a target="_blank" href="http://stackoverflow.com/questions/8144214/learning-express-for-node-js">StackOverflow</a> for more info.</p>
</div>
<div class="col-md-3">
<h2>
@@ -91,4 +91,4 @@
</div>
<br>Enjoy &amp; Keep Us Updated,
<br>The MEAN.JS Team.
</section>
</section>

View File

@@ -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
});
};

View File

@@ -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:

View File

@@ -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);
};

View File

@@ -8,6 +8,10 @@
<meta charset="utf-8">
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="fragment" content="!" />
<base href="/"/>
<!-- Responsive META -->
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
@@ -55,14 +59,28 @@
<script type="text/javascript">
var user = {{ user | json | safe }};
</script>
<!--Load The Socket.io File-->
<script type="text/javascript" src="/socket.io/socket.io.js"></script>
<!--Application JavaScript Files-->
{% for jsFile in jsFiles %}<script type="text/javascript" src="{{jsFile}}"></script>{% endfor %}
{% if process.env.NODE_ENV === 'development' %}
<!--Livereload script rendered -->
<script type="text/javascript" src="http://{{request.hostname}}:35729/livereload.js"></script>
<script type="text/javascript" src="{{host}}:35729/livereload.js"></script>
{% endif %}
<!-- Google Analytics -->
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', '{{googleAnalyticsTrackingID}}', 'auto');
ga('send', 'pageview');
</script>
</body>
</html>
</html>

View File

@@ -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',
}
]);
}
]);
]);

View File

@@ -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: '<ui-view/>'
}).
state('password.forgot', {
url: '/forgot',
templateUrl: 'modules/users/views/password/forgot-password.client.view.html'
}).
state('password.reset', {
abstract: true,
url: '/reset',
template: '<ui-view/>'
}).
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'
});
}
]);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
});
};
}
]);
]);

View File

@@ -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;
});
};
}
]);

View File

@@ -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;
};
}
]);

View File

@@ -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;
}
};
}
]);

View File

@@ -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;
});
};
}
]);

View File

@@ -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('/');
}
]);

View File

@@ -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;
}

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Some files were not shown because too many files have changed in this diff Show More