Implement multiple middleware for authentication and Multer in NodeJS - node.js

I have a form sent from an Angular front-end to a nodeJS server.
This form can contain input file (not mandatory) and other text fields.
So i'm using FormData for the multipart encoding.
Here is the code from my Angular service :
const formDataGenerated = generateFormDataFromForm(form);
formDataGenerated.append('id', this.tokenStorage.getUser().id);
if (file !== null) {
formDataGenerated.append('file', file);
formDataGenerated.set('riddleFile', file.name);
}
return this.http.post(QUEST_API + 'create', formDataGenerated)
.pipe(
map((res: any) => {
return res;
})
);
i'm using this route in nodeJS: router.post('/create', [auth, multer], questCtrl.create);
The second middleware 'multer' is working fine
const util = require("util");
const multer = require("multer");
const path = require('path')
const pathFile = path.resolve('./resources/assets/uploads/')
let storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, pathFile);
},
filename: (req, file, cb) => {
cb(null, file.originalname);
},
});
let uploadFile = multer({
storage: storage,
}).single("file");
let uploadFileMiddleware = util.promisify(uploadFile);
module.exports = uploadFileMiddleware;
And when i'm in my questController i can get all the form values like this :
const formValue = JSON.parse(JSON.stringify(req.body));
At this point, my file is correctly uploaded and i can get all the needed data from the submited form
But now, i want to add another middleware that will allow me to authenticate the user using JwtToken.
In this code i need to access req.body.id that is submitted in FormData
But now, due to the multipart enconding i can't access this property so easily.
So my question is, how to implement my 'auth' middleware and after the user is authorized, continue on my multer middleware and controller (as it's currently working)

Generally, the request in Express falls through middleware until it is not broken by res.send(). The "jump" to next middleware is secured with next() function, that is passed to the middleware as third argument.
app.use((req, res, next) => {
if (req.body.password === 'mypassword') {
req.myAuthentication = true // append whatever props to req
console.log('I have authenticated the user!')
next() <-- sends request to the next middleware
})
app.get('/authenticated', (req, res, next) => {
if (req.myAuthentication) { // use your property in next middleware
res.sendFile('authenticated.html')
}
res.status(401)
next() <-- no effect, res.send/res.status finishes the middleware fn

Related

How to use multer with express.Router()?

I want to use multer in my nodejs app to upload user profile pictures. My routes are managed by express router. I have checked a lot of tutorials but nothing matches my exact use case. I want to let the users upload their profile pictures to my API, but before the request reaches the upload function I want to perform some validations like password and API key checks.
here is my upload controller,
const multer = require("multer");
const path = require("path");
const dp_storage = multer.diskStorage({
destination: path.join(__dirname, "../user_uploads/images/dp"),
filename: function (req, file, cb) {
cb(
null,
file.fieldname + "-" + Date.now() + path.extname(file.originalname)
);
},
});
// Init dp Upload
const dp_upload = multer({
storage: dp_storage,
limits: { fileSize: 2000000 }, // 1 mb
fileFilter: function (req, file, cb) {
checkFileTypeForUserDP(file, cb);
},
}).single("dp");
function checkFileTypeForUserDP(file, cb) {
// Allowed ext
let filetypes = /jpeg|jpg|png|gif|webp/;
// Check ext
let extname = filetypes.test(path.extname(file.originalname).toLowerCase());
// Check mime
let mimetype = filetypes.test(file.mimetype);
if (mimetype && extname) {
return cb(null, true);
} else {
cb("Error: jpeg, jpg, png, gif Images Only!");
}
}
exports.uploadDP = async (req, res) => {
try {
dp_upload(req, res, (err) => {
if (err) {
console.log(err);
} else {
if (req.file == undefined) {
res.status(404).json({
success: false,
msg: "File is undefined!",
file: `uploads/${req.file.filename}`,
});
} else {
res.status(200).json({
success: true,
msg: "File Uploaded!",
file: `uploads/${req.file.filename}`,
});
}
}
});
} catch (error) {console.log(error);}
};
The above code works fine if I use it directly without any API key validation or user authentication.
Here is my router,
const express = require("express");
const router = express.Router();
const { authenticateUser ,apiKeyCheck} = require("../server");
const { uploadDP } = require("../controllers/file");
//this route works
router.post(
"/upload/dp_without_authentication",
uploadDP
);
//this is not working
router.post(
"/upload/dp",
apiKeyCheck,
authenticateUser,
uploadDP
);
module.exports = router;
The "/upload/dp" route is failing because the apiKeyCheck and authenticateUser functions can not read the user credentials from req.body.
So, in order to fix that I have added the following lines to my main server file,
const multer = require("multer");
const upload = multer();
app.use(upload.array());
But now the uploadDP function is not even called, instead it returns the following error:
MulterError: Unexpected field
at wrappedFileFilter (/Users/sujith/Documents/Personal_projects/VocabularyServer/node_modules/multer/index.js:40:19)
at Busboy.<anonymous> (/Users/sujith/Documents/Personal_projects/VocabularyServer/node_modules/multer/lib/make-middleware.js:115:7)
at Busboy.emit (node:events:394:28)
at Busboy.emit (/Users/sujith/Documents/Personal_projects/VocabularyServer/node_modules/busboy/lib/main.js:38:33)
at PartStream.<anonymous> (/Users/sujith/Documents/Personal_projects/VocabularyServer/node_modules/busboy/lib/types/multipart.js:213:13)
at PartStream.emit (node:events:394:28)
at HeaderParser.<anonymous> (/Users/sujith/Documents/Personal_projects/VocabularyServer/node_modules/busboy/node_modules/dicer/lib/Dicer.js:51:16)
at HeaderParser.emit (node:events:394:28)
at HeaderParser._finish (/Users/sujith/Documents/Personal_projects/VocabularyServer/node_modules/busboy/node_modules/dicer/lib/HeaderParser.js:68:8)
at SBMH.<anonymous> (/Users/sujith/Documents/Personal_projects/VocabularyServer/node_modules/busboy/node_modules/dicer/lib/HeaderParser.js:40:12)
If I remove the file from postman request, it is able to call uploadDP function.
What am I doing wrong here?
As Multer official docs warns the use of multer as a global middleware;
WARNING: Make sure that you always handle the files that a user uploads. Never add multer as a global middleware since a malicious
user could upload files to a route that you didn't anticipate. Only
use this function on routes where you are handling the uploaded files.
Therefore I recommend exploring a safer way of handling files as answered
here

Multer middlware in Node.js returns empty object in req.body and undefined in req.file

The problem is when I use multer and send a request in Postman the req.body comes as an empty object and the req.file comes as undefined. I've unchecked the content-type header in postman.
And here's the code:
//Route
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, '../uploads/');
},
filename: function (req, file, cb) {
cb(null, new Date().toISOString() + file.originalname);
}
});
const upload = multer({
storage,
limits: {fileSize: 1024 * 1024 * 10}
});
router.post('/test', upload.single('profilePicture'), authController.test);
//Controller
const test = (req, res) => {
console.log(req.body)
console.log(req.files)
res.json({body:req.body, files:req.files})
}
//app.js
app.use(express.json({extended: true, limit: '30mb'}));
app.use(express.urlencoded({extended: true, limit: '30mb'}))
app.use(cookieParser());
app.use('/api/auth', authRoutes);
app.use('/api/product', productRoutes);
app.use('/api/profile', profileRoutes);
Edit: turnes out, the problem is in Postman. I made a request with axios from a React app and everything works. So the question is, why doesn't it work in Postman? Is it some Bug in software or is there some settings that we're supposed to change?
The problem is that Nodejs is by default uses Ansynchornus Javascript. You need to use the async-await approach and try-catch-finally methods over conventional JS programming.
So your controller would look like -
//Route
router.post('/test', async (req, res, next)=>
{
try{
await upload.single('profilePicture')
next()
} catch(err){
console.log(err)
res.send('failed!')
},
authController.test);
//Controller
const test = async (req, res) => {
try{
console.log(req.body)
console.log(req.files)
res.json({body:req.body, files:req.files})
} catch(err){
console.log(err);
}
}
A late addition to the answer.
If you're trying to just access the uploaded image, then you should make use of the buffer.
var storage = multer.memoryStorage()
var upload = multer({ storage: storage })

Read req.body whether multipart/form-data or application/x-www-form-urlencoded entype form?

I'm building a form backend like Formspree on NextJS, so I need to be able to receive all form POSTs at the same endpoint, without being able to control what the forms enctype is set as. I need to be able to read the req.body no matter the enctype, and if it's a multipart form then I need to get the files that are attached to the request too.
Currently I do this by running the request though both multer and bodyparser everytime like so:
// Middleware helper
const runMiddleware = (req, res, fn) => new Promise((resolve, reject) => {
fn(req, res, (result) => {
if (result instanceof Error) {
return reject(result);
}
return resolve(result);
});
});
handler.post(async (req, res) => {
const fileUploadAllowed = // business logic
if (fileUploadAllowed) {
// Parse req.files
await runMiddleware(
req,
res,
multer({
limits: { fileSize: MAX_FILE_SIZE },
storage: multerS3({
s3,
bucket: process.env.FILE_SERVER_BUCKET,
acl: 'public-read',
key(_, file, cb) {
cb(null, `${user._id}/${form._id}/${submissionId}/${file.originalname}`);
},
}),
}).array('_file', 5),
);
}
// Parse the req.body
await runMiddleware(
req,
res,
bodyParser.urlencoded({ extended: true }),
);
await runMiddleware(
req,
res,
bodyParser.json(),
);
// Access here
console.log(req.files, req.body);
});
This setup works if the forms enctype is multipart/form-data or application/x-www-form-urlencoded, but the problem is if the fileUploadAllowed is false then the req never runs through Multer, and if the form is multipart then the body will be empty too and the whole POST is rejected. I can't just run it through Multer no matter what because the files are automatically uploaded to my S3 bucket.
What is the best way to handle this setup?

How to call multer middleware inside a controller in nodejs?

I'm trying to upload an image in my server.
In the front-end I'm working with Angular.
The front-end is working fine, I only posted to show you how I'm passing the file to back-end!
component.html
<div fxLayout="column" fxLayoutAlign="center center">
<div>
<mat-form-field>
<ngx-mat-file-input placeholder="Only photos" [accept]="'.jpg, .jpeg, .png'" (change)="onChange($event)"></ngx-mat-file-input>
</mat-form-field>
</div>
<div>
<button mat-button (click)="onSubmit()">Send</button>
</div>
</div>
component.ts - functions
imagem: File;
constructor(private uploadService: UploadService) { }
onChange(event) {
this.imagem = event.target.files[0];
}
onSubmit() {
this.uploadService.upload(this.imagem);
}
upload.service.ts - functions
constructor(private http: HttpClient) { }
upload(file: File) {
const formData = new FormData();
formData.append('img', file, file.name);
this.http.post(environment.apiBaseUrl + '/upload', formData, {responseType: 'text'}).subscribe(
res => console.log('Done')
);
}
In the back-end I have this structure:
app.js
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const rtsIndex = require('./routes/index.router');
var app = express();
// middleware
app.use(bodyParser.json());
app.use(cors());
app.use('/api', rtsIndex);
// start server
app.listen(3000, () => console.log('Port: 3000'));
index.router.js
const express = require('express');
const router = express.Router();
const ctrlUpload = require('../controllers/upload.controller');
router.post('/upload', ctrlUpload.send);
module.exports = router;
upload.controller.js
const express = require('express');
const multer = require('multer');
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
cb(null, Date.now()+'-'+file.originalname);
}
});
const upload = multer({ storage });
module.exports.send = (req, res) => {
upload.single('img');
console.log(req.body, req.files);
res.send('ok');
}
I've tried to call the middleware inside the routing, but I don't think it's correctly and I didn't reach the goal. Algo, the upload is not one.
On server side I get: {} undefined as result, which probably means the multer is not treating the file.
On client side I get: Done.
So what am I doing wrong? And how can I make it works with this back end structure?
Express middlewares are designed to be installed at the routing level. Indeed, in the MVC model express programmers call controllers "routes" (personally I perefer to call them controllers instead of routes in my code). Separating controllers from routes (they both mean the same thing) doesn't really make sense when viewed from traditional MVC frameworks - but you can if you want.
To use multer as designed you need to do it in index.router.js:
index.router.js
const express = require('express');
const router = express.Router();
const multer = require('multer');
const ctrlUpload = require('../controllers/upload.controller');
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
cb(null, Date.now()+'-'+file.originalname);
}
});
const upload = multer({ storage });
router.post('/upload', upload.single('img'), ctrlUpload.send);
module.exports = router;
Then you need to remove all the multer related code from upload.controller.js
You can however insist on doing it in upload.controller.js. The key here is to understand what middlewares are.
In Express, a middleware is a function with the prototype:
function (req, res, next) { // next is optional
// middleware logic
}
Yes, that's right. The code in your upload.controller.js file is a middleware. You are writing a middleware yourself that happens to be at the end of the middleware chain.
You see, Express only accepts middlewares. Express has nothing else. Routes are middlewares that happen to be at the end.
Express .use(), .get(), .post() and related methods accept an infinite number of arguments. The first is optionally a route specifier (but not necessary) and the rest of the arguments are middlewares. For example:
app.get('/foo',
(req, res, next) => {
// first middleware
next(); // next is what allows processing to continue
},
(req, res, next) => {
// second middleware
next();
},
(req, res, next) => {
res.send('hello'); // controller logic - a controller
// is just the last middleware
// Note: if you call next() instead of res.send() in a
// controller express will respond with a 500 internal
// server error status with whatever string you pass
// to next() as the error message.
}
);
Knowing this, we know what the function upload.single('img') returns. The function does not execute the middleware logic. Instead it returns the middleware function:
let middleware = upload.single('img');
// middleware is now a function with the prototype:
// (req, res, next) => {}
So to execute the middleware logic we have to call it (express would automatically call it as part of route processing, just like how it calls your controller function, but if we want to do it ourselves we can).
Here's what you need to do if you want to implement the middleware in upload.controller.js:
module.exports.send = (req, res, next) => {
upload.single('img')(req, res, () => {
// Remember, the middleware will call it's next function
// so we can inject our controller manually as the next()
console.log(req.body, req.files);
res.send('ok');
});
}
That's a lot to unpack. We can make the code easier to understand if we refactor it a little:
let middleware = upload.single('img');
module.exports.send = (req, res, next) => {
// Define the controller here to capture
// req and res in a closure:
let controller = () => {
console.log(req.body, req.files);
res.send('ok');
};
middleware(req, res, controller); // call the middleware with
// our controller as callback
}
But this is very non-standard and would be highly unexpected to an experienced Express.js programmer. I wouldn't do this even though it's possible. It also tightly couple the middleware with your controller completely negating the very flexible nature of Express middleware configuration system.
An example of a separated file of Multer Middleware based in the #slebetman answer
./middlewares/multer.js
const multer = require('multer')
const ErrorMessages = require('../constants/ErrorMessages')
function makeid (length) {
var result = ''
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
var charactersLength = characters.length
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength))
}
return result
}
const DIR = './uploads/'
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, DIR)
},
filename: (req, file, cb) => {
const fileName = file.originalname.toLowerCase().split(' ').join('-')
cb(null, makeid(16) + '_' + fileName)
}
})
const upload = multer({
storage: storage,
fileFilter: (req, file, cb) => {
if (file.mimetype === 'image/png' || file.mimetype === 'application/pdf') {
cb(null, true)
} else {
cb(null, false)
return cb(new Error('Only .png, .jpg, .mp4 and .jpeg format allowed!'))
}
}
})
module.exports.send = (req, res, next) => {
return upload.single('file')(req, res, () => {
// Remember, the middleware will call it's next function
// so we can inject our controller manually as the next()
if (!req.file) return res.json({ error: ErrorMessages.invalidFiletype })
next()
})
}
./routes.js
routes.post('/object', multer.send, ObjectController.createObject)
This avoids the status 500 for wrong filetype
Hope that helps someone :D
A working example of how you can use it in an expressjs handler
import multer from 'multer';
export default {
async upload(req: Request, res: Response, next: NextFunction) {
const middleware = upload.single('photo');
return middleware(req, res, () => {
try {
const file = req.file;
console.log('req.file', req.file);
if (!file) {
throw new ResourceValidationError('media-library', [
{
property: 'avatar',
constraints: {
isNotEmpty: 'avatar should not be empty',
},
},
]);
}
console.log('filename:', file.filename);
res.status(StatusCodes.OK).json({
status: { code: StatusCodes.OK, phrase: ReasonPhrases.OK },
});
} catch (error) {
next(error);
}
});
},
};

Sending file from one node js server to another

So on first server I have route like this:
const express = require('express');
const router = express.Router();
const FormData = require('form-data');
const fetch = require('node-fetch');
const multer = require('multer');
const storage = multer.memoryStorage();
const upload = multer({ storage });
router.post('/', upload.single('file'), async (req, res) => {
const form = new FormData();
form.append('folderId', req.body.folderId);
form.append('file', req.file.buffer, req.file.filename);
const result = await fetch('http://localhost:3003/users', { method: 'POST', body: form }).then(res => res.json());
res.json(result);
})
On this server, it works fine, I can see req.file and it's buffer. So I wanna send this file (without storing it on first server, it exists only in memory and as buffer) to another.
Other server route is like this:
const express = require('express');
const router = express.Router();
const multer = require('multer');
const path = require('path');
const putanja = path.join(__dirname, '../uploads/users');
const storage = multer.diskStorage({
destination: (req, file, cb) => {
console.log('entered here?')
if (!req.body.folderId) return cb({ message: 'no folderId' });
if (!fs.existsSync(putanja + '/' + folderId)) fs.mkdirSync(putanja + '/' + folderId);
cb(null, putanja + '/' + folderId);
},
filename: (req, file, cb) => cb(null, file.originalname)
});
const upload = multer({ storage });
const fs = require('fs');
router.post('/', upload.single('file'), async (req, res) => {
console.log(req.body)
console.log(req.file)
res.json({ status: 'ok' })
})
So on second server, it doesn't even enter the multer middleware, req.file is always defined, and that console.log('entered here?') is never seen. Looks like I'm not passing data as multipart-form?
Also, second server, when sending file directly to it via postman, works.
So my question, how do I send that file? As a buffer? Stream? Base64? I think I tried everything, even changed node-fetch to request, but still no luck.
So on second server, it doesn't even enter the multer middleware, req.file is always defined, and that console.log('entered here?') is never seen. Looks like I'm not passing data as multipart-form?
So this mean your second server doesn't understand the Content-Type of request.
So do one thing add Content-Type parameter in header when you are sending request to second server
Add Content-Type to multipart/form-data
or if you don't know pass headers : {
'Content-Type' : undefined
} http will set header for you
You send your request to /users (http://localhost:3003/users) yet your second server expects the request on /.
Try changing either one to match the other.
'use strict';
const express = require('express');
const multer= require('multer');
const concat = require('concat-stream');
const request = require('request');
const router = express.Router();
function HttpRelay (opts) {}
HttpRelay.prototype._handleFile = function _handleFile (req, file, cb) {
console.log('hell0 proto');
file.stream.pipe(concat({ encoding: 'buffer' }, function (data) {
const r = request.post('/Endpoint you want to upload file', function (err, resp, body) {
if (err) return cb(err);
req.relayresponse=body;
cb(null, {});
});
const form = r.form();
form.append('uploaded_file', data, {
filename: file.originalname,
contentType: file.mimetype
});
}))
};
HttpRelay.prototype._removeFile = function _removeFile (req, file, cb) {
console.log('hello');
cb(null);
};
const relayUpload = multer({ storage: new HttpRelay() }).any();
router.post('/uploadMsgFile', function(req, res) {
relayUpload(req, res, function(err) {
res.send(req.relayresponse);
});
});
module.exports = router;

Resources