1

I have an Express.js route to retrieve a user from my MongoDB database by username and password. My goal is to take a username and password from the request body, find the user via username, then verify that the password matches. If the password matches I would like to return the user object and a 200 status code, if the passwords do not match, I would like to return a 404 with an error message.

Currently my route is working, but the user is always returned, even if the passwords do not match. I am checking if they match and throwing an error, but it seems like it is not throwing as I would have expected it to. Ideally, I would like to throw an error if the passwords do not match in my service call, and catch it in my route to return a proper status code.

I have attempted catching the error in my route and returning a rejected promise from the service, but the user is still returned. My hunch is that the callback that findOne accepts is ran after the record is returned?

I get an UnhandledPromiseRejectionWarning in the console which corresponds to the line I am throwing an error if passwords do not match:

app_1            | GET /user 200 221 - 4.297 ms
app_1            | (node:257) UnhandledPromiseRejectionWarning: Error: Password does not match
app_1            |     at /usr/src/app/services/userService.ts:17:37
app_1            |     at Generator.next (<anonymous>)
app_1            |     at fulfilled (/usr/src/app/services/userService.ts:5:58)

server.ts

NOTE I am renaming find on import to findUser via find as findUser

app.get('/user', authenticateToken, async (req: Request, res: Response): Promise<void> => {
    const user = await findUser(req.body.username, req.body.password);
    if (!user) {
      res.status(404).send();
      return;
    }
    res.status(200).send({ user });
});

userService.ts

const find = (username: string, password: string) => {
    return User.findOne({ username }, async (err: Error, user: any) => {
        if (err) throw err;
        const passwordMatches = await user.validatePassword(password);
        if (!passwordMatches) throw Error("Password does not match")
    });
}

user.ts (User mongoose model)

import mongoose from 'mongoose';
import bcrypt from 'bcrypt';

interface User {
  username: string;
  password: string;
  isModified: (string: any) => boolean;
}
const SALT_WORK_FACTOR = 10;
const userSchema = new mongoose.Schema({
  username: { type: String, required: true, index: { unique: true } },
  password: {
    type: String,
    unique: false,
    required: true
  }
},
{ timestamps: true }
);

userSchema.pre('save', function(next) {
  const user = this as unknown as User;
  if (!user.isModified('password')) return next();
  bcrypt.genSalt(SALT_WORK_FACTOR, (err: any, salt: any) => {
      if (err) return next(err);

      bcrypt.hash(user.password, salt, function(err: any, hash: any) {
          if (err) return next(err);

          user.password = hash;
          next();
      });
  });
});

userSchema.methods.validatePassword = function (pass: string) {
  return bcrypt.compare(pass, this.password);
};

const User = mongoose.model('User', userSchema);

export { User };

Riza Khan
  • 2,712
  • 4
  • 18
  • 42

1 Answers1

1

In my userService I changed the way I queried with findOne to resolve this. I got this idea from reading Does mongoose findOne on model return a promise?

const find = (username: string, password: string) => {
  return User.findOne({ username }).exec().then((user: any) => {
    const passwordMatches = user.validatePassword(password);
    if (!passwordMatches) throw Error("Password does not match")
    return user;
  });
}