I have the following schema with required validations:
var mongoose = require("mongoose");
var validator = require("validator");
var userSchema = new mongoose.Schema(
{
email: {
type: String,
required: [true, "Email is a required field"],
trim: true,
lowercase: true,
unique: true,
validate(value) {
if (!validator.isEmail(value)) {
throw new Error("Please enter a valid E-mail!");
}
},
},
password: {
type: String,
required: [true, "Password is a required field"],
validate(value) {
if (!validator.isLength(value, { min: 6, max: 1000 })) {
throw Error("Length of the password should be between 6-1000");
}
if (value.toLowerCase().includes("password")) {
throw Error(
'The password should not contain the keyword "password"!'
);
}
},
},
},
{
timestamps: true,
}
);
var User = mongoose.model('User', userSchema);
I pass the email and password through a form by sending post request using the following route:
router.post("/user", async (req, res) => {
try {
var user = new User(req.body);
await user.save();
res.status(200).send(user);
} catch (error) {
res.status(400).send(error);
}
});
module.exports = mongoose.model("User", user);
Whenever I enter a field against the validation rules, I get a very long error message, which is obvious. But now, I want to improve the error handling so that it gets easy to interpret for the users. Rather than redirecting to a generic error page, how can I redirect to the same signup page and display the flash messages near the incorrect fields telling about the error? And also on success, something similar should be done, like a green flash message on the top.
I am using ejs for my signup pages.
In the catch block, you can check if the error is a mongoose validation error, and dynamically create an error object like this:
router.post("/user", async (req, res) => {
try {
var user = new User(req.body);
await user.save();
res.status(200).send(user);
} catch (error) {
if (error.name === "ValidationError") {
let errors = {};
Object.keys(error.errors).forEach((key) => {
errors[key] = error.errors[key].message;
});
return res.status(400).send(errors);
}
res.status(500).send("Something went wrong");
}
});
When we send a request body like this:
{
"email": "test",
"password": "abc"
}
Response will be:
{
"email": "Please enter a valid E-mail!",
"password": "Length of the password should be between 6-1000"
}
you can use validator like this instead of throwing an error :
password:{
type:String,
required:[true, "Password is a required field"],
validate: {
validator: validator.isLength(value,{min:6,max:1000}),
message: "Length of the password should be between 6-1000"
}
}
you can send
res.status(400).send(error.message);
instead of :
res.status(400).send(error);
and you should make some changes in Schema also.
validate(value){
if (!validator.isEmail(value)) {
throw new Error("Please enter a valid E-mail!");
}
}
with
validate: [validator.isEmail, "Please enter a valid E-mail!" ]
and for password:
minlength: 6,
maxlength:1000,
validate:{
validator: function(el){
return el.toLowerCase() !== "password"
}
message: 'The password should not contain the keyword "password"!'
}
If you are using mongoose then you can catch error easily using this method for signup route, by chaining if and else condition depending on your error methods
try {
} catch (error) {
console.log(error);
if (error.errors) {
if (error.errors.password) {
//if password is less than 6 characters
return res.status(400).json(error.errors.password.message)
}
if (error.errors.email) {
//if email is not in correct format. i'm using validator package
return res.status(400).json(error.errors.email.message)
}
} else {
return res.status(400).json(error.message)
}
}
if found simpler one here..
try { } catch (error) {
console.log(error);
// checking validation
if (error.name === "ValidationError") {
const message = Object.values(error.errors).map(value => value.message);
return res.status(400).json({
error: message
})
}
res.status(400).json(error.message)
}
}
Related
I'm trying to follow the MVC architectural pattern and do all of my validation in my Mongoose model, rather than my controller.
I'm wondering how I can set error codes and truly custom error messages in my model (I.E. without the part that mongoose adds to the beginning of the message.)
At the moment my error message for the name field is: "message": "User validation failed: email: Please enter a valid email address", where it should be "Please enter a valid email address".
The response code from the server was 200 until I changed it in my errorHandlerMiddleware file, which is not ideal as it should be a 400 not the general 500.
So, somebody please help me to set the status code in my model and also make a custom error message.
Many thanks in advance!
const mongoose = require("mongoose");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const validator = require("validator");
const Schema = mongoose.Schema;
const UserSchema = new Schema(
{
name: {
type: String,
required: [true, "Please add a name"],
minLength: [3, "Name must be at least 3 characters"],
},
email: {
type: String,
required: [true, "Please add an email address"],
unique: [true, "It looks like you already have an account!"],
validate: {
validator: (value) => {
if (!validator.isEmail(value)) {
throw new Error("Please enter a valid email address");
}
},
},
},
password: {
type: String,
required: [true, "Please add a password"],
},
tokens: [
{
token: {
type: String,
required: true,
},
},
],
},
{ timestamps: true }
);
UserSchema.methods.toJSON = function () {
const user = this;
const userObject = user.toObject();
delete userObject.password;
delete userObject.tokens;
return userObject;
};
UserSchema.methods.generateAuthToken = async function () {
const user = this;
const token = jwt.sign({ _id: user._id.toString() }, process.env.JWT_SECRET, {
expiresIn: "7 days",
});
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) {
statusCode(401);
throw new Error("Unable to login");
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
statusCode(401);
throw new Error("Unable to login");
}
return user;
};
UserSchema.pre("save", function (next) {
if (this.password.length < 6) {
throw new Error("Password must be at least 6 characters");
}
if (!this.isModified("password")) {
return next();
}
this.password = bcrypt.hashSync(this.password, 10);
return next();
});
module.exports = User = mongoose.model("User", UserSchema);
i need a real custom error code and message from mongoose
I decided to catch the errors in the try/catch block on the controller, as so:
try {
await user.save();
} catch (err) {
// Error handling for duplicate email address
if (err.code === 11000) {
return res.status(400).send("It looks like you already have an account.");
}
// Error handling for misc validation errors
if (err.name === "ValidationError") {
res.status(400);
return res.send(Object.values(err.errors)[0].message);
}
}
I have a user model like this:
module.exports = {
attributes: {
email: {
type: 'string',
isEmail: true,
unique: true,
required: true
},
password: {
type: 'string',
required: true
}
},
beforeCreate: (value, next) => {
bcrypt.hash(value.password, 10, (err, hash) => {
if (err){
throw new Error(err);
}
value.password = hash;
next();
});
},
};
Now when I want to match the password during login, how do I decrypt the password, if possible I would prefer to perform it in the user model file.
controller/ login.js
module.exports = {
login: async (req, res) => {
try{
const user = await User.findOne({email: req.body.email});
if (!user){
throw new Error('Failed to find User');
}
// here I want to match the password by calling some compare
//function from userModel.js
res.status(201).json({user: user});
}catch(e){
res.status(401).json({message: e.message});
}
},
};
first try to find the user with the given username by user
const find = Users.find(user=>user.username===req.body.username)
if(!find){
res.send('User Not Found')
}
else{
if( await bcrypt.compare(req.body.password,find.password)){
//now your user has been found
}
else{
//Password is Wrong
}
}
You must use bcrypt.compare(a,b)
a = given password by user
b = original password if username exist
hope it solve your problem
i was following some react/express tutorials.
im confused with code below in contacts.js file
if (!contact) {
return res.status(404).json({ msg: "Contact not found" })
}
Is above code necessary ? or whats is there better way to catch if the contact doesn't exist in mongodb ?
when i do put request from postman with invalid id for example
localhost:44022/api/contacts/<invalid ID>
the execution never reach if (!contact) part. if i console log the catch(err) section i get something below
CastError: Cast to ObjectId failed for value "bcvbxcxc" at path "_id" for model "contact"
at model.Query.exec (/Users/becker/Desktop/RJS/ckeeper/node_modules/mongoose/lib/query.js:4380:21)
at model.Query.Query.then (/Users/becker/Desktop/RJS/ckeeper/node_modules/mongoose/lib/query.js:4472:15)
at runMicrotasks (<anonymous>)
at processTicksAndRejections (internal/process/task_queues.js:97:5) {
messageFormat: undefined,
stringValue: '"bcvbxcxc"',
kind: 'ObjectId',
value: 'bcvbxcxc',
path: '_id',
reason: Error: Argument passed in must be a single String of 12 bytes or a string of 24 hex characters
at new ObjectID (/Users/becker/Desktop/RJS/ckeeper/node_modules/bson/lib/bson/objectid.js:59:11)
contacts.js
router.put('/:id', auth, async (req, res) => {
const { name, email, phone, type } = req.body;
const contactFields = {};
if (name) contactFields.name = name;
if (email) contactFields.email = email;
if (phone) contactFields.phone = phone
if (type) contactFields.type = type;
try {
let contact = await Contact.findById(req.params.id);
if (!contact) {
return res.status(404).json({ msg: "Contact not found" })
}
//makesure contact belongs to right user
if (contact.user.toString() != req.user.id) {
return res.status(401).json({ msg: 'Not authorized' })
}
contact = await Contact.findByIdAndUpdate(req.params.id, { $set: contactFields }, { new: true })
res.json(contact)
}
catch (err) {
console.log("reached error, contact not found")
console.error(err)
res.status(500).send(err)
}
});
Contacts.js model
const mongoose = require('mongoose');
const ContactSchema = mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'users'
},
name: {
type: String,
required: true
},
email: {
type: String,
required: true,
},
phone: {
type: String
},
type: {
type: String,
default: 'personal'
},
date: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('contact', ContactSchema)
The reason for not executing the if(!contact) condition upon exception is because catch block will be called and further execution in the try block is halted. You should rather wrap each db call in its own try catch block. Something like:
let contact;
try {
contact = await Contact.findById(req.params.id);
} catch(err) {
console.log('Some db operations failed due to reason', err);
return res.status(500).json({ msg: "DB operations failed or whatever message" })
}
if (!contact) {
return res.status(404).json({ msg: "Contact not found" })
}
//makesure contact belongs to right user
if (contact.user.toString() != req.user.id) {
return res.status(401).json({ msg: 'Not authorized' })
}
try {
contact = await Contact.findByIdAndUpdate(req.params.id, { $set: contactFields }, { new: true })
return res.json(contact)
}
catch (err) {
console.log("db update operations failed")
console.error(err)
res.status(500).send(err)
}
currently I'm trying to create a new user in my mongodb database. The user also has to submit his email.
My model looks like this:
const userSchema = mongoose.Schema({
_id: mongoose.Schema.Types.ObjectId,
email: {
type: String,
required: true,
unique: true,
match: /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*#(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/
}
This is just the important part of it because there I state, that mail has to match this specific charset.
Now my problem: If I update the email with the PATCH method
router.patch('/user', checkAuth, async (req, res, next) => {
try {
const id = req.body._id;
const update = req.body;
const options = { new: true };
const result = await User.findByIdAndUpdate(id, update, options);
if (!result) {
throw createError(404, 'User does not exist');
}
res.send(result);
} catch (error) {
console.log(error.message);
if (error instanceof mongoose.CastError) {
return next(createError(400, 'Invalid User Id'));
}
next(error);
}
})
The user can enter whatever he wants into the email field without it being checked for the charset. Do you have any ideas how to fix it?
Thanks in advance, Tom
If you are asking for pattern-matching in HTML, then you can validate your email address using this <input> tag:
<input pattern="/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*#(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/" required />
Check ValidatorJS to validate at an API level.
I have registration form with username, mail, password and password2. I want to verify passwords that they actually match. I verify practically everything in Mongoose Scheme but I cannot find any useful information in documentation how to grab password2 without actually saving it to database. (I have function to crypt password which runs only before saving)
const userSchema = new mongoose.Schema({
username: {
type: String,
unique: true,
required: true,
trim: true,
validate(value) {
if (!validator.isAlphanumeric(value , 'pl-PL')) {
throw new Error('Name cannot contain special characters.')
}
}
},
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,
validate(value) {
console.log(value)
if(value !== this.password2) {
throw new Error("Passwords don't match. Try again.")
}
if(value.length < 8) {
throw new Error("Passwords is too short. At least 8 characters.")
}
}
},
tokens: [{
token: {
type: String,
required: true
}
}]
})
You don't need to make password2 a part of userSchema. The better way is to make a compare password function like this:
UserSchema.methods.comparePassword = function(plaintext, callback) {
return callback(null, Bcrypt.compareSync(plaintext, this.password));
};
also you can make a use of Schema.pre:
UserSchema.pre("save", function(next) {
if(!this.isModified("password")) {
return next();
}
this.password = Bcrypt.hashSync(this.password, 10);
next();
});
After this, you need to call the compare function from user controller. Something like this (depending on your logic):
var user = await UserModel.findOne({ username: request.body.username }).exec();
if(!user) {
return response.status(400).send({ message: "The username does not exist" });
}
user.comparePassword(request.body.password, (error, match) => {
if(!match) {
return response.status(400).send({ message: "The password is invalid" });
}
});
For details you can read this excellent article.
You can check password and password2 in your register route, and if they are same you can continue to register.
A sample register route would be like this:
router.post("/register", async (req, res) => {
try {
const { username, email, password, password2 } = req.body;
if (password !== password2) return res.status(400).send("Passwords dont match");
let user = await User.findOne({ email });
//or
//let user = await User.findOne({ username });
if (user) return res.status(400).send("User already registered.");
user = new User({ username, email, password });
user = await user.save();
//todo: at this point you may generate a token, and send to the client in response header or body
res.send(user);
} catch (err) {
console.log(err);
res.status(500).send("Server error");
}
});