Problem in this email verification approach? - node.js

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

Related

Userschema and controller

I am building a user signup and login api and admin signup and login using express and currently I am testing in the postman, but somehow postman keeps return "error": "firstName is not defined" even though I posted firstname etc. here is my code, can anyone help me to explain it what is wrong? I saw so many videos using all different kinds of method, like generateAuthtakoken in the user.model or joi password library, it is just so overwhelming, can you help me to point to a direction as to how to use express to create ?
this is my user.model file:
const mongoose = require("mongoose");
const bcrypt = require("bcrypt");
const validator = require("validator");
const userSchema = new mongoose.Schema(
{
firstName: {
type: String,
required: true,
trim: true,
},
lastName: {
type: String,
required: true,
trim: true,
},
email: {
type: String,
required: true,
trim: true,
unique: true,
},
password: {
type: String,
required: true,
},
role: {
type: String,
enum: ["user", "admin"],
default: "user",
},
contactNumber: { type: String },
profilePicture: { type: String },
},
{ timestamps: true }
);
//static signup method
userSchema.statics.signup = async function (email, password) {
//validation
if (!firstName || !lastName || !email || !password) {
throw Error("All fields must be filled");
}
if (!validator.isEmail(email)) {
throw Error("Email is not valid");
}
if (!validator.isStrongPassword(password)) {
throw Error("Password is not strong enough");
}
const exists = await this.findOne({ email });
if (exists) {
throw Error("Email already in use");
}
const salt = await bcrypt.genSalt(10);
const hash = await bcrypt.hash(password, salt);
const user = await this.create({ email, password: bcrypt.hash });
return user;
};
//static login method
userSchema.statics.login = async function (email, password) {
if (!firstName || !lastName || !email || !password) {
throw Error("All fields must be filled");
}
const user = await this.findOne({ email });
if (!user) {
throw Error("Incorrect Email");
}
const match = await bcrypt.compare(password, user.password);
if (!match) {
throw Error("Incorrect password");
}
return user;
};
module.exports = mongoose.model("User", userSchema);
this is my controller file:
const User = require("../models/user");
const jwt = require("jsonwebtoken");
const createToken = (_id) => {
jwt.sign({ _id }, process.env.JWT_SECRET, { expiresIn: "3d" });
};
//login user
const loginUser = async (req, res) => {
const { email, password } = req.body;
try {
const user = await User.login(email, password);
// create token
const token = createToken(user._id);
res.status(200).json({ email, token });
} catch (error) {
res.status(400).json({ error: error.message });
}
res.json({ msg: "login user" });
};
//signup user
const signupUser = async (req, res) => {
const { email, password } = req.body;
try {
const user = await User.signup(email, password);
// create token
const token = createToken(user._id);
res.status(200).json({ email, token });
} catch (error) {
res.status(400).json({ error: error.message });
}
res.json({ msg: "login user" });
};
module.exports = { signupUser, loginUser };
and my router file:
const express = require("express");
const router = express.Router();
const { signupUser, loginUser } = require("../controller/auth");
//login route
router.post("/login", loginUser);
//signup route
router.post("/signup", signupUser);
module.exports = router;
where exactly do you get this error. Please provide full details to regenerate this error.
But as i could guess
In your static login method you do not need firstName and LastName.
In your signup user method you should be passing those missing required db fields as in your model.

How to implement multi roles user authentication using express and mongoose?

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?

Getting error data and salt arguments required on bcrypt?

I am trying to save a user to MongoDB as follows, but I am getting the error bcrypt Error: data and hash arguments required. I have checked the same error question asked by other Dev on StackOverflow but it doesn't help. I have attached the codes of the model file and router file.
User Model file
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const uSchema = new mongoose.Schema({
fullName: {
type: String,
required: true,
min: 4,
max: 30
},
email: {
type: String,
required: true,
trim: true,
unique: true,
index: true
},
hash_password: {
type: String,
required: true,
min: 6,
max: 12
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'admin'
}
}, { timestamps: true });
uSchema.virtual('password')
.set(function (password) {
this.hash_password = bcrypt.hashSync(password, 10);
});
uSchema.methods = {
authenticate: function (password) {
return bcrypt.compareSync(password, this.hash_password);
}
}
module.exports = mongoose.model('User', uSchema);
User Router file
const express = require('express');
const router = express.Router();
const User = require('../models/user.model');
router.post('/login', (req, res) => {
});
router.post('/signin', (req, res) => {
User.findOne({ email: req.body.email })
.exec((error, user) => {
if (user) return res.status(400).json({
message: 'User already exists.'
});
const {
fullName,
email,
password
} = req.body;
const _user = new User({
fullName,
email,
password
});
_user.save((error, data) => {
if (error) {
return res.status(400).json({
message: 'Something went wrong'
});
} if (data) {
return res.status(201).json({
user: data
})
}
})
});
});
module.exports = router;
You can do it in the router file instead.
const bcrypt = require("bcrypt")
// ...
router.post('/signin', (req, res) => { // Change this to signup
User.findOne({ email: req.body.email })
.exec((error, user) => {
if (user) return res.status(400).json({
message: 'User already exists.'
});
const {
fullName,
email,
password
} = req.body;
const hashedPassword = bcrypt.hashSync(password, 10);
const _user = new User({
fullName,
email,
hashedPassword
});
_user.save((error, data) => {
if (error) {
return res.status(400).json({
message: 'Something went wrong'
});
} if (data) {
return res.status(201).json({
user: data
})
}
})
});
});
module.exports = router;
and delete the password virtual from the model.

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()

Unable to reference from token schema token mongodb express passport

i need help with this problem been dealing it for days.
i am trying to make a verification email route by using passport to hash passwords while issuing a verification token to the user.
here is my code for index.js in controllers folder
const User = require("../models/user");
const Token = require("../models/token")
const crypto = require("crypto");
const nodemailer = require("nodemailer");
var smtpTransport = nodemailer.createTransport({
service: 'Gmail',
auth: {
user: process.env.GMAILUSER,
pass: process.env.GMAILPW
}
});
module.exports = {
async postRegister(req, res, next) {
var user = new User({
name: req.body.name,
email: req.body.email,
isVerified: false,
username: req.body.username
});
await User.register(user, req.body.password);
res.redirect('/');
var token = new Token({ _userId: user._id, token: crypto.randomBytes(16).toString('hex') });
token.save(function (err) {
if (err) { return res.status(500).send({ msg: err.message
});
}
var mailOptions = {
to: user.email,
from: 'xxxt#xxx.com',
subject: 'xxxxx verify email',
text:'You are receiving this because you need to verify your email for your account.\n\n' +
'Please click on the following link, or paste this into your browser to complete the process:\n\n' +
'http://' + req.headers.host + '/confirmation/' + token.token + '\n\n' +
'If you did not request this, please ignore this email.\n'
};
smtpTransport.sendMail(mailOptions, function(err) {
if (err) { return res.status(500).send({ msg: err.message }); }
res.status(200).send('A verification email has been sent to ' + user.email + '.');
});
})
},
confirmationPost(req,res, next) {
Token.findOne({ token: req.params.token }, function (err, token) {
if (!token)
{console.log("sss")
} else {
User.findOne({ _id: token._userId, email: req.body.email }, function (err, user) {
if (!user) return console.log(user)
if (user.isVerified) return res.status(400).send({ type: 'already-verified', msg: 'This user has already been verified.' });
user.isVerified = true;
user.save(function (err) {
if (err) { return res.status(500).send({ msg: err.message }); }
res.status(200).send("The account has been verified. Please log in.");
})
});
};
})
}
}
This is my Token Schema
const mongoose = require('mongoose');
const passportLocalMongoose = require('passport-local-mongoose');
const Schema = mongoose.Schema;
const tokenSchema = new mongoose.Schema({
_userId: {
type: Schema.Types.ObjectId,
ref: 'User' },
token: {
type: String,
required: true },
createdAt: {
type: Date, required: true,
default: Date.now, expires: 43200 }
});
tokenSchema.plugin(passportLocalMongoose);
module.exports = mongoose.model('Token', tokenSchema);
lastly my user schema
const mongoose = require('mongoose');
const passportLocalMongoose = require('passport-local-mongoose');
const Schema = mongoose.Schema;
const UserSchema = new Schema({
username: String,
name: String,
email: { type: String, unique: true },
image: String,
isVerified: { type: Boolean, default: false },
password: String,
passwordResetToken: String,
passwordResetExpires: Date,
posts: [
{
type: Schema.Types.ObjectId,
ref: 'Post'
}
]
});
UserSchema.plugin(passportLocalMongoose);
module.exports = mongoose.model('User', UserSchema);
everything works fine until the part where the email verification was sent to my email and when i clicked on the link. It gives an error, i tried to console.log
and found that this line from controllers folder index.js
confirmationPost(req,res, next) {
Token.findOne({ token: req.params.token }, function (err, token) {
if (!token)
{console.log("err")
} else {
User.findOne({ _id: token._userId, email: req.body.email }, function (err, user) {
gives me back null.
how do i link that current line to get the token from the registered user?
i've used postman to send a get request to the confirmation route while giving it back the same token and it works.

Resources