Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mongoose/NextJS - Model is not defined / Cannot overwrite model once compiled

TL;DR EDIT: If you're coming from Google, this is the solution:

module.exports = mongoose.models.User || mongoose.model("User", UserSchema);

For the non-TL;DR answer, check accepted answer.

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

I'm working on a NextJS site which in the backend I'm using Mongoose and Express. Whenever I use signUp function, I get this error on backend:

{"name":"Test","hostname":"DESKTOP-K75DG72","pid":7072,"level":40,"route":"/api/v1/signup","method":"POST","errorMessage":"UserModel is not defined","msg":"","time":"2020-06-17T23:51:34.566Z","v":0}

I suspect this error is because I'm using the UserModel in other controllers. This error wasn't happening until I made a new controller. So my question is, how do I fix this issue/how do I use the same model in different controllers/middlewares?

I think the issue is related to node.js - Cannot overwrite model once compiled Mongoose this post, I was getting this error earlier but somehow managed to fix it.

EDIT: The error lies in models/User.js, in the pre save middleware, UserModel isn't defined at the level, how do I validate whether a user already exists with that username and if so, reject the new document?

At controllers/RegisterLogin.js [where the bug is happening]

const UserModel = require("../models/User");
// More packages...

async function signUp(req, res) {
  try {
    const value = await signUpSchema.validateAsync(req.body);
    const response = await axios({
      method: "POST",
      url: "https://hcaptcha.com/siteverify",
      data: qs.stringify({
        response: value.token,
        secret: process.env.HCAPTCHA,
      }),
      headers: {
        "content-type": "application/x-www-form-urlencoded;charset=utf-8",
      },
    });

    if (!response.data.success) {
      throw new Error(errorHandler.errors.HCAPTCHA_EXPIRED);
    }

    const hashPassword = await new Promise((res, rej) => {
      bcrypt.hash(
        value.password,
        parseInt(process.env.SALTNUMBER, 10),
        function (err, hash) {
          if (err) rej(err);
          res(hash);
        }
      );
    });

    await UserModel.create({
      userName: value.username,
      userPassword: hashPassword,
      userBanned: false,
      userType: "regular",
      registeredIP: req.ip || "N/A",
      lastLoginIP: req.ip || "N/A",
    });

    return res.status(200).json({
      success: true,
      details:
        "Your user has been created successfully! Redirecting in 6 seconds",
    });
  } catch (err) {
    const { message } = err;
    if (errorHandler.isUnknownError(message)) {
      logger.warn({
        route: "/api/v1/signup",
        method: "POST",
        errorMessage: message,
      });
    }

    return res.status(200).json({
      success: false,
      details: errorHandler.parseError(message),
    });
  }
}

module.exports = { signUp };

At controllers/Profile.js [if I use UserModel here, it breaks everything]

const UserModel = require("../models/User");
//plus other packages...

async function changePassword(req, res) {
  try {
    const value = await passwordChangeSchema.validateAsync(req.body);

    const username = await new Promise((res, rej) => {
      jwt.verify(value.token, process.env.PRIVATE_JWT, function (err, decoded) {
        if (err) rej(err);
        res(decoded.username);
      });
    });

    const userLookup = await UserModel.find({ userName: username });

    if (userLookup == null || userLookup.length == 0) {
      throw new Error(errorHandler.errors.BAD_TOKEN_PROFILE);
    }

    const userLookupHash = userLookup[0].userPassword;

    try {
      // We wrap this inside a try/catch because the rej() doesnt reach block-level
      await new Promise((res, rej) => {
        bcrypt.compare(value.currentPassword, userLookupHash, function (
          err,
          result
        ) {
          if (err) {
            rej(errorHandler.errors.BAD_CURRENT_PASSWORD);
          }
          if (result == true) {
            res();
          } else {
            rej(errorHandler.errors.BAD_CURRENT_PASSWORD);
          }
        });
      });
    } catch (err) {
      throw new Error(err);
    }

    const hashPassword = await new Promise((res, rej) => {
      bcrypt.hash(
        value.newPassword,
        parseInt(process.env.SALTNUMBER, 10),
        function (err, hash) {
          if (err) rej(err);
          res(hash);
        }
      );
    });

    await UserModel.findOneAndUpdate(
      { userName: username },
      { userPassword: hashPassword }
    );
    return res.status(200).json({
      success: true,
      details: "Your password has been updated successfully",
    });
  } catch (err) {
    const { message } = err;
    if (errorHandler.isUnknownError(message)) {
      logger.warn({
        route: "/api/v1/changepassword",
        method: "POST",
        errorMessage: message,
      });
    }

    return res.status(200).json({
      success: false,
      details: errorHandler.parseError(message),
    });
  }
}

At models/User.js

const mongoose = require("mongoose");
const errorHandler = require("../helpers/errorHandler");

const Schema = mongoose.Schema;

const UserSchema = new Schema({
  userName: String,
  userPassword: String,
  userBanned: Boolean,
  userType: String,
  registeredDate: { type: Date, default: Date.now },
  registeredIP: String,
  lastLoginDate: { type: Date, default: Date.now },
  lastLoginIP: String,
});

UserSchema.pre("save", async function () {
  const userExists = await UserModel.find({
    userName: this.get("userName"),
  })
    .lean()
    .exec();
  if (userExists.length > 0) {
    throw new Error(errorHandler.errors.REGISTER_USERNAME_EXISTS);
  }
});

module.exports = mongoose.model("User", UserSchema);
like image 367
Stan Loona Avatar asked Jun 18 '20 00:06

Stan Loona


2 Answers

I've managed to fix it. There were two problems here.

1) "UserModel" variable doesn't exist in the pre middleware. Solved by instantiating this.constructor which apparently solves the issue (will need further testing)

2) There's apparently a issue with NextJS building everything, it seems like it's trying to create a new model whenever I use any function from UserModel. This is fixed exporting the already created model

const mongoose = require("mongoose");
const errorHandler = require("../helpers/errorHandler");

const Schema = mongoose.Schema;

const UserSchema = new Schema({
  userName: String,
  userPassword: String,
  userBanned: Boolean,
  userType: String,
  registeredDate: { type: Date, default: Date.now },
  registeredIP: String,
  lastLoginDate: { type: Date, default: Date.now },
  lastLoginIP: String,
});

UserSchema.pre("save", async function () {
  try {
    const User = this.constructor;
    const userExists = await User.find({
      userName: this.get("userName"),
    })
      .lean()
      .exec();
    if (userExists.length > 0) {
      throw new Error(errorHandler.errors.REGISTER_USERNAME_EXISTS);
    }
  } catch (err) {
    throw new Error(errorHandler.errors.REGISTER_USERNAME_EXISTS);
  }
});

module.exports = mongoose.models.User || mongoose.model("User", UserSchema);
like image 157
Stan Loona Avatar answered Nov 09 '22 07:11

Stan Loona


For me it was simply adding the last line of Stan Loona's answer:

module.exports = mongoose.models.User || mongoose.model("User", UserSchema);
like image 3
Trey Avatar answered Nov 09 '22 07:11

Trey