i have 3 tables "users" , "users_passwords", "users_emails"
"users" is the main table
users_password and users_email are relashionship OnetoOne with tables users
I am using this scheme in the models:
users_email :
const UserEmail = new mongoose.Schema({
user: {
type : mongoose.Schema.Types.ObjectId,
ref: 'User'
},
email: {
type : String,
require: [true, "Required E-mail"],
validate: [isEmail, "Please enter a valid email"]
}
})
users_password:
const UserPassword = new mongoose.Schema({
user: {
type : mongoose.Schema.Types.ObjectId,
ref: 'User'
},
password: {
type : String,
require: [true, "Required Password"],
minlength: [8, "Minimum passowrd lenght is 8 characters"]
}
})
Than in my controller i have the function regist
try{
const email = new UserEmail({
email: req.body.email
})
const auth = new UserAuth({
password: req.body.password,
})
const user = new User({
email: email._id,
auth: auth._id,
})
await email.save()
await auth.save()
const result = await user.save()
const {...data} = await result.toJSON()
res.send(data)
}catch{
....
}
What is happening is if the email is OK and valid it goes to password and if it fails on password will save the UserEmail.
What i want is if in any one of the tables fails don't execut the save!
I am realy new at express and mongoDb,i am learning and this is project to scholl thanks for help
validate them first, before save it into a database
try {
// check email function
function validateEmail(emailAdress) {
let regexEmail = /^\w+([\.-]?\w+)*#\w+([\.-]?\w+)*(\.\w{2,3})+$/;
if (emailAdress.match(regexEmail)) {
return true;
} else {
return false;
}
}
// check password function
function validatePassword(password) {
return password.length <= 8 ? false : true;
}
// check them here
if (!validateEmail(req.body.email)) return res.status(400).send("error email")
if (!validatePassword(req.body.password)) return res.status(400).send("error password")
const email = new UserEmail({
email: req.body.email
})
const auth = new UserAuth({
password: req.body.password,
})
const user = new User({
email: email._id,
auth: auth._id,
})
await email.save()
await auth.save()
const result = await user.save()
const { ...data } = await result.toJSON()
res.send(data)
} catch {
....
}
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);
}
}
Edit: [how to handle case of jwt expiration ]
I have read some article on how to implement email verification for your web application and each one follow up:
Creating a unique string, saving it in db with reference to user being verified and sending that unique string as a link for verification. When user visits that link, unique string is run against db and refernced user is validated.
But, I tried it in a different way, that user model contains verify status and will be false by default and when new user sign_up then a jwt token is created and that is sent to user as verification link and when the link is visited, jwt token is verified and user verify status is changed to true.
Above implementation worked for me and removes the use of creating and storing token in separate db but I am afraid this approach might have problems which I might not be aware of. here's the code for above.
passport configuration for auth(config-passport.js)
const bcrypt = require('bcrypt')
const LocalStrategy = require('passport-local').Strategy
const { User } = require('./models/user');
module.exports = (passport) => {
// passport local strategy
const authUser = (email, password, done) => {
User.findOne({ email: email }, function(err, user){
if(err) return done(err);
if(!user || !user.verify) return done(null, false);
if(user.verify){
bcrypt.compare(password, user.password, (err, isValid) => {
if (err) {
return done(err)
}
if (!isValid) {
return done(null, false)
}
return done(null, user)
})
}
})
}
passport.serializeUser((user, done) => {
done(null, user.id)
});
passport.deserializeUser((id, done) => {
User.findOne({ _id: id }, function(err, user){
done(err, user)
});
});
passport.use(new LocalStrategy({
usernameField: 'email'
}, authUser));
}
user model
'use strict';
const mongoose = require('mongoose');
const bcrypt = require('bcrypt')
const Joi = require('joi');
const Schema = mongoose.Schema;
//any changes done to userSchema will need changes done to userValidation.js
const userSchema = new Schema({
username: {type: String, required: true, maxlength: 100},
email: {type: String, unique: true, lowercase: true, required: true},
mobile: {type: Number, unique: true, required: true},
password: {type: String, required: true},
verify: { type: Boolean, enum: [false, true], default: false },
lib: [{ type: Schema.Types.ObjectId, ref: 'Book' }],
book_id: [{ type: Schema.Types.ObjectId, ref: 'Book' }]
});
const JoiValidUser = Joi.object({
username: Joi.string().min(3).max(50).required(),
email: Joi.string().email().min(5).max(50).required(),
mobile: Joi.string().regex(/^[0-9]{10}$/).required().messages({ 'string.pattern.base': `Phone number must have 10 digits.` }),
password: Joi.string().min(5).max(255).required()
});
userSchema.pre('save', async function(next){
const user = this;
const hash = await bcrypt.hash(user.password, 10);
this.password = hash;
next();
})
userSchema.methods.isValidPassword = async function(password) {
const user = this;
const compare = await bcrypt.compare(password, user.password);
return compare;
}
const User = mongoose.model('User', userSchema);
module.exports = { User, JoiValidUser };
user creation controller(userCreate.js)
const { User, JoiValidUser } = require('../models/user');
const mailer = require('../controller/mailHandler')
//takes data posted and form it in a readable format
//then validate/sanitize it against schema
//if error arises or user already exists a msg is passed on
//else user creation process is executed
module.exports = async function(req, res){
let user = {
username: req.body.username,
email: req.body.email,
mobile: req.body.mobile,
password: req.body.password
}
try{
JoiValidUser.validate(user);
const ExistUser = await User.findOne({
$or: [
{ email: req.body.email },
{ mobile: req.body.mobile }
]
});
if(ExistUser)
throw new Error("Email/Mobile Number already Registered");
await (new User(user)).save();
mailer(user.username, user.email);
res.send({ msg: "A Verification link is sent to mail" });
} catch(err) {
res.render('error', { message: err.message })
}
}
user verification route (verify.js)
const router = require('express').Router();
const jwt = require('jsonwebtoken');
const config = require('dotenv').config().parsed
const { User } = require('../models/user')
const routePlan = require('../route_plan');
router.get('/:token', async(req, res) => {
const { email } = jwt.verify(req.params.token, config.SECRET);
await User.findOneAndUpdate({ email: email }, {
$set: { verify: true }
});
res.send("Welcome ...")
})
module.exports = router;
EDIT:
Thank you all for your feedback but there is another problem I want to be clear of on how to handle case when jwt token expires because link will be invalid and user cannot try to sign up again because his info is already in db and he cannot register again
I'm trying to create a MERN app where there will be multiple roles like 'principle', 'teacher', 'student', 'guardian'. Primarily I have created userModel to register users and create a role key that will have the different role values pointing to other models (like teacherModel/ studentModel).
My userModel is like that
const mongoose = require('mongoose')
const userSchema = mongoose.Schema(
{
email: {
type: String,
required: [true, 'Please add an email'],
unique: true
},
password: {
type: String,
required: [true, 'Please add a password']
},
role: [
{
type: mongoose.Schema.ObjectId,
ref: 'Teacher'
},
{
type: mongoose.Schema.ObjectId,
ref: 'Student'
}
]
},
{
timestamps: true
}
)
module.exports = mongoose.model('User', userSchema)
and the teacherModel is
const mongoose = require('mongoose')
const teacherSchema = mongoose.Schema({
name: {
type: String,
required: [true, 'Please add your name']
},
teacherId: {
type: Number,
required: [true, 'Please add your teacher id']
}
})
module.exports = mongoose.model('Teacher', teacherSchema)
I have created an userConroller, I want to register a user with their name, email, password, and role. I have put the name into the individual role so that I can search teachers or students separately, not both. Here, is my userController
const asyncHandler = require('express-async-handler')
const bcrypt = require('bcryptjs')
const User = require('../models/userModel')
// #description Register a new user
// #route /api/users
// #access Public
const registerUser = asyncHandler(async (req, res) => {
const { email, password, role } = req.body
// Validation
if (!name || !email || !password || !role) {
res.status(400)
throw new Error('Please include all fields')
}
// Find if user already exists
const userExists = await User.findOne({ email })
if (userExists) {
res.status(400)
throw new Error('User already exists')
}
// Hash Password
const salt = await bcrypt.genSalt(10)
const hashedPassword = await bcrypt.hash(password, salt)
const user = await User.create({
email,
password: hashedPassword,
role,
})
if (user) {
res.status(201).json({
_id: user._id,
email: user.email,
role: user.role
})
} else {
res.status(400)
throw new error('Invalid user data')
}
})
When I try to register with postman, I get this error message,
"User validation failed: role.0: Cast to [ObjectId] failed for value
"[ 'teacher' ]" (type string) at path "role.0" because of
"CastError""
What have I messed up?
I am trying to create controller for resetting user password in Node.JS.
The idea is to fetch the user from DB based on reset password token, do some validation update the relevant field and save it back to the DB.
However, I get an error when trying to save the updated user ("user.save is not a function").
What might be the reason?
I have a user model defined as follows:
const mongoose = require("mongoose");
const validator = require("validator");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, "Please enter valid name"],
maxLength: [30, "Your name cannot exceed 30 characters]"],
},
email: {
type: String,
required: [true, "Please enter valid email"],
unique: true,
validate: [validator.isEmail, "Please enter valid email address"],
},
password: {
type: String,
requires: [true, "Please enter your password"],
minLength: [6, "Your password must be at least 6 characters"],
select: false,
},
avatar: {
public_id: { type: String, required: true },
url: { type: String, required: true },
},
role: { type: String, default: "user" },
createdAt: { type: Date, default: new Date().now },
resetPasswordToken: { type: String },
resetPasswordExpire: { type: Date },
});
userSchema.pre("save", async function (next) {
if (!this.isModified("password")) {
next();
}
this.password = await bcrypt.hash(this.password, 10);
});
// check password matching
userSchema.methods.isPasswordMatched = async function (inputPassword) {
return await bcrypt.compare(inputPassword, this.password);
};
// return JSON Web Token
userSchema.methods.getJwtToken = function () {
return jwt.sign({ id: this.id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRESIN_TIME,
});
};
// Generate password token
userSchema.methods.getResetPasswordToken = function () {
// Generate Token
const resetToken = crypto.randomBytes(20).toString("hex");
// Hash token
this.resetPasswordToken = crypto
.createHash("sha256")
.update(resetToken)
.digest("hex");
// set expired time
this.resetPasswordExpire = new Date(Date.now() + 30 * 60 * 1000);
return resetToken;
};
module.exports = mongoose.model("User", userSchema);
When I try to reset user password I try the following:
// get the user document from db (make sure token and expiration time are valid)
let user = User.findOne({
resetPasswordToken: resetPasswordToken,
resetPasswordExpire: { $gt: Date.now() },
});
// update password
user.password = req.body.password;
user.resetPasswordToken = undefined;
user.resetPasswordExpire = undefined;
user.save();
sendToken(user, 200, res);
for some reason I get an error:
"errorMsg": "user.save is not a function"
What might be the problem?
Probably, user is null or undefined, so you should handle the user null condition.
Also findOne and save returns promise, so you need to add await keyword before them.
Also you have a typo in user schema password field, requires should be required .
let user = await User.findOne({
resetPasswordToken: resetPasswordToken,
resetPasswordExpire: { $gt: Date.now() },
});
if (user) {
// update password
user.password = req.body.password;
user.resetPasswordToken = undefined;
user.resetPasswordExpire = undefined;
await user.save();
sendToken(user, 200, res);
} else {
res.status(400).send("No user found");
}
If you get the user null, you need to fix your query in findOne.
use await keyword to get mongoose hydrating the response
let user = await User.findOne({
resetPasswordToken: resetPasswordToken,
resetPasswordExpire: { $gt: Date.now() },
});
// update password
user.password = req.body.password;
user.resetPasswordToken = undefined;
user.resetPasswordExpire = undefined;
user.save();
sendToken(user, 200, res);
or you can do it like this
await User.findOneAndUpdate({
resetPasswordToken: resetPasswordToken,
resetPasswordExpire: { $gt: Date.now() },
},{password : req.body.password, resetPasswordToken : undefined,
resetPasswordExpire : undefined,});
I have a user I can save in MongoDB, when I enter correct data, the save works.
But when I enter wrong data, I can't catch the errors to be seen for the user. All I can see is this on the code editor:
...UnhandledPromiseRejectionWarning: ValidationError: User validation
failed: username: username is not there!...
This error "kills" the server, and so I'm not rendering home-guest template.
The question is how I can catch the errors and show them to the user?
Schema:
const mongoose = require("mongoose")
const userSchema = new mongoose.Schema({
username: {
type: String,
required: [true, "username is not there!"],
minlength: 3,
maxlength: 20,
},
email: {
type: String,
required: true,
},
password: {
type: String,
required: true,
minlength: 6,
maxlength: 20,
},
})
module.exports = mongoose.model("User", userSchema)
Controller:
const mongoose = require("mongoose")
const userModel = require("../models/userModel")
exports.signUp = async (req, res) => {
const { username, email, password } = req.body
try {
const user = await new userModel({
username,
email,
password,
})
user.save()
} catch (error) {
res.render("home-guest", { error })
}
}
You just need to add an await to the save operation, since that's also async:
const mongoose = require("mongoose")
const userModel = require("../models/userModel")
exports.signUp = async (req, res) => {
const { username, email, password } = req.body
try {
const user = await new userModel({
username,
email,
password,
})
// Wait for the save to complete, also allowing you to catch errors
await user.save()
} catch (error) {
res.render("home-guest", { error })
}
}
EDIT: And note that you do not need an async in front of new userModel(). new cannot return a promise, it is always synchronous.