How to implement multi roles user authentication using express and mongoose? - node.js

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?

Related

Problem in this email verification approach?

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

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

Express & mangoDB save only if all data is valid

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 {
....
}

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

Unable to add properties in a MongoDB document

I am trying to implement password reset functionality in a MERN application. Whenever a user enters their email (for which they want to reset the password) and clicks on the "Send Password Reset Link" button, a POST request is made to the route "/account/forgot".
In the route handler function, I check whether any user with the given email exists or not. If a user exists, then I add resetPasswordLink and resetPasswordExpires properties to the user object and send a message "You have been emailed a password link" to the client.
The problem I am facing is I get the message at the frontend.
However, whenever I check the database, I don't see resetPasswordLink and resetPassworExpires properties being added to the user.
Where is the problem?
The code snippets are given below:
server/routes/passwordResetRoutes.js
const express = require("express");
const crypto = require("crypto");
const asyncHandler = require("express-async-handler");
const User = require("../models/userModel");
const router = express.Router();
router.post(
"/forgot",
asyncHandler(async (req, res, next) => {
const user = await User.findOne({ email: req.body.email });
if (user) {
user.passwordResetToken = crypto.randomBytes(20).toString("hex");
user.passwordResetExpires = Date.now() + 3600000;
await user.save();
res.json({
message: "You have been emailed a password reset link",
});
} else {
const err = new Error("No account with that email exists");
err.status = 404;
next(err);
}
})
);
module.exports = router;
server/models/userModel.js
const mongoose = require("mongoose");
const bcrypt = require("bcryptjs");
const userSchema = new mongoose.Schema({
firstName: {
type: String,
required: true,
},
lastName: {
type: String,
required: true,
},
email: {
type: String,
unique: true,
required: true,
},
password: {
type: String,
required: true,
},
resetPasswordToken: {
type: String,
},
resetPasswordExpires: {
type: Date,
},
});
userSchema.methods.matchPassword = async function (incomingPassword) {
return await bcrypt.compare(incomingPassword, this.password);
};
userSchema.pre("save", async function (next) {
if (!this.isModified("password")) {
next();
}
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
});
const User = mongoose.model("User", userSchema);
module.exports = User;
You're trying to update the passwordResetToken and passwordResetExpires fields but they are not present in the User model. That's why the user.save() call does nothing. They should be resetPasswordToken and resetPasswordExpires respectively.
user.resetPasswordToken = crypto.randomBytes(20).toString('hex')
user.resetPasswordExpires = Date.now() + 3600000
await user.save()

Resources