Skip to content

Instantly share code, notes, and snippets.

/user.js Secret

Created April 15, 2015 23:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save anonymous/7b85bcc133a9a90fd218 to your computer and use it in GitHub Desktop.
Save anonymous/7b85bcc133a9a90fd218 to your computer and use it in GitHub Desktop.
import mongoose from 'mongoose'
import bcrypt from 'bcrypt'
const SALT_FACTOR = 10
const MAX_LOGIN_ATTEMPTS = 6
const LOCK_TIME = 2 * 60 * 60 * 1000
let UserSchema = new mongoose.Schema({
username: { type: String, required: true, index: { unique: true } },
password: { type: String, required: true },
email: { type: String, required: true },
firstName: { type: String, required: true },
lastName: { type: String },
loginAttempts: { type: Number, required: true, default: 0 },
lockedUntil: { type: Number },
created_at: { type: Date },
updated_at: { type: Date }
})
UserSchema.pre('save', next => {
let now = new Date()
this.updated_at = now
if(!this.created_at) this.created_at = now
next()
})
UserSchema.pre('update', next => {
let now = new Date)
this.updated_at = now
next()
})
UserSchema.pre('save', true, (next, done) => {
let user = this
if(!user.isModified('password')) return next()
bcrypt.genSalt(SALT_FACTOR, (err, salt) => {
if(err) return done(err)
bcrypt.hash(user.password, salt, (err, hash) => {
if(err) return done(err)
user.password = hash
done()
})
})
next()
})
UserSchema.statics.loginErrors = {
USER_NOT_FOUND: 'USER_NOT_FOUND',
PASSWORD_INCORRECT: 'PASSWORD_INCORRECT',
MAX_ATTEMPTS: 'MAX_ATTEMPTS'
}
UserSchema.virtual('isLocked').get(() => {
return !!(this.lockedUntil && this.lockedUntil > Date.now())
})
UserSchema.virtual('fullName').get(() => {
return `${this.firstName} ${this.lastName}`
})
UserSchema.methods.checkPassword = (candidate, cb) => {
bcrypt.compare(candidate, this.password, (err, result) => {
if(err) return cb(err)
cb(null, result)
})
}
UserSchema.methods.incrementLoginAttempts = (cb) => {
if(this.lockedUntil && this.lockedUntil < Date.now()) {
return this.update({
$set: { loginAttempts: 1 },
$unset: { lockedUntil: 1 }
}, cb)
}
let updates = { $inc: { loginAttempts: 1 } }
if(this.loginAttempts + 1 >= MAX_LOGIN_ATTEMPTS && !this.isLocked) {
updates.$set = { lockedUntil: Date.now() + LOCK_TIME }
}
return this.update(updates, cb)
}
UserSchema.statics.authenticate = (username, password, cb) => {
this.findOne({ username: username }, (err, user) => {
if(err) return cb(err)
if(!user) return cb(null, null, UserSchema.loginErrors.USER_NOT_FOUND)
if(user.isLocked) {
return user.incrementLoginAttempts(err => {
if(err) return cb(err)
return cb(null, null, UserSchema.loginErrors.MAX_ATTEMPTS)
})
}
user.checkPassword(password, (err, result) => {
if(err) return cb(err)
if(result) {
if(!user.loginAttempts && !user.lockedUntil) return cb(null,user)
let updates = {
$set: { loginAttempts: 0 },
$unset: { lockedUntil: 1 }
}
return user.update(updates, err => {
if(err) return cb(err)
return cb(null, user)
})
} else {
user.incrementLoginAttempts(err => {
if(err) return cb(err)
return cb(null, null, UserSchema.loginErrors.PASSWORD_INCORRECT)
})
}
})
})
}
export default mongoose.model('User', UserSchema)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment