Created
January 20, 2017 06:41
-
-
Save brianlovin/1a68442b83dd18354d1bcbe946977268 to your computer and use it in GitHub Desktop.
User Model
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var mongoose = require('mongoose'), | |
Schema = mongoose.Schema, | |
bcrypt = require('bcrypt-nodejs'), | |
SALT_WORK_FACTOR = 10, | |
// these values can be whatever you want - we're defaulting to a | |
// max of 5 attempts, resulting in a 2 hour lock | |
MAX_LOGIN_ATTEMPTS = 5, | |
LOCK_TIME = 2 * 60 * 60 * 1000; | |
var UserSchema = new Schema({ | |
email: { type: String, required: true, index: { unique: true } }, | |
password: { type: String, required: true }, | |
name: { type: String }, | |
admin: { type: Boolean }, | |
public: { type: Boolean, default: false }, | |
loginAttempts: { type: Number, required: true, default: 0 }, | |
lockUntil: { type: Number } | |
}); | |
UserSchema.virtual('isLocked').get(function() { | |
// check for a future lockUntil timestamp | |
return !!(this.lockUntil && this.lockUntil > Date.now()); | |
}); | |
UserSchema.pre('save', function(next) { | |
var user = this; | |
// only hash the password if it has been modified (or is new) | |
if (!user.isModified('password')) return next(); | |
// generate a salt | |
bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) { | |
if (err) return next(err); | |
// hash the password using our new salt | |
bcrypt.hash(user.password, salt, null, function (err, hash) { | |
if (err) return next(err); | |
// set the hashed password back on our user document | |
user.password = hash; | |
next(); | |
}); | |
}); | |
}); | |
UserSchema.methods.comparePassword = function(candidatePassword, cb) { | |
bcrypt.compare(candidatePassword, this.password, function(err, isMatch) { | |
if (err) return cb(err); | |
cb(null, isMatch); | |
}); | |
}; | |
UserSchema.methods.incLoginAttempts = function(cb) { | |
// if we have a previous lock that has expired, restart at 1 | |
if (this.lockUntil && this.lockUntil < Date.now()) { | |
return this.update({ | |
$set: { loginAttempts: 1 }, | |
$unset: { lockUntil: 1 } | |
}, cb); | |
} | |
// otherwise we're incrementing | |
var updates = { $inc: { loginAttempts: 1 } }; | |
// lock the account if we've reached max attempts and it's not locked already | |
if (this.loginAttempts + 1 >= MAX_LOGIN_ATTEMPTS && !this.isLocked) { | |
updates.$set = { lockUntil: Date.now() + LOCK_TIME }; | |
} | |
return this.update(updates, cb); | |
}; | |
// expose enum on the model, and provide an internal convenience reference | |
var reasons = UserSchema.statics.failedLogin = { | |
NOT_FOUND: 0, | |
PASSWORD_INCORRECT: 1, | |
MAX_ATTEMPTS: 2 | |
}; | |
UserSchema.static('getAuthenticated', function(email, password, cb) { | |
this.findOne({ email: email }, function(err, user) { | |
if (err) return cb(err); | |
// make sure the user exists | |
if (!user) { | |
return cb(null, null, reasons.NOT_FOUND); | |
} | |
// check if the account is currently locked | |
if (user.isLocked) { | |
// just increment login attempts if account is already locked | |
return user.incLoginAttempts(function(err) { | |
if (err) return cb(err); | |
return cb(null, null, reasons.MAX_ATTEMPTS); | |
}); | |
} | |
// test for a matching password | |
user.comparePassword(password, function(err, isMatch) { | |
if (err) return cb(err); | |
// check if the password was a match | |
if (isMatch) { | |
// if there's no lock or failed attempts, just return the user | |
if (!user.loginAttempts && !user.lockUntil) return cb(null, user); | |
// reset attempts and lock info | |
var updates = { | |
$set: { loginAttempts: 0 }, | |
$unset: { lockUntil: 1 } | |
}; | |
return user.update(updates, function(err) { | |
if (err) return cb(err); | |
return cb(null, user); | |
}); | |
} | |
// password is incorrect, so increment login attempts before responding | |
user.incLoginAttempts(function(err) { | |
if (err) return cb(err); | |
return cb(null, null, reasons.PASSWORD_INCORRECT); | |
}); | |
}); | |
}); | |
}); | |
module.exports = mongoose.model('User', UserSchema); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment