Bcryptjs Compare method always returns false? - node.js

Bit stumped.. my code is identical to a tutorial I'm following for this section. However, bcryptjs.compare is always returning false.
Database is mongodb and string length limit is set to 16mb from what I read so I dont think it has to do with that.
userModel.js
const mongoose = require('mongoose')
const bcrypt = require('bcryptjs')
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
isAdmin: {
type: Boolean,
required: true,
default: false
}
},{
timestamps: true
})
userSchema.methods.comparePW = async function(password) {
console.log(await bcrypt.compare(password, this.password))
return await bcrypt.compare(password, this.password)
}
module.exports = mongoose.model('User', userSchema)
userController.js
const userModel = require('../models/userModel')
const asyncHandler = require('express-async-handler')
const userAuth = asyncHandler(async(req, res) => {
const { email, password } = req.body
// check if reqbody pw and email matches userModel pw/email
const user = await userModel.findOne({ email })
if (user && (await user.comparePW(password))) {
res.send('match')
} else {
res.send('no match')
}
})
module.exports = { userAuth }
dummy user filler data in the database
const bcrypt = require('bcryptjs')
const users = [
{
name: 'Admin',
email: 'admin#test.com',
password: bcrypt.hashSync('admin123, 10'),
isAdmin: 'true',
},
{
name: 'Max Smith',
email: 'Max#test.com',
password: bcrypt.hashSync('admin123, 10'),
},
{
name: 'Jennifer Garnett',
email: 'Jen#test.com',
password: bcrypt.hashSync('admin123, 10'),
},
]
module.exports = users
using console.log, the bcrypt.compare method always returns false.
Strange as this is how the tutorial has it and it seems to be working for the instructor.
Using Postman when I run a post request with email "admin#test.com" and password: "admin123" it is return false every time.
I tried reimporting the dummy data and also reloading data on mongodb compass.
Not sure what to do at this point to fix this issue? Thoughts?

While hashing the password you are combining the password and salt strength into a string like this bcrypt.hashSync('admin123, 10') which should be like this bcrypt.hashSync('admin123', 10). If you want to work with the current situation then u need to enter password "admin123, 10" instead of "admin123".

Related

jest.spyOn "Number of calls: 0", but actual implementation still called

I want to spy on the hashPassword function in authController to see if it's called with the correct arguments from the user model, while keeping it's implementation.
number of calls 0
The mock function is being set but is not being called, meanwhile the original hashPassword function is being called. It seems that it can be tricky to make jest.spyOn work, but I think it should work as I am using 2 separate files.
logged controller and user
test
const request = require('supertest')
const app = require('../src/app.js')
const db = require('./db.js')()
const authController = require('../src/controllers/authController')
//in memory database
beforeAll(async () => await db.connect())
afterEach(async () => await db.clear())
afterAll(async () => await db.disconnect())
it('hashes the password', async () => {
const hashPassword = jest.spyOn(authController, 'hashPassword')
console.log(require('../src/controllers/authController'))
const user = await request(app)
.post('/auth/signup')
.send({ name: 'User', password: 'password', confirmPassword: 'password' })
console.log(user.body)
expect(hashPassword).toHaveBeenCalledWith('password')
})
controller
const bcrypt = require('bcrypt')
async function hashPassword(password) {
return await bcrypt.hash(password, 12)
}
function test() {
return 'not mocked'
}
module.exports = { hashPassword, test }
model
const mongoose = require('mongoose')
const { isEmail } = require('validator')
const { hashPassword } = require('../controllers/authController')
const userSchema = mongoose.Schema({
name: {
type: String,
unique: true,
required: [true, 'Username required'],
minLength: [4, 'Username should have at least 4 characters'],
},
email: {
type: String,
unique: true,
sparse: true, //ignore documents without the email field when determining uniqueness
validate: [isEmail, 'Please enter a valid email.'],
},
password: {
type: String,
required: [true, 'Password required'],
minLength: [6, 'Password should have at least 6 characters'],
},
confirmPassword: {
type: String,
required: true,
validate: [matches, 'The passwords do not match'],
},
hashedPassword: {
type: String,
select: false,
},
})
function matches() {
const { password, confirmPassword } = this
return password === confirmPassword
}
userSchema.post('validate', async function (doc) {
//hash password
this.hashedPassword = await hashPassword(this.password)
//remove plain password before saving
doc.password = undefined
doc.confirmPassword = undefined
})
const User = mongoose.model('User', userSchema)
module.exports = User
I am guessing the model file isn't using the mock version of hashPassword for some reason. However, if I simply use mock, it works as expected.
const request = require('supertest')
const app = require('../src/app.js')
const db = require('./db.js')()
const authController = require('../src/controllers/authController')
//in memory database
beforeAll(async () => await db.connect())
afterEach(async () => await db.clear())
afterAll(async () => await db.disconnect())
jest.mock('../src/controllers/authController')
it('hashes the password', async () => {
console.log(require('../src/controllers/authController'))
const user = await request(app)
.post('/auth/signup')
.send({ name: 'User', password: 'password', confirmPassword: 'password' })
console.log(user.body)
expect(authController.hashPassword).toHaveBeenCalled()
})
(Is it relevant that it says "[Function: hashPassword]" instead of "[Function: mockConstructor]")
logged controller
(no hashed password, meaning the model is using the mocked version of the function)
logged response
I am confused by why jest.mock works while jest.spyOn doesn't. I found many similar questions, but I haven't really found an answer.

How to implement multi roles user authentication using express and mongoose?

I'm trying to create a MERN app where there will be multiple roles like 'principle', 'teacher', 'student', 'guardian'. Primarily I have created userModel to register users and create a role key that will have the different role values pointing to other models (like teacherModel/ studentModel).
My userModel is like that
const mongoose = require('mongoose')
const userSchema = mongoose.Schema(
{
email: {
type: String,
required: [true, 'Please add an email'],
unique: true
},
password: {
type: String,
required: [true, 'Please add a password']
},
role: [
{
type: mongoose.Schema.ObjectId,
ref: 'Teacher'
},
{
type: mongoose.Schema.ObjectId,
ref: 'Student'
}
]
},
{
timestamps: true
}
)
module.exports = mongoose.model('User', userSchema)
and the teacherModel is
const mongoose = require('mongoose')
const teacherSchema = mongoose.Schema({
name: {
type: String,
required: [true, 'Please add your name']
},
teacherId: {
type: Number,
required: [true, 'Please add your teacher id']
}
})
module.exports = mongoose.model('Teacher', teacherSchema)
I have created an userConroller, I want to register a user with their name, email, password, and role. I have put the name into the individual role so that I can search teachers or students separately, not both. Here, is my userController
const asyncHandler = require('express-async-handler')
const bcrypt = require('bcryptjs')
const User = require('../models/userModel')
// #description Register a new user
// #route /api/users
// #access Public
const registerUser = asyncHandler(async (req, res) => {
const { email, password, role } = req.body
// Validation
if (!name || !email || !password || !role) {
res.status(400)
throw new Error('Please include all fields')
}
// Find if user already exists
const userExists = await User.findOne({ email })
if (userExists) {
res.status(400)
throw new Error('User already exists')
}
// Hash Password
const salt = await bcrypt.genSalt(10)
const hashedPassword = await bcrypt.hash(password, salt)
const user = await User.create({
email,
password: hashedPassword,
role,
})
if (user) {
res.status(201).json({
_id: user._id,
email: user.email,
role: user.role
})
} else {
res.status(400)
throw new error('Invalid user data')
}
})
When I try to register with postman, I get this error message,
"User validation failed: role.0: Cast to [ObjectId] failed for value
"[ 'teacher' ]" (type string) at path "role.0" because of
"CastError""
What have I messed up?

Circular Dependency Error for deleteMany MongoDB

I am writing the Model for my Web App API, and am getting the following circular dependency error:
Warning: Accessing non-existent property 'deleteMany' of module exports inside circular dependency
(Use node --trace-warnings ... to show where the warning was created)
.
Here is my code:
const validator = require('validator')
const bcrypt = require('bcrypt')
const jwt = require('jsonwebtoken')
const Task = require('./task')
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const userSchema = new Schema({
email: {
type: String,
unique: true,
required: true,
trim: true,
lowercase: true,
validate(value) {
if (!validator.isEmail(value)) {
throw new Error('Email is invalid.')
}
}
},
password: {
type: String,
required: true,
trim: true,
minLength: 8
},
name: {
type: String,
unique: true,
required: true,
trim: true
},
tokens: [{
token: {
type: String,
required: true
}
}]
})
userSchema.pre('save', async function(next) {
const user = this
if (user.isModified('password')) {
user.password = await bcrypt.hash(user.password, 8)
}
next() // run the save() method
})
userSchema.pre('deleteOne', {document: true, query: false}, async function(next) {
const user = this
await Task.deleteMany({owner: user._id})
next()
})
userSchema.methods.toJSON = function() {
const user = this
const userObject = user.toObject()
delete userObject.password
delete userObject.__v
delete userObject.tokens
return userObject
}
userSchema.methods.generateAuthToken = async function () {
const user = this
const token = jwt.sign({ _id: user._id.toString() }, process.env.JSON_WEB_TOKEN_SECRET)
user.tokens = user.tokens.concat({ token })
await user.save()
return token
}
userSchema.statics.findByCredentials = async (email, password) => {
const user = await User.findOne({email})
if (!user) {
throw new Error('Unable to login')
}
const isMatch = await bcrypt.compare(password, user.password)
if (!isMatch) {
throw new Error('Unable to login')
}
return user
}
userSchema.virtual('tasks', {
localField: '_id',
foreignField: 'owner',
ref: 'Task'
})
const User = mongoose.model('User', userSchema);
module.exports = User
Any idea what could be going wrong? I have checked my Node.js and MongoDB versions and updated them, but continue to get this same error when I try to delete. I can provide further details of my code if necessary. The problem area in question is the one leading with userScheme.pre('deleteOne'....

Why do I get an error when I try to save mongoose model?

I am trying to create controller for resetting user password in Node.JS.
The idea is to fetch the user from DB based on reset password token, do some validation update the relevant field and save it back to the DB.
However, I get an error when trying to save the updated user ("user.save is not a function").
What might be the reason?
I have a user model defined as follows:
const mongoose = require("mongoose");
const validator = require("validator");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, "Please enter valid name"],
maxLength: [30, "Your name cannot exceed 30 characters]"],
},
email: {
type: String,
required: [true, "Please enter valid email"],
unique: true,
validate: [validator.isEmail, "Please enter valid email address"],
},
password: {
type: String,
requires: [true, "Please enter your password"],
minLength: [6, "Your password must be at least 6 characters"],
select: false,
},
avatar: {
public_id: { type: String, required: true },
url: { type: String, required: true },
},
role: { type: String, default: "user" },
createdAt: { type: Date, default: new Date().now },
resetPasswordToken: { type: String },
resetPasswordExpire: { type: Date },
});
userSchema.pre("save", async function (next) {
if (!this.isModified("password")) {
next();
}
this.password = await bcrypt.hash(this.password, 10);
});
// check password matching
userSchema.methods.isPasswordMatched = async function (inputPassword) {
return await bcrypt.compare(inputPassword, this.password);
};
// return JSON Web Token
userSchema.methods.getJwtToken = function () {
return jwt.sign({ id: this.id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRESIN_TIME,
});
};
// Generate password token
userSchema.methods.getResetPasswordToken = function () {
// Generate Token
const resetToken = crypto.randomBytes(20).toString("hex");
// Hash token
this.resetPasswordToken = crypto
.createHash("sha256")
.update(resetToken)
.digest("hex");
// set expired time
this.resetPasswordExpire = new Date(Date.now() + 30 * 60 * 1000);
return resetToken;
};
module.exports = mongoose.model("User", userSchema);
When I try to reset user password I try the following:
// get the user document from db (make sure token and expiration time are valid)
let user = User.findOne({
resetPasswordToken: resetPasswordToken,
resetPasswordExpire: { $gt: Date.now() },
});
// update password
user.password = req.body.password;
user.resetPasswordToken = undefined;
user.resetPasswordExpire = undefined;
user.save();
sendToken(user, 200, res);
for some reason I get an error:
"errorMsg": "user.save is not a function"
What might be the problem?
Probably, user is null or undefined, so you should handle the user null condition.
Also findOne and save returns promise, so you need to add await keyword before them.
Also you have a typo in user schema password field, requires should be required .
let user = await User.findOne({
resetPasswordToken: resetPasswordToken,
resetPasswordExpire: { $gt: Date.now() },
});
if (user) {
// update password
user.password = req.body.password;
user.resetPasswordToken = undefined;
user.resetPasswordExpire = undefined;
await user.save();
sendToken(user, 200, res);
} else {
res.status(400).send("No user found");
}
If you get the user null, you need to fix your query in findOne.
use await keyword to get mongoose hydrating the response
let user = await User.findOne({
resetPasswordToken: resetPasswordToken,
resetPasswordExpire: { $gt: Date.now() },
});
// update password
user.password = req.body.password;
user.resetPasswordToken = undefined;
user.resetPasswordExpire = undefined;
user.save();
sendToken(user, 200, res);
or you can do it like this
await User.findOneAndUpdate({
resetPasswordToken: resetPasswordToken,
resetPasswordExpire: { $gt: Date.now() },
},{password : req.body.password, resetPasswordToken : undefined,
resetPasswordExpire : undefined,});

how to return the user email

I have created post route to store posts in the database. It's a protected route so user can store post only after entering the login details. When I post in postman, I've seen that the user email is not returned in the object. Even in the mongodb collection, I don't see the email associated with the post. How do I include the email as well with the post object. I don't want the user to enter the email again and again when posting because they have already logged in. So I kinda want to store the email automatically with the post. Hope I make sense. Can someone help me with this?
Right now the object is kinda stored like this in the posts collection in mongodb
_id: ObjectId("5f1a99d3ea3ac2afe5"),
text: "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. ",
user:ObjectId("5f1a99d3eac2c82afe5"),
age:20,
country:"India",
gender:"male",
date:2020-07-24T08:23:35.349+00:00,
__v:0
I want the email too in the above object.
Post model
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const PostSchema = new Schema ({
user: {
type: Schema.Types.ObjectId,
ref: 'users'
},
text: {
type: String,
required: true
},
name: {
type: String
},
email: {
type: String
}
,
age: {
type: Number,
required: true
},
gender: {
type: String,
required: true
},
country: {
type: String,
required: true
},
date: {
type: Date,
default: Date.now
}
})
module.exports = Post = mongoose.model('post', PostSchema)
post route
const express = require('express');
const router = express.Router();
const auth = require('../../middleware/auth')
const { check, validationResult} = require('express-validator');
const User = require('../../models/User')
const Post = require('../../models/Post')
router.post('/', [auth, [
check('text', 'Text is required').not().isEmpty()
]], async (req,res)=>{
const errors = validationResult(req);
if(!errors.isEmpty()){
return res.status(400).json({errors: errors.array()})
}
try {
const user = await (await User.findById(req.user.id)).isSelected('-password')
const newPost = new Post({
text: req.body.text,
name: user.name,
user: req.user.id,
age: req.body.age,
country: req.body.country,
gender: req.body.gender,
email: req.user.email // this email is not stored with the post and I want this to be automatically posted in the collection without the user having to type it again to save the post
})
const post = await newPost.save();
res.json(post);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error')
}
})
module.exports = router;
User model
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
date: {
type: Date,
default: Date.now
}
})
module.exports = User = mongoose.model('user', UserSchema);
Change isSelected to select
const user = await (await User.findById(req.user.id)).isSelected(password')
What I potentially see the problem here is, once you have grabed the object of user, you're still referring to req.user.email instead of user.email.
If that does not solve your problem, try to console.log the user returned from after User.findById
Update:
You can see here that isSelected returns boolean. So you're essentialy getting true for having password field in user. Also instead of req.user.email use user.email

Resources