How can I implement joi-password-complexity in Joi validation? - node.js

I want to enforce password complexity by using the joi-password-complexity package when users register.
https://github.com/kamronbatman/joi-password-complexity
I tried but I got the following error:
(node:14872) UnhandledPromiseRejectionWarning: AssertionError
[ERR_ASSERTION]: Invalid schema content:
(password.$_root.alternatives)
This is the code I'm using:
const mongoose = require("mongoose");
const Joi = require("joi");
const passwordComplexity = require("joi-password-complexity");
const complexityOptions = {
min: 5,
max: 250,
lowerCase: 1,
upperCase: 1,
numeric: 1,
symbol: 1,
requirementCount: 2,
};
const userSchema = new mongoose.Schema({
name: {
type: String,
minlenght: 1,
maxlength: 55,
required: true
},
email: {
type: String,
minlength: 5,
maxlength: 255,
unique: true,
required: true
},
password: {
type: String,
minlength: 5,
maxlength: 1024,
required: true
}
})
const User = mongoose.model("User", userSchema);
function validateUser(user) {
const schema = {
name: Joi.string().min(1).max(55).required(),
email: Joi.string().min(5).max(255).required().email(),
password: passwordComplexity(complexityOptions) // This is not working
}
return Joi.validate(user, schema);
}
exports.User = User;
exports.validate = validateUser;
I also tried to follow this example: https://forum.codewithmosh.com/d/215-joi-password-complexity-problem but it seems outdated since the "new" keyword will throw another error (not a constructor).
Any help is appreciated!

Couldn't reproduce your exact error, but I had the thing working this way:
#hapi/joi: ^17.1.0 (latest at the time of the writing, also works with 16.1.8)
joi-password-complexity: ^4.0.0 (latest as well)
Code:
function validateUser(user) {
// no change here
const schema = Joi.object({
name: Joi.string().min(1).max(55).required(),
email: Joi.string().min(5).max(255).required().email(),
password: passwordComplexity(complexityOptions)
});
// note that we call schema.validate instead of Joi.validate
// (which doesn't seem to exist anymore)
return schema.validate(user);
}

Here is how i solved the issue:
if You are using joi#14.3.1 or older then install joi-password-complexity#2.0.1 then try this code:
User.js model
const mongoose = require("mongoose");
const Joi = require("joi");
const PasswordComplexity = require("joi-password-complexity");
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
email: { type: String, required: true, unique: true },
password: { type: String, required: true }
});
const User = mongoose.model("User", userSchema);
function validateUser(user) {
const schema = {
name: Joi.string()
.min(3)
.max(50)
.required(),
email: Joi.string()
.email()
.max(255)
.required(),
password: new PasswordComplexity({
min: 8,
max: 25,
lowerCase: 1,
upperCase: 1,
numeric: 1,
symbol: 1,
requirementCount: 4
});
};
return Joi.validate(user, schema);
}
module.exports.validate = validateUser;
module.exports.User = User;
This will validate password complexity but for making password REQUIRED you have to validate it in your route...
Users.js route
const _ = require("lodash");
const express = require("express");
const { User, validate } = require("../models/user");
const router = express.Router();
//POST
router.post("/", async (req, res) => {
const { error } = validate(req.body);
if (error) return res.status(400).send(error.details[0].message);
//
if (!req.body.password) return res.status(400).send("Password is required..");
let user = await User.findOne({ email: req.body.email });
if (user) return res.status(400).send("User already registered..");
user = new User(_.pick(req.body, ["name", "email", "password"]));
await user.save();
res.send(_.pick(user, ["_id", "name", "email"]));
});
module.exports = router;

I had to add .default to import the function that returns the Joi object. Using Joi 17.3.0 and joi-password-complexity 5.0.1, this is what worked for me:
const passwordComplexity = require('joi-password-complexity').default;
const schema = Joi.object({
email: Joi.string().min(5).max(255).required().email(),
password: passwordComplexity().required(),
});

Since the passwordComplexity() returns a Joi object, you can write required in a fluent way. Thus, you don't have to write extra validation in the router.
function validateUser(user) {
// no change here
const schema = Joi.object({
name: Joi.string().min(1).max(55).required(),
email: Joi.string().min(5).max(255).required().email(),
password: passwordComplexity(complexityOptions).required()
});
// note that we call schema.validate instead of Joi.validate
// (which doesn't seem to exist anymore)
return schema.validate(user);
}

Versions used of Joi and JPC:
├── ...
├── joi#17.2.1
├── joi-password-complexity#4.2.1
The validation function must be updated to fit with the new updated version of JOI. joi.validate() is no longer supported in Joi v16+
const Joi = require("joi");
const passwordComplexity = require("joi-password-complexity");
function validateUser(user) {
const schema = Joi.object({
name: Joi.string().min(5).max(50).required(),
email: Joi.string().min(5).max(255).required().email(),
password: passwordComplexity(complexityOptions),
});
return schema.validate(user); // NOT >> Joi.validate(user, schema); !!!!!
}
and the complexityOptions can be something like:
const complexityOptions = {
min: 5,
max: 1024,
lowerCase: 1,
upperCase: 1,
numeric: 1,
symbol: 1,
requirementCount: 4,
};

Another way is Regex
const joi = require("joi");
const strongPasswordRegex = /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!#$%^&*-]).{8,}$/;
const stringPassswordError = new Error("Password must be strong. At least one upper case alphabet. At least one lower case alphabet. At least one digit. At least one special character. Minimum eight in length")
const schema = joi.object().keys({
username: joi.string().required().min(4).max(15),
password: joi.string().regex(strongPasswordRegex).error(stringPassswordError).required()
});
const notValid = schema.validate({ username: "MHamzaRajput", password: "Admin#admin123" }).error;
if (notValid) {
console.log(notValid.message);
} else {
console.log("payload validated successfully");
}

Related

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'....

Bcryptjs Compare method always returns false?

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".

schema.methods is not a function

I have been trying to create a method on my user schema in mongoose, however it keeps saying method is not a function and I have no idea why. I am fairly new to mongoose and express, and I'm pretty sure I have my files set up currently so I don't know what could be causing this issue. As a last attempt, I tried switching to arrow functions , but that didn't work either.
user routes file
const router = require("express").Router();
let user = require("../models/user_model");
const Joi = require("#hapi/joi");
// GET dreams
// POST dreams
// DELETE dreams
// UPDATE dreams
router.route("/").get((req, res) => {
console.log(user.addType());
res.send("hello this is a users page");
});
user model file
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const userSchema = new Schema(
{
username: {
type: String,
required: true,
unique: true,
trim: true,
min: 3
},
password: {
type: String,
trim: true,
required: true,
min: 6
}
},
{
timestamps: true
}
);
userSchema.methods.addTypes = function() {
console.log("woof");
};
userSchema.methods.joiValidate = data => {
let Joi = require("#hapi/joi");
const schema = {
username: Joi.string()
.min(6)
.required(),
password: Joi.string()
.min(6)
.required()
};
return schema.validate(data);
};
module.exports = mongoose.model("User", userSchema);
UPDATE! Other than having typo on your code, you also need to create an instance of your model ('user'). You cannot just call the function of the model.
let user = new user({ // Create an instance first
username: 'Tester',
password: '12345678'
})
console.log(user.addType())
you declared
addTypes()
Cheers

How to do proper validation for array of objects ids in nodeJS rest api

I am currently struggling to implement rest api build in nodeJS, mongoDB, express. One of fields takes array of objects ids (field sprints in model project). The question is how to do proper validation in route file (projects.js) when doing post method (keeping in mind that field sprints is not required)?
I was trying to check typeof req.body.sprints and also to do lenght on this, but with no result. I was also trying to find answer on stackoverflow etc., but didn't came across nothing similar.
project.js (model file)
const mongoose = require("mongoose");
const Project = mongoose.model(
"Project",
new mongoose.Schema({
name: {
...
sprints: [
{
type: new mongoose.Schema({
name: {
type: String,
required: true,
minlength: 5,
maxlength: 255
},
isClosed: { type: Boolean, default: false }
})
}
]
})
);
function validateProject(project) {
const schema = {
name: Joi.string()
.min(5)
.max(255)
.required(),
createdBy: Joi.objectId().required(),
isClosed: Joi.boolean(),
sprints: Joi.array().items(Joi.objectId())
};
return Joi.validate(project, schema);
}
exports.Project = Project;
exports.validate = validateProject;
projects.js (route file)
const express = require("express");
const { User } = require("../models/user");
const { Sprint } = require("../models/sprint");
const { Project, validate } = require("../models/project");
const router = express.Router();
...
/* POST project */
router.post("/", async (req, res) => {
const { error } = validate(req.body);
if (error) return res.status(400).send(error.details[0].message);
const user = await User.findById(req.body.createdBy);
if (!user) return res.status(400).send("Invalid user");
const sprint = await Sprint.findById(req.body.sprints);
if (!sprint) return res.status(400).send("Invalid sprint");
//i think that here i have to validate if sprint has even one
element and only then do object id validation
const project = new Project({
name: req.body.name,
createDate: req.body.createDate,
createdBy: { _id: user._id, name: user.name },
isClosed: req.body.isClosed,
sprints: [
{
_id: sprint._id,
name: sprint.name,
isClosed: sprint.isClosed
}
]
});
await project.save();
res.send(project);
});
...
module.exports = router;
In my post method i am checking if given id exist in table Sprints but i have problem if no sprint id is provided (i get 'Invalid sprint' message). Expected result will be: i can provide array of sprints, one sprint, or none at all.
try this out
function validateProject(project) {
const schema = {
name: Joi.string()
.min(5)
.max(255)
.required(),
createdBy: Joi.objectId().required(),
isClosed: Joi.boolean(),
sprints:Joi.array().items(
Joi.object({
name: Joi.string(),
isClosed: Joi.boolean
})
)
})
};
return Joi.validate(project, schema);
}

Mongoose - Multiple models for 1 schema

I am using mongoose v5.2.17.
I was wondering is it possible to have multiple models map to the 1 schema.
For example - I have the following model
const mongoose = require('mongoose');
const validator = require('validator');
const jwt = require('jsonwebtoken');
const _ = require('lodash');
const bcrypt = require('bcryptjs');
const UserSchema = new mongoose.Schema({
email: {
type: String,
required: true,
trim: true,
minlength: 1,
unique: true,
validate: {
validator: validator.isEmail,
message: '{VALUE} is not a valid email',
},
},
password: {
type: String,
required: true,
minlength: 6,
},
isTrialUser: {
type: Boolean,
default: true,
},
isAdminUser: {
type: Boolean,
default: false,
}
});
UserSchema.methods.toJSON = function () {
const user = this;
const userObject = user.toObject();
return _.pick(userObject, ['_id', 'email', 'isTrialUser']);
};
UserSchema.pre('save', function (next) {
const user = this;
if (user.isModified('password')) {
bcrypt.genSalt(10, (err, salt) => {
bcrypt.hash(user.password, salt, (hashErr, hash) => {
user.password = hash;
next();
});
});
} else {
next();
}
});
const User = mongoose.model('User', UserSchema);
module.exports = { User, UserSchema };
Is it possible for me to create another AdminModel where admin specific methods can live?
I also want to return all data from the toJSON method from the AdminModel.
Please let me know if this is possible or if there is a better way to perform such a task
Thanks
Damien
If I am understanding you correctly you want to inherit the UserModel in an AdminModel and decorate that one with extra methods etc. For that you can use util.inherits (or the so called Mongoose discriminators) like so:
function BaseSchema() {
Schema.apply(this, arguments);
this.add({
name: String,
createdAt: Date
});
}
util.inherits(BaseSchema, Schema);
var UserSchema = new BaseSchema();
var AdminSchema = new BaseSchema({ department: String });
You can read more about it in Mongoose docs.
There is also a good article on the mongoose discriminators here

Resources