Let's follow these steps to add a User model to our application with secure password handling:
- First, we will replace our /models/authors.js model configuration with a slightly enhanced /models/users.js model configuration:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var userSchema = new Schema({
firstName: String,
lastName: String,
email: { type: String, unique: true, required: true },
password: { type: String, required: true },
role: String,
});
module.exports = mongoose.model('user', userSchema);
- Next, we will include bcrypt and configure a pre-save method to perform a password hashing function on our password before writing it to the database. We will also implement a custom function for comparing a provided password with this hash to check whether it's a match, without ever requiring us to save the actual password to our database:
var bcrypt = require('bcrypt');
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var userSchema = new Schema({
firstName: String,
lastName: String,
email: { type: String, unique: true, required: true },
password: { type: String, required: true },
role: String,
});
userSchema.pre('save', function(next) {
var user = this;
if (!user.isModified('password')) return next();
bcrypt.genSalt(function(error, salt) {
if (error) return next(err);
bcrypt.hash(user.password, salt, function(error, hash) {
if (error) return next(err);
user.password = hash;
next();
});
});
});
userSchema.methods.comparePassword = function(testPassword, callback) {
bcrypt.compare(testPassword, this.password, function(error, isMatch) {
if (error) return callback(error);
callback(null, isMatch);
});
};
module.exports = mongoose.model('user', userSchema);
- We will need to upgrade our /routes/api/login.js route configuration to use our new User model to validate that the username and password are still a match:
...
var Users = require('../../models/users');
...
var login = function(req, res, next) {
var username = req.body.username;
var password = req.body.password;
if (username && password) {
Users.findOne({ email: username }, function(error, user) {
if (error || !user) return res.status(401).json(authError('Invalid username or password for user authentication.'));
user.comparePassword(password, function(error, match) {
if (error) return res.status(401).json(authError('Invalid username or password for user authentication.'));
if (match) {
req.session.user = user;
next();
} else {
res.status(401).json(authError('Invalid username or password for user authentication.'));
}
});
});
} else {
res.status(401).json(authError('Must provide username or password for user authentication.'));
}
};
...
- Finally, we can update our /database.js mocks to use our new User model. We will also manually create an admin user that is guaranteed to have the same username and password:
...
var User = require('./models/users');
...
mongoose.connect('mongodb://localhost/mean-db').then(function() {
if (env == 'development') {
var faker = require('faker');
generateMock(Post, 30, function() {
var markdown = faker.lorem.sentences();
markdown += "## " + faker.lorem.sentence() + " ";
markdown += "[" + faker.lorem.words() + "](" + faker.internet.url() + ") ";
return {
title: faker.lorem.words(),
content: markdown,
published: faker.date.past(),
author: new User({
firstName: faker.name.firstName(),
lastName: faker.name.lastName(),
email: faker.internet.email(),
password: faker.internet.password(),
role: 'author'
})
}
});
User.deleteMany({}, function() {
var admin = new User({
firstName: faker.name.firstName(),
lastName: faker.name.lastName(),
email: '[email protected]',
password: 'Secret',
role: 'admin'
});
admin.save().then(function(user) {
console.log("created new admin: " + user.email);
});
});
}
}, function(error) {
console.error('failed to connect to MongoDB...', error);
});
module.exports = mongoose.connection;