This commit is contained in:
Sahat Yalkabov
2013-11-13 15:49:58 -05:00
parent d8c0505420
commit 06b777cce9
32 changed files with 569 additions and 1077 deletions

View File

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2013 Sahat Yalkabov
Copyright (c) 2013 <Author>
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

157
README.md
View File

@ -1,157 +0,0 @@
# MEAN Stack
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.

View File

@ -1,51 +0,0 @@
/**
* 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);

View File

@ -1,124 +0,0 @@
/**
* 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);

View File

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

View File

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

View File

@ -1,29 +0,0 @@
//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')

View File

@ -1,29 +0,0 @@
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')

View File

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

View File

@ -1,9 +0,0 @@
!!! 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

View File

@ -1,22 +0,0 @@
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

View File

@ -1,20 +0,0 @@
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

View File

@ -1,29 +0,0 @@
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

View File

@ -1,12 +0,0 @@
{
"name": "angular-quickstart",
"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"
}
}

18
conf.json Normal file
View File

@ -0,0 +1,18 @@
{
"db": "mongodb://localhost",
"facebook": {
"clientID": "APP_ID",
"clientSecret": "APP_SECRET",
"callbackURL": "http://localhost:3000/auth/facebook/callback"
},
"twitter": {
"clientID": "CONSUMER_KEY",
"clientSecret": "CONSUMER_SECRET",
"callbackURL": "http://localhost:3000/auth/twitter/callback"
},
"google": {
"clientID": "APP_ID",
"clientSecret": "APP_SECRET",
"callbackURL": "http://localhost:3000/auth/google/callback"
}
}

8
config/env/all.js vendored
View File

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

View File

@ -1,26 +0,0 @@
{
"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"
}
}

View File

@ -1,26 +0,0 @@
{
"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
View File

@ -1,27 +0,0 @@
{
"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"
}
}

View File

@ -1,27 +0,0 @@
{
"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"
}
}

View File

View File

@ -1,33 +0,0 @@
/**
* 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();
}
};

View File

@ -1,172 +0,0 @@
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);
}
});
}
));
};

View File

@ -1,78 +0,0 @@
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);
};

7
models/article.js Executable file
View File

@ -0,0 +1,7 @@
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema;

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

444
server.js
View File

@ -1,9 +1,10 @@
var express = require('express'),
mongoose = require('mongoose'),
mongoStore = require('connect-mongo')(express),
fs = require('fs'),
flash = require('connect-flash'),
helpers = require('view-helpers'),
config = require('./config'),
config = require('./conf'),
passport = require('passport'),
logger = require('mean-logger');
@ -14,33 +15,377 @@ var express = require('express'),
//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');
var env = process.env.NODE_ENV = process.env.NODE_ENV || 'development';
/**
* Generic require login routing middleware
*/
var requiresLogin = function(req, res, next) {
if (!req.isAuthenticated()) {
return res.send(401, 'User is not authorized');
}
next();
};
/**
* User authorizations routing middleware
*/
var 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
*/
var hasAuthorization = function(req, res, next) {
if (req.article.user.id != req.user.id) {
return res.send(401, 'User is not authorized');
}
next();
};
//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);
}
/**
* 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;
};
walk(models_path);
// 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);
/**
* 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);
//bootstrap passport config
require('./config/passport')(passport);
var 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;
//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);
}
});
}
));
var app = express();
@ -48,6 +393,7 @@ var app = express();
* Express Settings
*/
app.set('showStackError', true);
app.set('port', process.env.PORT || 3000);
app.locals.pretty = true;
app.use(express.compress({
filter: function(req, res) {
@ -97,16 +443,54 @@ app.use(function(req, res, next) {
});
//Bootstrap routes
require('./config/routes')(app, passport, auth);
//User Routes
app.post('/users', users.create);
app.get('/users/me', users.me);
app.get('/users/:userId', users.show);
//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);
//Setting the facebook oauth routes
app.get('/auth/facebook', passport.authenticate('facebook', {
scope: ['email', 'user_about_me'],
failureRedirect: '/signin'
}), users.signin);
//Initializing logger
logger.init(app, passport, mongoose);
app.get('/auth/facebook/callback', passport.authenticate('facebook', {
failureRedirect: '/signin'
}), users.authCallback);
//expose app
exports = module.exports = app;
//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);
//Home route
var index = require('../app/controllers/index');
app.get('/', index.render);
console.log('Express app started on port ' + app.get('port'));
app.listen(app.get('port'));

View File

@ -1,65 +0,0 @@
/**
* 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();
});
});
});

View File

@ -1,66 +0,0 @@
/**
* 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();
});
});
});