Update a property in document in an Express route (Mongoose, MongoDB, Express) - node.js

I've successfully set up the registration and login functionality using Express, MongoDB and Mongoose.
I would like to log when the user last visited the site once the user's credential is accepted in a lastConnection property of the user document,
I tried but "lastConnection" is null (see the line below where I add a comment)
router.post("/login", async function(req, res) {
const { errors, isValid } = validateLoginInput(req.body);
if (!isValid) {
return res.status(400).json(errors);
}
const email = req.body.email;
const password = req.body.password;
const user = await User.findOne({ email }).then(user => {
if (!user) {
errors.email = "Email already exists";
}
console.log("user ", user); <-- returns an object with the datas of user
bcrypt.compare(password, user.password).then(isMatch => {
if (isMatch) {
const payload = {
id: user.id,
name: user.name
};
user.lastConnection = new Date(); <-- doesn't work
jwt.sign(
payload,
keys.secretOrKey,
{
expiresIn: 7200
},
(err, token) => {
res.json({
success: true,
token: "Bearer " + token
});
}
);
} else {
errors.password = "Password is not correct";
// return res
// .status(400)
// .json({ passwordincorrect: "Password incorrect" });
}
});
});
return {
errors,
isValid: isEmpty(errors)
};
});
Any ideas? I think I have to do an update but I don't know where to put it

Try replacing user.lastConnection = new Date(); with
user.update({ lastConnection: new Date() })
.then( updatedUser => {
console.log(updatedUser)
// put jwt.sign code here
})

Related

Error: Invalid email/password combination with Node.js and MongoDB

Am trying to login my admin , i defined the login credentials both in the mongodb and in the .env so here is the code which has a problem.
const Admin = require('../models/admin');
const Voters = require('../models/voters');
const bcrypt = require('bcrypt');
exports.checkCredentials = async (req, res, next) => {
const email = req.body.email;
const password = req.body.password;
Admin.findOne({ email: email }).exec(async (error, adminData) => {
if (error) {
// some error occured
return res.status(400).json({ error });
}
if (adminData) {
// email is correct checking for password
const match = await bcrypt.compare(password, adminData.password);
if (match) {
req.adminID = adminData._id;
next();
} else {
return res.status(200).json({
msg: 'Invalid email/password combination yyy',
});
}
} else {
// no data found for given email
return res.status(200).json({
msg: 'Invalid email/password combination !!!!',
});
}
});
};
exports.verifyVoter = async (req, res, next) => {
let query;
if (req.query.voterID) {
query = {
voterID: req.query.voterID,
};
} else {
query = {
phone: req.body.phone,
};
}
console.log(query);
Voters.findOne(query).exec(async (error, voterData) => {
if (error) {
// some error occured
return res.status(400).json({ error });
}
if (voterData) {
// Voter found
if (voterData.hasRegistered === true) {
return res.status(200).json({
msg: 'Voter already registered',
});
} else {
req.phone = voterData.phone;
req.district = voterData.pinCode;
req._id = voterData._id;
next();
}
} else {
// no data found for given Voter
return res.status(200).json({
msg: 'Invalid VoterID',
});
}
});
};
that code above brings an error but this is how i defined my admin credentials in the .env
ADMIN_EMAIL = bkroland19#gmail.com
ADMIN_PASSWORD =felinho/013
and this is how i defined them in mongodb
{
"email": "bkroland19#gmail.com",
"password": "felinho/013"
}
and this is the resulting error i get yet the email am entering matches those two emails.
Any help please
Am expecting to be allowed to login in when i enter the credentials as they are in the mongodb database
If you store the password in cleartext you don't need bcrypt.compare:
const match = password === adminData.password;
if (match) {
req.adminID = adminData._id;
next();
}
Anyway, it is strongly suggested to encrypt it, you can do it with:
const salt = await bcrypt.genSalt(12);
const encryptedPassword = await bcrypt.hash(password, salt);
const user = await Admin.create({ email, password: encryptedPassword });

Getting an undefined token in backend

Hello i am trying to use my token in my application after user is logged in but am getting an undefined response in my console. Below are my codes. How can i correct my code to be able to access token inside application and use to do other features of the application?
my controller
import User from "../models/user";
import Stripe from "stripe";
const stripe = Stripe(process.env.STRIPE_SECRET);
export const createConnectAccount = async (req, res) => {
console.log(req.user);
try {
const user = await User.findById(req.user._id).exec();
console.log("USER ==> ", user);
if (!user.stripe_account_id) {
const account = await stripe.accounts.create({
type: "express",
});
console.log("ACCOUNT ===>", account);
user.stripe_account_id = account.id;
user.save();
}
} catch (error) {
res.status(500).json();
}
};
my middleware
var { expressjwt: jwt } = require("express-jwt");
// req.user
export const requireSignin = jwt({
//secret, expiryDate
secret: process.env.JWT_SECRET,
algorithms: ["HS256"],
});
my routes
import express from "express";
const router = express.Router();
import { requireSignin } from "../middlewares";
import { createConnectAccount } from "../controllers/stripe";
router.post("/create-connect-account", requireSignin, createConnectAccount);
module.exports = router;
my auth controller
import User from "../models/user";
import jwt from "jsonwebtoken";
export const register = async (req, res) => {
console.log(req.body);
const { name, email, password } = req.body;
if (!name) return res.status(400).send("Name is required");
if (!password || password.length < 6)
return res
.status(400)
.send("Password is required and should be minimum 6 characters long");
let userExist = await User.findOne({ email }).exec();
if (userExist) return res.status(400).send("Email is taken");
const user = new User(req.body);
try {
await user.save();
console.log("User saved successfully", user);
return res.json({ ok: true });
} catch (err) {
console.log("CREATE USER FAILED", err);
return res.status(400).send("Error.Try again");
}
};
export const login = async (req, res) => {
// console.log(req.body);
const { email, password } = req.body;
try {
//check if user with credentials
let user = await User.findOne({ email }).exec();
// console.log("USER EXISTS", user);
if (!user) res.status(400).send("User with email not found");
//compare password
user.comparePassword(password, (err, match) => {
console.log("COMPARE PASSWORD IN LOGIN ERR", err);
if (!match || err) return res.status(400).send("Wrong password");
//("GENERATE A TOKEN THEN SEND AS RESPONSE TO CLIENT");
let token = jwt.sign({ _id: user._id }, process.env.JWT_SECRET, {
expiresIn: "7d",
});
res.json({
token,
user: {
_id: user._id,
name: user.name,
email: user.email,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
},
});
});
} catch (err) {
console.log("LOGIN ERROR", err);
res.status(400).send("Signin failed");
}
};
my terminal output
POST /api/login 200 1142.309 ms - 349
undefined
POST /api/create-connect-account 500 9.092 ms - -
Headers
import axios from "axios";
export const createConnectAccount = async (token) => {
await axios.post(
`${process.env.REACT_APP_API}/create-connect-account`,
{},
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
};
I'm sorry to tell you your code has other errors in it.
My guess is that your res is not well written in auth controller, login function :
res.status(201).json({
token :token,
user: user
})
Also when reading your token trying to authenticate : it will be easier to use the same package than the one that sign it.
const jwt = require("jsonwebtoken");
exports. requireSignin = () => {
return async (req, res, next) => {
try {
const token = req?.headers?.authorization?.split(" ")[1];
const decodedToken = jwt.verify(token, process.env.JWT_SECRET);
const userId = decodedToken._id;
const user = await User.findOne({ _id: userId });
if (user) {
req.auth = {
user: user,
};
} else {
throw new Error("user not found");
}
next();
} catch (error) {
console.log(error.message);
res.status(401).json({ error: "failed to authenticate" });
}
};
};
But your code is pretty hard to read :
To make it easier to read and clearer for you, try and use joy or yup
Joi : https://www.npmjs.com/package/joi
Yup : https://www.npmjs.com/package/yup
With those you will be able to create middlewares to avoid wrong entries in your body : for example
if (!name) return res.status(400).send("Name is required");
is processed automatically with those packages
Also, you shouldn't use 'import' and 'require' in the same project, choose either one of them
I hope this will help

Mongoose return "new ObjectId("//id")" instead of just the id

I am trying to do my login function (I am using bcrypt and jsonwebtoken) the problem is that console.log (user._id) returns me "new ObjectId (" 6148f043ebbaa0ab41ac8499 ")" instead of just "6148f043ebbaa0ab41ac8499" , which would be easier for the creation of the token.
module.exports.login = async (req, res) => {
const { email, password } = req.body;
// Compare the req.body.password to the hashed password in DB
const user = await UserModel.findOne({ email: email });
const match = await bcrypt.compare(password, user.password);
if (match) {
try {
const user = await UserModel.findOne({ email: email });
console.log(user._id);
// Assign a token
const token = jwt.sign({ userId: user._id }, process.env.LOGIN_TOKEN, {
expiresIn: "1h",
});
console.log(token);
res.cookie("jwt", token, { httpOnly: true});
res.status(200).json({ user: user._id });
} catch (err) {
res.status(500).json(err);
}
} else {
res.status(500).json({ message: "error!!!" });
}
};
How to fix this please?
That is a normal behaviour. Since you got an ObjectId, you can convert it to a string by calling the toHexString() method on it. I have also modified the code to check for an undefined user, and removed the extra call to find a user since you already did in the previous line. Please see updated code:
module.exports.login = async (req, res) => {
const { email, password } = req.body;
const user = await UserModel.findOne({ email: email });
if (!user) {
return res.status(400).json({ message: "Unauthorised"});
}
// Compare the req.body.password to the hashed password in DB
const match = await bcrypt.compare(password, user.password);
if (match) {
try {
// Convert user id (ObjectId) to a string
const userId = user._id.toHexString();
// Now user id is a string
console.log(userId);
// Assign a token
const token = jwt.sign({ userId }, process.env.LOGIN_TOKEN, {
expiresIn: "1h",
});
console.log(token);
res.cookie("jwt", token, { httpOnly: true});
res.status(200).json({ user });
} catch (err) {
res.status(500).json(err);
}
} else {
res.status(400).json({ message: "Unauthorised" });
}
};
use the following
const id = _id.toHexString();

Node.js bcrypt compare problem only return false

I want to do my login API with Node.js and MongoDB and when I compare my pass from input to the one that's in the db I always get false I read other post on StackOverFlow but didn't helped me.
I think I also found the problem: When I use hash on my password input to check it manually with the one from db at every request is onatherone.
So maybe that's the problem but I don't know how to solve. I read a lot about this but still cant solve here is my code:
const match = await bcrypt.compare(password, user.password, (res) => {console.log(res)}) //false
my login api
router.post('/login', body('email').isEmail(), body('password').exists(), async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() })
}
const { email, password } = req.body
const user = await User.findOne({ email })
if (!user) {
return res.status(401).json({ "err": "invalid credentials" })
}
// const match = await bcrypt.compare(password, user.password).then(function (res) { console.log(res) })
const match = await bcrypt.compare(password, user.password);
console.log(match)
})
and here is the register api
router.post("/register", body("email").isEmail(), body("password").isLength({ min: 5 }), body("username").isLength({ min: 1 }), async (req, res) => {
const { email, username, password } = req.body
const errors = validationResult(req);
const saltRounds = 10;
try {
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() })
}
//check if the user exist
const duplicated = await User.findOne({ email })
if (duplicated) { return res.status(401).json({ "err": "Email is taken" }) }
const user = new User({ username, email, password })
//crypt pass
user.password = bcrypt.hashSync(process.env.secret, saltRounds);
//generate token with jwt
const payload = {
id: user.id
}
jwt.sign({
payload,
}, process.env.jwtSecret, { expiresIn: '999h' }, (err, token) => { console.log(token) });
//save the user
await user.save()
res.status(200).send("User Stored")
} catch (error) {
res.status(500).send(error.body)
}
})
Your problem is that you using bcrypt.hash in the wrong way.
It seems like you provide this method kind of key, which you shall not provide.
This method accepts the value to hash, and saltRounds.
So you basically need to change your Registration API code to:
router.post("/register", body("email").isEmail(), body("password").isLength({ min: 5 }), body("username").isLength({ min: 1 }), async (req, res) => {
const { email, username, password } = req.body
const errors = validationResult(req);
const saltRounds = 10;
try {
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() })
}
//check if the user exist
const duplicated = await User.findOne({ email })
if (duplicated) { return res.status(401).json({ "err": "Email is taken" }) }
const user = new User({ username, email, password })
//crypt pass
user.password = bcrypt.hashSync(password, saltRounds);
//generate token with jwt
const payload = {
id: user.id
}
jwt.sign({
payload,
}, process.env.jwtSecret, { expiresIn: '999h' }, (err, token) => { console.log(token) });
//save the user
await user.save()
res.status(200).send("User Stored")
} catch (error) {
res.status(500).send(error.body)
}
}
And more specific, you store the password with:
user.password = bcrypt.hashSync(password, saltRounds);
try use Promise for this
const match = await new Promise((resolve, reject) => {
bcrypt.compare(password, user.password, function(error, res){
if (error) { reject(error); }
resolve(res);
})
})

ExpressJS: Can't set headers after they are sent

I have an API in ExpressJS. Within that API I have a login endpoint, when posting to that endpoint however I keep getting the exception that headers cannot be set after they have been sent.
I understand this is normally a callback that is being called twice or not properly returning from something that has set headers, causing the app to attempt to set them again, however in my /login endpoint I am not doing this.
I cannot understand why this happening, I would love some input as to why as I am close to pulling my hair out reading the same replies and answers. I hope it is something obvious I am missing.
import User from '../../models/user';
import { Router } from 'express';
import jwt from 'jsonwebtoken';
export default () => {
const route = Router();
route.post('/create', async (req, res, next) => {
if (!req.body.email || !req.body.password) {
return res
.status(400)
.json({ message: 'username or password is missing' });
}
const { email, password } = req.body;
const count = await User.count({ email });
if (count > 0) {
return res.status(409).json({ message: 'email must be unique' });
}
const newUser = await new User({ email, password });
const doc = await newUser.save();
return res.status(201).json({ type: 'account', attributes: doc });
});
route.post('/login', async (req, res, next) => {
if (req.body.email && req.body.password) {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (user) {
user.comparePassword(password, isMatch => {
if (isMatch) {
const token = jwt.sign(
{ sub: user.id, roles: [], email: user.email },
process.env.SECRET_KEY,
{ expiresIn: '12h' },
);
return res
.status(200)
.json({ type: 'account', attributes: { token } });
}
});
}
}
res.sendStatus(401);
});
return route;
};
import User from '../../models/user';
import { Router } from 'express';
import jwt from 'jsonwebtoken';
export default () => {
const route = Router();
route.post('/create', async (req, res, next) => {
if (!req.body.email || !req.body.password) {
return res
.status(400)
.json({ message: 'username or password is missing' });
}
const { email, password } = req.body;
const count = await User.count({ email });
if (count > 0) {
return res.status(409).json({ message: 'email must be unique' });
}
const newUser = await new User({ email, password });
const doc = await newUser.save();
return res.status(201).json({ type: 'account', attributes: doc });
});
route.post('/login', async (req, res, next) => {
if (req.body.email && req.body.password) {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (user) {
return user.comparePassword(password, isMatch => {
if (isMatch) {
const token = jwt.sign(
{ sub: user.id, roles: [], email: user.email },
process.env.SECRET_KEY,
{ expiresIn: '12h' },
);
return res
.status(200)
.json({ type: 'account', attributes: { token } });
} else {
return res.status(400)
.json({ message: 'username or password is invalid' });
}
});
}
}
res.sendStatus(401);
});
return route;
};
Have a look at the updated code return was missing at
return user.comparePassword(password, isMatch => {
Hope it'll fix your issue.
The problem in here. Your callback in comparePassword return only inside that callback. So the code still run to res.sendStatus(401) and after the callback is done it will run res.status(200).json...
user.comparePassword(password, isMatch => {
if (isMatch) {
const token = jwt.sign(
{ sub: user.id, roles: [], email: user.email },
process.env.SECRET_KEY,
{ expiresIn: '12h' },
);
return res
.status(200)
.json({ type: 'account', attributes: { token } });
}
});
Try to promisify comparePassword method in the user model:
userSchema.methods.comparePassword = function (password) {
return new Promise( function(resolve, reject) {
resolve(password === this.password);
});
}
Now you can use await syntax to get the promise result:
route.post('/login', async (req, res, next) => {
if (req.body.email && req.body.password) {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (user) {
const isMatch = await user.comparePassword(password);
if (isMatch) {
const token = jwt.sign(
{ sub: user.id, roles: [], email: user.email },
process.env.SECRET_KEY,
{ expiresIn: '12h' },
);
return res
.status(200)
.json({ type: 'account', attributes: { token } });
}
}
}
res.sendStatus(401);
});

Resources