(MongoDB Express Angular Node)
(actual Google Image result for "men stack")
var mongoose = require('mongoose');
var bcrypt = require('bcrypt');
var async = require('async');
var config = require('../config');
var userSchema = new mongoose.Schema({
email: { type: String, unique: true, required: true },
password: { type: String, required: true },
verificationToken: { type: String, unique: true, required: true },
isVerified: { type: Boolean, required: true, default: false },
passwordResetToken: { type: String, unique: true },
passwordResetExpires: Date,
loginAttempts: { type: Number, required: true, default: 0 },
lockUntil: Date,
role: String
});
userSchema.virtual('isLocked').get(function() {
return !!(this.lockUntil && this.lockUntil > Date.now());
});
userSchema.methods.comparePassword = function(passwordToCompare, callback) {
var user = this;
async.waterfall([
function(waterfallCb) {
bcrypt.compare(passwordToCompare, user.password, function(err, isMatch) {
if (err) {
return waterfallCb(err);
}
waterfallCb(null, isMatch);
});
},
function(isMatch, waterfallCb) {
if (bcrypt.getRounds(user.password) !== config.login.passwordHashRounds) {
user.password = passwordToCompare;
user.save(function(err, user) {
if (err) {
return waterfallCb(err, isMatch);
}
waterfallCb(null, isMatch);
});
}
else {
waterfallCb(null, isMatch);
}
}
], function(err, isMatch) {
if (err) {
return callback(err);
}
callback(null, isMatch);
});
};
userSchema.methods.incrementLoginAttempts = function(callback) {
var lockExpired = !!(this.lockUntil && this.lockUntil < Date.now());
if (lockExpired) {
return this.update({
$set: { loginAttempts: 1 },
$unset: { lockUntil: 1 }
}, callback);
}
var updates = { $inc: { loginAttempts: 1 } };
var needToLock = !!(this.loginAttempts + 1 >= config.login.maxAttempts && !this.isLocked);
if (needToLock) {
updates.$set = { lockUntil: Date.now() + config.login.lockoutHours };
}
return this.update(updates, callback);
};
module.exports = mongoose.model('User', userSchema);
// models/user.js
userSchema.pre('save', function(next) {
var user = this;
if (!user.isModified('password')) {
return next();
}
bcrypt.genSalt(config.login.passwordHashRounds, function(err, salt) {
if (err) {
console.log(err);
req.flash('errors', { msg: 'There was an error generating your password salt.' });
return res.redirect('/');
}
bcrypt.hash(user.password, salt, function(err, hash) {
if (err) {
console.log(err);
req.flash('errors', { msg: 'There was an error hashing your password.' });
return res.redirect('/');
}
user.password = hash;
next();
});
});
});
// controllers/user.js
req.assert('email', 'Please provide a valid email address.').isEmail();
req.assert('password', 'Please enter a password of at least ' + config.login.minimumPasswordLength + ' characters.').len(config.login.minimumPasswordLength);
req.assert('confirmPassword', 'Your passwords must match.').equals(req.body.password);
var errors = req.validationErrors();
if (errors) {
req.flash('errors', errors);
return res.redirect('/user/register');
}
// lib/utility.js
exports.createRandomToken = function() {
return crypto.randomBytes(20).toString('hex');
};
// other files
var utility = require('../lib/utility');
var token = utility.createRandomToken();
// lib/utility.js
exports.constructUrl = function(req, path) {
return req.protocol + '://' + req.get('host') + path;
};
// other files
var utility = require('../lib/utility.js');
var token = utility.createRandomToken();
utility.constructUrl(req, '/user/reset-password/' + token);
*they made sending a simple email way more cumbersome :(
// lib/utility.js
exports.sendEmail = function(to, from, subject, contents, contentType, callback) {
var email = {
to: to,
from: from,
subject: subject,
};
email[contentType] = contents;
sendgrid.send(email, function(err, json) {
callback(err, json);
});
};
// other files
var utility = require('../lib/utility.js');
var token = utility.createRandomToken();
utility.sendEmail(req.body.email, config.email.sendFrom, 'Click this link: <a href="' + utility.constructUrl(req, '/user/reset-password/' + token) + '">reset password</a>', 'html', function(err, json) {
// handle response
});
// controllers/user.js
exports.register = {
post: function(req, res, next) {
req.assert('email', 'Please provide a valid email address.').isEmail();
req.assert('password', 'Please enter a password of at least ' + config.login.minimumPasswordLength + ' characters.').len(config.login.minimumPasswordLength);
req.assert('confirmPassword', 'Your passwords must match.').equals(req.body.password);
var errors = req.validationErrors();
if (errors) {
req.flash('errors', errors);
return res.redirect('/user/register');
}
var verificationToken = utility.createRandomToken();
var user = new User({
email: req.body.email,
password: req.body.password,
verificationToken: verificationToken,
role: req.body.role,
isVerified: false
});
var acl = require('../authorization').getAcl();
User.findOne({ email: req.body.email }, function(err, existingUser) {
if (existingUser) {
req.flash('errors', { msg: 'A user with that email address already exists. Please try another email address.' });
return res.redirect('/user/register');
}
user.save(function(err, newUser) {
if (err) {
console.log(err);
req.flash('errors', { msg: 'There was an error creating the user in the database. Please try again.' });
return res.redirect('/user/register');
}
acl.addUserRoles(newUser._id.toString(), req.body.role, function(err) {
if (err) {
console.log(err);
req.flash('errors', { msg: 'There was an error setting your roles in the database. Please contact an administrator.' });
return res.redirect('/');
}
utility.sendEmail(req.body.email, config.email.sendFrom, 'Email Verification Required', '<p>Before you can log in, you must verify your email address:</p><a href="' + utility.constructUrl(req, '/user/verify/' + verificationToken) + '">Verify your email address</a>', 'html', function(err, json) {
if (err) {
console.log(err);
req.flash('errors', { msg: 'There was an error sending your verification email. Please contact an administrator.' });
return res.redirect('/');
}
req.flash('info', { msg: 'Your account has been created, but you must verify your email before logging in.'});
res.redirect('/');
});
});
});
});
}
};
// authentication.js
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var User = require('./models/user');
var moment = require('moment-timezone');
var config = require('./config');
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
passport.use(new LocalStrategy({ usernameField: 'email' }, function(email, password, done) {
User.findOne({ email: email }, function(err, user) {
if (!user) {
return done(null, false, { msg: 'No user with the email ' + email + ' was found.' });
}
if (!user.isVerified) {
return done(null, false, { msg: 'Your email has not been verified. Check your inbox for a verification email.<p><a href="/user/verify-resend/' + email + '" class="btn waves-effect white black-text"><i class="material-icons left">email</i>Re-send verification email</a></p>' });
}
if (user.isLocked) {
return user.incrementLoginAttempts(function(err) {
if (err) {
return done(err);
}
return done(null, false, { msg: 'You have exceeded the maximum number of login attempts. Your account is locked until ' + moment(user.lockUntil).tz(config.server.timezone).format('LT z') + '. You may attempt to log in again after that time.' });
});
}
user.comparePassword(password, function(err, isMatch) {
if (isMatch) {
return done(null, user);
}
else {
user.incrementLoginAttempts(function(err) {
if (err) {
return done(err);
}
return done(null, false, { msg: 'Invalid password. Please try again.' });
});
}
});
});
}));
exports.isAuthenticated = function(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
req.flash('info', { msg: "You must be logged in to visit that page." });
res.redirect('/user/login');
};
// authorization.js
var acl = require('acl');
var mongoose = require('mongoose');
var config = require('./config');
acl = new acl(new acl.mongodbBackend(mongoose.connection.db, config.db.aclCollectionPrefix), { debug: function(string) { console.log(string); } });
module.exports = {
init: function() {
acl.addRoleParents('superAdmin', 'admin');
acl.addRoleParents('admin', 'user');
acl.allow([
{
roles: ['admin'],
allows: [
{
resources: '/user/list',
permissions: 'get'
}
]
},
{
roles: ['superAdmin'],
allows: [
{
resources: '/admin/list',
permissions: 'get'
}
]
}
]);
},
getAcl: function() {
return acl;
}
};
// app.js
mongoose.connect(config.db.uri);
mongoose.connection.on('connected', function(test) {
require('./authorization').init();
});
// lib/utility.js
exports.getUserId = function(req, res) {
if (typeof req.user !== 'undefined') {
return req.user.id;
}
return false;
};
// routes/user.js
var express = require('express');
var router = express.Router();
var userController = require('../controllers/user');
var utility = require('../lib/utility');
var authentication = require('../authentication');
var acl = require('../authorization').getAcl();
router.get('/list', acl.middleware(2, utility.getUserId), userController.list.get);
// Procfile
web: node ./bin/www
Heroku can be deployed in a few ways: