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();
});
Related
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
I use node.js to build the backend and persist the data in MongoDB. When I do a patch request, I can change the values of all the fields of other types except for the one of date type.
This is the backend code for the patch request.
router.patch('/:id', isLoggedIn, async (req, res) => {
try {
const updatedBooking = await Booking.updateOne(
{_id: req.params.id},
{
$set: {userEmail: req.body.userEmail},
$set: {shiftDate: req.body.shiftDate},
$set: {isMorningShift: req.body.isMorningShift}
}
);
res.json(updatedBooking);
} catch (err) {
res.send({message: err});
}
});
This is the database scheme:
const BookingSchema=mongoose.Schema({
userEmail:{
type:String,
required:true
},
shiftDate:{
type:Date,
required:true
},
isMorningShift:{
type: Boolean,
required: true
}
});
The objects in MongoDB look like this:
{
"_id": "61787183e67b6822180175f9",
"userEmail": "admin2#parantion.nl",
"isMorningShift": false,
"__v": 0,
"shiftDate": "2066-06-23T00:00:00.000Z"
}
What might be the problem?
Instead of multiple $set, update all the keys in one,
const updatedBooking = await Booking.updateOne(
{_id: req.params.id},
{
$set: {
userEmail: req.body.userEmail,
shiftDate: new Date(req.body.shiftDate),
isMorningShift: req.body.isMorningShift
}
}
);
#fractal397's answer will work fine. If you want a more cleaner code, you can use this.
const bookingId = req.params.id;
const payload =
userEmail: req.body.userEmail,
shiftDate: new Date(req.body.shiftDate),
isMorningShift: req.body.isMorningShift
}
const booking = await Booking.findByIdAndUpdate(bookingId, payload);
P.S. - After Mongoose 4.0, new value for findByIdAndUpdate has been changed to false by default. So in this operation, data will be updated in the database but it will return the old value booking. To get updated value in response too, you will have to do -
const booking = await Booking.findByIdAndUpdate(bookingId, payload, { new : true });
Change the line:
$set: {shiftDate: req.body.shiftDate}
to
$set: {shiftDate: new Date(req.body.shiftDate)}
or
$set: {shiftDate: new Date()} //for todays date in your local format
This works:
I tested this with express like so:
app.get('/updateOne', async (req, res) => {
//get data through query params in url
const id = req.query.id;
const date = req.query.date;
//connect to db and collection
//1 connect
//2 set db and collection
const client = await MongoClient.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
const collection = client.db("sample_airbnb").collection("listingsAndReviews");
//update function for date field
try {
const updatedBooking = await collection.updateOne(
{_id: id},
{
$set: {name: new Date(date)} //2066-06-23T00:00:00.000Z
}
);
res.json(updatedBooking);
} catch (err) {
res.send({'message': err});
}
})
Response:
{
"acknowledged": true,
"modifiedCount": 1,
"upsertedId": null,
"upsertedCount": 0,
"matchedCount": 1
}
And updated data in Mongoscloud:
_id
:
"100009690"
name
:
2066-06-23T00:00:00.000+00:00
The I called the endpoint like so:
http://localhost:5000/updateOne?id=100009690&date=2066-06-23T00:00:00.000Z and you see it's the same date format you say you expect.
Can you update your OP and show us the exact format you are passing in?? DO a console.log(req.body.shiftDate) on line 7 just before you pass it. I suspect here is where the issue is.
Obviously I shouldn't add dates to names field but this is purely for a quick test.
If updating multiple fields I'd with:
//update function
try {
const updatedBooking = await collection.updateOne(
{_id: id},
{
$set: {
name: name,
email: email,
lastShift: new Date(date)
}
}
);
res.json(updatedBooking);
} catch (err) {
res.send({'message': err});
}
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);
});
I have a mongoose Schema like this:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const CitizenSchema = new Schema({
SSN: {
type: Number,
required: true,
unique: true,
},
firstName: {
type: String,
required: true,
},
lastName: {
type: String,
required: true,
}
});
const Citizen = mongoose.model('Citizen', CitizenSchema);
module.exports = Citizen;
and I have a route in my express app that is like this:
router.get('/', (req, res) => {
if (Object.keys(req.query).length === 0) {
// Get all the citizens if there is no query
Citizen.find({})
.select('-SSN')
.then((citizens) => res.send(citizens));
} else {
const query = req.query;
// Add case insensitive to all queries
for (const q in query) {
query[q] = {
$regex: new RegExp(query[q], 'i')
};
}
Citizen.find(query)
.select('-SSN')
.then((citizens) => res.send(citizens))
.catch((err) => res.status(400).send({ msg: 'Bad query request' }));
}
});
So what I do is that if there is not a query, I return all the citizens and if there is a query, I return the query result.
For example if I send a http request to my route with a GET request like http://localhost:5000/api/citizens/, I get all the citizens. And if I send it with a query like http://localhost:5000/api/citizens/?lastName=Doe&firstName=john, I only get the citizens with the first name as John and last name as Doe.
So my problem is that if I try and do a request like http://localhost:5000/api/citizens/?lastName=Doe&q=test, I get an empty array and not the citizens that have Doe as their last name. The query q makes it so that the Citizen.find() does not work properly and does not return the result that I am waiting for.
I will be appreciate it if someone could help me fix this.
You can try using $or in your queries: https://kb.objectrocket.com/mongo-db/or-in-mongoose-1018
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.