Nodejs Multer - Receiving 404 and Cannot Set Headers When Uploading MULTIPLE Files - node.js

I’m receiving a 404 Not Found and "Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client" when I attempt to submit a form that has two images uploaded. Neither image is a required field, so when I upload one image (can be either) everything works correctly, and I do not get an error. The issue only occurs when I upload both in one request. Checking my uploads folder, when I get a 404, both images are correctly uploaded.
Here is the code for multer:
const multerOptions = {
storage: multer.memoryStorage(),
fileFilter(req, file, next) {
const isPhoto = file.mimetype.startsWith('image/');
if (isPhoto) {
next(null, true);
} else {
next({ message: 'That filetype isn\'t allowed!' }, false);
}
},
};
export const upload = multer(multerOptions).fields([
{ name: 'avatar' },
{ name: 'accolade' },
]);
export const resize = async (req, res, next) => {
if (!req.files) {
next();
return;
}
Object.keys(req.files).forEach(async (file) => {
const extension = req.files[file][0].mimetype.split('/')[1];
req.body[file] = `${uuid.v4()}.${extension}`;
const uploaded = await jimp.read(req.files[file][0].buffer);
if (file === 'avatar') {
await uploaded.resize(300, jimp.AUTO);
} else if (file === 'accolade') {
await uploaded.resize(30, jimp.AUTO);
}
await uploaded.write(`./public/uploads/${req.body[file]}`);
next();
});
};
Here is the route:
router.post(
'/team-members/add/:id',
authController.authCheck,
userController.isAdmin,
userController.upload,
userController.resize,
userController.validateUser,
catchErrors(userController.addTeamMember),
);
And here are the other middleware methods in the route:
export const authCheck = (req, res, next) => {
(req.isAuthenticated()) ? next() : res.redirect('/login');
};
export const isAdmin = (req, res, next) => {
(req.user.role !== 'admin') ? res.redirect('/dashboard') : next();
};
export const validateUser = (req, res, next) => {
req.checkBody('firstName', 'There must be a first name!').notEmpty();
req.checkBody('lastName', 'There must be a last name!').notEmpty();
req.checkBody('email', 'There must be an email!').notEmpty();
req.checkBody('role', 'A role must be specified!').notEmpty();
const errors = req.validationErrors();
if (errors) {
req.flash('error', errors.map(err => err.msg));
res.redirect('back');
}
next();
};
And finally the function to add a user (it's wrapped in a function that catches errors rather than catching errors in the controller):
export const addTeamMember = async (req, res) => {
const org = await Org.findOne({ _id: req.params.id });
if (org) {
const newUser = new User(req.body);
newUser.organization = org._id;
newUser.invitation = true;
await newUser.save();
await org.update({ $push: { users: newUser } });
const inviteLink = `http://${req.headers.host}/join/${org._id}`;
await send({
user: newUser,
filename: 'invitation',
subject: `Welcome ${newUser.email}`,
inviteLink,
});
req.flash('success', `Yay! An invitation has been sent to ${newUser.email}`);
res.redirect(`/team-members/${org._id}`);
} else {
req.flash('error', 'No organization found!');
req.redirect('back');
}
};
I only get the error when I upload both an avatar and an accolade in one request. If I upload just one in a single request, I get no errors. In both cases, the image(s) are uploaded to the uploads directory I've specified, the user is added to my db, and an email for an invite is fired off. The redirect on success is a single GET request to a view with the same authCheck and isAdmin middlewares.
I've gone through and commented out the portions of code that are not necessary to submit the request (checkAuth, isAdmin, validateUser, and sending the email) but as long as I upload two in one request I get an error. Any ideas where I'm going wrong?

Posting the answer in case this ever trips anyone else up.
The next() call is inside the forEach block in the resize method, thus it is being called for each file that is being uploaded. Moving it outside the block (obviously) fixed the issue.

Related

Login route looks for /:id middleware in Nodejs

I have a middleware function for my post route /blog that prevents users not logged in to add blogs to the site, below is the function:
app.use('/blog',(req,res,next) => {
if ( req.path == '/blog/login') next();
if (!req.session.user){
res.status(403)
res.send('Please login to add blog')
return
}
else{
next()
}
})
I have a login route that looks like this:
router.post('/blog/login', async (req, res) => {
let { username,password } = req.body;
try{
checkString(username);
checkString(password);
username = username.toLowerCase();
if (!username.match(/^[a-zA-Z0-9!##\$%\^\&*\)\(+=._-]+$/g)) throw 'Only alphanumeric characters allowed for username'
if(!password.match(/^[a-zA-Z0-9!##\$%\^\&*\)\(+=._-]+$/g)) throw 'Only alphanumeric and special characters allowed for password'
if (username.length<4) throw 'Username should have 4 characters or more';
if (password.length<6) throw 'Password should have 6 characters or more';
const ans = await userData.checkUser(username,password)
if (ans==1){
const [id,user,name,pass] = await userData.getValues(username);
req.session.user = { username:user, name:name, password:pass, userId:id };
res.send({authenticated:true})
}
} catch(e){
res.send(400,{msg:e})
}
});
Below is the route that I want to be able to run without going to the middleware:
router.get('/blog/:id', async (req, res) => {
try{
const blog = await blogData.getOneBlog(req.params.id);
res.send(blog)
}
catch{
res.sendStatus(500);
}
});
My index.js:
const blogRoutes = require('./blogs');
const constructorMethod = (app) => {
app.use('/blog', blogRoutes);
app.use('*', (req, res) => {
res.sendStatus(404);
});
};
module.exports = constructorMethod;
The above route goes directly to the middleware because anything I call after the /blog sends it to the middleware but I only want to send the /blog/:id to the middleware.

how to get data by name as well as its id in express route prameter, mongoose

finding data by id, route parameter like this
url is : http://localhost:8000/products/60d1789867bc6624403ade6e
// getting a single product
router.get("/:id", async (req, res, next) => {
const id = req.params.id;
try {
const result = await Product.findById(id);
return res.json({
result,
});
} catch (error) {
return res.status(400).json({
msg: "product not found",
error: error,
});
}
});
but when I try to find by name
url : http://localhost:8000/products/product_name
// getting products by name
router.get("/:name", async (req, res, next) => {
const name = req.params.name;
try {
const result = await Product.find({ name: name });
return res.json({
result,
});
} catch (error) {
return res.status(400).json({
msg: "product not found",
error: error,
});
}
});
this block of code does not execute, url req goes to :id parameter
how to differenciate this
You can't. For Express, both routes are same (giving the different name to variable doesn't work here).
You need to go with either one of the mentioned route and need to modifies the logic to find the product data.
router.get("/:id_or_name", async (req, res, next) => {
Get the value of this id_or_name variable and check if it is the valid ObjectId is not using mongoose.Types.ObjectId.isValid(). If it is valid then execute the .findById() else go for .find() method.
const id_or_name = req.params.id_or_name;
// ... code ...
if (mongoose.Types.ObjectId.isValid(id_or_name)) {
result = await Product.findById(id_or_name);
} else {
result = await Product.find({ firstName: id_or_name });
}
// ... code ...

api call getting affected by another api call's validation

not really sure if my title is correct but my problem is that I have this reset password token checker in my api that seems to get affected by another api that finds a specific user, this api has user validation.
Here is what they look like:
//get specific user
router.get('/:id', validateToken, async (req, res) => {
const id = req.params.id
const user = await User.findByPk(id);
res.json(user);
});
//reset-password token check
router.get('/reset-pass', async (req, res) => {
await User.findOne({
where: {
resetPasswordToken: req.body.resetPasswordToken,
resetPasswordExpires: {
[Op.gt]: Date.now()
}
}
}).then(user => {
if(!user) {
res.status(401).json({ error: 'Password reset link is invalid or has expired.'})
} else {
res.status(200).send({
username: user.username,
message: 'Password reset link Ok!'
});
}
});
});
then here is the validateToken
const validateToken = (req, res, next) => {
const accessToken = req.cookies['access-token'];
if (!accessToken)
return res.status(401).json({error: 'User not authenticated!'});
try {
const validToken = verify(accessToken, JWT_SECRET)
req.user = validToken;
if(validToken) {
req.authenticated = true;
return next();
}
} catch(err) {
res.clearCookie('access-token')
return res.status(400).json({error: err}).redirect('/');
}
};
when I comment out the get specific user api the reset password token check works. If I remove validateToken it returns null instead of giving me the username and message.
One of the things I notice is the route param "/:id", that means that literally everything would be processed by get specific user because all routes start with "/", only use params in routes with a prefix like "/user/:id" that way only the routes that starts with "/user" will execute that code.
Change your code to:
//get specific user
router.get('/user/:id', validateToken, async (req, res) => {
const id = req.params.id
const user = await User.findByPk(id);
res.json(user);
});

Express routing and jsonwebtoken, staying logged in after token creation

I'm having a hard time connecting the last dots building a role based access control api in Express.
Following this tutorial and implementing onto my existing program, but I think I am missing the last step and after countless tutorials analysis paralysis has set in. I have since scaled back all my necessary code to what I think is the bare minimum.
Currently I am able to create a new user and save them to the mongoose database. I can see the hash by bcrypt is doing its thing and I can see the token being generated in the response after signing up. However as soon as I navigate to a new page after signup or login, for eg the users own id page/user/:userId as per tutorial, I keep getting You need to be logged in. I know I need to check for a token on every request but my question is, why doesn't it seem like the middleware is checking for the token or something is holding it back?
Since the token is shown in the json reponse surely I should be able to check for the tokens existence with the next get request at for eg the /user/:userId page? Isn't that the idea? Or is the browser just showing the response but I still need to actually store it? I don't understand where it goes to so to speak..
Any advice? Or is this a session thing? I know its a bit hard without all the code but if anyone could spot anything relevant so that I could research my next steps I would much appreciate it!
First this middleware in app.js
app.use(express.json());
app.use(express.urlencoded({extended: true}));
app.use('/', async (req, res, next) => {
if (req.headers['x-access-token']) {
try {
const accessToken = req.headers['x-access-token'];
const {userId, exp} = await jwt.verify(accessToken, process.env.JWT_SECRET);
console.log('token verified'); // not printing to console
// If token has expired
if (exp < Date.now().valueOf() / 1000) {
return res.status(401).json({
error: 'JWT token has expired, please login to obtain a new one',
});
}
res.locals.loggedInUser = await User.findById(userId);
next();
} catch (error) {
next(error);
}
} else {
next();
}
});
app.use('/', userRoutes);
I have built the roles using the module access-control which is required
const AccessControl = require('accesscontrol');
const ac = new AccessControl();
exports.roles = (function() {
ac.grant('basic')
.readOwn('profile')
.updateOwn('profile');
ac.grant('supervisor')
.extend('basic')
.readAny('profile');
ac.grant('admin')
.extend('basic')
.extend('supervisor')
.updateAny('profile')
.deleteAny('profile');
return ac;
})();
routes examples as per tutorial.
router.get('/signup', (req, res, next) => {
res.render('signup', {
viewTitle: 'User SignUp',
});
});
router.post('/signup', userController.signup);
router.get('/login', (req, res, next) => {
res.render('login', {
viewTitle: 'User Login - WTCT OPS',
});
});
router.post('/login', userController.login );
router.get('/add', userController.allowIfLoggedin, userController.grantAccess('readAny', 'profile'), userController.add);
router.get('/users', userController.allowIfLoggedin, userController.grantAccess('readAny', 'profile'), userController.getUsers);
router.get('/user/:userId', userController.allowIfLoggedin, userController.getUser);
router.put('/user/:userId', userController.allowIfLoggedin, userController.grantAccess('updateAny', 'profile'), userController.updateUser);
router.delete('/user/:userId', userController.allowIfLoggedin, userController.grantAccess('deleteAny', 'profile'), userController.deleteUser);
relevant part of controller
async function hashPassword(password) {
return await bcrypt.hash(password, 10);
}
async function validatePassword(plainPassword, hashedPassword) {
return await bcrypt.compare(plainPassword, hashedPassword);
}
// grant access depending on useraccess role
exports.grantAccess = function(action, resource) {
return async (req, res, next) => {
try {
const permission = roles.can(req.user.role)[action](resource);
if (!permission.granted) {
return res.status(401).json({
error: 'You don\'t have enough permission to perform this action',
});
}
next();
} catch (error) {
next(error);
}
};
};
// allow actions if logged in
exports.allowIfLoggedin = async (req, res, next) => {
try {
const user = res.locals.loggedInUser;
if (!user) {
return res.status(401).json({
error: 'You need to be logged in to access this route',
});
}
req.user = user;
next();
} catch (error) {
next(error);
}
};
// sign up
exports.signup = async (req, res, next) => {
try {
const {role, email, password} = req.body;
const hashedPassword = await hashPassword(password);
const newUser = new User({email, password: hashedPassword, role: role || 'basic'});
const accessToken = jwt.sign({userId: newUser._id}, process.env.JWT_SECRET, {
expiresIn: '1d',
});
newUser.accessToken = accessToken;
await newUser.save();
res.send({
data: newUser,
message: 'You have signed up successfully',
});
} catch (error) {
next(error);
}
};
exports.login = async (req, res, next) => {
try {
const {email, password} = req.body;
const user = await User.findOne({email});
if (!user) return next(new Error('Email does not exist'));
const validPassword = await validatePassword(password, user.password);
if (!validPassword) return next(new Error('Password is not correct'));
const accessToken = jwt.sign({userId: user._id}, process.env.JWT_SECRET, {
expiresIn: '1d',
});
await User.findByIdAndUpdate(user._id, {accessToken});
res.status(200).json({
data: {email: user.email, role: user.role},
accessToken,
});
} catch (error) {
next(error);
}
};
// get one user
exports.getUser = async (req, res, next) => {
try {
const userId = req.params.userId;
const user = await User.findById(userId);
if (!user) return next(new Error('User does not exist'));
// console.log(req.params);
res.send(200).json({
data: user,
});
} catch (error) {
next(error);
}
};
Why when trying to post to the endpoint /user/:userId is the middleware not checking for the token?
Thank you for any advice!
Update:
So far I have tried to removed the / from app.use. I saw I made that mistake now, but also tried removing it from the app.use(userRoutes); middleware to make it apply to all http requests but no luck.
app.use(async (req, res, next) => {
if (req.headers['x-access-token']) {
try {
const accessToken = req.headers['x-access-token'];
const {userId, exp} = await jwt.verify(accessToken, process.env.JWT_SECRET);
// If token has expired
if (exp < Date.now().valueOf() / 1000) {
return res.status(401).json({
error: 'JWT token has expired, please login to obtain a new one',
});
}
res.locals.loggedInUser = await User.findById(userId);
// console.log('Time:', Date.now());
next();
} catch (error) {
next(error);
}
} else {
next();
}
});
app.use(userRoutes);
I also thought that maybe because my server makes http requests in the backend maybe that was causing a problem in setting the x-access-token header? So I tried to change the x-access-token mw to use router.use on all routes but still nothing. I don't understand what I am missing. And just to be sure I'm not missing something fundamental, since I am using the JWT I do not need to use local storage or cookies to allow for browsing between pages while logged in since I can use the token set in the header, correct?
Thanks again for any advice!
That's because your middleware is only tied to the / route. Remove it if you want it to be used for every route. Take a look at the ExpressJS Docs regarding middleware.

Cast to ObjectId failed for value ... at path "_id" for model but I am not doing any query

I have an express route that gets call with axios from the frontend. The thing is, not matter what I put into the route I always get the same error:
"Cast to ObjectId failed for value "getTodosMisProductos" at path "_id" for model "local""
I'm not doing any query to mongoose in that route but in any other route where I make a query everything works fine.
I've checked the middleware but there is not any query to mongoose
getTodosMisProductos
router.get("/getTodosMisProductos", auth, async (req, res) => {
/*
try {
const data = await Local.findOne({ user: req.user.id }).populate("products.producto");
console.log(data);
if (!data) {
return res
.status(404)
.json({ errors: [{ msg: "No se encontro el local" }] });
}
return res.status(200).json(data.products);
} catch (error) {
console.log(req.user.id);
console.error("error en llamado");
return res.status(500).send("Server Error");
}
*/
console.log("algo");
return res.status(200).json({ msg: "success" });
});
the code commented is the code I need to use, I changed it for testing purposes but even with that simple new code I get the same error.
auth middleware
const jwt = require("jsonwebtoken");
const config = require("config");
module.exports = function (req, res, next) {
// Get token from header
const token = req.header("x-auth-token");
// Check if not token
if (!token) {
return res
.status(401)
.json({ msg: "No tienes autorización para hacer esto" });
}
// Verify token
try {
const decoded = jwt.verify(token, require("../config/keys").jwtSecret);
req.user = decoded.user;
next();
} catch (error) {
res.status(401).json({ msg: "El token es inválido" });
}
};
action from where the route gets called
export const getAllProductos = () => async (dispatch) => {
try {
console.log("Esto se llama");
const res = await axios.get("/api/local/getTodosMisProductos/");
dispatch({
type: SET_PRODUCTS,
payload: res.data,
});
} catch (err) {
const errors = err.response.data.errors;
if (errors) {
errors.forEach((error) => dispatch(setAlert(error.msg, "danger")));
}
}
};
The response status is always 500 (Internal Server Error)
EDIT
//#route GET api/local/:id
//#desc obtener local por id
//#access private
router.get("/:id", auth, async (req, res) => {
try {
const local = await Local.findById(req.params.id);
if (!local) {
return res
.status(404)
.json({ errors: [{ msg: "No se encontro el local" }] });
}
return res.status(200).json(local);
} catch (error) {
console.error(error.message);
res.status(500).send("Server Error");
}
});
You have another route that also match /api/local/getTodosMisProductos/
Apparently it got matched with /api/local/:id,
where you get req.params.id = "getTodosMisProductos" and got passed down to await Local.findById(req.params.id)
And mongoose can't convert "getTodosMisProductos" to ObjectId, hence the error.
The order in which you declare the route affects the matching priority.
The order is first comes first serves, so make sure you declare /api/local/addProducto or any other routes that starts with /api/local/ before declaring /api/local/:id

Resources