Initial Commit

This commit is contained in:
Fergal Moran
2014-03-20 11:32:56 +00:00
commit d49dc8b3bf
63 changed files with 3176 additions and 0 deletions

13
lib/.jshintrc Normal file
View File

@@ -0,0 +1,13 @@
{
"node": true,
"esnext": true,
"bitwise": true,
"eqeqeq": true,
"immed": true,
"latedef": true,
"newcap": true,
"noarg": true,
"regexp": true,
"undef": true,
"smarttabs": true
}

10
lib/config/config.js Normal file
View File

@@ -0,0 +1,10 @@
'use strict';
var _ = require('lodash');
/**
* Load environment configuration
*/
module.exports = _.merge(
require('./env/all.js'),
require('./env/' + process.env.NODE_ENV + '.js') || {});

37
lib/config/dummydata.js Normal file
View File

@@ -0,0 +1,37 @@
'use strict';
var mongoose = require('mongoose'),
User = mongoose.model('User'),
Promotion = mongoose.model('Promotion'),
Thing = mongoose.model('Thing');
var user;
console.log("Creating users");
// Clear old users, then add a default user
User.find({}).remove(function() {
user = new User({
provider: 'local',
name: 'Test User',
email: 'test@test.com',
password: 'test' }, function() {
console.log('finished populating users');
}
);
});
console.log("Creating promotions");
Promotion.find({}).remove(function(){
Promotion.create({
title: 'Dev prom 1',
user: user
}, {
title: 'Dev prom 2',
user: user
}, {
title: 'Dev prom 3',
user: user
}, function(){
console.log('finished populating promotions');
});
});

17
lib/config/env/all.js vendored Normal file
View File

@@ -0,0 +1,17 @@
'use strict';
var path = require('path');
var rootPath = path.normalize(__dirname + '/../../..');
module.exports = {
root: rootPath,
port: process.env.PORT || 3000,
mongo: {
options: {
db: {
safe: true
}
}
}
};

8
lib/config/env/development.js vendored Normal file
View File

@@ -0,0 +1,8 @@
'use strict';
module.exports = {
env: 'development',
mongo: {
uri: 'mongodb://localhost/roboto-dev'
}
};

10
lib/config/env/production.js vendored Normal file
View File

@@ -0,0 +1,10 @@
'use strict';
module.exports = {
env: 'production',
mongo: {
uri: process.env.MONGOLAB_URI ||
process.env.MONGOHQ_URL ||
'mongodb://localhost/roboto'
}
};

8
lib/config/env/test.js vendored Normal file
View File

@@ -0,0 +1,8 @@
'use strict';
module.exports = {
env: 'test',
mongo: {
uri: 'mongodb://localhost/roboto-test'
}
};

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

@@ -0,0 +1,69 @@
'use strict';
var express = require('express'),
path = require('path'),
config = require('./config'),
passport = require('passport'),
mongoStore = require('connect-mongo')(express);
/**
* Express configuration
*/
module.exports = function(app) {
app.configure('development', function(){
app.use(require('connect-livereload')());
// Disable caching of scripts for easier testing
app.use(function noCache(req, res, next) {
if (req.url.indexOf('/scripts/') === 0) {
res.header('Cache-Control', 'no-cache, no-store, must-revalidate');
res.header('Pragma', 'no-cache');
res.header('Expires', 0);
}
next();
});
app.use(express.static(path.join(config.root, '.tmp')));
app.use(express.static(path.join(config.root, 'app')));
app.set('views', config.root + '/app/views');
});
app.configure('production', function(){
app.use(express.favicon(path.join(config.root, 'public', 'favicon.ico')));
app.use(express.static(path.join(config.root, 'public')));
app.set('views', config.root + '/views');
});
app.configure(function(){
app.engine('html', require('ejs').renderFile);
app.set('view engine', 'html');
app.use(express.logger('dev'));
app.use(express.json());
app.use(express.urlencoded());
app.use(express.methodOverride());
app.use(express.cookieParser());
// Persist sessions with mongoStore
app.use(express.session({
secret: 'angular-fullstack secret',
store: new mongoStore({
url: config.mongo.uri,
collection: 'sessions'
}, function () {
console.log("db connection open");
})
}));
//use passport session
app.use(passport.initialize());
app.use(passport.session());
// Router (only error handlers should come after this)
app.use(app.router);
});
// Error handler
app.configure('development', function(){
app.use(express.errorHandler());
});
};

48
lib/config/passport.js Normal file
View File

@@ -0,0 +1,48 @@
'use strict';
var mongoose = require('mongoose'),
User = mongoose.model('User'),
passport = require('passport'),
LocalStrategy = require('passport-local').Strategy;
/**
* Passport configuration
*/
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findOne({
_id: id
}, '-salt -hashedPassword', function(err, user) { // don't ever give out the password or salt
done(err, user);
});
});
// add other strategies for more authentication flexibility
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'password' // this is the virtual field on the model
},
function(email, password, done) {
User.findOne({
email: email
}, function(err, user) {
if (err) return done(err);
if (!user) {
return done(null, false, {
message: 'This email is not registered.'
});
}
if (!user.authenticate(password)) {
return done(null, false, {
message: 'This password is not correct.'
});
}
return done(null, user);
});
}
));
module.exports = passport;

18
lib/controllers/api.js Normal file
View File

@@ -0,0 +1,18 @@
'use strict';
var mongoose = require('mongoose'),
Promotion = mongoose.model('Promotion'),
Thing = mongoose.model('Thing');
/**
* Get awesome things
*/
exports.awesomeThings = function(req, res) {
return Thing.find(function (err, things) {
if (!err) {
return res.json(things);
} else {
return res.send(err);
}
});
};

27
lib/controllers/index.js Normal file
View File

@@ -0,0 +1,27 @@
'use strict';
var path = require('path');
/**
* Send partial, or 404 if it doesn't exist
*/
exports.partials = function(req, res) {
var stripped = req.url.split('.')[0];
var requestedView = path.join('./', stripped);
res.render(requestedView, function(err, html) {
if(err) {
console.log("Error rendering partial '" + requestedView + "'\n", err);
res.status(404);
res.send(404);
} else {
res.send(html);
}
});
};
/**
* Send our single page app
*/
exports.index = function(req, res) {
res.render('index');
};

View File

@@ -0,0 +1,59 @@
'use strict';
var mongoose = require('mongoose'),
Promotion = mongoose.model('Promotion'),
passport = require('passport');
exports.list = function(req, res){
return Promotion.find(function(err, promotions){
if (!err){
return res.json(promotions);
}else{
return res.send(err);
}
});
};
exports.create = function (req, res, next) {
var newPromotion = new Promotion(req.body);
newPromotion.provider = 'local';
newPromotion.save(function(err) {
if (err) return res.json(400, err);
req.logIn(newPromotion, function(err) {
if (err) return next(err);
return res.json(req.promotion.promotionInfo);
});
});
};
exports.show = function (req, res, next) {
var promotionId = req.params.id;
Promotion.findById(promotionId, function (err, promotion) {
if (err) return next(err);
if (!promotion) return res.send(404);
res.send({ profile: promotion.profile });
});
};
exports.changePassword = function(req, res, next) {
var promotionId = req.promotion._id;
var oldPass = String(req.body.oldPassword);
var newPass = String(req.body.newPassword);
Promotion.findById(promotionId, function (err, promotion) {
if(promotion.authenticate(oldPass)) {
promotion.password = newPass;
promotion.save(function(err) {
if (err) return res.send(400);
res.send(200);
});
} else {
res.send(403);
}
});
};

View File

@@ -0,0 +1,28 @@
'use strict';
var mongoose = require('mongoose'),
passport = require('passport');
/**
* Logout
*/
exports.logout = function (req, res) {
req.logout();
res.send(200);
};
/**
* Login
*/
exports.login = function (req, res, next) {
passport.authenticate('local', function(err, user, info) {
var error = err || info;
if (error) return res.json(401, error);
req.logIn(user, function(err) {
if (err) return res.send(err);
res.json(req.user.userInfo);
});
})(req, res, next);
};

65
lib/controllers/users.js Normal file
View File

@@ -0,0 +1,65 @@
'use strict';
var mongoose = require('mongoose'),
User = mongoose.model('User'),
passport = require('passport');
/**
* Create user
*/
exports.create = function (req, res, next) {
var newUser = new User(req.body);
newUser.provider = 'local';
newUser.save(function(err) {
if (err) return res.json(400, err);
req.logIn(newUser, function(err) {
if (err) return next(err);
return res.json(req.user.userInfo);
});
});
};
/**
* Get profile of specified user
*/
exports.show = function (req, res, next) {
var userId = req.params.id;
User.findById(userId, function (err, user) {
if (err) return next(err);
if (!user) return res.send(404);
res.send({ profile: user.profile });
});
};
/**
* Change password
*/
exports.changePassword = function(req, res, next) {
var userId = req.user._id;
var oldPass = String(req.body.oldPassword);
var newPass = String(req.body.newPassword);
User.findById(userId, function (err, user) {
if(user.authenticate(oldPass)) {
user.password = newPass;
user.save(function(err) {
if (err) return res.send(400);
res.send(200);
});
} else {
res.send(403);
}
});
};
/**
* Get current user
*/
exports.me = function(req, res) {
res.json(req.user || null);
};

25
lib/middleware.js Normal file
View File

@@ -0,0 +1,25 @@
'use strict';
/**
* Custom middleware used by the application
*/
module.exports = {
/**
* Protect routes on your api from unauthenticated access
*/
auth: function auth(req, res, next) {
if (req.isAuthenticated()) return next();
res.send(401);
},
/**
* Set a cookie for angular so it knows we have an http session
*/
setUserCookie: function(req, res, next) {
if(req.user) {
res.cookie('user', JSON.stringify(req.user.userInfo));
}
next();
}
};

22
lib/models/promotion.js Normal file
View File

@@ -0,0 +1,22 @@
'use strict';
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
/** Promotion Schema */
var PromotionSchema = new Schema({
created: { type: Date, default: Date.now },
title: { type: String, default: '', trim: true, required: true },
user: { type: Schema.ObjectId, ref: 'User', required: true }
});
/* Indexes */
PromotionSchema.index({title: 1, user: 1}, {unique: true});
var _model = mongoose.model('Promotion', PromotionSchema);
/* Operations */
PromotionSchema.statics.findByTitle = function(title, cb){
console.log("findByTitle: " + title);
_model.findOne({title: title}, cb);
};

22
lib/models/thing.js Normal file
View File

@@ -0,0 +1,22 @@
'use strict';
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
/**
* Thing Schema
*/
var ThingSchema = new Schema({
name: String,
info: String,
awesomeness: Number
});
/**
* Validations
*/
ThingSchema.path('awesomeness').validate(function (num) {
return num >= 1 && num <= 10;
}, 'Awesomeness must be between 1 and 10');
mongoose.model('Thing', ThingSchema);

145
lib/models/user.js Normal file
View File

@@ -0,0 +1,145 @@
'use strict';
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
crypto = require('crypto');
var authTypes = ['github', 'twitter', 'facebook', 'google'];
/**
* User Schema
*/
var UserSchema = new Schema({
name: String,
email: String,
role: {
type: String,
default: 'user'
},
hashedPassword: String,
provider: String,
salt: String,
facebook: {},
twitter: {},
github: {},
google: {}
});
/**
* Virtuals
*/
UserSchema
.virtual('password')
.set(function(password) {
this._password = password;
this.salt = this.makeSalt();
this.hashedPassword = this.encryptPassword(password);
})
.get(function() {
return this._password;
});
// Basic info to identify the current authenticated user in the app
UserSchema.virtual('userInfo').get(function() {
return {
'name': this.name,
'role': this.role,
'provider': this.provider
};
});
// Public profile information
UserSchema.virtual('profile').get(function() {
return {
'name': this.name,
'role': this.role
};
});
/**
* Validations
*/
// Validate empty email
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');
// Validate empty password
UserSchema.path('hashedPassword').validate(function(hashedPassword) {
// if you are authenticating by any of the oauth strategies, don't validate
if (authTypes.indexOf(this.provider) !== -1) return true;
return hashedPassword.length;
}, 'Password cannot be blank');
// Validate email is not taken
UserSchema.path('email').validate(function(value, respond) {
var self = this;
this.constructor.findOne({email: value}, function(err, user) {
if(err) throw err;
if(user) {
if(self.id === user.id) return respond(true);
return respond(false);
}
respond(true);
});
}, 'The specified email address is already in use.');
var validatePresenceOf = function(value) {
return value && value.length;
};
/**
* Pre-save hook
*/
UserSchema.pre('save', function(next) {
if (!this.isNew) return next();
if (!validatePresenceOf(this.hashedPassword) && 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.hashedPassword;
},
/**
* Make salt
*
* @return {String}
* @api public
*/
makeSalt: function() {
return crypto.randomBytes(16).toString('base64');
},
/**
* Encrypt password
*
* @param {String} password
* @return {String}
* @api public
*/
encryptPassword: function(password) {
if (!password || !this.salt) return '';
var salt = new Buffer(this.salt, 'base64');
return crypto.pbkdf2Sync(password, salt, 10000, 64).toString('base64');
}
};
module.exports = mongoose.model('User', UserSchema);

36
lib/routes.js Normal file
View File

@@ -0,0 +1,36 @@
'use strict';
var api = require('./controllers/api'),
index = require('./controllers'),
users = require('./controllers/users'),
promotions = require('./controllers/promotions'),
session = require('./controllers/session');
var middleware = require('./middleware');
/**
* Application routes
*/
module.exports = function(app) {
// Server API Routes
app.get('/api/awesomeThings', api.awesomeThings);
app.get('/api/promotions', promotions.list);
app.post('/api/users', users.create);
app.put('/api/users', users.changePassword);
app.get('/api/users/me', users.me);
app.get('/api/users/:id', users.show);
app.post('/api/session', session.login);
app.del('/api/session', session.logout);
// All undefined api routes should return a 404
app.get('/api/*', function(req, res) {
res.send(404);
});
// All other routes to use Angular routing in app/scripts/app.js
app.get('/partials/*', index.partials);
app.get('/*', middleware.setUserCookie, index.index);
};