Mongoose userSchema.pre("save") Not Working - javascript

I wrote a method to update a user in my application and everything works correctly there. In addition, I wrote some code that fires before the document is saved and it isn't functioning correctly.
The point of the code is to determine if the user modified their password. If they didn't, simply call next(). If they did, bcrypt will hash the password.
Here's the code in my controller where I'm doing the update:
// #desc Update user by ID
// #route PUT /api/users/:id
// #access Private
const updateUserById = asyncHandler(async (req, res) => {
// Destructure body content from request
const {
firstName,
lastName,
username,
email,
role,
manager,
learningStyle,
departments,
facility,
company,
isActive,
} = req.body;
// Search the database for the user
const user = await User.findById(req.params.id);
// Check to ensure the user was found. Else, respond with 404 error
if (user) {
// Update information accordingly
user.firstName = firstName || user.firstName;
user.lastName = lastName || user.lastName;
user.username = username || user.username;
user.email = email || user.email;
user.role = role || user.role;
user.manager = manager || user.manager;
user.learningStyle = learningStyle || user.learningStyle;
user.departments = departments || user.departments;
user.facility = facility || user.facility;
user.company = company || user.company;
user.isActive = isActive === undefined ? user.isActive : isActive;
// Save user to the database with updated information
const updatedUser = await user.save();
// Send the updated user to the client
res.json(updatedUser);
} else {
res.status(404);
throw new Error("User not found");
}
});
Here's my User model code:
import mongoose from "mongoose";
import bcrypt from "bcryptjs";
const userSchema = mongoose.Schema(
{
firstName: {
// The user's first name
required: true,
type: String,
trim: true,
},
lastName: {
// The user's last name
required: true,
type: String,
trim: true,
},
username: {
// The user's username - user can create their own
required: true,
type: String,
unique: true,
lowercase: true,
},
email: {
// The user's email
required: false,
trim: true,
lowercase: true,
unique: true,
type: String,
},
password: {
// The user's encrypted password (bcrypt hash)
required: true,
type: String,
},
role: {
// The user's role - drives what they're able to do within the application
required: true,
type: mongoose.Schema.Types.ObjectId,
ref: "Role",
},
manager: {
// The user's manager
type: mongoose.Schema.Types.ObjectId,
required: false,
ref: "User",
},
learningStyle: {
// The user's learning style (after assessment is taken)
type: mongoose.Schema.Types.ObjectId,
required: false,
ref: "LearningStyle",
},
departments: [
// The departments the user belongs to (used to drive what the user sees)
// Example: Finishing, Shipping, Printing
{
type: mongoose.Schema.Types.ObjectId,
required: false,
ref: "Department",
},
],
facility: {
// The facility the user works at. Example: Austin Facility
type: mongoose.Schema.Types.ObjectId,
required: false,
ref: "Facility",
},
company: {
// The company the user works for. Example: Microsoft
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: "Company",
},
isActive: {
required: true,
type: Boolean,
default: true,
},
},
{ timestamps: true }
);
// Match user's password using bcrypt
userSchema.methods.matchPassword = async function (enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
// Generate user's encrypted password on save
userSchema.pre("save", async function (next) {
// Check to see if password is modified. If it is, encrypt it. If not, execute next();
if (!this.isModified("password")) {
console.log("Does this run?");
next();
}
console.log("Does this run as well?");
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
});
const User = mongoose.model("User", userSchema);
export default User;
I have:
Added two console logs, both of which fire (see userSchema.pre("save")) in model.
Tried to prevent the encryption from firing by checking if password is modified
Tried dropping the entire users collection and starting over
Another application from a course with the exact same approach working fine
Notice in my controller I am NOT updating the password at all. Yet, every time I use Postman to send a PUT request and modify even the name, the password gets hashed again and both console logs fire.

The "save" middleware is calling next() but continuing on to complete the function. Use return or else to guard the rest of the code.
userSchema.pre("save", async function (next) {
// Check to see if password is modified. If it is, encrypt it. If not, execute next();
if (!this.isModified("password")) {
// Finish here
return next();
}
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
});

Related

How to perform user-input validation using if statements

I'm trying to perform user validation using if statements but on testing using post man, I keep on getting only the first return statement 'Email is required ' even after adding a valid email address and also an invalid email address. I have attached the user model, user controller logic and a picture of postman's response
user schema model
const mongoose = require('mongoose');
const { Schema } = mongoose
const userSchema = new Schema({
firstName: {
type: String,
required: true,
min: 3,
max: 20
},
lastName: {
type: String,
required: true,
min: 3,
max: 20
},
email: {
type: String,
required: true,
unique: true,
},
phoneNumber: {
type: String,
required: true,
},
password: {
type: String,
required: true,
min: 5
},
confirmPassword: {
type: String,
required: true,
min: 5
}
});
const User = mongoose.model('User', userSchema);
module.exports = User;
user.controller.js
module.exports.users = async(req, res) => {
try {
const email = await user.findOne({ email: req.body.email });
const firstName = await user.find({ firstName: req.body.firstName });
const lastName = await user.find({ lastName: req.body.lastName });
const password = await user.find({ password: req.body.password });
const confirmPassword = await user.find({ confirmPassword: req.body.confirmPassword });
const phoneNumber = await user.find({ phoneNumber: req.body.phoneNumber });
if (!email) return res.send('Email is required')
const filterEmail = /^([a-zA-Z0-9_\.\-])+\#(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
if (!filterEmail.test(email.value)) return
res.send('Please provide a valid email address');
if (email) return res.send('Email already exists');
if (!firstName) return res.send('First Name is required');
if (firstName.length < 3 || firstName.length > 20) return
res.send('First name must be at least 3 characters and less than 20 characters');;
if (!lastName) return res.send('Last Name is required');
if (lastName.length < 3 || lastName.length > 20) return
res.send('Last name must be at least 3 characters and less than 20 characters')
if (!password) return res.send('PassWord is required');
const filterPassword = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{5,15}$/;
if (!filterPassword.test(password.value)) return
res.send('Password must include at least one lowercase letter, one uppercase letter, one digit, and one special character');
if (!confirmPassword) return res.send(' Please confirm password');
if (password.value !== confirmPassword.value) return res.send('Passwords do not match');
if (!phoneNumber) return res.send('Phone Number is required');
phone(phoneNumber.value);
let User = new user(_.pick(req.body, ['firstName', 'lastName', 'email', 'phoneNumber', 'password']));
bcrypt.genSalt(10, async(err, salt) => {
if (err) throw err;
return user.password = await bcrypt.hash(user.password, salt);
});
await User.save();
} catch (err) {
res.status(500).send('Something went wrong');
console.warn(err);
}
}
Your controller is not good. You need to get the email, lastName and firstName from req.body.
const {email, lastName} = req.body
And then do the validation, which by the way will already happen in mongoose.
const email = await user.findOne({ email: req.body.email });
In this line you are looking in your DB for a user with email = req.body.email. But since there is no such a user it return your if statement
if (!email) return res.send('Email is required')
to get your user you only need to compare with one value if you set it to be unique in your schema, for instead user email.
const user = await user.findOne({ email });
In this case email was already destructed and if there is no user, you can create one.
you can use await user.create() and pass your values and hash the password with pre("save") in your schema.
UserSchema.pre("save", async function () {
if (!this.isModified("password")) return;
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
});
You can also validate user email in the schema
email: {
type: String,
required: [true, "Please provide email"],
match: [
/^(([^<>()[\]\\.,;:\s#"]+(\.[^<>()[\]\\.,;:\s#"]+)*)|(".+"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
"Please provide a valid email",
],
unique: true,
},
You can simply install the validator package npm i validator then call it in in the user model const { isEmail } = require('validator'); and finally type this in your schema instead of hardcoding the verification code in the function.
email: { type: String, required: true, validate: [isEmail], unique: true },
Instead of writing custom logic for validation Please add a validation layer after performing security checks
you can use Express validator, to perform all your validations in a smoother way, It will eliminate unnecessary logic and code lines and add structured code to validate all types inputs, nested objects, DB call for existing data handling. It will handle the response and response message on its own without hitting the controller
just hit
npm install --save express-validator
and you are ready to validate requests very easily and effectively
Beside other answers i thought one thing which is missing but yet it is very strong validation method in Node.JS is JOI package, you can define your validation schema and get rid of all other pain full if and else, example is following
const Joi = require('joi');
const schema = Joi.object().keys({
firstName: Joi.string().alphanum().min(3).max(20).required(),
lastName: Joi.string().alphanum().min(3).max(20).required(),
email: Joi.email().required(),
phoneNumber: Joi.string().length(10).pattern(/^[0-9]+$/).required(),
password: Joi.string().min(5).required(),
confirmPassword: Joi.any().valid(Joi.ref('password')).required()
});
const dataToValidate = {
firstName: 'chris',
lastName: 'John',
email: 'test#test.com'
}
const result = Joi.validate(dataToValidate, schema);
// result.error == null means valid
Without node script you can always test your JOI schema and your data object on this webpage

Node JS preventin users from deleting other user's products

I have a REST API built with Node JS and I'm currently using MongoDB as my database. I want to prevent the users from deleting another user's products and for this I checked if the userId from the decoded token is the same as the product userId.
Product schema
const mongoose = require("mongoose");
const productSchema = mongoose.Schema(
{
_id: mongoose.Schema.Types.ObjectId,
userId: mongoose.Schema.Types.ObjectId,
name: { type: String, required: true },
price: { type: Number, required: true },
productImage: { type: String, required: false },
category: {
type: mongoose.Schema.Types.ObjectId,
ref: "Category",
required: true
},
gender: { type: String, required: true }
},
{ timestamps: { createdAt: "created_at" } }
);
module.exports = mongoose.model("Product", productSchema);
The delete product method:
const id = req.params.productId;
Product.findById({ _id: id }).then((product) => {
if (product.userId != req.user._id) {
return res.status(401).json("Not authorized");
} else {
Product.deleteOne({ _id: id })
.exec()
.then(() => {
return res.status(200).json({
message: "Product deleted succesfully",
});
})
.catch((err) => {
console.log(err);
return res.status(500).json({
error: err,
});
});
}
});
};
As you guys see first I'm searching executing the findByID method to access the userId property of the product, then I'm comparing the userId from the response with the userId from the decoded token.
I don't think my method is very efficient since it's running both findById and deleteOne methods.
Can you help me with finding a better solution for this?
as Guy Incognito mentioned, what you are trying to do is an OK thing and you may want to keep it this way in case you want to send a 404 status stating the product they are trying to remove does not exist.
however, if you are trying to do it with only one request
Product.deleteOne({ _id: id, userId: req.user._id })
hope it helps!

Sequelize how to properly create a User Data with constraints?

This question does not seek answer as "best practice", but rather the more appropriate way compared to what I have below.
Basically I have a User model/table that has this associate with UserScore model/table.
User.belongsTo(models.UserScore, {
foreignKey: {
name: "userScoreId",
allowNull: false,
defaultValue: 1
},
as: "score",
targetKey: "id"
})
And then the UserScore model is defined as:
'use strict';
module.exports = (sequelize, DataTypes) => {
const UserScore = sequelize.define('UserScore', {
id: { type: DataTypes.INTEGER, allowNull: false, autoIncrement: true, unique: true, primaryKey: true },
score: DataTypes.INTEGER,
coins: DataTypes.INTEGER
}, {});
UserScore.associate = function (models) {
UserScore.hasMany(models.User, {
foreignKey: "userScoreId",
sourceKey: "id"
})
};
return UserScore;
};
The PROBLEM that I have is that I feel like I'm doing my Sequelize queries below incorrectly.
First I create UserScore assuming that I know the userId, using Facebook works because I can get id from Facebook. BUT NOT with signup with email and password! This is the first problem.
And after creating the UserScore, I then create a User data. This just feels so wrong. Although it works well with login with Facebook because like I said, I have the userId for creating UserScore, but what if I don't have it (e.g. using sign up with email and password)?
EDIT: The question is related to the error about constraints, because I tried making a User data without making a UserScore first. I can't it reproduce now because I've fixed it using UUId (https://www.npmjs.com/package/uuid-int).
AuthController.js
module.exports = {
// SIGN UP WITH EMAIL AND PASSWORD
signup: async (req, res, next) => {
const email = req.body.email
const password = req.body.password
if (!email || !password) {
return res.status(403).send({
message: "Error! Required parameters are: {email} and {password}."
})
}
// Check if there is a user with the same email
db.User.findOne({
where: { email: email }
})
.then(data => {
if (data) {
return res.status(409).send({
message: "Email is already in use."
})
}
// Create a new user...
const newUser = {
fbId: null,
email: email,
firstName: null,
lastName: null,
photoUrl: null
}
const newUserScore = {
score: 0,
userId: id, //// <---------- NO IDEA!!!
coins: 0
}
db.UserScore.create(newUserScore)
.then(userScoreData => {
db.User.create(newUser)
.then(data => {
console.log("Created new user! ✅")
// Generate the token
const token = signToken(newUser)
// Respond with token
res.status(200).json({ token })
})
.catch(err => {
console.log("Error creating a new user!!!." + err)
return res.status(409).send({
message: "An error has occured while creating a new user"
})
})
})
.catch(err => {
console.log("Error creating user score.")
done(err, null, err.message)
})
})
.catch(err => {
console.log(err)
res.status(500).send({
message: "An error has occured while retrieving data."
})
})
}
}
User Relations:
I tried making a User data without making a UserScore first.
The foreign key constraint you have defined between User and UserScore
will never allow you to add a UserScoreId in the table User that does not exist in the UserScore table, as this will violate
the SQL foreign key constraint. This is indeed a constraint !
From what I understand (correct me if I'm wrong),
you are modeling the fact that a user may have one or more score(s),
if so, it's the userId that should be in the UserScore table.
This way, if the user with the same email does not exist you can add the user first then add the user score.
So you have to change how you defined your sequelize associations
and set userId as a foreign key on the UserScore table.
Here's how you could define the associations
//will add userId to UserScore table
User.hasMany(UserScore, {
foreignKey: { name: "userId", allowNull: false, defaultValue: 1 },
as: "score",
targetKey: "id"
})
//same here, will add userId to UserScore table
UserScore.belongsTo(User, {
foreignKey: "userId",
sourceKey: "id"
})

Show inline field validation errors along with email sent message to an end user filling up a form

I'm working on an app being developed on Nodejs, Expressjs, Mongoose stack.
I'm building a form on submission of which if there are some validation errors[ want to use only mongoose validation, just to keep validation code at one place] then re-render these error messages back to form and if all fields are valid then send an email to end user if that email is not already available in mongoose.
So this form is very usual form except I'm doing rendering this form by expressjs and also want that enduser should get the info that there are field errors also send an email and notify the end user at the same time that email is in process of being sent, and if email already exists then notify the user instantly on form.
Some psuedocode which can help me to think in Nodejs way would be much appreciated. I'm coming from python/Django stack.
Below is email sender module which returns a promise object.
mailer.js
var path = require('path')
, templatesDir = path.resolve(__dirname, '..', 'email_templates')
, emailTemplates = require('email-templates')
, nodemailer = require('nodemailer')
, rsvp = require('rsvp')
;
var sendEmail = function (template_name, locals) {
return new rsvp.Promise(function(resolve,reject){
emailTemplates(templatesDir, function(err, template) {
if (err) {
return reject({error: err})
} else {
emailFrom = 'an_email_address',
mailer = {
host: 'some_amazon_instance',
port: 25,
service: 'SMTP', // Gmail, SMTP, Zoho
auth: {
user: 'credential',
pass: 'credential'
}
};
var transport = nodemailer.createTransport("SMTP", mailer);
var emailTo = emailFrom;
if (!locals.admin) {
emailTo = locals.email;
}
// Send a single email
template(template_name, locals, function(err, html, text) {
if (err) {
} else {
transport.sendMail({
from: emailFrom,
to: emailTo,
subject: locals.title,
html: html,
text: text
}, function(err, responseStatus) {
if (err) {
transport.close();
return reject({error: err});
} else {
resolve({result: responseStatus.message});
}
transport.close();
});
}
});
}
});
});
}
exports.sendEmail = sendEmail;
Below is the mongoose model to handle form data, validation and email sending.
invite.js
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose')
, Schema = mongoose.Schema
, sendEmail = require('meanio').loadConfig().sendEmail
, rsvp = require('rsvp')
;
var StudioInviteSchema = new Schema({
name: {
type: String, required: [true, 'name is required']
},
email: {
type: String,
index: true,
unique: true,
required: [true, 'email is required']
},
phone: {
type: Number,
index: true,
unique: true,
required: [true, 'phone is required']
},
address: {
type: String, required: [true, 'address is required']
},
email_template: {type: String, required: true},
title: {type: String, require: true}
});
StudioInviteSchema.path('phone').validate(function (value) {
return parseInt(value)!=NaN && value.toString().length == 10;
}, 'Enter a valid phone number');
StudioInviteSchema.path('email').validate(function (value) {
var re = /^(([^<>()[\]\\.,;:\s#\"]+(\.[^<>()[\]\\.,;:\s#\"]+)*)| (\".+\"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(value)
}, 'Enter a valid email');
StudioInviteSchema.methods = {
saveAndEmailToAdmin: function () {
var instance = this;
console.log(instance, this, 42);
return new rsvp.Promise(function (resolve, reject) {
instance.save(function (err) {
if (err) {
if (err.code == 11000) {
return reject({message: 'duplicate invite'})
}
return reject({message: err})
}
var admin_email_context = {
name: instance.name,
email: instance.email,
phone: instance.phone,
address: instance.address,
title: instance.title,
admin: true
}
var studio_email_context = {
name: instance.name,
message: "We'll contact you shortly",
email: instance.email,
title: 'invite received',
admin: false
}
var admin_email = sendEmail(instance.email_template, admin_email_context)
var studio_email = sendEmail('userinvite', studio_email_context)
rsvp.allSettled([admin_email, studio_email])
.then(function (array) {
console.log(array)
resolve({message: 'success'})
}, function (error) {
resolve({message: 'error'})
})
})
});
}
};
mongoose.model('StudioInvite', StudioInviteSchema);
And finally below is the controller of html view which invoke saveAndEmailToAdmin of invite.js model for form validation and email sending.
invite_controller.js
exports.studioInvite = function (req, res) {
var invite = new StudioInvite({
name: req.body.name,
email: req.body.email,
phone: req.body.phone,
address: req.body.address,
email_template: 'studioinvite',
title: 'Invite request from studio'
});
/*
TODO:
have to use notifications to send messages to end users
like invite is sent successfully or there is some duplicacy
*/
invite.saveAndEmailToAdmin()
.then(function (result) {
console.log(result)
return [ send success messages ]
}, function (error) {
console.log(error);
return [ send error messages]
});
return [ i dont want to return at this line but if email sending takes time then have to return a response ]
}
I've found that i can use blocking code that means i've to put the user on hold untill everything is done on server, also I can use socket.io to send realtime notifications to end users about validation messages and email sent notifications.
But solution doesn't seem good to me.

mongoose how to handle password encoding nicely?

I want to refactor my user schema. The main reason for this decision is that I do not want to worry about password and salt generation. So I would like to move the encoding logic from the pre save handler to a setter. Unfortunately I do not have access from a setter to other properties of the object (like salt).
So defaulting a salt does not work and encoding a password with a salt also not.
My current implementation is:
var userSchema = new mongoose.Schema({
username: {
type: String,
index: { unique: true, sparse: true },
required: true, lowercase: true, trim: true
},
email: {
type: String,
index: { unique: true, sparse: true },
required: true, lowercase: true, trim: true
},
salt: {
type: String,
select: false
},
password: {
type: String,
select: false
},
plainPassword: {
type: String,
select: false
}
});
// FIXME: password encoding only on change, not always
userSchema.pre('save', function(next) {
// check if a plainPassword was set
if (this.plainPassword !== '') {
// generate salt
crypto.randomBytes(64, function(err, buf) {
if (err) return next(err);
this.salt = buf.toString('base64');
// encode password
crypto.pbkdf2(this.plainPassword, this.salt, 25000, 512, function(err, encodedPassword) {
if (err) return next(err);
this.password = new Buffer(encodedPassword, 'binary').toString('base64');
this.plainPassword = '';
}.bind(this));
}.bind(this));
}
next();
});
// statics
userSchema.methods.hasEqualPassword = function(plainPassword, cb) {
crypto.pbkdf2(plainPassword, this.salt, 25000, 512, function(err, encodedPassword) {
if (err) return next(err);
encodedPassword = new Buffer(encodedPassword, 'binary').toString('base64');
cb((this.password === encodedPassword));
}.bind(this));
}
module.exports = mongoose.model('User', userSchema, 'Users');
Has somebody managed to move encryption to mongoose setters?
Regards, bodo
You DO have access to other properties from within the setter with the use of the this keyword. For example:
userSchema.path('pass').set(function(v) {
console.log(this); // Returns model instance
return v;
});
However, setters are unfit for your use case. As you probably know, HMAC-SHA1 is super expensive and therefore will block unless performed asynchronously. Mongoose setters require the function to return a value and there is no way to route the result of crypto.pbkdf2()'s callback to the return value of the setter function. This is a limitation of asynchronous javascript and not Mongoose itself: you can't wrap an async call within a sync function, as this destroys the nature of the async chain.
Setters are most widely used for simple string manipulations and data sanitization.
Here is a demo for encryption using only instance methods:
// Model method
userSchema.methods.hashPassword = function(pass, callback) {
// Generate salt (this should probably be async too)
var salt = this.salt = crypto.createHash('md5').update(Math.random().toString()).digest('hex');
// Salt and Hash password
crypto.pbkdf2(pass, salt, 25000, 512, callback);
});
// Implementation
var user = new User({
email: req.body.email
});
user.hashPassword(req.body.pass, function(err, hash){
user.pass = hash;
user.save();
});

Categories