Merge branch '0.4.0'

Releasing MEAN.JS 0.4.0 version on-top of previously MEAN.JS 0.3.3 on master branch
This commit is contained in:
Liran Tal
2015-08-03 18:30:03 +03:00
229 changed files with 7372 additions and 4445 deletions

View File

@@ -1,3 +1,3 @@
{
"directory": "public/lib"
"directory": "public/lib"
}

View File

@@ -1,15 +1,15 @@
{
"adjoining-classes": false,
"box-model": false,
"box-sizing": false,
"floats": false,
"font-sizes": false,
"important": false,
"known-properties": false,
"overqualified-elements": false,
"qualified-headings": false,
"regex-selectors": false,
"unique-headings": false,
"universal-selector": false,
"unqualified-attributes": false
"adjoining-classes": false,
"box-model": false,
"box-sizing": false,
"floats": false,
"font-sizes": false,
"important": false,
"known-properties": false,
"overqualified-elements": false,
"qualified-headings": false,
"regex-selectors": false,
"unique-headings": false,
"universal-selector": false,
"unqualified-attributes": false
}

View File

@@ -1,25 +1,43 @@
# EditorConfig is awesome: http://EditorConfig.org
# How-to with your editor: http://editorconfig.org/#download
# Howto with your editor: http://editorconfig.org/#download
# Sublime: https://github.com/sindresorhus/editorconfig-sublime
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
[**]
end_of_line = lf
indent_style = tab
insert_final_newline = true
[{Dockerfile,Procfile}]
trim_trailing_whitespace = true
# Standard at: https://github.com/felixge/node-style-guide
[{*.js,*.json}]
# Standard at: https://github.com/felixge/node-style-guide
[**.js, **.json]
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
quote_type = single
curly_bracket_next_line = false
spaces_around_operators = true
space_after_control_statements = true
space_after_anonymous_functions = false
space_after_anonymous_functions = true
spaces_in_brackets = false
# No Standard. Please document a standard if different from .js
[**.yml, **.html, **.css]
trim_trailing_whitespace = true
indent_style = tab
# No standard. Please document a standard if different from .js
[**.md]
indent_style = tab
# Standard at:
[Makefile]
indent_style = tab
# The indentation in package.json will always need to be 2 spaces
# https://github.com/npm/npm/issues/4718
[package.json, bower.json]
indent_style = space
indent_size = 2

47
.gitignore vendored
View File

@@ -1,27 +1,45 @@
# iOS / Apple
# OS
# ===========
.DS_Store
ehthumbs.db
Icon?
Thumbs.db
config/env/local.js
# Node and related ecosystem
# ==========================
.nodemonignore
.sass-cache/
npm-debug.log
node_modules/
public/lib/
app/tests/coverage/
.bower-*/
.idea/
coverage/
# MEAN.js app and assets
# ======================
config/sslcerts/*.pem
access.log
public/dist/
uploads
modules/users/client/img/profile/uploads
config/env/local.js
*.pem
# Ignoring MEAN.JS's gh-pages branch for documenation
_site/
# General
# =======
*.log
*.csv
*.dat
*.out
*.pid
*.gz
*.tmp
*.bak
*.swp
logs/
build/
# Sublime editor
# ==============
@@ -48,16 +66,9 @@ local.properties
data/
mongod
# General
# =======
*.log
*.csv
*.dat
*.out
*.pid
*.gz
*.tmp
*.bak
*.swp
logs/
build/
# Visual Studio
# =========
*.suo
*.ntvs*
*.njsproj
*.sln

View File

@@ -1,42 +1,36 @@
{
"node": true, // Enable globals available when code is running inside of the NodeJS runtime environment.
"browser": true, // Standard browser globals e.g. `window`, `document`.
"esnext": true, // Allow ES.next specific features such as `const` and `let`.
"bitwise": false, // Prohibit bitwise operators (&, |, ^, etc.).
"camelcase": false, // Permit only camelcase for `var` and `object indexes`.
"curly": false, // Require {} for every new block or scope.
"eqeqeq": true, // Require triple equals i.e. `===`.
"immed": true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );`
"latedef": true, // Prohibit variable use before definition.
"newcap": true, // Require capitalization of all constructor functions e.g. `new F()`.
"noarg": true, // Prohibit use of `arguments.caller` and `arguments.callee`.
"quotmark": "single", // Define quotes to string values.
"regexp": true, // Prohibit `.` and `[^...]` in regular expressions.
"undef": true, // Require all non-global variables be declared before they are used.
"unused": false, // Warn unused variables.
"strict": true, // Require `use strict` pragma in every file.
"trailing": true, // Prohibit trailing whitespaces.
"smarttabs": false, // Suppresses warnings about mixed tabs and spaces
"globals": { // Globals variables.
"jasmine": true,
"angular": true,
"ApplicationConfiguration": true
},
"predef": [ // Extra globals.
"define",
"require",
"exports",
"module",
"describe",
"before",
"beforeEach",
"after",
"afterEach",
"it",
"inject",
"expect"
],
"indent": 4, // Specify indentation spacing
"devel": true, // Allow development statements e.g. `console.log();`.
"noempty": true // Prohibit use of empty blocks.
}
"node": true, // Enable globals available when code is running inside of the NodeJS runtime environment.
"mocha": true, // Enable globals available when code is running inside of the Mocha tests.
"jasmine": true, // Enable globals available when code is running inside of the Jasmine tests.
"browser": true, // Standard browser globals e.g. `window`, `document`.
"esnext": true, // Allow ES.next specific features such as `const` and `let`.
"bitwise": false, // Prohibit bitwise operators (&, |, ^, etc.).
"camelcase": false, // Permit only camelcase for `var` and `object indexes`.
"curly": false, // Require {} for every new block or scope.
"eqeqeq": true, // Require triple equals i.e. `===`.
"immed": true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );`
"latedef": true, // Prohibit variable use before definition.
"newcap": true, // Require capitalization of all constructor functions e.g. `new F()`.
"noarg": true, // Prohibit use of `arguments.caller` and `arguments.callee`.
"quotmark": "single", // Define quotes to string values.
"regexp": true, // Prohibit `.` and `[^...]` in regular expressions.
"undef": true, // Require all non-global variables be declared before they are used.
"unused": false, // Warn unused variables.
"strict": true, // Require `use strict` pragma in every file.
"trailing": true, // Prohibit trailing whitespaces.
"smarttabs": false, // Suppresses warnings about mixed tabs and spaces
"globals": { // Globals variables.
"angular": true,
"io": true,
"ApplicationConfiguration": true
},
"predef": [ // Extra globals.
"inject",
"by",
"browser",
"element"
],
"indent": 4, // Specify indentation spacing
"devel": true, // Allow development statements e.g. `console.log();`.
"noempty": true // Prohibit use of empty blocks.
}

View File

@@ -1 +1 @@
/app/tests
/app/tests

View File

@@ -1,7 +1,7 @@
language: node_js
node_js:
- "0.10"
- "0.11"
- "0.12"
env:
- NODE_ENV=travis
services:

View File

@@ -1,4 +1,4 @@
FROM dockerfile/nodejs
FROM node:0.10
MAINTAINER Matthias Luebken, matthias@catalyst-zero.com
@@ -20,7 +20,7 @@ RUN bower install --config.interactive=false --allow-root
# Make everything available for start
ADD . /home/mean
# currently only works for development
# Set development environment as default
ENV NODE_ENV development
# Port 3000 for server

0
Procfile Executable file → Normal file
View File

View File

@@ -1,8 +1,14 @@
[![MEAN.JS Logo](http://meanjs.org/img/logo-small.png)](http://meanjs.org/)
[![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/meanjs/mean?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Master Branch:
[![Build Status](https://travis-ci.org/meanjs/mean.svg?branch=master)](https://travis-ci.org/meanjs/mean)
[![Dependencies Status](https://david-dm.org/meanjs/mean.svg)](https://david-dm.org/meanjs/mean)
[![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/meanjs/mean?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Dev Branch:
[![Build Status](https://travis-ci.org/meanjs/mean.svg?branch=0.4.1)](https://travis-ci.org/meanjs/mean)
[![Dependencies Status](https://david-dm.org/meanjs/mean/0.4.1.svg)](https://david-dm.org/meanjs/mean/0.4.1)
MEAN.JS is a full-stack JavaScript open-source solution, which provides a solid starting point for [MongoDB](http://www.mongodb.org/), [Node.js](http://www.nodejs.org/), [Express](http://expressjs.com/), and [AngularJS](http://angularjs.org/) based applications. The idea is to solve the common issues with connecting those frameworks, build a robust framework to support daily development needs, and help developers use better practices while working with popular JavaScript components.
@@ -33,23 +39,27 @@ $ npm install -g grunt-cli
## Downloading MEAN.JS
There are several ways you can get the MEAN.JS boilerplate:
### Yo Generator
The recommended way would be to use the [Official Yo Generator](http://meanjs.org/generator.html), which generates the latest stable copy of the MEAN.JS boilerplate and supplies multiple sub-generators to ease your daily development cycles.
### Cloning The GitHub Repository
You can also use Git to directly clone the MEAN.JS repository:
The recommended way to get MEAN.js is to use git to directly clone the MEAN.JS repository:
```bash
$ git clone https://github.com/meanjs/mean.git meanjs
```
This will clone the latest version of the MEAN.JS repository to a **meanjs** folder.
### Downloading The Repository Zip File
Another way to use the MEAN.JS boilerplate is to download a zip copy from the [master branch on GitHub](https://github.com/meanjs/mean/archive/master.zip). You can also do this using `wget` command:
```bash
$ wget https://github.com/meanjs/mean/archive/master.zip -O meanjs.zip; unzip meanjs.zip; rm meanjs.zip
```
Don't forget to rename **mean-master** after your project name.
### Yo Generator
-Another way would be to use the [Official Yo Generator](http://meanjs.org/generator.html), which generates a copy of the MEAN.JS 0.3.x boilerplate and supplies multiple sub-generators to ease your daily development cycles.
## Quick Install
Once you've downloaded the boilerplate and installed all the prerequisites, you're just a few steps away from starting to develop your MEAN application.
@@ -64,24 +74,50 @@ $ npm install
This command does a few things:
* First it will install the dependencies needed for the application to run.
* If you're running in a development environment, it will then also install development dependencies needed for testing and running your application.
* Finally, when the install process is over, npm will initiate a bower install command to install all the front-end modules needed for the application.
* Finally, when the install process is over, npm will initiate a bower install command to install all the front-end modules needed for the application
## Running Your Application
After the install process is over, you'll be able to run your application using Grunt. Just run grunt default task:
After the install process is over, you'll be able to run your application using Grunt, just run grunt default task:
```bash
```
$ grunt
```
Your application should run on port 3000, so in your browser just go to [http://localhost:3000](http://localhost:3000)
Your application should run on port 3000 with the *development* environment configuration, so in your browser just go to [http://localhost:3000](http://localhost:3000)
That's it! Your application should be running. To proceed with your development, check the other sections in this documentation.
If you encounter any problems, try the Troubleshooting section.
* explore `config/env/development.js` for development environment configuration options
### Running in Production mode
To run your application with *production* environment configuration, execute grunt as follows:
```bash
$ grunt prod
```
* explore `config/env/production.js` for production environment configuration options
### Running with TLS (SSL)
Application will start by default with secure configuration (SSL mode) turned on and listen on port 8443.
To run your application in a secure manner you'll need to use OpenSSL and generate a set of self-signed certificates. Unix-based users can use the following command:
```bash
$ sh ./scripts/generate-ssl-certs.sh
```
Windows users can follow instructions found [here](http://www.websense.com/support/article/kbarticle/How-to-use-OpenSSL-and-Microsoft-Certification-Authority).
After you've generated the key and certificate, place them in the *config/sslcerts* folder.
Finally, execute grunt's prod task `grunt prod`
* enable/disable SSL mode in production environment change the `secure` option in `config/env/production.js`
## Testing Your Application
You can run the full test suite included with MEAN.JS with the test task:
```
```bash
$ grunt test
```
@@ -89,24 +125,24 @@ This will run both the server-side tests (located in the app/tests/ directory) a
To execute only the server tests, run the test:server task:
```
```bash
$ grunt test:server
```
And to run only the client tests, run the test:client task:
```
```bash
$ grunt test:client
```
## Development and deployment With Docker
* Install [Docker](http://www.docker.com/)
* Install [Fig](https://github.com/orchardup/fig)
* Install [Docker](https://docs.docker.com/installation/#installation)
* Install [Compose](https://docs.docker.com/compose/install/)
* Local development and testing with fig:
* Local development and testing with compose:
```bash
$ fig up
$ docker-compose up
```
* Local development and testing with just Docker:
@@ -122,14 +158,6 @@ $
$ docker run -p 3000:3000 -p 35729:35729 -v /Users/mdl/workspace/mean-stack/mean/public:/home/mean/public -v /Users/mdl/workspace/mean-stack/mean/app:/home/mean/app --link db:db_1 mean
```
## Running in a secure environment
To run your application in a secure manner you'll need to use OpenSSL and generate a set of self-signed certificates. Unix-based users can use the following command:
```bash
$ sh ./scripts/generate-ssl-certs.sh
```
Windows users can follow instructions found [here](http://www.websense.com/support/article/kbarticle/How-to-use-OpenSSL-and-Microsoft-Certification-Authority).
After you've generated the key and certificate, place them in the *config/sslcerts* folder.
## Getting Started With MEAN.JS
You have your application running, but there is a lot of stuff to understand. We recommend you go over the [Official Documentation](http://meanjs.org/docs.html).
In the docs we'll try to explain both general concepts of MEAN components and give you some guidelines to help you improve your development process. We tried covering as many aspects as possible, and will keep it updated by your request. You can also help us develop and improve the documentation by checking out the *gh-pages* branch of this repository.

View File

@@ -1,120 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
errorHandler = require('./errors.server.controller'),
Article = mongoose.model('Article'),
_ = require('lodash');
/**
* Create a article
*/
exports.create = function(req, res) {
var article = new Article(req.body);
article.user = req.user;
article.save(function(err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(article);
}
});
};
/**
* Show the current article
*/
exports.read = function(req, res) {
res.json(req.article);
};
/**
* Update a article
*/
exports.update = function(req, res) {
var article = req.article;
article = _.extend(article, req.body);
article.save(function(err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(article);
}
});
};
/**
* Delete an article
*/
exports.delete = function(req, res) {
var article = req.article;
article.remove(function(err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(article);
}
});
};
/**
* List of Articles
*/
exports.list = function(req, res) {
Article.find().sort('-created').populate('user', 'displayName').exec(function(err, articles) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(articles);
}
});
};
/**
* Article middleware
*/
exports.articleByID = function(req, res, next, id) {
if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(400).send({
message: 'Article is invalid'
});
}
Article.findById(id).populate('user', 'displayName').exec(function(err, article) {
if (err) return next(err);
if (!article) {
return res.status(404).send({
message: 'Article not found'
});
}
req.article = article;
next();
});
};
/**
* Article authorization middleware
*/
exports.hasAuthorization = function(req, res, next) {
if (req.article.user.id !== req.user.id) {
return res.status(403).send({
message: 'User is not authorized'
});
}
next();
};

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,42 +0,0 @@
'use strict';
/**
* Get unique error field name
*/
var getUniqueErrorMessage = function(err) {
var output;
try {
var fieldName = err.err.substring(err.err.lastIndexOf('.$') + 2, err.err.lastIndexOf('_1'));
output = fieldName.charAt(0).toUpperCase() + fieldName.slice(1) + ' already exists';
} catch (ex) {
output = 'Unique field already exists';
}
return output;
};
/**
* Get the error message from error object
*/
exports.getErrorMessage = function(err) {
var message = '';
if (err.code) {
switch (err.code) {
case 11000:
case 11001:
message = getUniqueErrorMessage(err);
break;
default:
message = 'Something went wrong';
}
} else {
for (var errName in err.errors) {
if (err.errors[errName].message) message = err.errors[errName].message;
}
}
return message;
};

View File

@@ -1,16 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var _ = require('lodash');
/**
* Extend user's controller
*/
module.exports = _.extend(
require('./users/users.authentication.server.controller'),
require('./users/users.authorization.server.controller'),
require('./users/users.password.server.controller'),
require('./users/users.profile.server.controller')
);

View File

@@ -1,206 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var _ = require('lodash'),
errorHandler = require('../errors.server.controller'),
mongoose = require('mongoose'),
passport = require('passport'),
User = mongoose.model('User');
/**
* Signup
*/
exports.signup = function(req, res) {
// For security measurement we remove the roles from the req.body object
delete req.body.roles;
// Init Variables
var user = new User(req.body);
var message = null;
// Add missing user fields
user.provider = 'local';
user.displayName = user.firstName + ' ' + user.lastName;
// Then save the user
user.save(function(err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
// Remove sensitive data before login
user.password = undefined;
user.salt = undefined;
req.login(user, function(err) {
if (err) {
res.status(400).send(err);
} else {
res.json(user);
}
});
}
});
};
/**
* Signin after passport authentication
*/
exports.signin = function(req, res, next) {
passport.authenticate('local', function(err, user, info) {
if (err || !user) {
res.status(400).send(info);
} else {
// Remove sensitive data before login
user.password = undefined;
user.salt = undefined;
req.login(user, function(err) {
if (err) {
res.status(400).send(err);
} else {
res.json(user);
}
});
}
})(req, res, next);
};
/**
* Signout
*/
exports.signout = function(req, res) {
req.logout();
res.redirect('/');
};
/**
* OAuth callback
*/
exports.oauthCallback = function(strategy) {
return function(req, res, next) {
passport.authenticate(strategy, function(err, user, redirectURL) {
if (err || !user) {
return res.redirect('/#!/signin');
}
req.login(user, function(err) {
if (err) {
return res.redirect('/#!/signin');
}
return res.redirect(redirectURL || '/');
});
})(req, res, next);
};
};
/**
* Helper function to save or update a OAuth user profile
*/
exports.saveOAuthUserProfile = function(req, providerUserProfile, done) {
if (!req.user) {
// Define a search query fields
var searchMainProviderIdentifierField = 'providerData.' + providerUserProfile.providerIdentifierField;
var searchAdditionalProviderIdentifierField = 'additionalProvidersData.' + providerUserProfile.provider + '.' + providerUserProfile.providerIdentifierField;
// Define main provider search query
var mainProviderSearchQuery = {};
mainProviderSearchQuery.provider = providerUserProfile.provider;
mainProviderSearchQuery[searchMainProviderIdentifierField] = providerUserProfile.providerData[providerUserProfile.providerIdentifierField];
// Define additional provider search query
var additionalProviderSearchQuery = {};
additionalProviderSearchQuery[searchAdditionalProviderIdentifierField] = providerUserProfile.providerData[providerUserProfile.providerIdentifierField];
// Define a search query to find existing user with current provider profile
var searchQuery = {
$or: [mainProviderSearchQuery, additionalProviderSearchQuery]
};
User.findOne(searchQuery, function(err, user) {
if (err) {
return done(err);
} else {
if (!user) {
var possibleUsername = providerUserProfile.username || ((providerUserProfile.email) ? providerUserProfile.email.split('@')[0] : '');
User.findUniqueUsername(possibleUsername, null, function(availableUsername) {
user = new User({
firstName: providerUserProfile.firstName,
lastName: providerUserProfile.lastName,
username: availableUsername,
displayName: providerUserProfile.displayName,
email: providerUserProfile.email,
provider: providerUserProfile.provider,
providerData: providerUserProfile.providerData
});
// And save the user
user.save(function(err) {
return done(err, user);
});
});
} else {
return done(err, user);
}
}
});
} else {
// User is already logged in, join the provider data to the existing user
var user = req.user;
// Check if user exists, is not signed in using this provider, and doesn't have that provider data already configured
if (user.provider !== providerUserProfile.provider && (!user.additionalProvidersData || !user.additionalProvidersData[providerUserProfile.provider])) {
// Add the provider data to the additional provider data field
if (!user.additionalProvidersData) user.additionalProvidersData = {};
user.additionalProvidersData[providerUserProfile.provider] = providerUserProfile.providerData;
// Then tell mongoose that we've updated the additionalProvidersData field
user.markModified('additionalProvidersData');
// And save the user
user.save(function(err) {
return done(err, user, '/#!/settings/accounts');
});
} else {
return done(new Error('User is already connected using this provider'), user);
}
}
};
/**
* Remove OAuth provider
*/
exports.removeOAuthProvider = function(req, res, next) {
var user = req.user;
var provider = req.param('provider');
if (user && provider) {
// Delete the additional provider
if (user.additionalProvidersData[provider]) {
delete user.additionalProvidersData[provider];
// Then tell mongoose that we've updated the additionalProvidersData field
user.markModified('additionalProvidersData');
}
user.save(function(err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
req.login(user, function(err) {
if (err) {
res.status(400).send(err);
} else {
res.json(user);
}
});
}
});
}
};

View File

@@ -1,52 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var _ = require('lodash'),
mongoose = require('mongoose'),
User = mongoose.model('User');
/**
* User middleware
*/
exports.userByID = function(req, res, next, id) {
User.findById(id).exec(function(err, user) {
if (err) return next(err);
if (!user) return next(new Error('Failed to load User ' + id));
req.profile = user;
next();
});
};
/**
* Require login routing middleware
*/
exports.requiresLogin = function(req, res, next) {
if (!req.isAuthenticated()) {
return res.status(401).send({
message: 'User is not logged in'
});
}
next();
};
/**
* User authorizations routing middleware
*/
exports.hasAuthorization = function(roles) {
var _this = this;
return function(req, res, next) {
_this.requiresLogin(req, res, function() {
if (_.intersection(req.user.roles, roles).length) {
return next();
} else {
return res.status(403).send({
message: 'User is not authorized'
});
}
});
};
};

View File

@@ -1,249 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var _ = require('lodash'),
errorHandler = require('../errors.server.controller'),
mongoose = require('mongoose'),
passport = require('passport'),
User = mongoose.model('User'),
config = require('../../../config/config'),
nodemailer = require('nodemailer'),
async = require('async'),
crypto = require('crypto');
var smtpTransport = nodemailer.createTransport(config.mailer.options);
/**
* Forgot for reset password (forgot POST)
*/
exports.forgot = function(req, res, next) {
async.waterfall([
// Generate random token
function(done) {
crypto.randomBytes(20, function(err, buffer) {
var token = buffer.toString('hex');
done(err, token);
});
},
// Lookup user by username
function(token, done) {
if (req.body.username) {
User.findOne({
username: req.body.username
}, '-salt -password', function(err, user) {
if (!user) {
return res.status(400).send({
message: 'No account with that username has been found'
});
} else if (user.provider !== 'local') {
return res.status(400).send({
message: 'It seems like you signed up using your ' + user.provider + ' account'
});
} else {
user.resetPasswordToken = token;
user.resetPasswordExpires = Date.now() + 3600000; // 1 hour
user.save(function(err) {
done(err, token, user);
});
}
});
} else {
return res.status(400).send({
message: 'Username field must not be blank'
});
}
},
function(token, user, done) {
res.render('templates/reset-password-email', {
name: user.displayName,
appName: config.app.title,
url: 'http://' + req.headers.host + '/auth/reset/' + token
}, function(err, emailHTML) {
done(err, emailHTML, user);
});
},
// If valid email, send reset email using service
function(emailHTML, user, done) {
var mailOptions = {
to: user.email,
from: config.mailer.from,
subject: 'Password Reset',
html: emailHTML
};
smtpTransport.sendMail(mailOptions, function(err) {
if (!err) {
res.send({
message: 'An email has been sent to ' + user.email + ' with further instructions.'
});
} else {
return res.status(400).send({
message: 'Failure sending email'
});
}
done(err);
});
}
], function(err) {
if (err) return next(err);
});
};
/**
* Reset password GET from email token
*/
exports.validateResetToken = function(req, res) {
User.findOne({
resetPasswordToken: req.params.token,
resetPasswordExpires: {
$gt: Date.now()
}
}, function(err, user) {
if (!user) {
return res.redirect('/#!/password/reset/invalid');
}
res.redirect('/#!/password/reset/' + req.params.token);
});
};
/**
* Reset password POST from email token
*/
exports.reset = function(req, res, next) {
// Init Variables
var passwordDetails = req.body;
async.waterfall([
function(done) {
User.findOne({
resetPasswordToken: req.params.token,
resetPasswordExpires: {
$gt: Date.now()
}
}, function(err, user) {
if (!err && user) {
if (passwordDetails.newPassword === passwordDetails.verifyPassword) {
user.password = passwordDetails.newPassword;
user.resetPasswordToken = undefined;
user.resetPasswordExpires = undefined;
user.save(function(err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
req.login(user, function(err) {
if (err) {
res.status(400).send(err);
} else {
// Return authenticated user
res.json(user);
done(err, user);
}
});
}
});
} else {
return res.status(400).send({
message: 'Passwords do not match'
});
}
} else {
return res.status(400).send({
message: 'Password reset token is invalid or has expired.'
});
}
});
},
function(user, done) {
res.render('templates/reset-password-confirm-email', {
name: user.displayName,
appName: config.app.title
}, function(err, emailHTML) {
done(err, emailHTML, user);
});
},
// If valid email, send reset email using service
function(emailHTML, user, done) {
var mailOptions = {
to: user.email,
from: config.mailer.from,
subject: 'Your password has been changed',
html: emailHTML
};
smtpTransport.sendMail(mailOptions, function(err) {
done(err, 'done');
});
}
], function(err) {
if (err) return next(err);
});
};
/**
* Change Password
*/
exports.changePassword = function(req, res) {
// Init Variables
var passwordDetails = req.body;
if (req.user) {
if (passwordDetails.newPassword) {
User.findById(req.user.id, function(err, user) {
if (!err && user) {
if (user.authenticate(passwordDetails.currentPassword)) {
if (passwordDetails.newPassword === passwordDetails.verifyPassword) {
user.password = passwordDetails.newPassword;
user.save(function(err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
req.login(user, function(err) {
if (err) {
res.status(400).send(err);
} else {
res.send({
message: 'Password changed successfully'
});
}
});
}
});
} else {
res.status(400).send({
message: 'Passwords do not match'
});
}
} else {
res.status(400).send({
message: 'Current password is incorrect'
});
}
} else {
res.status(400).send({
message: 'User is not found'
});
}
});
} else {
res.status(400).send({
message: 'Please provide a new password'
});
}
} else {
res.status(400).send({
message: 'User is not signed in'
});
}
};

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,34 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
/**
* Article Schema
*/
var ArticleSchema = new Schema({
created: {
type: Date,
default: Date.now
},
title: {
type: String,
default: '',
trim: true,
required: 'Title cannot be blank'
},
content: {
type: String,
default: '',
trim: true
},
user: {
type: Schema.ObjectId,
ref: 'User'
}
});
mongoose.model('Article', ArticleSchema);

View File

@@ -1,146 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
crypto = require('crypto');
/**
* A Validation function for local strategy properties
*/
var validateLocalStrategyProperty = function(property) {
return ((this.provider !== 'local' && !this.updated) || property.length);
};
/**
* A Validation function for local strategy password
*/
var validateLocalStrategyPassword = function(password) {
return (this.provider !== 'local' || (password && password.length > 6));
};
/**
* User Schema
*/
var UserSchema = new Schema({
firstName: {
type: String,
trim: true,
default: '',
validate: [validateLocalStrategyProperty, 'Please fill in your first name']
},
lastName: {
type: String,
trim: true,
default: '',
validate: [validateLocalStrategyProperty, 'Please fill in your last name']
},
displayName: {
type: String,
trim: true
},
email: {
type: String,
trim: true,
default: '',
validate: [validateLocalStrategyProperty, 'Please fill in your email'],
match: [/.+\@.+\..+/, 'Please fill a valid email address']
},
username: {
type: String,
unique: 'Username already exists',
required: 'Please fill in a username',
trim: true
},
password: {
type: String,
default: '',
validate: [validateLocalStrategyPassword, 'Password should be longer']
},
salt: {
type: String
},
provider: {
type: String,
required: 'Provider is required'
},
providerData: {},
additionalProvidersData: {},
roles: {
type: [{
type: String,
enum: ['user', 'admin']
}],
default: ['user']
},
updated: {
type: Date
},
created: {
type: Date,
default: Date.now
},
/* For reset password */
resetPasswordToken: {
type: String
},
resetPasswordExpires: {
type: Date
}
});
/**
* Hook a pre save method to hash the password
*/
UserSchema.pre('save', function(next) {
if (this.password && this.password.length > 6) {
this.salt = crypto.randomBytes(16).toString('base64');
this.password = this.hashPassword(this.password);
}
next();
});
/**
* Create instance method for hashing a password
*/
UserSchema.methods.hashPassword = function(password) {
if (this.salt && password) {
return crypto.pbkdf2Sync(password, new Buffer(this.salt, 'base64'), 10000, 64).toString('base64');
} else {
return password;
}
};
/**
* Create instance method for authenticating user
*/
UserSchema.methods.authenticate = function(password) {
return this.password === this.hashPassword(password);
};
/**
* Find possible not used username
*/
UserSchema.statics.findUniqueUsername = function(username, suffix, callback) {
var _this = this;
var possibleUsername = username + (suffix || '');
_this.findOne({
username: possibleUsername
}, function(err, user) {
if (!err) {
if (!user) {
callback(possibleUsername);
} else {
return _this.findUniqueUsername(username, (suffix || 0) + 1, callback);
}
} else {
callback(null);
}
});
};
mongoose.model('User', UserSchema);

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,64 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var should = require('should'),
mongoose = require('mongoose'),
User = mongoose.model('User'),
Article = mongoose.model('Article');
/**
* Globals
*/
var user, article;
/**
* Unit tests
*/
describe('Article Model Unit Tests:', function() {
beforeEach(function(done) {
user = new User({
firstName: 'Full',
lastName: 'Name',
displayName: 'Full Name',
email: 'test@test.com',
username: 'username',
password: 'password'
});
user.save(function() {
article = new Article({
title: 'Article Title',
content: 'Article Content',
user: user
});
done();
});
});
describe('Method Save', function() {
it('should be able to save without problems', function(done) {
return article.save(function(err) {
should.not.exist(err);
done();
});
});
it('should be able to show an error when try to save without title', function(done) {
article.title = '';
return article.save(function(err) {
should.exist(err);
done();
});
});
});
afterEach(function(done) {
Article.remove().exec(function() {
User.remove().exec(done);
});
});
});

View File

@@ -1,280 +0,0 @@
'use strict';
var should = require('should'),
request = require('supertest'),
app = require('../../server'),
mongoose = require('mongoose'),
User = mongoose.model('User'),
Article = mongoose.model('Article'),
agent = request.agent(app);
/**
* Globals
*/
var credentials, user, article;
/**
* Article routes tests
*/
describe('Article CRUD tests', function() {
beforeEach(function(done) {
// Create user credentials
credentials = {
username: 'username',
password: 'password'
};
// Create a new user
user = new User({
firstName: 'Full',
lastName: 'Name',
displayName: 'Full Name',
email: 'test@test.com',
username: credentials.username,
password: credentials.password,
provider: 'local'
});
// Save a user to the test db and create new article
user.save(function() {
article = {
title: 'Article Title',
content: 'Article Content'
};
done();
});
});
it('should be able to save an article if logged in', function(done) {
agent.post('/auth/signin')
.send(credentials)
.expect(200)
.end(function(signinErr, signinRes) {
// Handle signin error
if (signinErr) done(signinErr);
// Get the userId
var userId = user.id;
// Save a new article
agent.post('/articles')
.send(article)
.expect(200)
.end(function(articleSaveErr, articleSaveRes) {
// Handle article save error
if (articleSaveErr) done(articleSaveErr);
// Get a list of articles
agent.get('/articles')
.end(function(articlesGetErr, articlesGetRes) {
// Handle article save error
if (articlesGetErr) done(articlesGetErr);
// Get articles list
var articles = articlesGetRes.body;
// Set assertions
(articles[0].user._id).should.equal(userId);
(articles[0].title).should.match('Article Title');
// Call the assertion callback
done();
});
});
});
});
it('should not be able to save an article if not logged in', function(done) {
agent.post('/articles')
.send(article)
.expect(401)
.end(function(articleSaveErr, articleSaveRes) {
// Call the assertion callback
done(articleSaveErr);
});
});
it('should not be able to save an article if no title is provided', function(done) {
// Invalidate title field
article.title = '';
agent.post('/auth/signin')
.send(credentials)
.expect(200)
.end(function(signinErr, signinRes) {
// Handle signin error
if (signinErr) done(signinErr);
// Get the userId
var userId = user.id;
// Save a new article
agent.post('/articles')
.send(article)
.expect(400)
.end(function(articleSaveErr, articleSaveRes) {
// Set message assertion
(articleSaveRes.body.message).should.match('Title cannot be blank');
// Handle article save error
done(articleSaveErr);
});
});
});
it('should be able to update an article if signed in', function(done) {
agent.post('/auth/signin')
.send(credentials)
.expect(200)
.end(function(signinErr, signinRes) {
// Handle signin error
if (signinErr) done(signinErr);
// Get the userId
var userId = user.id;
// Save a new article
agent.post('/articles')
.send(article)
.expect(200)
.end(function(articleSaveErr, articleSaveRes) {
// Handle article save error
if (articleSaveErr) done(articleSaveErr);
// Update article title
article.title = 'WHY YOU GOTTA BE SO MEAN?';
// Update an existing article
agent.put('/articles/' + articleSaveRes.body._id)
.send(article)
.expect(200)
.end(function(articleUpdateErr, articleUpdateRes) {
// Handle article update error
if (articleUpdateErr) done(articleUpdateErr);
// Set assertions
(articleUpdateRes.body._id).should.equal(articleSaveRes.body._id);
(articleUpdateRes.body.title).should.match('WHY YOU GOTTA BE SO MEAN?');
// Call the assertion callback
done();
});
});
});
});
it('should be able to get a list of articles if not signed in', function(done) {
// Create new article model instance
var articleObj = new Article(article);
// Save the article
articleObj.save(function() {
// Request articles
request(app).get('/articles')
.end(function(req, res) {
// Set assertion
res.body.should.be.an.Array.with.lengthOf(1);
// Call the assertion callback
done();
});
});
});
it('should be able to get a single article if not signed in', function(done) {
// Create new article model instance
var articleObj = new Article(article);
// Save the article
articleObj.save(function() {
request(app).get('/articles/' + articleObj._id)
.end(function(req, res) {
// Set assertion
res.body.should.be.an.Object.with.property('title', article.title);
// Call the assertion callback
done();
});
});
});
it('should return proper error for single article which doesnt exist, if not signed in', function(done) {
request(app).get('/articles/test')
.end(function(req, res) {
// Set assertion
res.body.should.be.an.Object.with.property('message', 'Article is invalid');
// Call the assertion callback
done();
});
});
it('should be able to delete an article if signed in', function(done) {
agent.post('/auth/signin')
.send(credentials)
.expect(200)
.end(function(signinErr, signinRes) {
// Handle signin error
if (signinErr) done(signinErr);
// Get the userId
var userId = user.id;
// Save a new article
agent.post('/articles')
.send(article)
.expect(200)
.end(function(articleSaveErr, articleSaveRes) {
// Handle article save error
if (articleSaveErr) done(articleSaveErr);
// Delete an existing article
agent.delete('/articles/' + articleSaveRes.body._id)
.send(article)
.expect(200)
.end(function(articleDeleteErr, articleDeleteRes) {
// Handle article error error
if (articleDeleteErr) done(articleDeleteErr);
// Set assertions
(articleDeleteRes.body._id).should.equal(articleSaveRes.body._id);
// Call the assertion callback
done();
});
});
});
});
it('should not be able to delete an article if not signed in', function(done) {
// Set article user
article.user = user;
// Create new article model instance
var articleObj = new Article(article);
// Save the article
articleObj.save(function() {
// Try deleting article
request(app).delete('/articles/' + articleObj._id)
.expect(401)
.end(function(articleDeleteErr, articleDeleteRes) {
// Set message assertion
(articleDeleteRes.body.message).should.match('User is not logged in');
// Handle article error error
done(articleDeleteErr);
});
});
});
afterEach(function(done) {
User.remove().exec(function() {
Article.remove().exec(done);
});
});
});

View File

@@ -1,75 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var should = require('should'),
mongoose = require('mongoose'),
User = mongoose.model('User');
/**
* Globals
*/
var user, user2;
/**
* Unit tests
*/
describe('User Model Unit Tests:', function() {
before(function(done) {
user = new User({
firstName: 'Full',
lastName: 'Name',
displayName: 'Full Name',
email: 'test@test.com',
username: 'username',
password: 'password',
provider: 'local'
});
user2 = new User({
firstName: 'Full',
lastName: 'Name',
displayName: 'Full Name',
email: 'test@test.com',
username: 'username',
password: 'password',
provider: 'local'
});
done();
});
describe('Method Save', function() {
it('should begin with no users', function(done) {
User.find({}, function(err, users) {
users.should.have.length(0);
done();
});
});
it('should be able to save without problems', function(done) {
user.save(done);
});
it('should fail to save an existing user again', function(done) {
user.save(function() {
user2.save(function(err) {
should.exist(err);
done();
});
});
});
it('should be able to show an error when try to save without first name', function(done) {
user.firstName = '';
return user.save(function(err) {
should.exist(err);
done();
});
});
});
after(function(done) {
User.remove().exec(done);
});
});

View File

@@ -1,8 +0,0 @@
{% extends 'layout.server.view.html' %}
{% block content %}
<h1>Page Not Found</h1>
<pre>
{{url}} is not a valid path.
</pre>
{% endblock %}

View File

@@ -1,81 +0,0 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>{{title}}</title>
<!-- General META -->
<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="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">
<!-- Semantic META -->
<meta name="keywords" content="{{keywords}}">
<meta name="description" content="{{description}}">
<!-- Facebook META -->
<meta property="fb:app_id" content="{{facebookAppId}}">
<meta property="og:site_name" content="{{title}}">
<meta property="og:title" content="{{title}}">
<meta property="og:description" content="{{description}}">
<meta property="og:url" content="{{url}}">
<meta property="og:image" content="/img/brand/logo.png">
<meta property="og:type" content="website">
<!-- Twitter META -->
<meta name="twitter:title" content="{{title}}">
<meta name="twitter:description" content="{{description}}">
<meta name="twitter:url" content="{{url}}">
<meta name="twitter:image" content="/img/brand/logo.png">
<!-- Fav Icon -->
<link href="/modules/core/img/brand/favicon.ico" rel="shortcut icon" type="image/x-icon">
<!--Application CSS Files-->
{% for cssFile in cssFiles %}
<link rel="stylesheet" href="{{cssFile}}">
{% endfor %}
<!-- HTML5 Shim -->
<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>
<body class="ng-cloak">
<header data-ng-include="'/modules/core/views/header.client.view.html'" class="navbar navbar-fixed-top navbar-inverse"></header>
<section class="content">
<section class="container">
{% block content %}{% endblock %}
</section>
</section>
<!--Embedding The User Object-->
<script type="text/javascript">
var user = {{ user | json | safe }};
</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>
{% endif %}
<!--[if lt IE 9]>
<section class="browsehappy jumbotron hide">
<h1>Hello there!</h1>
<p>You are using an old browser which we unfortunately do not support.</p>
<p>Please <a href="http://browsehappy.com/">click here</a> to update your browser before using the website.</p>
<p><a href="http://browsehappy.com" class="btn btn-primary btn-lg" role="button">Yes, upgrade my browser!</a></p>
</section>
<![endif]-->
</body>
</html>

View File

@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
</head>
<body>
<p>Dear {{name}},</p>
<p></p>
<p>This is a confirmation that the password for your account has just been changed</p>
<br>
<br>
<p>The {{appName}} Support Team</p>
</body>
</html>

View File

@@ -1,18 +0,0 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
</head>
<body>
<p>Dear {{name}},</p>
<br>
<p>
You have requested to have your password reset for your account at {{appName}}
</p>
<p>Please visit this url to reset your password:</p>
<p>{{url}}</p>
<strong>If you didn't make this request, you can ignore this email.</strong>
<br>
<br>
<p>The {{appName}} Support Team</p>
</body>
</html>

View File

@@ -1,16 +1,19 @@
{
"name": "meanjs",
"version": "0.3.2",
"description": "Fullstack JavaScript with MongoDB, Express, AngularJS, and Node.js.",
"dependencies": {
"bootstrap": "~3",
"angular": "~1.2",
"angular-resource": "~1.2",
"angular-animate": "~1.2",
"angular-mocks": "~1.2",
"angular-bootstrap": "~0.11.2",
"angular-bootstrap": "~0.12.0",
"angular-ui-utils": "~0.1.1",
"angular-ui-router": "~0.2.11"
}
"name": "meanjs",
"version": "0.4.0",
"description": "Fullstack JavaScript with MongoDB, Express, AngularJS, and Node.js.",
"dependencies": {
"bootstrap": "~3",
"angular": "~1.3",
"angular-resource": "~1.3",
"angular-animate": "~1.3",
"angular-mocks": "~1.3",
"angular-bootstrap": "~0.13",
"angular-ui-utils": "bower",
"angular-ui-router": "~0.2",
"angular-file-upload": "1.1.5"
},
"resolutions": {
"angular": "~1.3"
}
}

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

@@ -0,0 +1,49 @@
'use strict';
module.exports = {
client: {
lib: {
css: [
'public/lib/bootstrap/dist/css/bootstrap.css',
'public/lib/bootstrap/dist/css/bootstrap-theme.css'
],
js: [
'public/lib/angular/angular.js',
'public/lib/angular-resource/angular-resource.js',
'public/lib/angular-animate/angular-animate.js',
'public/lib/angular-ui-router/release/angular-ui-router.js',
'public/lib/angular-ui-utils/ui-utils.js',
'public/lib/angular-bootstrap/ui-bootstrap-tpls.js',
'public/lib/angular-file-upload/angular-file-upload.js'
],
tests: ['public/lib/angular-mocks/angular-mocks.js']
},
css: [
'modules/*/client/css/*.css'
],
less: [
'modules/*/client/less/*.less'
],
sass: [
'modules/*/client/scss/*.scss'
],
js: [
'modules/core/client/app/config.js',
'modules/core/client/app/init.js',
'modules/*/client/*.js',
'modules/*/client/**/*.js'
],
views: ['modules/*/client/views/**/*.html']
},
server: {
gruntConfig: 'gruntfile.js',
gulpConfig: 'gulpfile.js',
allJS: ['server.js', 'config/**/*.js', 'modules/*/server/**/*.js'],
models: 'modules/*/server/models/**/*.js',
routes: ['modules/!(core)/server/routes/**/*.js', 'modules/core/server/routes/**/*.js'],
sockets: 'modules/*/server/sockets/**/*.js',
config: 'modules/*/server/config/*.js',
policies: 'modules/*/server/policies/*.js',
views: 'modules/*/server/views/*.html'
}
};

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,87 +4,183 @@
* Module dependencies.
*/
var _ = require('lodash'),
glob = require('glob'),
fs = require('fs');
/**
* Resolve environment configuration by extending each env configuration file,
* and lastly merge/override that with any local repository configuration that exists
* in local.js
*/
var resolvingConfig = function() {
var conf = {};
conf = _.extend(
require('./env/all'),
require('./env/' + process.env.NODE_ENV) || {}
);
return _.merge(conf, (fs.existsSync('./config/env/local.js') && require('./env/local.js')) || {});
};
/**
* Load app configurations
*/
module.exports = resolvingConfig();
chalk = require('chalk'),
glob = require('glob'),
fs = require('fs'),
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 {
var files = glob.sync(globPatterns);
if (excludes) {
files = files.map(function (file) {
if (_.isArray(excludes)) {
for (var i in excludes) {
file = file.replace(excludes[i], '');
}
} else {
file = file.replace(excludes, '');
}
return file;
});
}
output = _.union(output, files);
}
}
// If glob pattern is array so we use each pattern in a recursive way, otherwise we use glob
if (_.isArray(globPatterns)) {
globPatterns.forEach(function(globPattern) {
output = _.union(output, _this.getGlobbedFiles(globPattern, removeRoot));
});
} else if (_.isString(globPatterns)) {
if (urlRegex.test(globPatterns)) {
output.push(globPatterns);
} else {
glob(globPatterns, {
sync: true
}, function(err, files) {
if (removeRoot) {
files = files.map(function(file) {
return file.replace(removeRoot, '');
});
}
output = _.union(output, files);
});
}
}
return output;
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/');
// To include tests
if (includeTests) {
output = _.union(output, this.getGlobbedFiles(this.assets.tests));
}
return output;
var validateEnvironmentVariable = function () {
var environmentFiles = glob.sync('./config/env/' + process.env.NODE_ENV + '.js');
console.log();
if (!environmentFiles.length) {
if (process.env.NODE_ENV) {
console.error(chalk.red('+ Error: No configuration file found for "' + process.env.NODE_ENV + '" environment using development instead'));
} else {
console.error(chalk.red('+ Error: NODE_ENV is not defined! Using default development environment'));
}
process.env.NODE_ENV = 'development';
}
// Reset console color
console.log(chalk.white(''));
};
/**
* Get the modules CSS files
* Validate Secure=true parameter can actually be turned on
* because it requires certs and key files to be available
*/
module.exports.getCSSAssets = function() {
var output = this.getGlobbedFiles(this.assets.lib.css.concat(this.assets.css), 'public/');
return output;
var validateSecureMode = function (config) {
if (config.secure !== true) {
return true;
}
var privateKey = fs.existsSync('./config/sslcerts/key.pem');
var certificate = fs.existsSync('./config/sslcerts/cert.pem');
if (!privateKey || !certificate) {
console.log(chalk.red('+ Error: Certificate file or key file is missing, falling back to non-SSL mode'));
console.log(chalk.red(' To create them, simply run the following from your shell: sh ./scripts/generate-ssl-certs.sh'));
console.log();
config.secure = false;
}
};
/**
* Initialize global configuration files
*/
var initGlobalConfigFolders = function (config, assets) {
// Appending files
config.folders = {
server: {},
client: {}
};
// Setting globbed client paths
config.folders.client = getGlobbedPaths(path.join(process.cwd(), 'modules/*/client/'), process.cwd().replace(new RegExp(/\\/g), '/'));
};
/**
* Initialize global configuration files
*/
var initGlobalConfigFiles = function (config, assets) {
// Appending files
config.files = {
server: {},
client: {}
};
// Setting Globbed model files
config.files.server.models = getGlobbedPaths(assets.server.models);
// Setting Globbed route files
config.files.server.routes = getGlobbedPaths(assets.server.routes);
// Setting Globbed config files
config.files.server.configs = getGlobbedPaths(assets.server.config);
// Setting Globbed socket files
config.files.server.sockets = getGlobbedPaths(assets.server.sockets);
// Setting Globbed policies files
config.files.server.policies = getGlobbedPaths(assets.server.policies);
// 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 = _.merge(defaultAssets, environmentAssets);
// Get the default config
var defaultConfig = require(path.join(process.cwd(), 'config/env/default'));
// Get the current config
var environmentConfig = require(path.join(process.cwd(), 'config/env/', process.env.NODE_ENV)) || {};
// Merge config files
var envConf = _.merge(defaultConfig, environmentConfig);
var config = _.merge(envConf, (fs.existsSync(path.join(process.cwd(), 'config/env/local.js')) && require(path.join(process.cwd(), 'config/env/local.js'))) || {});
// Initialize global globbed files
initGlobalConfigFiles(config, assets);
// Initialize global globbed folders
initGlobalConfigFolders(config, assets);
// Validate Secure SSL mode can be used
validateSecureMode(config);
// Expose configuration utilities
config.utils = {
getGlobbedPaths: getGlobbedPaths
};
return config;
};
/**
* Set configuration object
*/
module.exports = initGlobalConfig();

72
config/env/all.js vendored
View File

@@ -1,72 +0,0 @@
'use strict';
module.exports = {
app: {
title: 'MEAN.JS',
description: 'Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js',
keywords: 'mongodb, express, angularjs, node.js, mongoose, passport'
},
port: process.env.PORT || 3000,
templateEngine: 'swig',
// The secret should be set to a non-guessable string that
// is used to compute a session hash
sessionSecret: 'MEAN',
// The name of the MongoDB collection to store sessions in
sessionCollection: 'sessions',
// The session cookie settings
sessionCookie: {
path: '/',
httpOnly: true,
// If secure is set to true then it will cause the cookie to be set
// only when SSL-enabled (HTTPS) is used, and otherwise it won't
// set a cookie. 'true' is recommended yet it requires the above
// mentioned pre-requisite.
secure: false,
// Only set the maxAge to null if the cookie shouldn't be expired
// at all. The cookie will expunge when the browser is closed.
maxAge: null,
// To set the cookie in a specific domain uncomment the following
// setting:
// domain: 'yourdomain.com'
},
// The session cookie name
sessionName: 'connect.sid',
log: {
// Can specify one of 'combined', 'common', 'dev', 'short', 'tiny'
format: 'combined',
// Stream defaults to process.stdout
// Uncomment to enable logging to a log on the file system
options: {
stream: 'access.log'
}
},
assets: {
lib: {
css: [
'public/lib/bootstrap/dist/css/bootstrap.css',
'public/lib/bootstrap/dist/css/bootstrap-theme.css',
],
js: [
'public/lib/angular/angular.js',
'public/lib/angular-resource/angular-resource.js',
'public/lib/angular-animate/angular-animate.js',
'public/lib/angular-ui-router/release/angular-ui-router.js',
'public/lib/angular-ui-utils/ui-utils.js',
'public/lib/angular-bootstrap/ui-bootstrap-tpls.js'
]
},
css: [
'public/modules/**/css/*.css'
],
js: [
'public/config.js',
'public/application.js',
'public/modules/*/*.js',
'public/modules/*/*[!tests]*/*.js'
],
tests: [
'public/lib/angular-mocks/angular-mocks.js',
'public/modules/*/tests/*.js'
]
}
};

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

@@ -0,0 +1,16 @@
'use strict';
module.exports = {
app: {
title: 'MEAN.JS',
description: 'Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js',
keywords: 'mongodb, express, angularjs, node.js, mongoose, passport',
googleAnalyticsTrackingID: process.env.GOOGLE_ANALYTICS_TRACKING_ID || 'GOOGLE_ANALYTICS_TRACKING_ID'
},
port: process.env.PORT || 3000,
templateEngine: 'swig',
sessionSecret: 'MEAN',
sessionCollection: 'sessions',
logo: 'modules/core/img/brand/logo.png',
favicon: 'modules/core/img/brand/favicon.ico'
};

View File

@@ -1,58 +1,69 @@
'use strict';
var defaultEnvConfig = require('./default');
module.exports = {
db: {
uri: 'mongodb://localhost/mean-dev',
options: {
user: '',
pass: ''
}
},
log: {
// Can specify one of 'combined', 'common', 'dev', 'short', 'tiny'
format: 'dev',
// Stream defaults to process.stdout
// Uncomment to enable logging to a log on the file system
options: {
//stream: 'access.log'
}
},
app: {
title: 'MEAN.JS - Development Environment'
},
facebook: {
clientID: process.env.FACEBOOK_ID || 'APP_ID',
clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET',
callbackURL: '/auth/facebook/callback'
},
twitter: {
clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY',
clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET',
callbackURL: '/auth/twitter/callback'
},
google: {
clientID: process.env.GOOGLE_ID || 'APP_ID',
clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET',
callbackURL: '/auth/google/callback'
},
linkedin: {
clientID: process.env.LINKEDIN_ID || 'APP_ID',
clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET',
callbackURL: '/auth/linkedin/callback'
},
github: {
clientID: process.env.GITHUB_ID || 'APP_ID',
clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET',
callbackURL: '/auth/github/callback'
},
mailer: {
from: process.env.MAILER_FROM || 'MAILER_FROM',
options: {
service: process.env.MAILER_SERVICE_PROVIDER || 'MAILER_SERVICE_PROVIDER',
auth: {
user: process.env.MAILER_EMAIL_ID || 'MAILER_EMAIL_ID',
pass: process.env.MAILER_PASSWORD || 'MAILER_PASSWORD'
}
}
}
db: {
uri: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://' + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + '/mean-dev',
options: {
user: '',
pass: ''
},
// Enable mongoose debug mode
debug: process.env.MONGODB_DEBUG || false
},
log: {
// Can specify one of 'combined', 'common', 'dev', 'short', 'tiny'
format: 'dev',
// Stream defaults to process.stdout
// Uncomment to enable logging to a log on the file system
options: {
//stream: 'access.log'
}
},
app: {
title: defaultEnvConfig.app.title + ' - Development Environment'
},
facebook: {
clientID: process.env.FACEBOOK_ID || 'APP_ID',
clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET',
callbackURL: '/api/auth/facebook/callback'
},
twitter: {
clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY',
clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET',
callbackURL: '/api/auth/twitter/callback'
},
google: {
clientID: process.env.GOOGLE_ID || 'APP_ID',
clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET',
callbackURL: '/api/auth/google/callback'
},
linkedin: {
clientID: process.env.LINKEDIN_ID || 'APP_ID',
clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET',
callbackURL: '/api/auth/linkedin/callback'
},
github: {
clientID: process.env.GITHUB_ID || 'APP_ID',
clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET',
callbackURL: '/api/auth/github/callback'
},
paypal: {
clientID: process.env.PAYPAL_ID || 'CLIENT_ID',
clientSecret: process.env.PAYPAL_SECRET || 'CLIENT_SECRET',
callbackURL: '/api/auth/paypal/callback',
sandbox: true
},
mailer: {
from: process.env.MAILER_FROM || 'MAILER_FROM',
options: {
service: process.env.MAILER_SERVICE_PROVIDER || 'MAILER_SERVICE_PROVIDER',
auth: {
user: process.env.MAILER_EMAIL_ID || 'MAILER_EMAIL_ID',
pass: process.env.MAILER_PASSWORD || 'MAILER_PASSWORD'
}
}
},
livereload: true
};

View File

@@ -7,17 +7,17 @@
/* For example:
module.exports = {
db: {
uri: 'mongodb://localhost/local-dev',
options: {
user: '',
pass: ''
}
},
facebook: {
clientID: process.env.FACEBOOK_ID || 'APP_ID',
clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET',
callbackURL: '/auth/facebook/callback'
}
db: {
uri: 'mongodb://localhost/local-dev',
options: {
user: '',
pass: ''
}
},
facebook: {
clientID: process.env.FACEBOOK_ID || 'APP_ID',
clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET',
callbackURL: '/auth/facebook/callback'
}
};
*/
*/

View File

@@ -1,73 +1,65 @@
'use strict';
module.exports = {
db: {
uri: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://' + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + '/mean',
options: {
user: '',
pass: ''
}
},
log: {
// Can specify one of 'combined', 'common', 'dev', 'short', 'tiny'
format: 'combined',
// Stream defaults to process.stdout
// Uncomment to enable logging to a log on the file system
options: {
stream: 'access.log'
}
},
assets: {
lib: {
css: [
'public/lib/bootstrap/dist/css/bootstrap.min.css',
'public/lib/bootstrap/dist/css/bootstrap-theme.min.css',
],
js: [
'public/lib/angular/angular.min.js',
'public/lib/angular-resource/angular-resource.min.js',
'public/lib/angular-animate/angular-animate.min.js',
'public/lib/angular-ui-router/release/angular-ui-router.min.js',
'public/lib/angular-ui-utils/ui-utils.min.js',
'public/lib/angular-bootstrap/ui-bootstrap-tpls.min.js'
]
},
css: 'public/dist/application.min.css',
js: 'public/dist/application.min.js'
},
facebook: {
clientID: process.env.FACEBOOK_ID || 'APP_ID',
clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET',
callbackURL: '/auth/facebook/callback'
},
twitter: {
clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY',
clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET',
callbackURL: '/auth/twitter/callback'
},
google: {
clientID: process.env.GOOGLE_ID || 'APP_ID',
clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET',
callbackURL: '/auth/google/callback'
},
linkedin: {
clientID: process.env.LINKEDIN_ID || 'APP_ID',
clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET',
callbackURL: '/auth/linkedin/callback'
},
github: {
clientID: process.env.GITHUB_ID || 'APP_ID',
clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET',
callbackURL: '/auth/github/callback'
},
mailer: {
from: process.env.MAILER_FROM || 'MAILER_FROM',
options: {
service: process.env.MAILER_SERVICE_PROVIDER || 'MAILER_SERVICE_PROVIDER',
auth: {
user: process.env.MAILER_EMAIL_ID || 'MAILER_EMAIL_ID',
pass: process.env.MAILER_PASSWORD || 'MAILER_PASSWORD'
}
}
}
secure: true,
port: process.env.PORT || 8443,
db: {
uri: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://' + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + '/mean',
options: {
user: '',
pass: ''
},
// Enable mongoose debug mode
debug: process.env.MONGODB_DEBUG || false
},
log: {
// Can specify one of 'combined', 'common', 'dev', 'short', 'tiny'
format: 'combined',
// Stream defaults to process.stdout
// Uncomment to enable logging to a log on the file system
options: {
stream: 'access.log'
}
},
facebook: {
clientID: process.env.FACEBOOK_ID || 'APP_ID',
clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET',
callbackURL: '/api/auth/facebook/callback'
},
twitter: {
clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY',
clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET',
callbackURL: '/api/auth/twitter/callback'
},
google: {
clientID: process.env.GOOGLE_ID || 'APP_ID',
clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET',
callbackURL: '/api/auth/google/callback'
},
linkedin: {
clientID: process.env.LINKEDIN_ID || 'APP_ID',
clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET',
callbackURL: '/api/auth/linkedin/callback'
},
github: {
clientID: process.env.GITHUB_ID || 'APP_ID',
clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET',
callbackURL: '/api/auth/github/callback'
},
paypal: {
clientID: process.env.PAYPAL_ID || 'CLIENT_ID',
clientSecret: process.env.PAYPAL_SECRET || 'CLIENT_SECRET',
callbackURL: '/api/auth/paypal/callback',
sandbox: false
},
mailer: {
from: process.env.MAILER_FROM || 'MAILER_FROM',
options: {
service: process.env.MAILER_SERVICE_PROVIDER || 'MAILER_SERVICE_PROVIDER',
auth: {
user: process.env.MAILER_EMAIL_ID || 'MAILER_EMAIL_ID',
pass: process.env.MAILER_PASSWORD || 'MAILER_PASSWORD'
}
}
}
};

74
config/env/secure.js vendored
View File

@@ -1,74 +0,0 @@
'use strict';
module.exports = {
port: 8443,
db: {
uri: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://localhost/mean',
options: {
user: '',
pass: ''
}
},
log: {
// Can specify one of 'combined', 'common', 'dev', 'short', 'tiny'
format: 'combined',
// Stream defaults to process.stdout
// Uncomment to enable logging to a log on the file system
options: {
stream: 'access.log'
}
},
assets: {
lib: {
css: [
'public/lib/bootstrap/dist/css/bootstrap.min.css',
'public/lib/bootstrap/dist/css/bootstrap-theme.min.css',
],
js: [
'public/lib/angular/angular.min.js',
'public/lib/angular-resource/angular-resource.min.js',
'public/lib/angular-animate/angular-animate.min.js',
'public/lib/angular-ui-router/release/angular-ui-router.min.js',
'public/lib/angular-ui-utils/ui-utils.min.js',
'public/lib/angular-bootstrap/ui-bootstrap-tpls.min.js'
]
},
css: 'public/dist/application.min.css',
js: 'public/dist/application.min.js'
},
facebook: {
clientID: process.env.FACEBOOK_ID || 'APP_ID',
clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET',
callbackURL: 'https://localhost:443/auth/facebook/callback'
},
twitter: {
clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY',
clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET',
callbackURL: 'https://localhost:443/auth/twitter/callback'
},
google: {
clientID: process.env.GOOGLE_ID || 'APP_ID',
clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET',
callbackURL: 'https://localhost:443/auth/google/callback'
},
linkedin: {
clientID: process.env.LINKEDIN_ID || 'APP_ID',
clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET',
callbackURL: 'https://localhost:443/auth/linkedin/callback'
},
github: {
clientID: process.env.GITHUB_ID || 'APP_ID',
clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET',
callbackURL: 'https://localhost:443/auth/github/callback'
},
mailer: {
from: process.env.MAILER_FROM || 'MAILER_FROM',
options: {
service: process.env.MAILER_SERVICE_PROVIDER || 'MAILER_SERVICE_PROVIDER',
auth: {
user: process.env.MAILER_EMAIL_ID || 'MAILER_EMAIL_ID',
pass: process.env.MAILER_PASSWORD || 'MAILER_PASSWORD'
}
}
}
};

111
config/env/test.js vendored
View File

@@ -1,59 +1,60 @@
'use strict';
var defaultEnvConfig = require('./default');
module.exports = {
db: {
uri: 'mongodb://localhost/mean-test',
options: {
user: '',
pass: ''
}
},
port: 3001,
log: {
// Can specify one of 'combined', 'common', 'dev', 'short', 'tiny'
format: 'dev',
// Stream defaults to process.stdout
// Uncomment to enable logging to a log on the file system
options: {
//stream: 'access.log'
}
},
app: {
title: 'MEAN.JS - Test Environment'
},
facebook: {
clientID: process.env.FACEBOOK_ID || 'APP_ID',
clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET',
callbackURL: '/auth/facebook/callback'
},
twitter: {
clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY',
clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET',
callbackURL: '/auth/twitter/callback'
},
google: {
clientID: process.env.GOOGLE_ID || 'APP_ID',
clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET',
callbackURL: '/auth/google/callback'
},
linkedin: {
clientID: process.env.LINKEDIN_ID || 'APP_ID',
clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET',
callbackURL: '/auth/linkedin/callback'
},
github: {
clientID: process.env.GITHUB_ID || 'APP_ID',
clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET',
callbackURL: '/auth/github/callback'
},
mailer: {
from: process.env.MAILER_FROM || 'MAILER_FROM',
options: {
service: process.env.MAILER_SERVICE_PROVIDER || 'MAILER_SERVICE_PROVIDER',
auth: {
user: process.env.MAILER_EMAIL_ID || 'MAILER_EMAIL_ID',
pass: process.env.MAILER_PASSWORD || 'MAILER_PASSWORD'
}
}
}
db: {
uri: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://' + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + '/mean-test',
options: {
user: '',
pass: ''
},
// Enable mongoose debug mode
debug: process.env.MONGODB_DEBUG || false
},
port: process.env.PORT || 3001,
app: {
title: defaultEnvConfig.app.title + ' - Test Environment'
},
facebook: {
clientID: process.env.FACEBOOK_ID || 'APP_ID',
clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET',
callbackURL: '/api/auth/facebook/callback'
},
twitter: {
clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY',
clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET',
callbackURL: '/api/auth/twitter/callback'
},
google: {
clientID: process.env.GOOGLE_ID || 'APP_ID',
clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET',
callbackURL: '/api/auth/google/callback'
},
linkedin: {
clientID: process.env.LINKEDIN_ID || 'APP_ID',
clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET',
callbackURL: '/api/auth/linkedin/callback'
},
github: {
clientID: process.env.GITHUB_ID || 'APP_ID',
clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET',
callbackURL: '/api/auth/github/callback'
},
paypal: {
clientID: process.env.PAYPAL_ID || 'CLIENT_ID',
clientSecret: process.env.PAYPAL_SECRET || 'CLIENT_SECRET',
callbackURL: '/api/auth/paypal/callback',
sandbox: true
},
mailer: {
from: process.env.MAILER_FROM || 'MAILER_FROM',
options: {
service: process.env.MAILER_SERVICE_PROVIDER || 'MAILER_SERVICE_PROVIDER',
auth: {
user: process.env.MAILER_EMAIL_ID || 'MAILER_EMAIL_ID',
pass: process.env.MAILER_PASSWORD || 'MAILER_PASSWORD'
}
}
}
};

View File

@@ -1,165 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var fs = require('fs'),
http = require('http'),
https = require('https'),
express = require('express'),
morgan = require('morgan'),
logger = require('./logger'),
bodyParser = require('body-parser'),
session = require('express-session'),
compression = require('compression'),
methodOverride = require('method-override'),
cookieParser = require('cookie-parser'),
helmet = require('helmet'),
passport = require('passport'),
mongoStore = require('connect-mongo')({
session: session
}),
flash = require('connect-flash'),
config = require('./config'),
consolidate = require('consolidate'),
path = require('path');
module.exports = function(db) {
// Initialize express app
var app = express();
// Globbing model files
config.getGlobbedFiles('./app/models/**/*.js').forEach(function(modelPath) {
require(path.resolve(modelPath));
});
// Setting application local variables
app.locals.title = config.app.title;
app.locals.description = config.app.description;
app.locals.keywords = config.app.keywords;
app.locals.facebookAppId = config.facebook.clientID;
app.locals.jsFiles = config.getJavaScriptAssets();
app.locals.cssFiles = config.getCSSAssets();
// Passing the request url to environment locals
app.use(function(req, res, next) {
res.locals.url = req.protocol + '://' + req.headers.host + req.url;
next();
});
// Should be placed before express.static
app.use(compression({
// only compress files for the following content types
filter: function(req, res) {
return (/json|text|javascript|css/).test(res.getHeader('Content-Type'));
},
// zlib option for compression level
level: 3
}));
// Showing stack errors
app.set('showStackError', true);
// Set swig as the template engine
app.engine('server.view.html', consolidate[config.templateEngine]);
// Set views path and view engine
app.set('view engine', 'server.view.html');
app.set('views', './app/views');
// Enable logger (morgan)
app.use(morgan(logger.getLogFormat(), logger.getLogOptions()));
// Environment dependent middleware
if (process.env.NODE_ENV === 'development') {
// Disable views cache
app.set('view cache', false);
} else if (process.env.NODE_ENV === 'production') {
app.locals.cache = 'memory';
}
// Request body parsing middleware should be above methodOverride
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(bodyParser.json());
app.use(methodOverride());
// Use helmet to secure Express headers
app.use(helmet.xframe());
app.use(helmet.xssFilter());
app.use(helmet.nosniff());
app.use(helmet.ienoopen());
app.disable('x-powered-by');
// Setting the app router and static folder
app.use(express.static(path.resolve('./public')));
// CookieParser should be above session
app.use(cookieParser());
// Express MongoDB session storage
app.use(session({
saveUninitialized: true,
resave: true,
secret: config.sessionSecret,
store: new mongoStore({
db: db.connection.db,
collection: config.sessionCollection
}),
cookie: config.sessionCookie,
name: config.sessionName
}));
// use passport session
app.use(passport.initialize());
app.use(passport.session());
// connect flash for flash messages
app.use(flash());
// Globbing routing files
config.getGlobbedFiles('./app/routes/**/*.js').forEach(function(routePath) {
require(path.resolve(routePath))(app);
});
// Assume 'not found' in the error msgs is a 404. this is somewhat silly, but valid, you can do whatever you like, set properties, use instanceof etc.
app.use(function(err, req, res, next) {
// If the error object doesn't exists
if (!err) return next();
// Log it
console.error(err.stack);
// Error page
res.status(500).render('500', {
error: err.stack
});
});
// Assume 404 since no middleware responded
app.use(function(req, res) {
res.status(404).render('404', {
url: req.originalUrl,
error: 'Not Found'
});
});
if (process.env.NODE_ENV === 'secure') {
// Load SSL key and certificate
var privateKey = fs.readFileSync('./config/sslcerts/key.pem', 'utf8');
var certificate = fs.readFileSync('./config/sslcerts/cert.pem', 'utf8');
// Create HTTPS Server
var httpsServer = https.createServer({
key: privateKey,
cert: certificate
}, app);
// Return HTTPS server instance
return httpsServer;
}
// Return Express server instance
return app;
};

View File

@@ -1,31 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var glob = require('glob'),
chalk = require('chalk');
/**
* Module init function.
*/
module.exports = function() {
/**
* Before we begin, lets set the environment variable
* We'll Look for a valid NODE_ENV variable and if one cannot be found load the development NODE_ENV
*/
glob('./config/env/' + process.env.NODE_ENV + '.js', {
sync: true
}, function(err, environmentFiles) {
if (!environmentFiles.length) {
if (process.env.NODE_ENV) {
console.error(chalk.red('No configuration file found for "' + process.env.NODE_ENV + '" environment using development instead'));
} else {
console.error(chalk.red('NODE_ENV is not defined! Using default development environment'));
}
process.env.NODE_ENV = 'development';
}
});
};

52
config/lib/app.js Normal file
View File

@@ -0,0 +1,52 @@
'use strict';
/**
* Module dependencies.
*/
var config = require('../config'),
mongoose = require('./mongoose'),
express = require('./express'),
chalk = require('chalk');
// Initialize Models
mongoose.loadModels();
module.exports.loadModels = function loadModels() {
mongoose.loadModels();
};
module.exports.init = function init(callback) {
mongoose.connect(function (db) {
// Initialize express
var app = express.init(db);
if (callback) callback(app, db, config);
});
};
module.exports.start = function start(callback) {
var _this = this;
_this.init(function(app, db, config) {
// Start the app by listening on <port>
app.listen(config.port, function() {
// Logging initialization
console.log('--');
console.log(chalk.green(config.app.title));
console.log(chalk.green('Environment:\t\t\t' + process.env.NODE_ENV));
console.log(chalk.green('Port:\t\t\t\t' + config.port));
console.log(chalk.green('Database:\t\t\t\t' + config.db.uri));
if (process.env.NODE_ENV === 'secure') {
console.log(chalk.green('HTTPs:\t\t\t\ton'));
}
console.log('--');
if (callback) callback(app, db, config);
});
});
};

249
config/lib/express.js Normal file
View File

@@ -0,0 +1,249 @@
'use strict';
/**
* Module dependencies.
*/
var config = require('../config'),
express = require('express'),
morgan = require('morgan'),
bodyParser = require('body-parser'),
session = require('express-session'),
MongoStore = require('connect-mongo')(session),
multer = require('multer'),
favicon = require('serve-favicon'),
compress = require('compression'),
methodOverride = require('method-override'),
cookieParser = require('cookie-parser'),
helmet = require('helmet'),
flash = require('connect-flash'),
consolidate = require('consolidate'),
path = require('path');
/**
* Initialize local variables
*/
module.exports.initLocalVariables = function (app) {
// Setting application local variables
app.locals.title = config.app.title;
app.locals.description = config.app.description;
app.locals.secure = config.secure;
app.locals.keywords = config.app.keywords;
app.locals.googleAnalyticsTrackingID = config.app.googleAnalyticsTrackingID;
app.locals.facebookAppId = config.facebook.clientID;
app.locals.jsFiles = config.files.client.js;
app.locals.cssFiles = config.files.client.css;
app.locals.livereload = config.livereload;
app.locals.logo = config.logo;
app.locals.favicon = config.favicon;
// Passing the request url to environment locals
app.use(function (req, res, next) {
res.locals.host = req.protocol + '://' + req.hostname;
res.locals.url = req.protocol + '://' + req.headers.host + req.originalUrl;
next();
});
};
/**
* Initialize application middleware
*/
module.exports.initMiddleware = function (app) {
// Showing stack errors
app.set('showStackError', true);
// Enable jsonp
app.enable('jsonp callback');
// Should be placed before express.static
app.use(compress({
filter: function (req, res) {
return (/json|text|javascript|css|font|svg/).test(res.getHeader('Content-Type'));
},
level: 9
}));
// Initialize favicon middleware
app.use(favicon('./modules/core/client/img/brand/favicon.ico'));
// Environment dependent middleware
if (process.env.NODE_ENV === 'development') {
// Enable logger (morgan)
app.use(morgan('dev'));
// Disable views cache
app.set('view cache', false);
} else if (process.env.NODE_ENV === 'production') {
app.locals.cache = 'memory';
}
// Request body parsing middleware should be above methodOverride
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(bodyParser.json());
app.use(methodOverride());
// Add the cookie parser and flash middleware
app.use(cookieParser());
app.use(flash());
// Add multipart handling middleware
app.use(multer({
dest: './uploads/',
inMemory: true
}));
};
/**
* Configure view engine
*/
module.exports.initViewEngine = function (app) {
// Set swig as the template engine
app.engine('server.view.html', consolidate[config.templateEngine]);
// Set views path and view engine
app.set('view engine', 'server.view.html');
app.set('views', './');
};
/**
* Configure Express session
*/
module.exports.initSession = function (app, db) {
// Express MongoDB session storage
app.use(session({
saveUninitialized: true,
resave: true,
secret: config.sessionSecret,
store: new MongoStore({
mongooseConnection: db.connection,
collection: config.sessionCollection
})
}));
};
/**
* Invoke modules server configuration
*/
module.exports.initModulesConfiguration = function (app, db) {
config.files.server.configs.forEach(function (configPath) {
require(path.resolve(configPath))(app, db);
});
};
/**
* Configure Helmet headers configuration
*/
module.exports.initHelmetHeaders = function (app) {
// Use helmet to secure Express headers
app.use(helmet.xframe());
app.use(helmet.xssFilter());
app.use(helmet.nosniff());
app.use(helmet.ienoopen());
app.disable('x-powered-by');
};
/**
* Configure the modules static routes
*/
module.exports.initModulesClientRoutes = function (app) {
// Setting the app router and static folder
app.use('/', express.static(path.resolve('./public')));
// Globbing static routing
config.folders.client.forEach(function (staticPath) {
app.use(staticPath.replace('/client', ''), express.static(path.resolve('./' + staticPath)));
});
};
/**
* Configure the modules ACL policies
*/
module.exports.initModulesServerPolicies = function (app) {
// Globbing policy files
config.files.server.policies.forEach(function (policyPath) {
require(path.resolve(policyPath)).invokeRolesPolicies();
});
};
/**
* Configure the modules server routes
*/
module.exports.initModulesServerRoutes = function (app) {
// Globbing routing files
config.files.server.routes.forEach(function (routePath) {
require(path.resolve(routePath))(app);
});
};
/**
* Configure error handling
*/
module.exports.initErrorRoutes = function (app) {
app.use(function (err, req, res, next) {
// If the error object doesn't exists
if (!err) {
return next();
}
// Log it
console.error(err.stack);
// Redirect to error page
res.redirect('/server-error');
});
};
/**
* Configure Socket.io
*/
module.exports.configureSocketIO = function (app, db) {
// Load the Socket.io configuration
var server = require('./socket.io')(app, db);
// Return server object
return server;
};
/**
* Initialize the Express application
*/
module.exports.init = function (db) {
// Initialize express app
var app = express();
// Initialize local variables
this.initLocalVariables(app);
// Initialize Express middleware
this.initMiddleware(app);
// Initialize Express view engine
this.initViewEngine(app);
// Initialize Express session
this.initSession(app, db);
// Initialize Modules configuration
this.initModulesConfiguration(app);
// Initialize Helmet security headers
this.initHelmetHeaders(app);
// Initialize modules static client routes
this.initModulesClientRoutes(app);
// Initialize modules server authorization policies
this.initModulesServerPolicies(app);
// Initialize modules server routes
this.initModulesServerRoutes(app);
// Initialize error routes
this.initErrorRoutes(app);
// Configure Socket.io
app = this.configureSocketIO(app, db);
return app;
};

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

@@ -0,0 +1,44 @@
'use strict';
/**
* Module dependencies.
*/
var config = require('../config'),
chalk = require('chalk'),
path = require('path'),
mongoose = require('mongoose');
// Load the mongoose models
module.exports.loadModels = function () {
// Globbing model files
config.files.server.models.forEach(function (modelPath) {
require(path.resolve(modelPath));
});
};
// Initialize Mongoose
module.exports.connect = function (cb) {
var _this = this;
var db = mongoose.connect(config.db.uri, config.db.options, function (err) {
// Log Error
if (err) {
console.error(chalk.red('Could not connect to MongoDB!'));
console.log(err);
} else {
// Enabling mongoose debug mode if required
mongoose.set('debug', config.db.debug);
// Call callback FN
if (cb) cb(db);
}
});
};
module.exports.disconnect = function (cb) {
mongoose.disconnect(function (err) {
console.info(chalk.yellow('Disconnected from MongoDB.'));
cb(err);
});
};

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

@@ -0,0 +1,76 @@
'use strict';
// Load the module dependencies
var config = require('../config'),
path = require('path'),
fs = require('fs'),
http = require('http'),
https = require('https'),
cookieParser = require('cookie-parser'),
passport = require('passport'),
socketio = require('socket.io'),
session = require('express-session'),
MongoStore = require('connect-mongo')(session);
// Define the Socket.io configuration method
module.exports = function (app, db) {
var server;
if (config.secure === true) {
// Load SSL key and certificate
var privateKey = fs.readFileSync('./config/sslcerts/key.pem', 'utf8');
var certificate = fs.readFileSync('./config/sslcerts/cert.pem', 'utf8');
var options = {
key: privateKey,
cert: certificate
};
// Create new HTTPS Server
server = https.createServer(options, app);
} else {
// Create a new HTTP server
server = http.createServer(app);
}
// Create a new Socket.io server
var io = socketio.listen(server);
// Create a MongoDB storage object
var mongoStore = new MongoStore({
mongooseConnection: db.connection,
collection: config.sessionCollection
});
// Intercept Socket.io's handshake request
io.use(function (socket, next) {
// Use the 'cookie-parser' module to parse the request cookies
cookieParser(config.sessionSecret)(socket.request, {}, function (err) {
// Get the session id from the request cookies
var sessionId = socket.request.signedCookies['connect.sid'];
// Use the mongoStorage instance to get the Express session information
mongoStore.get(sessionId, function (err, session) {
// Set the Socket.io session information
socket.request.session = session;
// Use Passport to populate the user details
passport.initialize()(socket.request, {}, function () {
passport.session()(socket.request, {}, function () {
if (socket.request.user) {
next(null, true);
} else {
next(new Error('User is not authenticated'), false);
}
});
});
});
});
});
// Add an event listener to the 'connection' event
io.on('connection', function (socket) {
config.files.server.sockets.forEach(function (socketConfiguration) {
require(path.resolve(socketConfiguration))(io, socket);
});
});
return server;
};

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,33 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var passport = require('passport'),
User = require('mongoose').model('User'),
path = require('path'),
config = require('./config');
/**
* Module init function.
*/
module.exports = function() {
// Serialize sessions
passport.serializeUser(function(user, done) {
done(null, user.id);
});
// Deserialize sessions
passport.deserializeUser(function(id, done) {
User.findOne({
_id: id
}, '-salt -password', function(err, user) {
done(err, user);
});
});
// Initialize strategies
config.getGlobbedFiles('./config/strategies/**/*.js').forEach(function(strategy) {
require(path.resolve(strategy))();
});
};

View File

@@ -1,41 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var passport = require('passport'),
FacebookStrategy = require('passport-facebook').Strategy,
config = require('../config'),
users = require('../../app/controllers/users.server.controller');
module.exports = function() {
// Use facebook strategy
passport.use(new FacebookStrategy({
clientID: config.facebook.clientID,
clientSecret: config.facebook.clientSecret,
callbackURL: config.facebook.callbackURL,
passReqToCallback: true
},
function(req, accessToken, refreshToken, profile, done) {
// Set the provider data and include tokens
var providerData = profile._json;
providerData.accessToken = accessToken;
providerData.refreshToken = refreshToken;
// Create the user OAuth profile
var providerUserProfile = {
firstName: profile.name.givenName,
lastName: profile.name.familyName,
displayName: profile.displayName,
email: profile.emails[0].value,
username: profile.username,
provider: 'facebook',
providerIdentifierField: 'id',
providerData: providerData
};
// Save the user OAuth profile
users.saveOAuthUserProfile(req, providerUserProfile, done);
}
));
};

View File

@@ -1,46 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var passport = require('passport'),
GithubStrategy = require('passport-github').Strategy,
config = require('../config'),
users = require('../../app/controllers/users.server.controller');
module.exports = function() {
// Use github strategy
passport.use(new GithubStrategy({
clientID: config.github.clientID,
clientSecret: config.github.clientSecret,
callbackURL: config.github.callbackURL,
passReqToCallback: true
},
function(req, accessToken, refreshToken, profile, done) {
// Set the provider data and include tokens
var providerData = profile._json;
providerData.accessToken = accessToken;
providerData.refreshToken = refreshToken;
// Create the user OAuth profile
var displayName = profile.displayName.trim();
var iSpace = displayName.indexOf(' '); // index of the whitespace following the firstName
var firstName = iSpace !== -1 ? displayName.substring(0, iSpace) : displayName;
var lastName = iSpace !== -1 ? displayName.substring(iSpace + 1) : '';
var providerUserProfile = {
firstName: firstName,
lastName: lastName,
displayName: displayName,
email: profile.emails[0].value,
username: profile.username,
provider: 'github',
providerIdentifierField: 'id',
providerData: providerData
};
// Save the user OAuth profile
users.saveOAuthUserProfile(req, providerUserProfile, done);
}
));
};

View File

@@ -1,41 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var passport = require('passport'),
GoogleStrategy = require('passport-google-oauth').OAuth2Strategy,
config = require('../config'),
users = require('../../app/controllers/users.server.controller');
module.exports = function() {
// Use google strategy
passport.use(new GoogleStrategy({
clientID: config.google.clientID,
clientSecret: config.google.clientSecret,
callbackURL: config.google.callbackURL,
passReqToCallback: true
},
function(req, accessToken, refreshToken, profile, done) {
// Set the provider data and include tokens
var providerData = profile._json;
providerData.accessToken = accessToken;
providerData.refreshToken = refreshToken;
// Create the user OAuth profile
var providerUserProfile = {
firstName: profile.name.givenName,
lastName: profile.name.familyName,
displayName: profile.displayName,
email: profile.emails[0].value,
username: profile.username,
provider: 'google',
providerIdentifierField: 'id',
providerData: providerData
};
// Save the user OAuth profile
users.saveOAuthUserProfile(req, providerUserProfile, done);
}
));
};

View File

@@ -1,42 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var passport = require('passport'),
LinkedInStrategy = require('passport-linkedin').Strategy,
config = require('../config'),
users = require('../../app/controllers/users.server.controller');
module.exports = function() {
// Use linkedin strategy
passport.use(new LinkedInStrategy({
consumerKey: config.linkedin.clientID,
consumerSecret: config.linkedin.clientSecret,
callbackURL: config.linkedin.callbackURL,
passReqToCallback: true,
profileFields: ['id', 'first-name', 'last-name', 'email-address']
},
function(req, accessToken, refreshToken, profile, done) {
// Set the provider data and include tokens
var providerData = profile._json;
providerData.accessToken = accessToken;
providerData.refreshToken = refreshToken;
// Create the user OAuth profile
var providerUserProfile = {
firstName: profile.name.givenName,
lastName: profile.name.familyName,
displayName: profile.displayName,
email: profile.emails[0].value,
username: profile.username,
provider: 'linkedin',
providerIdentifierField: 'id',
providerData: providerData
};
// Save the user OAuth profile
users.saveOAuthUserProfile(req, providerUserProfile, done);
}
));
};

View File

@@ -1,38 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var passport = require('passport'),
LocalStrategy = require('passport-local').Strategy,
User = require('mongoose').model('User');
module.exports = function() {
// Use local strategy
passport.use(new LocalStrategy({
usernameField: 'username',
passwordField: 'password'
},
function(username, password, done) {
User.findOne({
username: username
}, function(err, user) {
if (err) {
return done(err);
}
if (!user) {
return done(null, false, {
message: 'Unknown user or invalid password'
});
}
if (!user.authenticate(password)) {
return done(null, false, {
message: 'Unknown user or invalid password'
});
}
return done(null, user);
});
}
));
};

View File

@@ -1,45 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var passport = require('passport'),
TwitterStrategy = require('passport-twitter').Strategy,
config = require('../config'),
users = require('../../app/controllers/users.server.controller');
module.exports = function() {
// Use twitter strategy
passport.use(new TwitterStrategy({
consumerKey: config.twitter.clientID,
consumerSecret: config.twitter.clientSecret,
callbackURL: config.twitter.callbackURL,
passReqToCallback: true
},
function(req, token, tokenSecret, profile, done) {
// Set the provider data and include tokens
var providerData = profile._json;
providerData.token = token;
providerData.tokenSecret = tokenSecret;
// Create the user OAuth profile
var displayName = profile.displayName.trim();
var iSpace = displayName.indexOf(' '); // index of the whitespace following the firstName
var firstName = iSpace !== -1 ? displayName.substring(0, iSpace) : displayName;
var lastName = iSpace !== -1 ? displayName.substring(iSpace + 1) : '';
var providerUserProfile = {
firstName: firstName,
lastName: lastName,
displayName: displayName,
username: profile.username,
provider: 'twitter',
providerIdentifierField: 'id_str',
providerData: providerData
};
// Save the user OAuth profile
users.saveOAuthUserProfile(req, providerUserProfile, done);
}
));
};

View File

@@ -1,194 +1,266 @@
'use strict';
var fs = require('fs');
/**
* Module dependencies.
*/
var _ = require('lodash'),
defaultAssets = require('./config/assets/default'),
testAssets = require('./config/assets/test'),
fs = require('fs'),
path = require('path');
module.exports = function(grunt) {
// Unified Watch Object
var watchFiles = {
serverViews: ['app/views/**/*.*'],
serverJS: ['gruntfile.js', 'server.js', 'config/**/*.js', 'app/**/*.js', '!app/tests/'],
clientViews: ['public/modules/**/views/**/*.html'],
clientJS: ['public/js/*.js', 'public/modules/**/*.js'],
clientCSS: ['public/modules/**/*.css'],
mochaTests: ['app/tests/**/*.js']
};
module.exports = function (grunt) {
// Project Configuration
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
env: {
test: {
NODE_ENV: 'test'
},
dev: {
NODE_ENV: 'development'
},
prod: {
NODE_ENV: 'production'
}
},
watch: {
serverViews: {
files: defaultAssets.server.views,
options: {
livereload: true
}
},
serverJS: {
files: _.union(defaultAssets.server.gruntConfig, defaultAssets.server.allJS),
tasks: ['jshint'],
options: {
livereload: true
}
},
clientViews: {
files: defaultAssets.client.views,
options: {
livereload: true
}
},
clientJS: {
files: defaultAssets.client.js,
tasks: ['jshint'],
options: {
livereload: true
}
},
clientCSS: {
files: defaultAssets.client.css,
tasks: ['csslint'],
options: {
livereload: true
}
},
clientSCSS: {
files: defaultAssets.client.sass,
tasks: ['sass', 'csslint'],
options: {
livereload: true
}
},
clientLESS: {
files: defaultAssets.client.less,
tasks: ['less', 'csslint'],
options: {
livereload: true
}
}
},
nodemon: {
dev: {
script: 'server.js',
options: {
nodeArgs: ['--debug'],
ext: 'js,html',
watch: _.union(defaultAssets.server.gruntConfig, defaultAssets.server.views, defaultAssets.server.allJS, defaultAssets.server.config)
}
}
},
concurrent: {
default: ['nodemon', 'watch'],
debug: ['nodemon', 'watch', 'node-inspector'],
options: {
logConcurrentOutput: true
}
},
jshint: {
all: {
src: _.union(defaultAssets.server.gruntConfig, defaultAssets.server.allJS, defaultAssets.client.js, testAssets.tests.server, testAssets.tests.client, testAssets.tests.e2e),
options: {
jshintrc: true,
node: true,
mocha: true,
jasmine: true
}
}
},
csslint: {
options: {
csslintrc: '.csslintrc'
},
all: {
src: defaultAssets.client.css
}
},
ngAnnotate: {
production: {
files: {
'public/dist/application.js': defaultAssets.client.js
}
}
},
uglify: {
production: {
options: {
mangle: false
},
files: {
'public/dist/application.min.js': 'public/dist/application.js'
}
}
},
cssmin: {
combine: {
files: {
'public/dist/application.min.css': defaultAssets.client.css
}
}
},
sass: {
dist: {
files: [{
expand: true,
src: defaultAssets.client.sass,
ext: '.css',
rename: function (base, src) {
return src.replace('/scss/', '/css/');
}
}]
}
},
less: {
dist: {
files: [{
expand: true,
src: defaultAssets.client.less,
ext: '.css',
rename: function (base, src) {
return src.replace('/less/', '/css/');
}
}]
}
},
'node-inspector': {
custom: {
options: {
'web-port': 1337,
'web-host': 'localhost',
'debug-port': 5858,
'save-live-edit': true,
'no-preload': true,
'stack-trace-limit': 50,
'hidden': []
}
}
},
mochaTest: {
src: testAssets.tests.server,
options: {
reporter: 'spec'
}
},
karma: {
unit: {
configFile: 'karma.conf.js'
}
},
protractor: {
options: {
configFile: 'protractor.conf.js',
keepAlive: true,
noColor: false
},
e2e: {
options: {
args: {} // Target-specific arguments
}
}
},
copy: {
localConfig: {
src: 'config/env/local.example.js',
dest: 'config/env/local.js',
filter: function () {
return !fs.existsSync('config/env/local.js');
}
}
}
});
// Project Configuration
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
watch: {
serverViews: {
files: watchFiles.serverViews,
options: {
livereload: true
}
},
serverJS: {
files: watchFiles.serverJS,
tasks: ['jshint'],
options: {
livereload: true
}
},
clientViews: {
files: watchFiles.clientViews,
options: {
livereload: true
}
},
clientJS: {
files: watchFiles.clientJS,
tasks: ['jshint'],
options: {
livereload: true
}
},
clientCSS: {
files: watchFiles.clientCSS,
tasks: ['csslint'],
options: {
livereload: true
}
},
mochaTests: {
files: watchFiles.mochaTests,
tasks: ['test:server'],
}
},
jshint: {
all: {
src: watchFiles.clientJS.concat(watchFiles.serverJS),
options: {
jshintrc: true
}
}
},
csslint: {
options: {
csslintrc: '.csslintrc'
},
all: {
src: watchFiles.clientCSS
}
},
uglify: {
production: {
options: {
mangle: false
},
files: {
'public/dist/application.min.js': 'public/dist/application.js'
}
}
},
cssmin: {
combine: {
files: {
'public/dist/application.min.css': '<%= applicationCSSFiles %>'
}
}
},
nodemon: {
dev: {
script: 'server.js',
options: {
nodeArgs: ['--debug'],
ext: 'js,html',
watch: watchFiles.serverViews.concat(watchFiles.serverJS)
}
}
},
'node-inspector': {
custom: {
options: {
'web-port': 1337,
'web-host': 'localhost',
'debug-port': 5858,
'save-live-edit': true,
'no-preload': true,
'stack-trace-limit': 50,
'hidden': []
}
}
},
ngAnnotate: {
production: {
files: {
'public/dist/application.js': '<%= applicationJavaScriptFiles %>'
}
}
},
concurrent: {
default: ['nodemon', 'watch'],
debug: ['nodemon', 'watch', 'node-inspector'],
options: {
logConcurrentOutput: true,
limit: 10
}
},
env: {
test: {
NODE_ENV: 'test'
},
secure: {
NODE_ENV: 'secure'
}
},
mochaTest: {
src: watchFiles.mochaTests,
options: {
reporter: 'spec',
require: 'server.js'
}
},
karma: {
unit: {
configFile: 'karma.conf.js'
}
},
copy: {
localConfig: {
src: 'config/env/local.example.js',
dest: 'config/env/local.js',
filter: function() {
return !fs.existsSync('config/env/local.js');
}
}
}
});
// Load NPM tasks
require('load-grunt-tasks')(grunt);
// Load NPM tasks
require('load-grunt-tasks')(grunt);
// Making grunt default to force in order not to break the project.
grunt.option('force', true);
// Making grunt default to force in order not to break the project.
grunt.option('force', true);
// Make sure upload directory exists
grunt.task.registerTask('mkdir:upload', 'Task that makes sure upload directory exists.', function () {
// Get the callback
var done = this.async();
// A Task for loading the configuration object
grunt.task.registerTask('loadConfig', 'Task that loads the config into a grunt option.', function() {
var init = require('./config/init')();
var config = require('./config/config');
grunt.file.mkdir(path.normalize(__dirname + '/modules/users/client/img/profile/uploads'));
grunt.config.set('applicationJavaScriptFiles', config.assets.js);
grunt.config.set('applicationCSSFiles', config.assets.css);
});
done();
});
// Default task(s).
grunt.registerTask('default', ['lint', 'copy:localConfig', 'concurrent:default']);
// 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();
// Debug task.
grunt.registerTask('debug', ['lint', 'copy:localConfig', 'concurrent:debug']);
// Use mongoose configuration
var mongoose = require('./config/lib/mongoose.js');
// Secure task(s).
grunt.registerTask('secure', ['env:secure', 'lint', 'copy:localConfig', 'concurrent:default']);
// Connect to database
mongoose.connect(function (db) {
done();
});
});
// Lint task(s).
grunt.registerTask('lint', ['jshint', 'csslint']);
grunt.task.registerTask('server', 'Starting the server', function () {
// Get the callback
var done = this.async();
// Build task(s).
grunt.registerTask('build', ['lint', 'loadConfig', 'ngAnnotate', 'uglify', 'cssmin']);
var path = require('path');
var app = require(path.resolve('./config/lib/app'));
var server = app.start(function () {
done();
});
});
// Test task.
grunt.registerTask('test', ['copy:localConfig', 'test:server', 'test:client']);
grunt.registerTask('test:server', ['env:test', 'mochaTest']);
grunt.registerTask('test:client', ['env:test', 'karma:unit']);
// Lint CSS and JavaScript files.
grunt.registerTask('lint', ['sass', 'less', 'jshint', 'csslint']);
// Lint project files and minify them into two production files.
grunt.registerTask('build', ['env:dev', 'lint', 'ngAnnotate', 'uglify', 'cssmin']);
// Run the project tests
grunt.registerTask('test', ['env:test', 'lint', 'mkdir:upload', 'copy:localConfig', 'server', 'mochaTest', 'karma:unit']);
grunt.registerTask('test:server', ['env:test', 'lint', 'server', 'mochaTest']);
grunt.registerTask('test:client', ['env:test', 'lint', 'server', 'karma:unit']);
// Run the project in development mode
grunt.registerTask('default', ['env:dev', 'lint', 'mkdir:upload', 'copy:localConfig', 'concurrent:default']);
// Run the project in debug mode
grunt.registerTask('debug', ['env:dev', 'lint', 'mkdir:upload', 'copy:localConfig', 'concurrent:debug']);
// Run the project in production mode
grunt.registerTask('prod', ['build', 'env:prod', 'mkdir:upload', 'copy:localConfig', 'concurrent:default']);
};

195
gulpfile.js Normal file
View File

@@ -0,0 +1,195 @@
'use strict';
/**
* Module dependencies.
*/
var _ = require('lodash'),
defaultAssets = require('./config/assets/default'),
testAssets = require('./config/assets/test'),
gulp = require('gulp'),
gulpLoadPlugins = require('gulp-load-plugins'),
runSequence = require('run-sequence'),
plugins = gulpLoadPlugins(),
path = require('path');
// Set NODE_ENV to 'test'
gulp.task('env:test', function () {
process.env.NODE_ENV = 'test';
});
// Set NODE_ENV to 'development'
gulp.task('env:dev', function () {
process.env.NODE_ENV = 'development';
});
// Set NODE_ENV to 'production'
gulp.task('env:prod', function () {
process.env.NODE_ENV = 'production';
});
// Nodemon task
gulp.task('nodemon', function () {
return plugins.nodemon({
script: 'server.js',
nodeArgs: ['--debug'],
ext: 'js,html',
watch: _.union(defaultAssets.server.views, defaultAssets.server.allJS, defaultAssets.server.config)
});
});
// Watch Files For Changes
gulp.task('watch', function() {
// Start livereload
plugins.livereload.listen();
// Add watch rules
gulp.watch(defaultAssets.server.gulpConfig, ['jshint']);
gulp.watch(defaultAssets.server.views).on('change', plugins.livereload.changed);
gulp.watch(defaultAssets.server.allJS, ['jshint']).on('change', plugins.livereload.changed);
gulp.watch(defaultAssets.client.views).on('change', plugins.livereload.changed);
gulp.watch(defaultAssets.client.js, ['jshint']).on('change', plugins.livereload.changed);
gulp.watch(defaultAssets.client.css, ['csslint']).on('change', plugins.livereload.changed);
gulp.watch(defaultAssets.client.sass, ['sass', 'csslint']).on('change', plugins.livereload.changed);
gulp.watch(defaultAssets.client.less, ['less', 'csslint']).on('change', plugins.livereload.changed);
});
// CSS linting task
gulp.task('csslint', function (done) {
return gulp.src(defaultAssets.client.css)
.pipe(plugins.csslint('.csslintrc'))
.pipe(plugins.csslint.reporter())
.pipe(plugins.csslint.reporter(function (file) {
if (!file.csslint.errorCount) {
done();
}
}));
});
// JS linting task
gulp.task('jshint', function () {
return gulp.src(_.union(defaultAssets.server.gulpConfig, defaultAssets.server.allJS, defaultAssets.client.js, testAssets.tests.server, testAssets.tests.client, testAssets.tests.e2e))
.pipe(plugins.jshint())
.pipe(plugins.jshint.reporter('default'))
.pipe(plugins.jshint.reporter('fail'));
});
// JS minifying task
gulp.task('uglify', function () {
return gulp.src(defaultAssets.client.js)
.pipe(plugins.ngAnnotate())
.pipe(plugins.uglify({
mangle: false
}))
.pipe(plugins.concat('application.min.js'))
.pipe(gulp.dest('public/dist'));
});
// CSS minifying task
gulp.task('cssmin', function () {
return gulp.src(defaultAssets.client.css)
.pipe(plugins.cssmin())
.pipe(plugins.concat('application.min.css'))
.pipe(gulp.dest('public/dist'));
});
// Sass task
gulp.task('sass', function () {
return gulp.src(defaultAssets.client.sass)
.pipe(plugins.sass())
.pipe(plugins.rename(function (file) {
file.dirname = file.dirname.replace(path.sep + 'scss', path.sep + 'css');
}))
.pipe(gulp.dest('./modules/'));
});
// Less task
gulp.task('less', function () {
return gulp.src(defaultAssets.client.less)
.pipe(plugins.less())
.pipe(plugins.rename(function (file) {
file.dirname = file.dirname.replace(path.sep + 'less', path.sep + 'css');
}))
.pipe(gulp.dest('./modules/'));
});
// Mocha tests task
gulp.task('mocha', function (done) {
// Open mongoose connections
var mongoose = require('./config/lib/mongoose.js');
var error;
// Connect mongoose
mongoose.connect(function() {
// Run the tests
gulp.src(testAssets.tests.server)
.pipe(plugins.mocha({
reporter: 'spec'
}))
.on('error', function (err) {
// If an error occurs, save it
error = err;
})
.on('end', function() {
// When the tests are done, disconnect mongoose and pass the error state back to gulp
mongoose.disconnect(function() {
done(error);
});
});
});
});
// Karma test runner task
gulp.task('karma', function (done) {
return gulp.src([])
.pipe(plugins.karma({
configFile: 'karma.conf.js',
action: 'run',
singleRun: true
}));
});
// Selenium standalone WebDriver update task
gulp.task('webdriver-update', plugins.protractor.webdriver_update);
// Protractor test runner task
gulp.task('protractor', function () {
gulp.src([])
.pipe(plugins.protractor.protractor({
configFile: 'protractor.conf.js'
}))
.on('error', function (e) {
throw e;
});
});
// Lint CSS and JavaScript files.
gulp.task('lint', function(done) {
runSequence('less', 'sass', ['csslint', 'jshint'], done);
});
// Lint project files and minify them into two production files.
gulp.task('build', function(done) {
runSequence('env:dev' ,'lint', ['uglify', 'cssmin'], done);
});
// Run the project tests
gulp.task('test', function(done) {
runSequence('env:test', ['karma', 'mocha'], done);
});
// Run the project in development mode
gulp.task('default', function(done) {
runSequence('env:dev', 'lint', ['nodemon', 'watch'], done);
});
// Run the project in debug mode
gulp.task('debug', function(done) {
runSequence('env:dev', 'lint', ['nodemon', 'watch'], done);
});
// Run the project in production mode
gulp.task('prod', function(done) {
runSequence('build', 'lint', ['nodemon', 'watch'], done);
});

View File

@@ -3,49 +3,63 @@
/**
* Module dependencies.
*/
var applicationConfiguration = require('./config/config');
var _ = require('lodash'),
defaultAssets = require('./config/assets/default'),
testAssets = require('./config/assets/test');
// Karma configuration
module.exports = function(config) {
config.set({
// Frameworks to use
frameworks: ['jasmine'],
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),
preprocessors: {
'modules/*/client/views/**/*.html': ['ng-html2js']
},
// Test results reporter to use
// Possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
reporters: ['progress'],
ngHtml2JsPreprocessor: {
moduleName: 'mean',
// Web server port
port: 9876,
cacheIdFromPath: function (filepath) {
return filepath.replace('/client', '');
},
},
// Enable / disable colors in the output (reporters and logs)
colors: true,
// List of files / patterns to load in the browser
files: _.union(defaultAssets.client.lib.js, defaultAssets.client.lib.tests, defaultAssets.client.js, testAssets.tests.client, defaultAssets.client.views),
// Level of logging
// Possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// Test results reporter to use
// Possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
reporters: ['progress'],
// Enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// Web server port
port: 9876,
// Start these browsers, currently available:
// - Chrome
// - ChromeCanary
// - Firefox
// - Opera
// - Safari (only Mac)
// - PhantomJS
// - IE (only Windows)
browsers: ['PhantomJS'],
// Enable / disable colors in the output (reporters and logs)
colors: true,
// If browser does not capture in given timeout [ms], kill it
captureTimeout: 60000,
// Level of logging
// Possible values: karmaConfig.LOG_DISABLE || karmaConfig.LOG_ERROR || karmaConfig.LOG_WARN || karmaConfig.LOG_INFO || karmaConfig.LOG_DEBUG
logLevel: karmaConfig.LOG_INFO,
// Continuous Integration mode
// If true, it capture browsers, run tests and exit
singleRun: true
});
// Enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// Start these browsers, currently available:
// - Chrome
// - ChromeCanary
// - Firefox
// - Opera
// - Safari (only Mac)
// - PhantomJS
// - IE (only Windows)
browsers: ['PhantomJS'],
// If browser does not capture in given timeout [ms], kill it
captureTimeout: 60000,
// Continuous Integration mode
// If true, it capture browsers, run tests and exit
singleRun: true
});
};

View File

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

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

@@ -0,0 +1,33 @@
'use strict';
// Setting up route
angular.module('articles').config(['$stateProvider',
function ($stateProvider) {
// Articles state routing
$stateProvider
.state('articles', {
abstract: true,
url: '/articles',
template: '<ui-view/>',
data: {
roles: ['user', 'admin']
}
})
.state('articles.list', {
url: '',
templateUrl: 'modules/articles/views/list-articles.client.view.html'
})
.state('articles.create', {
url: '/create',
templateUrl: 'modules/articles/views/create-article.client.view.html'
})
.state('articles.view', {
url: '/:articleId',
templateUrl: 'modules/articles/views/view-article.client.view.html'
})
.state('articles.edit', {
url: '/:articleId/edit',
templateUrl: 'modules/articles/views/edit-article.client.view.html'
});
}
]);

View File

@@ -0,0 +1,68 @@
'use strict';
// Articles controller
angular.module('articles').controller('ArticlesController', ['$scope', '$stateParams', '$location', 'Authentication', 'Articles',
function ($scope, $stateParams, $location, Authentication, Articles) {
$scope.authentication = Authentication;
// Create new Article
$scope.create = function () {
// Create new Article object
var article = new Articles({
title: this.title,
content: this.content
});
// Redirect after save
article.$save(function (response) {
$location.path('articles/' + response._id);
// Clear form fields
$scope.title = '';
$scope.content = '';
}, function (errorResponse) {
$scope.error = errorResponse.data.message;
});
};
// Remove existing Article
$scope.remove = function (article) {
if (article) {
article.$remove();
for (var i in $scope.articles) {
if ($scope.articles[i] === article) {
$scope.articles.splice(i, 1);
}
}
} else {
$scope.article.$remove(function () {
$location.path('articles');
});
}
};
// Update existing Article
$scope.update = function () {
var article = $scope.article;
article.$update(function () {
$location.path('articles/' + article._id);
}, function (errorResponse) {
$scope.error = errorResponse.data.message;
});
};
// Find a list of Articles
$scope.find = function () {
$scope.articles = Articles.query();
};
// Find existing Article
$scope.findOne = function () {
$scope.article = Articles.get({
articleId: $stateParams.articleId
});
};
}
]);

View File

@@ -0,0 +1,14 @@
'use strict';
//Articles service used for communicating with the articles REST endpoints
angular.module('articles').factory('Articles', ['$resource',
function ($resource) {
return $resource('api/articles/:articleId', {
articleId: '@_id'
}, {
update: {
method: 'PUT'
}
});
}
]);

View File

@@ -0,0 +1,29 @@
<section data-ng-controller="ArticlesController">
<div class="page-header">
<h1>New Article</h1>
</div>
<div class="col-md-12">
<form name="articleForm" class="form-horizontal" data-ng-submit="create()" novalidate>
<fieldset>
<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">
</div>
</div>
<div class="form-group">
<label class="control-label" for="content">Content</label>
<div class="controls">
<textarea name="content" data-ng-model="content" id="content" class="form-control" cols="30" rows="10" placeholder="Content"></textarea>
</div>
</div>
<div class="form-group">
<input type="submit" class="btn btn-default">
</div>
<div data-ng-show="error" class="text-danger">
<strong data-ng-bind="error"></strong>
</div>
</fieldset>
</form>
</div>
</section>

View File

@@ -0,0 +1,29 @@
<section data-ng-controller="ArticlesController" data-ng-init="findOne()">
<div class="page-header">
<h1>Edit Article</h1>
</div>
<div class="col-md-12">
<form name="articleForm" class="form-horizontal" data-ng-submit="update()" novalidate>
<fieldset>
<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>
<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"></textarea>
</div>
</div>
<div class="form-group">
<input type="submit" value="Update" class="btn btn-default">
</div>
<div data-ng-show="error" class="text-danger">
<strong data-ng-bind="error"></strong>
</div>
</fieldset>
</form>
</div>
</section>

View File

@@ -0,0 +1,20 @@
<section data-ng-controller="ArticlesController" data-ng-init="find()">
<div class="page-header">
<h1>Articles</h1>
</div>
<div class="list-group">
<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>
by
<span data-ng-bind="article.user.displayName"></span>
</small>
<h4 class="list-group-item-heading" data-ng-bind="article.title"></h4>
<p class="list-group-item-text" data-ng-bind="article.content"></p>
</a>
</div>
<div class="alert alert-warning text-center" data-ng-if="articles.$resolved && !articles.length">
No articles yet, why don't you <a data-ui-sref="articles.create">create one</a>?
</div>
</section>

View File

@@ -0,0 +1,22 @@
<section data-ng-controller="ArticlesController" data-ng-init="findOne()">
<div class="page-header">
<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" data-ui-sref="articles.edit({articleId: article._id})">
<i class="glyphicon glyphicon-edit"></i>
</a>
<a class="btn btn-primary" data-ng-click="remove();">
<i class="glyphicon glyphicon-trash"></i>
</a>
</div>
<small>
<em class="text-muted">
Posted on
<span data-ng-bind="article.created | date:'mediumDate'"></span>
by
<span data-ng-bind="article.user.displayName"></span>
</em>
</small>
<p class="lead" data-ng-bind="article.content"></p>
</section>

View File

@@ -0,0 +1,110 @@
'use strict';
/**
* Module dependencies.
*/
var path = require('path'),
mongoose = require('mongoose'),
Article = mongoose.model('Article'),
errorHandler = require(path.resolve('./modules/core/server/controllers/errors.server.controller'));
/**
* Create a article
*/
exports.create = function (req, res) {
var article = new Article(req.body);
article.user = req.user;
article.save(function (err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(article);
}
});
};
/**
* Show the current article
*/
exports.read = function (req, res) {
res.json(req.article);
};
/**
* Update a article
*/
exports.update = function (req, res) {
var article = req.article;
article.title = req.body.title;
article.content = req.body.content;
article.save(function (err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(article);
}
});
};
/**
* Delete an article
*/
exports.delete = function (req, res) {
var article = req.article;
article.remove(function (err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(article);
}
});
};
/**
* List of Articles
*/
exports.list = function (req, res) {
Article.find().sort('-created').populate('user', 'displayName').exec(function (err, articles) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(articles);
}
});
};
/**
* Article middleware
*/
exports.articleByID = function (req, res, next, id) {
if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(400).send({
message: 'Article is invalid'
});
}
Article.findById(id).populate('user', 'displayName').exec(function (err, article) {
if (err) {
return next(err);
} else if (!article) {
return res.status(404).send({
message: 'No article with that identifier has been found'
});
}
req.article = article;
next();
});
};

View File

@@ -0,0 +1,34 @@
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
/**
* Article Schema
*/
var ArticleSchema = new Schema({
created: {
type: Date,
default: Date.now
},
title: {
type: String,
default: '',
trim: true,
required: 'Title cannot be blank'
},
content: {
type: String,
default: '',
trim: true
},
user: {
type: Schema.ObjectId,
ref: 'User'
}
});
mongoose.model('Article', ArticleSchema);

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

@@ -0,0 +1,210 @@
'use strict';
(function () {
// Articles Controller Spec
describe('Articles Controller Tests', function () {
// Initialize global variables
var ArticlesController,
scope,
$httpBackend,
$stateParams,
$location,
Authentication,
Articles,
mockArticle;
// The $resource service augments the response object with methods for updating and deleting the resource.
// If we were to use the standard toEqual matcher, our tests would fail because the test values would not match
// the responses exactly. To solve the problem, we define a new toEqualData Jasmine matcher.
// When the toEqualData matcher compares two objects, it takes only object properties into
// account and ignores methods.
beforeEach(function () {
jasmine.addMatchers({
toEqualData: function (util, customEqualityTesters) {
return {
compare: function (actual, expected) {
return {
pass: angular.equals(actual, expected)
};
}
};
}
});
});
// Then we can start by loading the main application module
beforeEach(module(ApplicationConfiguration.applicationModuleName));
// The injector ignores leading and trailing underscores here (i.e. _$httpBackend_).
// This allows us to inject a service but then attach it to a variable
// with the same name as the service.
beforeEach(inject(function ($controller, $rootScope, _$location_, _$stateParams_, _$httpBackend_, _Authentication_, _Articles_) {
// Set a new global scope
scope = $rootScope.$new();
// Point global variables to injected services
$stateParams = _$stateParams_;
$httpBackend = _$httpBackend_;
$location = _$location_;
Authentication = _Authentication_;
Articles = _Articles_;
// create mock article
mockArticle = new Articles({
_id: '525a8422f6d0f87f0e407a33',
title: 'An Article about MEAN',
content: 'MEAN rocks!'
});
// Mock logged in user
Authentication.user = {
roles: ['user']
};
// Initialize the Articles controller.
ArticlesController = $controller('ArticlesController', {
$scope: scope
});
}));
it('$scope.find() should create an array with at least one article object fetched from XHR', inject(function (Articles) {
// Create a sample articles array that includes the new article
var sampleArticles = [mockArticle];
// Set GET response
$httpBackend.expectGET('api/articles').respond(sampleArticles);
// Run controller functionality
scope.find();
$httpBackend.flush();
// Test scope value
expect(scope.articles).toEqualData(sampleArticles);
}));
it('$scope.findOne() should create an array with one article object fetched from XHR using a articleId URL parameter', inject(function (Articles) {
// Set the URL parameter
$stateParams.articleId = mockArticle._id;
// Set GET response
$httpBackend.expectGET(/api\/articles\/([0-9a-fA-F]{24})$/).respond(mockArticle);
// Run controller functionality
scope.findOne();
$httpBackend.flush();
// Test scope value
expect(scope.article).toEqualData(mockArticle);
}));
describe('$scope.craete()', function () {
var sampleArticlePostData;
beforeEach(function () {
// Create a sample article object
sampleArticlePostData = new Articles({
title: 'An Article about MEAN',
content: 'MEAN rocks!'
});
// Fixture mock form input values
scope.title = 'An Article about MEAN';
scope.content = 'MEAN rocks!';
spyOn($location, 'path');
});
it('should send a POST request with the form input values and then locate to new object URL', inject(function (Articles) {
// Set POST response
$httpBackend.expectPOST('api/articles', sampleArticlePostData).respond(mockArticle);
// Run controller functionality
scope.create();
$httpBackend.flush();
// Test form inputs are reset
expect(scope.title).toEqual('');
expect(scope.content).toEqual('');
// Test URL redirection after the article was created
expect($location.path.calls.mostRecent().args[0]).toBe('articles/' + mockArticle._id);
}));
it('should set scope.error if save error', function () {
var errorMessage = 'this is an error message';
$httpBackend.expectPOST('api/articles', sampleArticlePostData).respond(400, {
message: errorMessage
});
scope.create();
$httpBackend.flush();
expect(scope.error).toBe(errorMessage);
});
});
describe('$scope.update()', function () {
beforeEach(function () {
// Mock article in scope
scope.article = mockArticle;
});
it('should update a valid article', inject(function (Articles) {
// Set PUT response
$httpBackend.expectPUT(/api\/articles\/([0-9a-fA-F]{24})$/).respond();
// Run controller functionality
scope.update();
$httpBackend.flush();
// Test URL location to new object
expect($location.path()).toBe('/articles/' + mockArticle._id);
}));
it('should set scope.error to error response message', inject(function (Articles) {
var errorMessage = 'error';
$httpBackend.expectPUT(/api\/articles\/([0-9a-fA-F]{24})$/).respond(400, {
message: errorMessage
});
scope.update();
$httpBackend.flush();
expect(scope.error).toBe(errorMessage);
}));
});
describe('$scope.remove(article)', function () {
beforeEach(function () {
// Create new articles array and include the article
scope.articles = [mockArticle, {}];
// Set expected DELETE response
$httpBackend.expectDELETE(/api\/articles\/([0-9a-fA-F]{24})$/).respond(204);
// Run controller functionality
scope.remove(mockArticle);
});
it('should send a DELETE request with a valid articleId and remove the article from the scope', inject(function (Articles) {
expect(scope.articles.length).toBe(1);
}));
});
describe('scope.remove()', function () {
beforeEach(function () {
spyOn($location, 'path');
scope.article = mockArticle;
$httpBackend.expectDELETE(/api\/articles\/([0-9a-fA-F]{24})$/).respond(204);
scope.remove();
$httpBackend.flush();
});
it('should redirect to articles', function () {
expect($location.path).toHaveBeenCalledWith('articles');
});
});
});
}());

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

@@ -0,0 +1,64 @@
'use strict';
/**
* Module dependencies.
*/
var should = require('should'),
mongoose = require('mongoose'),
User = mongoose.model('User'),
Article = mongoose.model('Article');
/**
* Globals
*/
var user, article;
/**
* Unit tests
*/
describe('Article Model Unit Tests:', function () {
beforeEach(function (done) {
user = new User({
firstName: 'Full',
lastName: 'Name',
displayName: 'Full Name',
email: 'test@test.com',
username: 'username',
password: 'password'
});
user.save(function () {
article = new Article({
title: 'Article Title',
content: 'Article Content',
user: user
});
done();
});
});
describe('Method Save', function () {
it('should be able to save without problems', function (done) {
return article.save(function (err) {
should.not.exist(err);
done();
});
});
it('should be able to show an error when try to save without title', function (done) {
article.title = '';
return article.save(function (err) {
should.exist(err);
done();
});
});
});
afterEach(function (done) {
Article.remove().exec(function () {
User.remove().exec(done);
});
});
});

View File

@@ -0,0 +1,320 @@
'use strict';
var should = require('should'),
request = require('supertest'),
path = require('path'),
mongoose = require('mongoose'),
User = mongoose.model('User'),
Article = mongoose.model('Article'),
express = require(path.resolve('./config/lib/express'));
/**
* Globals
*/
var app, agent, credentials, user, article;
/**
* Article routes tests
*/
describe('Article CRUD tests', function () {
before(function (done) {
// Get application
app = express.init(mongoose);
agent = request.agent(app);
done();
});
beforeEach(function (done) {
// Create user credentials
credentials = {
username: 'username',
password: 'password'
};
// Create a new user
user = new User({
firstName: 'Full',
lastName: 'Name',
displayName: 'Full Name',
email: 'test@test.com',
username: credentials.username,
password: credentials.password,
provider: 'local'
});
// Save a user to the test db and create new article
user.save(function () {
article = {
title: 'Article Title',
content: 'Article Content'
};
done();
});
});
it('should be able to save an article if logged in', function (done) {
agent.post('/api/auth/signin')
.send(credentials)
.expect(200)
.end(function (signinErr, signinRes) {
// Handle signin error
if (signinErr) {
return done(signinErr);
}
// Get the userId
var userId = user.id;
// Save a new article
agent.post('/api/articles')
.send(article)
.expect(200)
.end(function (articleSaveErr, articleSaveRes) {
// Handle article save error
if (articleSaveErr) {
return done(articleSaveErr);
}
// Get a list of articles
agent.get('/api/articles')
.end(function (articlesGetErr, articlesGetRes) {
// Handle article save error
if (articlesGetErr) {
return done(articlesGetErr);
}
// Get articles list
var articles = articlesGetRes.body;
// Set assertions
(articles[0].user._id).should.equal(userId);
(articles[0].title).should.match('Article Title');
// Call the assertion callback
done();
});
});
});
});
it('should not be able to save an article if not logged in', function (done) {
agent.post('/api/articles')
.send(article)
.expect(403)
.end(function (articleSaveErr, articleSaveRes) {
// Call the assertion callback
done(articleSaveErr);
});
});
it('should not be able to save an article if no title is provided', function (done) {
// Invalidate title field
article.title = '';
agent.post('/api/auth/signin')
.send(credentials)
.expect(200)
.end(function (signinErr, signinRes) {
// Handle signin error
if (signinErr) {
return done(signinErr);
}
// Get the userId
var userId = user.id;
// Save a new article
agent.post('/api/articles')
.send(article)
.expect(400)
.end(function (articleSaveErr, articleSaveRes) {
// Set message assertion
(articleSaveRes.body.message).should.match('Title cannot be blank');
// Handle article save error
done(articleSaveErr);
});
});
});
it('should be able to update an article if signed in', function (done) {
agent.post('/api/auth/signin')
.send(credentials)
.expect(200)
.end(function (signinErr, signinRes) {
// Handle signin error
if (signinErr) {
return done(signinErr);
}
// Get the userId
var userId = user.id;
// Save a new article
agent.post('/api/articles')
.send(article)
.expect(200)
.end(function (articleSaveErr, articleSaveRes) {
// Handle article save error
if (articleSaveErr) {
return done(articleSaveErr);
}
// Update article title
article.title = 'WHY YOU GOTTA BE SO MEAN?';
// Update an existing article
agent.put('/api/articles/' + articleSaveRes.body._id)
.send(article)
.expect(200)
.end(function (articleUpdateErr, articleUpdateRes) {
// Handle article update error
if (articleUpdateErr) {
return done(articleUpdateErr);
}
// Set assertions
(articleUpdateRes.body._id).should.equal(articleSaveRes.body._id);
(articleUpdateRes.body.title).should.match('WHY YOU GOTTA BE SO MEAN?');
// Call the assertion callback
done();
});
});
});
});
it('should be able to get a list of articles if not signed in', function (done) {
// Create new article model instance
var articleObj = new Article(article);
// Save the article
articleObj.save(function () {
// Request articles
request(app).get('/api/articles')
.end(function (req, res) {
// Set assertion
res.body.should.be.instanceof(Array).and.have.lengthOf(1);
// Call the assertion callback
done();
});
});
});
it('should be able to get a single article if not signed in', function (done) {
// Create new article model instance
var articleObj = new Article(article);
// Save the article
articleObj.save(function () {
request(app).get('/api/articles/' + articleObj._id)
.end(function (req, res) {
// Set assertion
res.body.should.be.instanceof(Object).and.have.property('title', article.title);
// Call the assertion callback
done();
});
});
});
it('should return proper error for single article with an invalid Id, if not signed in', function (done) {
// test is not a valid mongoose Id
request(app).get('/api/articles/test')
.end(function (req, res) {
// Set assertion
res.body.should.be.instanceof(Object).and.have.property('message', 'Article is invalid');
// Call the assertion callback
done();
});
});
it('should return proper error for single article which doesnt exist, if not signed in', function (done) {
// This is a valid mongoose Id but a non-existent article
request(app).get('/api/articles/559e9cd815f80b4c256a8f41')
.end(function (req, res) {
// Set assertion
res.body.should.be.instanceof(Object).and.have.property('message', 'No article with that identifier has been found');
// Call the assertion callback
done();
});
});
it('should be able to delete an article if signed in', function (done) {
agent.post('/api/auth/signin')
.send(credentials)
.expect(200)
.end(function (signinErr, signinRes) {
// Handle signin error
if (signinErr) {
return done(signinErr);
}
// Get the userId
var userId = user.id;
// Save a new article
agent.post('/api/articles')
.send(article)
.expect(200)
.end(function (articleSaveErr, articleSaveRes) {
// Handle article save error
if (articleSaveErr) {
return done(articleSaveErr);
}
// Delete an existing article
agent.delete('/api/articles/' + articleSaveRes.body._id)
.send(article)
.expect(200)
.end(function (articleDeleteErr, articleDeleteRes) {
// Handle article error error
if (articleDeleteErr) {
return done(articleDeleteErr);
}
// Set assertions
(articleDeleteRes.body._id).should.equal(articleSaveRes.body._id);
// Call the assertion callback
done();
});
});
});
});
it('should not be able to delete an article if not signed in', function (done) {
// Set article user
article.user = user;
// Create new article model instance
var articleObj = new Article(article);
// Save the article
articleObj.save(function () {
// Try deleting article
request(app).delete('/api/articles/' + articleObj._id)
.expect(403)
.end(function (articleDeleteErr, articleDeleteRes) {
// Set message assertion
(articleDeleteRes.body.message).should.match('User is not authorized');
// Handle article error error
done(articleDeleteErr);
});
});
});
afterEach(function (done) {
User.remove().exec(function () {
Article.remove().exec(done);
});
});
});

View File

@@ -0,0 +1,4 @@
'use strict';
// Use Applicaion configuration module to register a new module
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,15 @@
'use strict';
// Configure the 'chat' module routes
angular.module('chat').config(['$stateProvider',
function ($stateProvider) {
$stateProvider
.state('chat', {
url: '/chat',
templateUrl: 'modules/chat/views/chat.client.view.html',
data: {
roles: ['user', 'admin']
}
});
}
]);

View File

@@ -0,0 +1,43 @@
'use strict';
// Create the 'chat' controller
angular.module('chat').controller('ChatController', ['$scope', '$location', 'Authentication', 'Socket',
function ($scope, $location, Authentication, Socket) {
// Create a messages array
$scope.messages = [];
// If user is not signed in then redirect back home
if (!Authentication.user) {
$location.path('/');
}
// Make sure the Socket is connected
if (!Socket.socket) {
Socket.connect();
}
// Add an event listener to the 'chatMessage' event
Socket.on('chatMessage', function (message) {
$scope.messages.unshift(message);
});
// Create a controller method for sending messages
$scope.sendMessage = function () {
// Create a new message object
var message = {
text: this.messageText
};
// Emit a 'chatMessage' message event
Socket.emit('chatMessage', message);
// Clear the message text
this.messageText = '';
};
// Remove the event listener when the controller instance is destroyed
$scope.$on('$destroy', function () {
Socket.removeListener('chatMessage');
});
}
]);

View File

@@ -0,0 +1,15 @@
.chat-message {
margin-top: 10px;
padding-top: 10px;
}
.chat-message:not(:first-child) {
border-top: 1px solid #e7e7e7;
}
.chat-message-details {
margin-left: 10px;
}
.chat-profile-image {
height: 28px;
width: 28px;
border-radius: 50%;
}

View File

@@ -0,0 +1,29 @@
<!-- 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,94 @@
'use strict';
/**
* Chat client controller tests
*/
(function () {
describe('ChatController', function () {
//Initialize global variables
var scope,
Socket,
ChatController,
$timeout,
$location,
Authentication;
// Load the main application module
beforeEach(module(ApplicationConfiguration.applicationModuleName));
beforeEach(inject(function ($controller, $rootScope, _Socket_, _Authentication_, _$timeout_, _$location_) {
scope = $rootScope.$new();
Socket = _Socket_;
$timeout = _$timeout_;
$location = _$location_;
Authentication = _Authentication_;
}));
describe('when user logged out', function () {
beforeEach(inject(function ($controller, $rootScope, _Socket_, _Authentication_, _$timeout_, _$location_) {
Authentication.user = undefined;
spyOn($location, 'path');
ChatController = $controller('ChatController', {
$scope: scope,
});
}));
it('should redirect logged out user to /', function () {
expect($location.path).toHaveBeenCalledWith('/');
});
});
describe('when user logged in', function () {
beforeEach(inject(function ($controller, $rootScope, _Socket_, _Authentication_, _$timeout_, _$location_) {
Authentication.user = {
name: 'user',
roles: ['user']
};
ChatController = $controller('ChatController', {
$scope: scope,
});
}));
it('should make sure socket is connected', function () {
expect(Socket.socket).toBeTruthy();
});
it('should define messages array', function () {
expect(scope.messages).toBeDefined();
expect(scope.messages.length).toBe(0);
});
describe('sendMessage', function () {
var text = 'hello world!';
beforeEach(function () {
scope.messageText = text;
scope.sendMessage();
$timeout.flush();
});
it('should add message to messages', function () {
expect(scope.messages.length).toBe(1);
});
it('should add message with proper text attribute set', function () {
expect(scope.messages[0].text).toBe(text);
});
it('should clear messageText', function () {
expect(scope.messageText).toBe('');
});
});
describe('$destroy()', function () {
beforeEach(function () {
scope.$destroy();
});
it('should remove chatMessage listener', function () {
expect(Socket.socket.cbs.chatMessage).toBeUndefined();
});
});
});
});
}());

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

@@ -0,0 +1,23 @@
'use strict';
// Init the application configuration module for AngularJS application
var ApplicationConfiguration = (function () {
// Init module configuration options
var applicationModuleName = 'mean';
var applicationModuleVendorDependencies = ['ngResource', 'ngAnimate', 'ui.router', 'ui.bootstrap', 'ui.utils', 'angularFileUpload'];
// Add a new vertical module
var registerModule = function (moduleName, dependencies) {
// Create angular module
angular.module(moduleName, dependencies || []);
// Add the module to the AngularJS configuration file
angular.module(applicationModuleName).requires.push(moduleName);
};
return {
applicationModuleName: applicationModuleName,
applicationModuleVendorDependencies: applicationModuleVendorDependencies,
registerModule: registerModule
};
})();

View File

@@ -0,0 +1,55 @@
'use strict';
//Start by defining the main module and adding the module dependencies
angular.module(ApplicationConfiguration.applicationModuleName, ApplicationConfiguration.applicationModuleVendorDependencies);
// Setting HTML5 Location Mode
angular.module(ApplicationConfiguration.applicationModuleName).config(['$locationProvider',
function ($locationProvider) {
$locationProvider.html5Mode(true).hashPrefix('!');
}
]);
angular.module(ApplicationConfiguration.applicationModuleName).run(function ($rootScope, $state, Authentication) {
// Check authentication before changing state
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
if (toState.data && toState.data.roles && toState.data.roles.length > 0) {
var allowed = false;
toState.data.roles.forEach(function (role) {
if (Authentication.user.roles !== undefined && Authentication.user.roles.indexOf(role) !== -1) {
allowed = true;
return true;
}
});
if (!allowed) {
event.preventDefault();
$state.go('authentication.signin', {}, {
notify: false
}).then(function () {
$rootScope.$broadcast('$stateChangeSuccess', 'authentication.signin', {}, toState, toParams);
});
}
}
});
// Record previous state
$rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) {
$state.previous = {
state: fromState,
params: fromParams,
href: $state.href(fromState, fromParams)
};
});
});
//Then define the init function for starting up the application
angular.element(document).ready(function () {
//Fixing facebook bug with redirect
if (window.location.hash === '#_=_') {
window.location.hash = '#!';
}
//Then init the app
angular.bootstrap(document, [ApplicationConfiguration.applicationModuleName]);
});

View File

@@ -0,0 +1,12 @@
'use strict';
angular.module('core.admin').run(['Menus',
function (Menus) {
Menus.addMenuItem('topbar', {
title: 'Admin',
state: 'admin',
type: 'dropdown',
roles: ['admin']
});
}
]);

View File

@@ -0,0 +1,16 @@
'use strict';
// Setting up route
angular.module('core.admin.routes').config(['$stateProvider',
function ($stateProvider) {
$stateProvider
.state('admin', {
abstract: true,
url: '/admin',
template: '<ui-view/>',
data: {
roles: ['admin']
}
});
}
]);

View File

@@ -0,0 +1,21 @@
'use strict';
// Setting up route
angular.module('core').config(['$stateProvider', '$urlRouterProvider',
function ($stateProvider, $urlRouterProvider) {
// Redirect to 404 when route not found
$urlRouterProvider.otherwise('not-found');
// Home state routing
$stateProvider
.state('home', {
url: '/',
templateUrl: 'modules/core/views/home.client.view.html'
})
.state('not-found', {
url: '/not-found',
templateUrl: 'modules/core/views/404.client.view.html'
});
}
]);

View File

@@ -0,0 +1,23 @@
'use strict';
angular.module('core').controller('HeaderController', ['$scope', '$state', 'Authentication', 'Menus',
function ($scope, $state, Authentication, Menus) {
// Expose view variables
$scope.$state = $state;
$scope.authentication = Authentication;
// Get the topbar menu
$scope.menu = Menus.getMenu('topbar');
// Toggle the menu items
$scope.isCollapsed = false;
$scope.toggleCollapsibleMenu = function () {
$scope.isCollapsed = !$scope.isCollapsed;
};
// Collapsing the menu after navigation
$scope.$on('$stateChangeSuccess', function () {
$scope.isCollapsed = false;
});
}
]);

View File

@@ -0,0 +1,8 @@
'use strict';
angular.module('core').controller('HomeController', ['$scope', 'Authentication',
function ($scope, Authentication) {
// This provides Authentication context.
$scope.authentication = Authentication;
}
]);

View File

@@ -0,0 +1,6 @@
'use strict';
// Use Applicaion configuration module to register a new module
ApplicationConfiguration.registerModule('core');
ApplicationConfiguration.registerModule('core.admin', ['core']);
ApplicationConfiguration.registerModule('core.admin.routes', ['ui.router']);

View File

@@ -0,0 +1,35 @@
.content {
margin-top: 50px;
}
.undecorated-link:hover {
text-decoration: none;
}
[ng\:cloak],
[ng-cloak],
[data-ng-cloak],
[x-ng-cloak],
.ng-cloak,
.x-ng-cloak {
display: none !important;
}
.ng-invalid.ng-dirty {
border-color: #FA787E;
}
.ng-valid.ng-dirty {
border-color: #78FA89;
}
.header-profile-image {
opacity: 0.8;
height: 28px;
width: 28px;
border-radius: 50%;
margin-right: 5px;
}
.open .header-profile-image,
a:hover .header-profile-image {
opacity: 1;
}
.user-header-dropdown-toggle {
padding-top: 11px !important;
padding-bottom: 11px !important;
}

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

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