Imported MEAN stack files

This commit is contained in:
Sahat Yalkabov
2013-11-13 12:32:22 -05:00
parent 276673cc47
commit d4b651a5c8
68 changed files with 1819 additions and 3 deletions

1
Procfile Executable file
View File

@ -0,0 +1 @@
web: ./node_modules/.bin/forever -m 5 server.js

159
README.md Normal file → Executable file
View File

@ -1,4 +1,157 @@
angular-quickstart # MEAN Stack
==================
hybdrid between mean stack and angular seed and BBB MEAN is a boilerplate that provides a nice 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. It is designed to give you quick and organized way to start developing of MEAN based web apps with useful modules like mongoose and passport pre-bundled and configured. We mainly try to take care of the connection points between existing popular frameworks and solve common integration problems.
## Prerequisites
* Node.js - Download and Install [Node.js](http://www.nodejs.org/download/). You can also follow [this gist](https://gist.github.com/isaacs/579814) for a quick and easy way to install Node.js and npm
* MongoDB - Download and Install [MongoDB](http://www.mongodb.org/downloads) - Make sure it's running on the default port (27017).
### Tools Prerequisites
* NPM - Node.js package manager, should be installed when you install node.js.
* Bower - Web package manager, installing [Bower](http://bower.io/) is simple when you have npm:
```
$ npm install -g bower
```
### Optional
* Grunt - Download and Install [Grunt](http://gruntjs.com).
## Additional Packages
* Express - Defined as npm module in the [package.json](package.json) file.
* Mongoose - Defined as npm module in the [package.json](package.json) file.
* Passport - Defined as npm module in the [package.json](package.json) file.
* AngularJS - Defined as bower module in the [bower.json](bower.json) file.
* Twitter Bootstrap - Defined as bower module in the [bower.json](bower.json) file.
* UI Bootstrap - Defined as bower module in the [bower.json](bower.json) file.
## Quick Install
The quickest way to get started with MEAN is to clone the project and utilize it like this:
Install dependencies:
$ npm install
We recommend using [Grunt](https://github.com/gruntjs/grunt-cli) to start the server:
$ grunt
When not using grunt you can use:
$ node server
Then open a browser and go to:
http://localhost:3000
## Troubleshooting
During install some of you may encounter some issues, most of this issues can be solved by one of the following tips.
If you went through all this and still can't solve the issue, feel free to contact me(Amos), via the repository issue tracker or the links provided below.
#### Update NPM, Bower or Grunt
Sometimes you may find there is a weird error during install like npm's *Error: ENOENT*, usually updating those tools to the latest version solves the issue.
Updating NPM:
```
$ npm update -g npm
```
Updating Grunt:
```
$ npm update -g grunt-cli
```
Updating Bower:
```
$ npm update -g bower
```
#### Cleaning NPM and Bower cache
NPM and Bower has a caching system for holding packages that you already installed.
We found that often cleaning the cache solves some troubles this system creates.
NPM Clean Cache:
```
$ npm cache clean
```
Bower Clean Cache:
```
$ bower cache clean
```
## Configuration
All configuration is specified in the [config](config/) folder, particularly the [config.js](config/config.js) file and the [env](config/env/) files. Here you will need to specify your application name, database name, as well as hook up any social app keys if you want integration with Twitter, Facebook, GitHub or Google.
### Environmental Settings
There are three environments provided by default, __development__, __test__, and __production__. Each of these environments has the following configuration options:
* __db__ - This is the name of the MongoDB database to use, and is set by default to __mean-dev__ for the development environment.
* __app.name__ - This is the name of your app or website, and can be different for each environment. You can tell which environment you are running by looking at the TITLE attribute that your app generates.
* __Social OAuth Keys__ - Facebook, GitHub, Google, Twitter. You can specify your own social application keys here for each platform:
* __clientID__
* __clientSecret__
* __callbackURL__
To run with a different environment, just specify NODE_ENV as you call grunt:
$ NODE_ENV=test grunt
If you are using node instead of grunt, it is very similar:
$ NODE_ENV=test node server
> NOTE: Running Node.js applications in the __production__ environment enables caching, which is disabled by default in all other environments.
## Getting Started
We pre-included an article example, check it out:
* [The Model](https://github.com/linnovate/mean/blob/master/app/models/article.js) - Where we define our object schema.
* [The Controller](https://github.com/linnovate/mean/blob/master/app/controllers/articles.js) - Where we take care of our backend logic.
* [NodeJS Routes](https://github.com/linnovate/mean/blob/master/config/routes.js) - Where we define our REST service routes.
* [AngularJs Routes](https://github.com/linnovate/mean/blob/master/public/js/config.js) - Where we define our CRUD routes.
* [The AngularJs Service](https://github.com/linnovate/mean/blob/master/public/js/services/articles.js) - Where we connect to our REST service.
* [The AngularJs Controller](https://github.com/linnovate/mean/blob/master/public/js/controllers/articles.js) - Where we take care of our frontend logic.
* [The AngularJs Views Folder](https://github.com/linnovate/mean/blob/master/public/views/articles) - Where we keep our CRUD views.
## Heroku Quick Deployment
Before you start make sure you have <a href="https://toolbelt.heroku.com/">heroku toolbelt</a> installed and an accessible mongo db instance - you can try <a href="http://www.mongohq.com/">mongohq</a> which have an easy setup )
```bash
git init
git add .
git commit -m "initial version"
heroku apps:create
git push heroku master
```
## More Information
* Contact Amos Haviv on any issue via [E-Mail](mailto:mail@amoshaviv.com), [Facebook](http://www.facebook.com/amoshaviv), or [Twitter](http://www.twitter.com/amoshaviv).
* Visit us at [Linnovate.net](http://www.linnovate.net/).
* Visit our [Ninja's Zone](http://www.meanleanstartupmachine.com/) for extended support.
## Credits
Inspired by the great work of [Madhusudhan Srinivasa](https://github.com/madhums/)
## License
(The MIT License)
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

90
app/controllers/articles.js Executable file
View File

@ -0,0 +1,90 @@
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
Article = mongoose.model('Article'),
_ = require('underscore');
/**
* Find article by id
*/
exports.article = function(req, res, next, id) {
Article.load(id, function(err, article) {
if (err) return next(err);
if (!article) return next(new Error('Failed to load article ' + id));
req.article = article;
next();
});
};
/**
* 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.send('users/signup', {
errors: err.errors,
article: article
});
} else {
res.jsonp(article);
}
});
};
/**
* Update a article
*/
exports.update = function(req, res) {
var article = req.article;
article = _.extend(article, req.body);
article.save(function(err) {
res.jsonp(article);
});
};
/**
* Delete an article
*/
exports.destroy = function(req, res) {
var article = req.article;
article.remove(function(err) {
if (err) {
res.render('error', {
status: 500
});
} else {
res.jsonp(article);
}
});
};
/**
* Show an article
*/
exports.show = function(req, res) {
res.jsonp(req.article);
};
/**
* List of Articles
*/
exports.all = function(req, res) {
Article.find().sort('-created').populate('user', 'name username').exec(function(err, articles) {
if (err) {
res.render('error', {
status: 500
});
} else {
res.jsonp(articles);
}
});
};

12
app/controllers/index.js Executable file
View File

@ -0,0 +1,12 @@
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
_ = require('underscore');
exports.render = function(req, res) {
res.render('index', {
user: req.user ? JSON.stringify(req.user) : "null"
});
};

103
app/controllers/users.js Executable file
View File

@ -0,0 +1,103 @@
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
User = mongoose.model('User');
/**
* Auth callback
*/
exports.authCallback = function(req, res, next) {
res.redirect('/');
};
/**
* Show login form
*/
exports.signin = function(req, res) {
res.render('users/signin', {
title: 'Signin',
message: req.flash('error')
});
};
/**
* Show sign up form
*/
exports.signup = function(req, res) {
res.render('users/signup', {
title: 'Sign up',
user: new User()
});
};
/**
* Logout
*/
exports.signout = function(req, res) {
req.logout();
res.redirect('/');
};
/**
* Session
*/
exports.session = function(req, res) {
res.redirect('/');
};
/**
* Create user
*/
exports.create = function(req, res) {
var user = new User(req.body);
user.provider = 'local';
user.save(function(err) {
if (err) {
return res.render('users/signup', {
errors: err.errors,
user: user
});
}
req.logIn(user, function(err) {
if (err) return next(err);
return res.redirect('/');
});
});
};
/**
* Show profile
*/
exports.show = function(req, res) {
var user = req.profile;
res.render('users/show', {
title: user.name,
user: user
});
};
/**
* Send User
*/
exports.me = function(req, res) {
res.jsonp(req.user || null);
};
/**
* Find user by id
*/
exports.user = function(req, res, next, id) {
User
.findOne({
_id: id
})
.exec(function(err, user) {
if (err) return next(err);
if (!user) return next(new Error('Failed to load User ' + id));
req.profile = user;
next();
});
};

51
app/models/article.js Executable file
View File

@ -0,0 +1,51 @@
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
config = require('../../config/config'),
Schema = mongoose.Schema;
/**
* Article Schema
*/
var ArticleSchema = new Schema({
created: {
type: Date,
default: Date.now
},
title: {
type: String,
default: '',
trim: true
},
content: {
type: String,
default: '',
trim: true
},
user: {
type: Schema.ObjectId,
ref: 'User'
}
});
/**
* Validations
*/
ArticleSchema.path('title').validate(function(title) {
return title.length;
}, 'Title cannot be blank');
/**
* Statics
*/
ArticleSchema.statics = {
load: function(id, cb) {
this.findOne({
_id: id
}).populate('user', 'name username').exec(cb);
}
};
mongoose.model('Article', ArticleSchema);

124
app/models/user.js Executable file
View File

@ -0,0 +1,124 @@
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
crypto = require('crypto'),
_ = require('underscore'),
authTypes = ['github', 'twitter', 'facebook', 'google'];
/**
* User Schema
*/
var UserSchema = new Schema({
name: String,
email: String,
username: {
type: String,
unique: true
},
provider: String,
hashed_password: String,
salt: String,
facebook: {},
twitter: {},
github: {},
google: {}
});
/**
* Virtuals
*/
UserSchema.virtual('password').set(function(password) {
this._password = password;
this.salt = this.makeSalt();
this.hashed_password = this.encryptPassword(password);
}).get(function() {
return this._password;
});
/**
* Validations
*/
var validatePresenceOf = function(value) {
return value && value.length;
};
// the below 4 validations only apply if you are signing up traditionally
UserSchema.path('name').validate(function(name) {
// if you are authenticating by any of the oauth strategies, don't validate
if (authTypes.indexOf(this.provider) !== -1) return true;
return name.length;
}, 'Name cannot be blank');
UserSchema.path('email').validate(function(email) {
// if you are authenticating by any of the oauth strategies, don't validate
if (authTypes.indexOf(this.provider) !== -1) return true;
return email.length;
}, 'Email cannot be blank');
UserSchema.path('username').validate(function(username) {
// if you are authenticating by any of the oauth strategies, don't validate
if (authTypes.indexOf(this.provider) !== -1) return true;
return username.length;
}, 'Username cannot be blank');
UserSchema.path('hashed_password').validate(function(hashed_password) {
// if you are authenticating by any of the oauth strategies, don't validate
if (authTypes.indexOf(this.provider) !== -1) return true;
return hashed_password.length;
}, 'Password cannot be blank');
/**
* Pre-save hook
*/
UserSchema.pre('save', function(next) {
if (!this.isNew) return next();
if (!validatePresenceOf(this.password) && authTypes.indexOf(this.provider) === -1)
next(new Error('Invalid password'));
else
next();
});
/**
* Methods
*/
UserSchema.methods = {
/**
* Authenticate - check if the passwords are the same
*
* @param {String} plainText
* @return {Boolean}
* @api public
*/
authenticate: function(plainText) {
return this.encryptPassword(plainText) === this.hashed_password;
},
/**
* Make salt
*
* @return {String}
* @api public
*/
makeSalt: function() {
return Math.round((new Date().valueOf() * Math.random())) + '';
},
/**
* Encrypt password
*
* @param {String} password
* @return {String}
* @api public
*/
encryptPassword: function(password) {
if (!password) return '';
return crypto.createHmac('sha1', this.salt).update(password).digest('hex');
}
};
mongoose.model('User', UserSchema);

13
app/views/404.jade Executable file
View File

@ -0,0 +1,13 @@
extends layouts/default
block main
h1 Oops something went wrong
br
span 404
block content
#error-message-box
#error-stack-trace
pre
code!= error

12
app/views/500.jade Executable file
View File

@ -0,0 +1,12 @@
extends layouts/default
block main
h1 Oops something went wrong
br
span 500
block content
#error-message-box
#error-stack-trace
pre
code!= error

29
app/views/includes/foot.jade Executable file
View File

@ -0,0 +1,29 @@
//AngularJS
script(type='text/javascript', src='/lib/angular/angular.js')
script(type='text/javascript', src='/lib/angular-cookies/angular-cookies.js')
script(type='text/javascript', src='/lib/angular-resource/angular-resource.js')
//Angular UI
script(type='text/javascript', src='/lib/angular-bootstrap/ui-bootstrap-tpls.js')
script(type='text/javascript', src='/lib/angular-bootstrap/ui-bootstrap.js')
script(type='text/javascript', src='/lib/angular-ui-utils/modules/route/route.js')
//Application Init
script(type='text/javascript', src='/js/app.js')
script(type='text/javascript', src='/js/config.js')
script(type='text/javascript', src='/js/directives.js')
script(type='text/javascript', src='/js/filters.js')
//Application Services
script(type='text/javascript', src='/js/services/global.js')
script(type='text/javascript', src='/js/services/articles.js')
//Application Controllers
script(type='text/javascript', src='/js/controllers/articles.js')
script(type='text/javascript', src='/js/controllers/index.js')
script(type='text/javascript', src='/js/controllers/header.js')
script(type='text/javascript', src='/js/init.js')
if (req.host='localhost')
//Livereload script rendered
script(type='text/javascript', src='http://localhost:35729/livereload.js')

29
app/views/includes/head.jade Executable file
View File

@ -0,0 +1,29 @@
head
meta(charset='utf-8')
meta(http-equiv='X-UA-Compatible', content='IE=edge,chrome=1')
meta(name='viewport', content='width=device-width,initial-scale=1')
title= appName+' - '+title
meta(http-equiv='Content-type', content='text/html;charset=UTF-8')
meta(name="keywords", content="node.js, express, mongoose, mongodb, angularjs")
meta(name="description", content="MEAN - A Modern Stack: MongoDB, ExpressJS, AngularJS, NodeJS. (BONUS: Passport User Support).")
link(href='/img/icons/favicon.ico', rel='shortcut icon', type='image/x-icon')
meta(property='fb:app_id', content='APP_ID')
meta(property='og:title', content='#{appName} - #{title}')
meta(property='og:description', content='MEAN - A Modern Stack: MongoDB, ExpressJS, AngularJS, NodeJS. (BONUS: Passport User Support).')
meta(property='og:type', content='website')
meta(property='og:url', content='APP_URL')
meta(property='og:image', content='APP_LOGO')
meta(property='og:site_name', content='MEAN - A Modern Stack')
meta(property='fb:admins', content='APP_ADMIN')
link(rel='stylesheet', href='/lib/bootstrap/docs/assets/css/bootstrap.css')
//- link(rel='stylesheet', href='/lib/bootstrap/dist/css/bootstrap-responsive.css')
link(rel='stylesheet', href='/css/common.css')
link(rel='stylesheet', href='/css/views/articles.css')
//if lt IE 9
script(src='http://html5shim.googlecode.com/svn/trunk/html5.js')

6
app/views/index.jade Executable file
View File

@ -0,0 +1,6 @@
extends layouts/default
block content
section(data-ng-view)
script(type="text/javascript").
window.user = !{user};

9
app/views/layouts/default.jade Executable file
View File

@ -0,0 +1,9 @@
!!! 5
html(lang='en', xmlns='http://www.w3.org/1999/xhtml', xmlns:fb='https://www.facebook.com/2008/fbml', itemscope='itemscope', itemtype='http://schema.org/Product')
include ../includes/head
body
.navbar.navbar-inverse.navbar-fixed-top(data-ng-include="'views/header.html'", data-role="navigation")
section.content
section.container
block content
include ../includes/foot

22
app/views/users/auth.jade Executable file
View File

@ -0,0 +1,22 @@
extends ../layouts/default
block content
.row
.offset1.span5
a(href="/auth/facebook")
img(src="/img/icons/facebook.png")
a(href="/auth/github")
img(src="/img/icons/github.png")
a(href="/auth/twitter")
img(src="/img/icons/twitter.png")
a(href="/auth/google")
img(src="/img/icons/google.png")
.span6
if (typeof errors !== 'undefined')
.fade.in.alert.alert-block.alert-error
a.close(data-dismiss="alert", href="javascript:void(0)") x
ul
each error in errors
li= error.type
block auth

20
app/views/users/signin.jade Executable file
View File

@ -0,0 +1,20 @@
extends auth
block auth
form.signin.form-horizontal(action="/users/session", method="post")
p.error= message
.control-group
label.control-label(for='email') Email
.controls
input#email(type='text', name="email", placeholder='Email')
.control-group
label.control-label(for='password') Password
.controls
input#password(type='password', name="password", placeholder='Password')
.form-actions
button.btn.btn-primary(type='submit') Sign in
&nbsp;
| or&nbsp;
a.show-signup(href="/signup") Sign up

29
app/views/users/signup.jade Executable file
View File

@ -0,0 +1,29 @@
extends auth
block auth
form.signup.form-horizontal(action="/users", method="post")
.control-group
label.control-label(for='name') Full name
.controls
input#name(type='text', name="name", placeholder='Full name', value=user.name)
.control-group
label.control-label(for='email') Email
.controls
input#email(type='text', name="email", placeholder='Email', value=user.email)
.control-group
label.control-label(for='username') Username
.controls
input#username(type='text', name="username", placeholder='Username', value=user.username)
.control-group
label.control-label(for='password') Password
.controls
input#password(type='password', name="password", placeholder='Password')
.form-actions
button.btn.btn-primary(type='submit') Sign up
&nbsp;
| or&nbsp;
a.show-login(href="/signin") login

12
bower.json Executable file
View File

@ -0,0 +1,12 @@
{
"name": "mean",
"version": "0.1.0",
"dependencies": {
"bootstrap": "2.3.2",
"angular": "1.0.6",
"angular-resource": "1.0.6",
"angular-cookies": "1.0.6",
"angular-bootstrap": "0.6.0",
"angular-ui-utils": "0.0.4"
}
}

8
config/env/all.js vendored Executable file
View File

@ -0,0 +1,8 @@
var path = require('path'),
rootPath = path.normalize(__dirname + '/../..');
module.exports = {
root: rootPath,
port: process.env.PORT || 3000,
db: process.env.MONGOHQ_URL
}

26
config/env/development.json vendored Executable file
View File

@ -0,0 +1,26 @@
{
"db": "mongodb://localhost/mean-dev",
"app": {
"name": "MEAN - A Modern Stack - Development"
},
"facebook": {
"clientID": "APP_ID",
"clientSecret": "APP_SECRET",
"callbackURL": "http://localhost:3000/auth/facebook/callback"
},
"twitter": {
"clientID": "CONSUMER_KEY",
"clientSecret": "CONSUMER_SECRET",
"callbackURL": "http://localhost:3000/auth/twitter/callback"
},
"github": {
"clientID": "APP_ID",
"clientSecret": "APP_SECRET",
"callbackURL": "http://localhost:3000/auth/github/callback"
},
"google": {
"clientID": "APP_ID",
"clientSecret": "APP_SECRET",
"callbackURL": "http://localhost:3000/auth/google/callback"
}
}

26
config/env/production.json vendored Executable file
View File

@ -0,0 +1,26 @@
{
"db": "mongodb://localhost/mean",
"app": {
"name": "MEAN - A Modern Stack - Production"
},
"facebook": {
"clientID": "APP_ID",
"clientSecret": "APP_SECRET",
"callbackURL": "http://localhost:3000/auth/facebook/callback"
},
"twitter": {
"clientID": "CONSUMER_KEY",
"clientSecret": "CONSUMER_SECRET",
"callbackURL": "http://localhost:3000/auth/twitter/callback"
},
"github": {
"clientID": "APP_ID",
"clientSecret": "APP_SECRET",
"callbackURL": "http://localhost:3000/auth/github/callback"
},
"google": {
"clientID": "APP_ID",
"clientSecret": "APP_SECRET",
"callbackURL": "http://localhost:3000/auth/google/callback"
}
}

27
config/env/test.json vendored Executable file
View File

@ -0,0 +1,27 @@
{
"db": "mongodb://localhost/mean-test",
"port": 3001,
"app": {
"name": "MEAN - A Modern Stack - Test"
},
"facebook": {
"clientID": "APP_ID",
"clientSecret": "APP_SECRET",
"callbackURL": "http://localhost:3000/auth/facebook/callback"
},
"twitter": {
"clientID": "CONSUMER_KEY",
"clientSecret": "CONSUMER_SECRET",
"callbackURL": "http://localhost:3000/auth/twitter/callback"
},
"github": {
"clientID": "APP_ID",
"clientSecret": "APP_SECRET",
"callbackURL": "http://localhost:3000/auth/github/callback"
},
"google": {
"clientID": "APP_ID",
"clientSecret": "APP_SECRET",
"callbackURL": "http://localhost:3000/auth/google/callback"
}
}

27
config/env/travis.json vendored Executable file
View File

@ -0,0 +1,27 @@
{
"db": "mongodb://localhost/mean-travis",
"port": 3001,
"app": {
"name": "MEAN - A Modern Stack - Test on travis"
},
"facebook": {
"clientID": "APP_ID",
"clientSecret": "APP_SECRET",
"callbackURL": "http://localhost:3000/auth/facebook/callback"
},
"twitter": {
"clientID": "CONSUMER_KEY",
"clientSecret": "CONSUMER_SECRET",
"callbackURL": "http://localhost:3000/auth/twitter/callback"
},
"github": {
"clientID": "APP_ID",
"clientSecret": "APP_SECRET",
"callbackURL": "http://localhost:3000/auth/github/callback"
},
"google": {
"clientID": "APP_ID",
"clientSecret": "APP_SECRET",
"callbackURL": "http://localhost:3000/auth/google/callback"
}
}

93
config/express.js Executable file
View File

@ -0,0 +1,93 @@
/**
* Module dependencies.
*/
var express = require('express'),
mongoStore = require('connect-mongo')(express),
flash = require('connect-flash'),
helpers = require('view-helpers'),
config = require('./config');
module.exports = function(app, passport, db) {
app.set('showStackError', true);
//Prettify HTML
app.locals.pretty = true;
//Should be placed before express.static
app.use(express.compress({
filter: function(req, res) {
return (/json|text|javascript|css/).test(res.getHeader('Content-Type'));
},
level: 9
}));
//Setting the fav icon and static folder
app.use(express.favicon());
app.use(express.static(config.root + '/public'));
//Don't use logger for test env
if (process.env.NODE_ENV !== 'test') {
app.use(express.logger('dev'));
}
//Set views path, template engine and default layout
app.set('views', config.root + '/app/views');
app.set('view engine', 'jade');
//Enable jsonp
app.enable("jsonp callback");
app.configure(function() {
//cookieParser should be above session
app.use(express.cookieParser());
//bodyParser should be above methodOverride
app.use(express.bodyParser());
app.use(express.methodOverride());
//express/mongo session storage
app.use(express.session({
secret: 'MEAN',
store: new mongoStore({
db: db.connection.db,
collection: 'sessions'
})
}));
//connect flash for flash messages
app.use(flash());
//dynamic helpers
app.use(helpers(config.app.name));
//use passport session
app.use(passport.initialize());
app.use(passport.session());
//routes should be at the last
app.use(app.router);
//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) {
//Treat as 404
if (~err.message.indexOf('not found')) 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, next) {
res.status(404).render('404', {
url: req.originalUrl,
error: 'Not found'
});
});
});
};

View File

@ -0,0 +1,33 @@
/**
* Generic require login routing middleware
*/
exports.requiresLogin = function(req, res, next) {
if (!req.isAuthenticated()) {
return res.send(401, 'User is not authorized');
}
next();
};
/**
* User authorizations routing middleware
*/
exports.user = {
hasAuthorization: function(req, res, next) {
if (req.profile.id != req.user.id) {
return res.send(401, 'User is not authorized');
}
next();
}
};
/**
* Article authorizations routing middleware
*/
exports.article = {
hasAuthorization: function(req, res, next) {
if (req.article.user.id != req.user.id) {
return res.send(401, 'User is not authorized');
}
next();
}
};

172
config/passport.js Executable file
View File

@ -0,0 +1,172 @@
var mongoose = require('mongoose'),
LocalStrategy = require('passport-local').Strategy,
TwitterStrategy = require('passport-twitter').Strategy,
FacebookStrategy = require('passport-facebook').Strategy,
GitHubStrategy = require('passport-github').Strategy,
GoogleStrategy = require('passport-google-oauth').OAuth2Strategy,
User = mongoose.model('User'),
config = require('./config');
module.exports = function(passport) {
//Serialize sessions
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findOne({
_id: id
}, function(err, user) {
done(err, user);
});
});
//Use local strategy
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'password'
},
function(email, password, done) {
User.findOne({
email: email
}, function(err, user) {
if (err) {
return done(err);
}
if (!user) {
return done(null, false, {
message: 'Unknown user'
});
}
if (!user.authenticate(password)) {
return done(null, false, {
message: 'Invalid password'
});
}
return done(null, user);
});
}
));
//Use twitter strategy
passport.use(new TwitterStrategy({
consumerKey: config.twitter.clientID,
consumerSecret: config.twitter.clientSecret,
callbackURL: config.twitter.callbackURL
},
function(token, tokenSecret, profile, done) {
User.findOne({
'twitter.id_str': profile.id
}, function(err, user) {
if (err) {
return done(err);
}
if (!user) {
user = new User({
name: profile.displayName,
username: profile.username,
provider: 'twitter',
twitter: profile._json
});
user.save(function(err) {
if (err) console.log(err);
return done(err, user);
});
} else {
return done(err, user);
}
});
}
));
//Use facebook strategy
passport.use(new FacebookStrategy({
clientID: config.facebook.clientID,
clientSecret: config.facebook.clientSecret,
callbackURL: config.facebook.callbackURL
},
function(accessToken, refreshToken, profile, done) {
User.findOne({
'facebook.id': profile.id
}, function(err, user) {
if (err) {
return done(err);
}
if (!user) {
user = new User({
name: profile.displayName,
email: profile.emails[0].value,
username: profile.username,
provider: 'facebook',
facebook: profile._json
});
user.save(function(err) {
if (err) console.log(err);
return done(err, user);
});
} else {
return done(err, user);
}
});
}
));
//Use github strategy
passport.use(new GitHubStrategy({
clientID: config.github.clientID,
clientSecret: config.github.clientSecret,
callbackURL: config.github.callbackURL
},
function(accessToken, refreshToken, profile, done) {
User.findOne({
'github.id': profile.id
}, function(err, user) {
if (!user) {
user = new User({
name: profile.displayName,
email: profile.emails[0].value,
username: profile.username,
provider: 'github',
github: profile._json
});
user.save(function(err) {
if (err) console.log(err);
return done(err, user);
});
} else {
return done(err, user);
}
});
}
));
//Use google strategy
passport.use(new GoogleStrategy({
clientID: config.google.clientID,
clientSecret: config.google.clientSecret,
callbackURL: config.google.callbackURL
},
function(accessToken, refreshToken, profile, done) {
User.findOne({
'google.id': profile.id
}, function(err, user) {
if (!user) {
user = new User({
name: profile.displayName,
email: profile.emails[0].value,
username: profile.username,
provider: 'google',
google: profile._json
});
user.save(function(err) {
if (err) console.log(err);
return done(err, user);
});
} else {
return done(err, user);
}
});
}
));
};

78
config/routes.js Executable file
View File

@ -0,0 +1,78 @@
module.exports = function(app, passport, auth) {
//User Routes
var users = require('../app/controllers/users');
app.get('/signin', users.signin);
app.get('/signup', users.signup);
app.get('/signout', users.signout);
//Setting up the users api
app.post('/users', users.create);
app.post('/users/session', passport.authenticate('local', {
failureRedirect: '/signin',
failureFlash: 'Invalid email or password.'
}), users.session);
app.get('/users/me', users.me);
app.get('/users/:userId', users.show);
//Setting the facebook oauth routes
app.get('/auth/facebook', passport.authenticate('facebook', {
scope: ['email', 'user_about_me'],
failureRedirect: '/signin'
}), users.signin);
app.get('/auth/facebook/callback', passport.authenticate('facebook', {
failureRedirect: '/signin'
}), users.authCallback);
//Setting the github oauth routes
app.get('/auth/github', passport.authenticate('github', {
failureRedirect: '/signin'
}), users.signin);
app.get('/auth/github/callback', passport.authenticate('github', {
failureRedirect: '/signin'
}), users.authCallback);
//Setting the twitter oauth routes
app.get('/auth/twitter', passport.authenticate('twitter', {
failureRedirect: '/signin'
}), users.signin);
app.get('/auth/twitter/callback', passport.authenticate('twitter', {
failureRedirect: '/signin'
}), users.authCallback);
//Setting the google oauth routes
app.get('/auth/google', passport.authenticate('google', {
failureRedirect: '/signin',
scope: [
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email'
]
}), users.signin);
app.get('/auth/google/callback', passport.authenticate('google', {
failureRedirect: '/signin'
}), users.authCallback);
//Finish with setting up the userId param
app.param('userId', users.user);
//Article Routes
var articles = require('../app/controllers/articles');
app.get('/articles', articles.all);
app.post('/articles', auth.requiresLogin, articles.create);
app.get('/articles/:articleId', articles.show);
app.put('/articles/:articleId', auth.requiresLogin, auth.article.hasAuthorization, articles.update);
app.del('/articles/:articleId', auth.requiresLogin, auth.article.hasAuthorization, articles.destroy);
//Finish with setting up the articleId param
app.param('articleId', articles.article);
//Home route
var index = require('../app/controllers/index');
app.get('/', index.render);
};

87
gruntfile.js Executable file
View File

@ -0,0 +1,87 @@
module.exports = function(grunt) {
// Project Configuration
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
watch: {
jade: {
files: ['app/views/**'],
options: {
livereload: true,
},
},
js: {
files: ['public/js/**', 'app/**/*.js'],
tasks: ['jshint'],
options: {
livereload: true,
},
},
html: {
files: ['public/views/**'],
options: {
livereload: true,
},
},
css: {
files: ['public/css/**'],
options: {
livereload: true
}
}
},
jshint: {
all: ['gruntfile.js', 'public/js/**/*.js', 'test/**/*.js', 'app/**/*.js']
},
nodemon: {
dev: {
options: {
file: 'server.js',
args: [],
ignoredFiles: ['README.md', 'node_modules/**', '.DS_Store'],
watchedExtensions: ['js'],
watchedFolders: ['app', 'config'],
debug: true,
delayTime: 1,
env: {
PORT: 3000
},
cwd: __dirname
}
}
},
concurrent: {
tasks: ['nodemon', 'watch'],
options: {
logConcurrentOutput: true
}
},
mochaTest: {
options: {
reporter: 'spec'
},
src: ['test/**/*.js']
},
env: {
test: {
NODE_ENV: 'test'
}
}
});
//Load NPM tasks
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-mocha-test');
grunt.loadNpmTasks('grunt-nodemon');
grunt.loadNpmTasks('grunt-concurrent');
grunt.loadNpmTasks('grunt-env');
//Making grunt default to force in order not to break the project.
grunt.option('force', true);
//Default task(s).
grunt.registerTask('default', ['jshint', 'concurrent']);
//Test task.
grunt.registerTask('test', ['env:test', 'mochaTest']);
};

51
package.json Executable file
View File

@ -0,0 +1,51 @@
{
"name": "mean",
"description": "MEAN - A Modern Stack: MongoDB, ExpressJS, AngularJS, NodeJS. (BONUS: Passport User Support).",
"version": "0.1.0",
"private": false,
"author": "Amos Haviv",
"repository": {
"type": "git",
"url": "https://github.com/linnovate/mean.git"
},
"engines": {
"node": "0.10.x",
"npm": "1.2.x"
},
"scripts": {
"start": "node node_modules/grunt-cli/bin/grunt",
"test": "node node_modules/grunt-cli/bin/grunt test",
"postinstall": "bower install"
},
"dependencies": {
"express": "latest",
"jade": "latest",
"mongoose": "latest",
"connect-mongo": "latest",
"connect-flash": "latest",
"crypto": "latest",
"passport": "latest",
"passport-local": "latest",
"passport-facebook": "latest",
"passport-twitter": "latest",
"passport-github": "latest",
"passport-google-oauth": "latest",
"underscore": "latest",
"view-helpers": "latest",
"mean-logger": "latest",
"forever": "latest",
"bower": "latest",
"grunt": "latest",
"grunt-cli": "latest",
"grunt-env": "latest"
},
"devDependencies": {
"supertest": "latest",
"should": "latest",
"grunt-contrib-watch": "latest",
"grunt-contrib-jshint": "latest",
"grunt-nodemon": "latest",
"grunt-concurrent": "latest",
"grunt-mocha-test": "latest"
}
}

25
public/css/common.css Executable file
View File

@ -0,0 +1,25 @@
.navbar .nav>li>a.brand {
padding-left:20px;
margin-left:0
}
.content {
margin-top:50px;
width:100%
}
footer {
position:fixed;
left:0px;
bottom:0px;
height:30px;
width:100%;
background:#ddd;
-webkit-box-shadow:0 8px 6px 6px black;
-moz-box-shadow:0 8px 6px 6px black;
box-shadow:0 8px 6px 6px black
}
footer p {
padding:5px 0 12px 10px
}

7
public/css/views/articles.css Executable file
View File

@ -0,0 +1,7 @@
h1 {
text-align:center
}
ul.articles li:not(:last-child) {
border-bottom:1px solid #ccc
}

15
public/humans.txt Executable file
View File

@ -0,0 +1,15 @@
# humanstxt.org/
# The humans responsible & technology colophon
# TEAM
<name> -- <role> -- <twitter>
# THANKS
<name>
# TECHNOLOGY COLOPHON
HTML5, CSS3
jQuery, Modernizr

0
public/img/.gitignore vendored Executable file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/img/apple/splash.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
public/img/apple/splash2x.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
public/img/icons/facebook.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
public/img/icons/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
public/img/icons/github.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/img/icons/google.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
public/img/icons/twitter.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
public/img/loaders/loader.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

4
public/js/app.js Executable file
View File

@ -0,0 +1,4 @@
window.app = angular.module('mean', ['ngCookies', 'ngResource', 'ui.bootstrap', 'ui.route', 'mean.system', 'mean.articles']);
angular.module('mean.system', []);
angular.module('mean.articles', []);

31
public/js/config.js Executable file
View File

@ -0,0 +1,31 @@
//Setting up route
window.app.config(['$routeProvider',
function($routeProvider) {
$routeProvider.
when('/articles', {
templateUrl: 'views/articles/list.html'
}).
when('/articles/create', {
templateUrl: 'views/articles/create.html'
}).
when('/articles/:articleId/edit', {
templateUrl: 'views/articles/edit.html'
}).
when('/articles/:articleId', {
templateUrl: 'views/articles/view.html'
}).
when('/', {
templateUrl: 'views/index.html'
}).
otherwise({
redirectTo: '/'
});
}
]);
//Setting HTML5 Location Mode
window.app.config(['$locationProvider',
function($locationProvider) {
$locationProvider.hashPrefix("!");
}
]);

View File

@ -0,0 +1,52 @@
angular.module('mean.articles').controller('ArticlesController', ['$scope', '$routeParams', '$location', 'Global', 'Articles', function ($scope, $routeParams, $location, Global, Articles) {
$scope.global = Global;
$scope.create = function() {
var article = new Articles({
title: this.title,
content: this.content
});
article.$save(function(response) {
$location.path("articles/" + response._id);
});
this.title = "";
this.content = "";
};
$scope.remove = function(article) {
article.$remove();
for (var i in $scope.articles) {
if ($scope.articles[i] == article) {
$scope.articles.splice(i, 1);
}
}
};
$scope.update = function() {
var article = $scope.article;
if (!article.updated) {
article.updated = [];
}
article.updated.push(new Date().getTime());
article.$update(function() {
$location.path('articles/' + article._id);
});
};
$scope.find = function() {
Articles.query(function(articles) {
$scope.articles = articles;
});
};
$scope.findOne = function() {
Articles.get({
articleId: $routeParams.articleId
}, function(article) {
$scope.article = article;
});
};
}]);

13
public/js/controllers/header.js Executable file
View File

@ -0,0 +1,13 @@
angular.module('mean.system').controller('HeaderController', ['$scope', 'Global', function ($scope, Global) {
$scope.global = Global;
$scope.menu = [{
"title": "Articles",
"link": "articles"
}, {
"title": "Create New Article",
"link": "articles/create"
}];
$scope.isCollapsed = false;
}]);

3
public/js/controllers/index.js Executable file
View File

@ -0,0 +1,3 @@
angular.module('mean.system').controller('IndexController', ['$scope', 'Global', function ($scope, Global) {
$scope.global = Global;
}]);

0
public/js/directives.js Executable file
View File

0
public/js/filters.js Executable file
View File

15
public/js/init.js Executable file
View File

@ -0,0 +1,15 @@
window.bootstrap = function() {
angular.bootstrap(document, ['mean']);
};
window.init = function() {
window.bootstrap();
};
angular.element(document).ready(function() {
//Fixing facebook bug with redirect
if (window.location.hash == "#_=_") window.location.hash = "";
//Then init the app
window.init();
});

10
public/js/services/articles.js Executable file
View File

@ -0,0 +1,10 @@
//Articles service used for articles REST endpoint
angular.module('mean.articles').factory("Articles", ['$resource', function($resource) {
return $resource('articles/:articleId', {
articleId: '@_id'
}, {
update: {
method: 'PUT'
}
});
}]);

9
public/js/services/global.js Executable file
View File

@ -0,0 +1,9 @@
angular.module('mean.system').factory("Global", [function() {
var _this = this;
_this._data = {
user: window.user,
authenticated: !! window.user
};
return _this._data;
}]);

3
public/robots.txt Executable file
View File

@ -0,0 +1,3 @@
# robotstxt.org/
User-agent: *

View File

@ -0,0 +1,21 @@
<section data-ng-controller="ArticlesController">
<form class="form-horizontal" data-ng-submit="create()">
<div class="control-group">
<label class="control-label" for="title">Title</label>
<div class="controls">
<input type="text" data-ng-model="title" id="title" placeholder="Title" required>
</div>
</div>
<div class="control-group">
<label class="control-label" for="content">Content</label>
<div class="controls">
<textarea data-ng-model="content" id="content" cols="30" rows="10" placeholder="Content"></textarea>
</div>
</div>
<div class="control-group">
<div class="controls">
<input type="submit" class="btn">
</div>
</div>
</form>
</section>

25
public/views/articles/edit.html Executable file
View File

@ -0,0 +1,25 @@
<section data-ng-controller="ArticlesController" data-ng-init="findOne()">
<form class="form-horizontal" data-ng-submit="update()">
<div class="control-group">
<label class="control-label" for="title">Title</label>
<div class="controls">
<input type="text" data-ng-model="article.title" id="title" placeholder="Title" required>
</input>
</div>
</div>
<div class="control-group">
<label class="control-label" for="content">Content</label>
<div class="controls">
<textarea data-ng-model="article.content" id="content" cols="30" rows="10" placeholder="Content">
</textarea>
</div>
</div>
<div class="control-group">
<div class="controls">
<input type="submit" class="btn">
</div>
</div>
</form>
</section>

11
public/views/articles/list.html Executable file
View File

@ -0,0 +1,11 @@
<section data-ng-controller="ArticlesController" data-ng-init="find()">
<ul class="articles unstyled">
<li data-ng-repeat="article in articles">
<span>{{article.created | date:'medium'}}</span> /
<span>{{article.user.name}}</span>
<h2><a data-ng-href="#!/articles/{{article._id}}">{{article.title}}</a></h2>
<div>{{article.content}}</div>
</li>
</ul>
<h1 data-ng-hide="!articles || articles.length">No articles yet. <br> Why don't you <a href="/#!/articles/create">Create One</a>?</h1>
</section>

View File

@ -0,0 +1,6 @@
<section data-ng-controller="ArticlesController" data-ng-init="findOne()">
<span>{{article.created | date:'medium'}}</span> /
<span>{{article.user.name}}</span>
<h2>{{article.title}} <a data-ng-show="global.user._id == article.user._id" href="/#!/articles/{{article._id}}/edit">edit</a></h2>
<div>{{article.content}}</div>
</section>

28
public/views/header.html Executable file
View File

@ -0,0 +1,28 @@
<div class="navbar-inner" data-ng-controller="HeaderController">
<ul class="nav">
<li>
<a class="brand" href="/">MEAN - A Modern Stack</a>
</li>
<li data-ng-repeat="item in menu" data-ng-show="global.user" ui-route="/{{item.link}}" ng-class="{active: $uiRoute}">
<a href="#!/{{item.link}}">{{item.title}}</a>
</li>
</ul>
<ul class="nav pull-right" data-ng-hide="global.authenticated">
<li><a href="signup">Signup</a>
</li>
<li class="divider-vertical"></li>
<li><a href="signin">Signin</a>
</li>
</ul>
<ul class="nav pull-right" data-ng-show="global.authenticated">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
{{global.user.name}} <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><a href="/signout">Signout</a>
</li>
</ul>
</li>
</ul>
</div>

3
public/views/index.html Executable file
View File

@ -0,0 +1,3 @@
<section data-ng-controller="IndexController">
<h1>This is the home view</h1>
</section>

61
server.js Executable file
View File

@ -0,0 +1,61 @@
/**
* Module dependencies.
*/
var express = require('express'),
fs = require('fs'),
passport = require('passport'),
logger = require('mean-logger');
/**
* Main application entry file.
* Please note that the order of loading is important.
*/
//Load configurations
//if test env, load example file
var env = process.env.NODE_ENV = process.env.NODE_ENV || 'development',
config = require('./config/config'),
auth = require('./config/middlewares/authorization'),
mongoose = require('mongoose');
//Bootstrap db connection
var db = mongoose.connect(config.db);
//Bootstrap models
var models_path = __dirname + '/app/models';
var walk = function(path) {
fs.readdirSync(path).forEach(function(file) {
var newPath = path + '/' + file;
var stat = fs.statSync(newPath);
if (stat.isFile()) {
if (/(.*)\.(js$|coffee$)/.test(file)) {
require(newPath);
}
} else if (stat.isDirectory()) {
walk(newPath);
}
});
};
walk(models_path);
//bootstrap passport config
require('./config/passport')(passport);
var app = express();
//express settings
require('./config/express')(app, passport, db);
//Bootstrap routes
require('./config/routes')(app, passport, auth);
//Start the app by listening on <port>
var port = process.env.PORT || config.port;
app.listen(port);
console.log('Express app started on port ' + port);
//Initializing logger
logger.init(app, passport, mongoose);
//expose app
exports = module.exports = app;

65
test/article/model.js Executable file
View File

@ -0,0 +1,65 @@
/**
* Module dependencies.
*/
var should = require('should'),
app = require('../../server'),
mongoose = require('mongoose'),
User = mongoose.model('User'),
Article = mongoose.model('Article');
//Globals
var user;
var article;
//The tests
describe('<Unit Test>', function() {
describe('Model Article:', function() {
beforeEach(function(done) {
user = new User({
name: 'Full name',
email: 'test@test.com',
username: 'user',
password: 'password'
});
user.save(function(err) {
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({});
User.remove({});
done();
});
after(function(done){
Article.remove().exec();
User.remove().exec();
done();
});
});
});

66
test/user/model.js Executable file
View File

@ -0,0 +1,66 @@
/**
* Module dependencies.
*/
var should = require('should'),
app = require('../../server'),
mongoose = require('mongoose'),
User = mongoose.model('User');
//Globals
var user;
//The tests
describe('<Unit Test>', function() {
describe('Model User:', function() {
before(function(done) {
user = new User({
name: 'Full name',
email: 'test@test.com',
username: 'user',
password: 'password'
});
user2 = new User({
name: 'Full name',
email: 'test@test.com',
username: 'user',
password: 'password'
});
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 whithout problems', function(done) {
user.save(done);
});
it('should fail to save an existing user again', function(done) {
user.save();
return user2.save(function(err) {
should.exist(err);
done();
});
});
it('should be able to show an error when try to save without name', function(done) {
user.name = '';
return user.save(function(err) {
should.exist(err);
done();
});
});
});
after(function(done) {
User.remove().exec();
done();
});
});
});