Sequelize model custom function cannot be called from controller - node.js

I have a sequelize model this custom functions like so:
'use strict';
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const config = require('../../config');
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('User', {
id: {
type: DataTypes.STRING,
primaryKey: true
},
name: DataTypes.STRING,
email: DataTypes.STRING,
bio: DataTypes.STRING,
phone: DataTypes.STRING,
username: DataTypes.STRING,
password: {
type: DataTypes.STRING,
set(value){
this.setDataValue('password', bcrypt.hashSync(value, 10));
}
}
}, {});
User.generateJWT = function(id, username) {
return jwt.sign({
id: id,
username: username,
expiresIn: config.auth.exp
}, config.secret);
};
User.toAuthJson = async function() {
return {
name: this.name,
email: this.email,
bio: this.bio,
phone: this.phone,
username: this.username
};
};
User.validatePassword = function(password, passwordHash){
return bcrypt.compareSync(password, passwordHash);
};
User.isUniqueEmail = async function(email) {
return await User.findOne({where: {email}}) === null;
};
User.isUniqueUsername = async function(username) {
return await User.findOne({where: {username}}) === null;
};
User.isUniquePhone = async function(phone) {
return await User.findOne({where: {phone}}) === null;
};
User.associate = function(models) {
// associations can be defined here
};
return User;
};
and a controller like so:
const {User} = require('../database/models/');
module.exports.register = async (req, res, next) => {
try {
const isUniqueEmail = await User.isUniqueEmail(req.body.email);
if (!isUniqueEmail) return res.status(422).json({'message': 'email already exists'});
const isUniquePhone = await User.isUniquePhone(req.body.phone);
if (!isUniquePhone) return res.status(422).json({'message': 'phone already exists'});
const isUniqueUsername = await User.isUniqueUsername(req.body.username);
if (!isUniqueUsername) return res.status(422).json({'message': 'username already exists'});
const user = await User.create(req.body);
console.log(user.toAuthJson()); //an error occurs here
return res.status(201).json({user: user.toAuthJson()});
}catch (e) {
next(e);
}
};
when i try to access the toAuthJson function from this controller like this user.toAuthJson. "notice the small u." it throws an error TypeError: User.toAuthJson is not a function. I should be able to access it normally. help. thanks

User.toAuthJson is currently a class method. Like with the other functions, you'd need to call it like User.toAuthJson(user).
You're probably looking for an instance method, so you'd want to define it in the prototype instead:
User.prototype.toAuthJson = function() {
return {
name: this.name,
email: this.email,
bio: this.bio,
phone: this.phone,
username: this.username
};
};
Now you can call it on a User instance, like you were attempting to do:
console.log(user.toAuthJson());
Notice also that I omitted the async since this function doesn't do anything asynchronous.

Related

Sequelize class method works fine but instance method is not working

I've defined a user model in Sequelize, and also defined a custom class method and an instance method for it. I'm calling these two methods in my login api (which works fine). The problem is that the class method works perfectly, but the instance method results an error, and I cannot recognize what is wrong with my code. PLEASE HELP.
This is my user model and its methods:
const Sequelize = require("sequelize");
const sequelize = require("../db/db.config");
const bcrypt = require("bcryptjs");
const _ = require("lodash");
const jwt = require("jsonwebtoken");
const User = sequelize.define("user", {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
allowNull: false,
primaryKey: true,
},
first_name: {
type: Sequelize.STRING(50),
allowNull: false,
},
last_name: {
type: Sequelize.STRING(50),
allowNull: false,
},
email: {
type: Sequelize.STRING(50),
allowNull: false,
validate: {
isEmail: true,
},
},
password: {
type: Sequelize.STRING(100),
allowNull: false,
},
});
User.prototype.testMethod = function () {
console.log("THIS IS A TEST");
};
User.beforeCreate(async (user, options) => {
const hashedPassword = await bcrypt.hash(user.password, 10);
user.password = hashedPassword;
});
User.findByEmailAndPassword = async function (inputEmail, inputPassword) {
try {
const user = await User.findOne({ where: { email: inputEmail } });
if (user === null) {
return null;
}
const passwordMatch = await bcrypt.compare(inputPassword, user.password);
if (!passwordMatch) {
return null;
}
return _.pick(user, "id", "first_name", "last_name", "email");
} catch (error) {
console.log("FIND BY EMAIL AND PASSWORD ERROR: ", error);
}
};
module.exports = User;
And this is my login router:
const express = require("express");
const router = express.Router();
const User = require("../models/user.model");
router.post("/api/login", async (req, res) => {
try {
const user = await User.findByEmailAndPassword(
req.body.email,
req.body.password
);
console.log("USER: ", user);
await user.testMethod();
if (!user) {
return res.status(400).send({
errorMessage: "Username and password combination is not correct!",
});
}
return res.status(200).send(user);
} catch (error) {
res.status(400).send({ errorMessage: error });
}
});
module.exports = router;
Thanks.
First, the reason that you get the issue is that findByEmailAndPassword is returning the regular object from _.pick and you are defining the instance method for Sequelize instance. This instance method can be callable on Sequelize instance and not on regular object.
However, your goal is
What I'm trying to do here is to avoid sending user password in my response body.
defaultScope is great for this use case. It allows you to define some repetitive options on a model.
You can define your User model as
const User = sequelize.define("user", {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
allowNull: false,
primaryKey: true,
},
...
}, {
defaultScope: {
attributes: {
exclude: ['password']
}
}
});
Defining the defaultScope on the model, this will be applied to many Sequelize functions by default.
Scopes apply to .find, .findAll, .count, .update, .increment and .destroy.
I also tested that it also applied to .findByPk, .findOne.
So, how to use...
Call regular Sequelize findOne function.
const user = User.findOne({
where: {
email: req.body.email,
password: req.body.password
}
});
By default, since defaultScope is applied, this won't return password in response.
In some scenarios where you need to return the password, use unscoped to disable the defaultScope.
// This will return `password` in response.
User.unscoped().fineOne(...)
For reference: https://sequelize.org/master/manual/scopes.html

Error with setting hasOne function in Node js

I have created 2 table Token and users and i want to associate it. After associating i want to set the token value to my user and associate my userId with the token table. for this i have used setUser & addToken but my setUser is throwing error while addToken is working fine.
const { nanoid } = require('nanoid/async')
const argon2 = require('argon2')
const jwt = require('../lib/jwt')
module.exports = (sequelize, Sequelize, Token, Task) => {
const User = sequelize.define("users", {
age: {
type: Sequelize.INTEGER
},
name: {
type: Sequelize.STRING
},
email: {
type: Sequelize.STRING
},
password: {
type: Sequelize.STRING
}
}, {
defaultScope: {
attributes: { exclude: ['password'] },
},
scopes: {
withPassword: {
attributes: {}
}
}
});
User.beforeCreate(async (user, options) => {
const hashedPassword = await argon2.hash(user.password)
user.password = hashedPassword
})
User.prototype.generateToken = async function generateToken() {
const jwtid = await nanoid()
const token = jwt.sign({ sub: this.id }, { jwtid })
const userToken = await Token.create({ jti: jwtid })
await this.setUser(token)
await this.addToken(userToken)
return token
}
User.prototype.verifyPassword = async function verifyPassword(password) {
console.log('verify Password instance method', { password, hash: this.password })
return argon2.verify(this.password, password)
}
User.hasMany(Token)
Token.belongsTo(User)
User.hasMany(Task)
Task.belongsTo(User)
return User;
};
Please help me fix this so that my token table associate with user table

Mongoose Schema.method() is not working, and showing an error message

I am taking password input from the user and encrypting the password using crypto, then saving into the database. This is my code, here I am storing the encrypted password into the encry_password property that comes from the userSchema. But, this is giving me error that "this.securePassword" is not a function.
const mongoose = require("mongoose");
const crypto = require("crypto");
const { v1: uuidv1 } = require("uuid");
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true,
maxlength: 32,
trim: true,
},
lastname: {
type: String,
maxlength: 32,
trim: true,
},
email: {
type: String,
trim: true,
required: true,
unique: true,
},
usrinfo: {
type: String,
trim: true,
},
encry_password: {
type: String,
required: true
},
salt: String,
role: {
type: Number,
default: 0,
},
purchases: {
type: Array,
default: [],
},
}, { timestamps: true });
userSchema.virtual("password")
.set((password) => {
this._password = password;
this.salt = uuidv1();
this.encry_password = securePassword(password, uuidv1());
console.log(this.encry_password);
})
.get(() => {
return this._password;
});
// const authenticate = function (plainPassword, encry_password) {
// return securePassword(plainPassword) === encry_password;
// };
const securePassword = function (plainPassword, salt) {
if (!plainPassword) return "";
try {
return crypto.createHmac("sha256", salt).update(plainPassword).digest("hex");
} catch (error) {
return "";
}
};
module.exports = mongoose.model("User", userSchema);
Route for user signup
exports.signup = (req, res) => {
console.log(req.body);
const user = new User(req.body);
user.save((err, user) => {
if (err) {
console.log(err);
res.status(400).json({
err: "Note able to save the user in database"
});
} else {
res.json(user);
}
});
};
first of all, in this situation you shouldn't use virtual
Virtuals
Virtuals are document properties that you can get and set but that do not get persisted to MongoDB. The getters are useful for formatting or combining fields, while setters are useful for de-composing a single value into multiple values for storage.
but in the scope of virtual, this cannot access to method, you can not access to the method like your manner, it's a example of method usage in mongoose
const Animal = mongoose.model('Animal', animalSchema);
const dog = new Animal({ type: 'dog' });
dog.findSimilarTypes((err, dogs) => {
console.log(dogs); // woof
});
you can check the method documantation:
if you want just access to securePassword in your manner you can like this and delete method mongoose complately because this is not the place to use method:
UserSchema.virtual("password")
.set((password) => {
this._password = password;
this.salt = uuidv1();
console.log("This is running");
this.encry_password = securePassword(password, this.salt);
console.log(encry_password);
})
.get(() => {
return this._password;
});
const authenticate = function (plainPassword, encry_password) {
return securePassword(plainPassword) === encry_password;
};
const securePassword = function (plainPassword, salt) {
if (!plainPassword) return "";
try {
return crypto
.createHmac("sha256", salt)
.update(plainPassword)
.digest("hex");
} catch (error) {
return "";
}
};
if you want to create authenticate service, change your manner, and don't use virtual for password and use pre save
before saving information about users in db this tasks will be done
check the pre documentation
userSchema.pre("save", async function (next) {
try {
this.password = securePassword (plainPassword, salt);
} catch (error) {
console.log(error);
}
});
after created a hash password save informations like this :
const userSchema = new mongoose.Schema({
.
.
.
password: { //convert encry_password to password
type: String,
}
.
.
.
}, { timestamps: true });
//every time want to user save this method called
userSchema.pre('save', function (next) {
this.salt = uuidv1()
this.password = securePassword(this.password, this.salt)
next()
})
//for have a clean routes, you can create a static methods
userSchema.statics.Create = async (data) => {
let model = new User(data);
let resUser = await model.save(); //save your user
return resUser;
};
const securePassword = function (plainPassword, salt) {
if (!plainPassword) return "";
try {
return crypto.createHmac("sha256", salt).update(plainPassword).digest("hex");
} catch (error) {
return "";
}
};
let User = mongoose.model("User", userSchema)
module.exports = {User};
change controller like this :
let {User} = require("./path of user schema")
exports.signup = async (req, res) => {
try {
console.log(req.body);
const user = await User.create(req.body); //create a user
res.json(user);
} catch (error) {
console.log(err);
res.status(400).json({
err: "Note able to save the user in database",
});
}
};
NOTE : in req.body, name of password field, should be password
It looks like the scope of the securePassword function is defined inside userSchema, and you're trying to call it in userSchema.virtual.

How to return a value from an async function in node.js

I have this function that is supposed to authenticate a user and return a WT token:
async function authenticate({username, password}) {
userModel.find({username: username, password: password}, (err, doc) => {
if (doc[0]) {
const user = {
id: doc[0]._id,
username: doc[0].username,
password: doc[0].password,
firstName: doc[0].firstName,
lastName: doc[0].lastName,
role: doc[0].role
};
const token = jwt.sign({sub: user.id, role: user.role}, config.secret);
const {password, ...userWithoutPassword} = user;
return {
...userWithoutPassword,
token
};
}
});
}
userModel is a mongoose model. The return statement at the end seems not to return anything but doing console.log inside it, the values are filled correctly. Am I missing something? I am at the first attempts with node so if anything is not clear please ask.
I checked out the suggested previous solution but I don't understand how to apply it to my specific case.
You need to use async and await instead of using callbacks:-
async function authenticate({username, password}) {
try {
const doc = await userModel.findOne({username: username, password: password});
if (doc) {
const user = {
id: doc._id,
username: doc.username,
password: doc.password,
firstName: doc.firstName,
lastName: doc.lastName,
role: doc.role
};
const token = jwt.sign({sub: user.id, role: user.role}, config.secret);
const {password, ...userWithoutPassword} = user;
return {
...userWithoutPassword,
token
};
}
} catch(err) {
// handle error
}
}
Wrap userModel in Promise, then you can await it
async function authenticate({ username, password }) {
return await new Promise(function(resolve) {
userModel.find({ username: username, password: password }, (err, doc) => {
if (doc[0]) {
const user = {
id: doc[0]._id,
username: doc[0].username,
password: doc[0].password,
firstName: doc[0].firstName,
lastName: doc[0].lastName,
role: doc[0].role
};
const token = jwt.sign(
{ sub: user.id, role: user.role },
config.secret
);
const { password, ...userWithoutPassword } = user;
resolve({
...userWithoutPassword,
token
});
}
});
});
}
Mongoose 5.0 will use native promises by default if available,
otherwise no promises. You will still be able to set a custom promises
library using mongoose.Promise = require('bluebird');, however,
mpromise will not be supported.
Mongoose 5.x
async function authenticate({ username, password }) {
let doc = await userModel.find({ username: username, password: password }).exec(); // exec() convert query to a native Promise.
if (doc[0]) {
const user = {
id: doc[0]._id,
username: doc[0].username,
password: doc[0].password,
firstName: doc[0].firstName,
lastName: doc[0].lastName,
role: doc[0].role
};
const token = jwt.sign({ sub: user.id, role: user.role }, config.secret);
const { password, ...userWithoutPassword } = user;
return {
...userWithoutPassword,
token
};
}
// else ?
return null;
}

my nodejs bcrypt compare not working properly

am building an app with nodes qraphQl using apollo, am trying to do a login page, but ater signing up and and i try to sign in, my bcrypt would always return false,
in my user model
import bcrypt from 'bcryptjs';
const user = (sequelize, DataTypes) => {
const User = sequelize.define('user', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
unique: true,
primaryKey: true,
field: 'id'
},
fullname: DataTypes.STRING,
username: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notEmpty: true,
},
},
email: {
type: DataTypes.STRING,
allowedNull: false,
validate: {
notEmpty: true,
isEmail: true,
}
},
password: {
type: DataTypes.STRING,
allowedNull: false,
validate: {
notEmpty: true,
len: [7, 42],
},
},
role: {
type: DataTypes.ENUM,
values: ['ADMIN', 'INSTRUCTOR', 'STUDENT'],
defaultValue: 'STUDENT'
}
});
User.beforeCreate(async user => {
user.password = await user.generatePasswordHash()
});
User.beforeSave(async user => {
user.password = await user.generatePasswordHash()
});
User.prototype.generatePasswordHash = async function() {
const saltRounds = 10;
return await bcrypt.hash(this.password, saltRounds)
};
User.prototype.validatePassword = async function(password) {
return await bcrypt.compare(password, this.password);
};
User.associate = models => {
User.hasMany(models.Message, { onDelete: 'CASCADE' });
};
User.findByLogin = async login => {
let user = await User.findOne({
where: { username: login },
});
if (!user) {
user = await User.findOne({
where: { email: login },
});
}
return user;
};
return User;
};
export default user;
And in my users resolver, here is the code
import { combineResolvers } from 'graphql-resolvers';
import Joi from 'joi'
import { isAuthenticated, isAdmin } from './authorization';
import {SignUp, SignIn} from '../functions/joi'
import {createToken} from '../functions/jwt'
export default {
Mutation: {
signUp: async (parent, { username, fullname, email, password, Rpassword}, { models, secret }) => {
if(password !== Rpassword){
return new Error('Password did not match')
}
var thejoi = { username, fullname, email, password }
const checkUserEm = await models.User.find({ where: { email: email }})
if (checkUserEm) {
return new Error('Email address already Exist')
}
const checkUserUs = await models.User.find({ where: { username: username }})
if (checkUserUs) {
return new Error('Username already Exist')
}
await Joi.validate(thejoi, SignUp, {abortEarly:false})
const user = await models.User.create({
username,
fullname,
email,
password,
role:'STUDENT'
});
return { token: createToken(user) };
},
signIn: async (parent, { login, password }, { models, secret }, ) => {
var varrh = { password }
await Joi.validate(varrh, SignIn, {abortEarly:false})
const user = await models.User.findByLogin(login);
if (!user) {
return new Error('No user found with this login credentials.');
}
const isValid = await user.validatePassword(password);
if (!isValid) {
return new Error('Invalid password .');
}
return { token: createToken(user) };
}
},
User: {
messages: async (user, args, { models }) => {
return await models.Message.findAll({
where: {
userId: user.id
}
});
},
},
}
pls am really confused because its it suppose to work, i have searched google but it didnt help me, pls how can i solve this issue thanks
inside generatePasswordHash you referencing to this, but you also using arrow functions for beforeCreate and beforeSave hooks.
Two options:
Do not use arrow function for beforeCreate and beforeSave hooks.
User.beforeCreate(async function (user) {
user.password = await user.generatePasswordHash()
});
User.beforeSave(async function (user) {
user.password = await user.generatePasswordHash()
});
Provide user object to generatePasswordHash and replace this with user.
User.beforeCreate(async user => {
user.password = await user.generatePasswordHash(user)
});
User.beforeSave(async user => {
user.password = await user.generatePasswordHash(user)
});
User.prototype.generatePasswordHash = async function(user) {
const saltRounds = 10;
return await bcrypt.hash(user.password, saltRounds)
};

Resources