Permission(ACL) middleware for Multer that acts based on multipart/form-data parameters - node.js

In my Express.js REST API, I'm using multer to upload images into server's static folder with diskStorage. I have wanted to build a generic file upload page for both users and admins. Detailed use case is as follows:
choose a category from a drop-down list
write an valid id in the form field,
choose a valid image(file) and submit.
if regular user (with isAdmin==false) selects category different than users , then the API responds with a 403 and won't upload the image at all.
What I want to achieve is to allow only users with isAdmin==true to upload images for the all categories. But, within the same route, regular users can only upload images for the users category.
The question is how to get access to the req.body.category inside authentication & authorization middleware and reject upload process pre-flight?
I need to check category and isAdmin fields at the same time to make decision about giving permission for upload process or not.
React-side
const formData = new FormData()
formData.append('id', this.state.id)
formData.append('category', this.state.category)
formData.append('photo', this.state.file)
this.props.fileUpload(formData)
Express-side
Router
router.post('/upload', AuthController.role('admin'), UploadController.uploadPhoto);
AuthController
public static role(role: any) {
return (req: Request, res: Response, next: NextFunction) => {
if (role === 'admin') {
console.dir(req.body); // req.body prints { user_id: 1, isAdmin: true }
// cannot decide whether give access to the route or not over here because we don't know the req.body.category yet.
if (req.body && req.body.isAdmin) {
next();
} else {
return res.status(403).send(new Unauthorized());
}
}
};
}
UploadController
const imageFilter = (req, file, cb) => {
console.dir(req.body); // prints { id: '1', category: 'users' } cannot access req.body.isAdmin over here
if (!file.originalname.match(/\.(jpg|jpeg|png|gif)$/)) {
return cb(new Error('Only image files are allowed!'), false);
}
cb(null, true);
};
const upload = util.promisify(multer({ storage, fileFilter: imageFilter, limits: {fileSize: MAX_SIZE} })
.single('photo'));
export class UploadController {
public static async uploadPhoto(req: any, res: Response, next: NextFunction) {
// req.body.isAdmin is available over here
try {
await upload(req, res);
imageUrl = `${HOST}/static/${req.file.filename}`;
} catch (e) {
return res.status(400).send(new BadRequest('Image upload operation is not successful.'));
}
}
UPDATE: My current workaround solution is to save the image temporarily and then check the category and isAdmin at the same time when they become available.
try {
await upload(req, res);
....
..
// Now req.body.category and req.body.isAdmin are both available here
}

Related

Node JS uploading file field as a field of an object in the request body using Formidable

I am building a Node JS application using Express JS. I need to implement the file upload logic for my application. I am using Formidable, https://www.npmjs.com/package/formidable for uploading file(s). I could upload the files using that library without any issue when the request body format is simple. Now, I am trying to upload the file which is a property/ field of an object in the request body. The following is the dummy request body structure.
{
users: [
{
name: `Test User 1`,
photoFile: {{ here I want to upload file for this user }}
},
{
name: `Test User 2`,
photoFile: {{ here I want to upload file for this user }}
},
// the list goes on
]
}
I am trying to send the test payload/ data in the Postman as in the screenshot below.
This is my code to parse the form
private getForm = async (
req: Request,
options: FormOptions = {
multiples: false
}
) => {
const tempUploadDir = this.getTempUploadDirectory(req);
if (!(await this.storage.exits(tempUploadDir))) {
await this.storage.mkdir(tempUploadDir);
}
return new IncomingForm({
multiples: options.multiples,
uploadDir: tempUploadDir,
maxFileSize: config.fileSystem.maxUploadLimit
});
};
public parseFormData = async <T>(
request: Request,
options: FormOptions = {
multiples: false
}
): Promise<T> => {
const form = await this.getForm(request, options);
return new Promise<T>((resolve) => {
form.parse(request, (err, fields, files) => {
if (err) {
if (err.code === FILE_TOO_LARGE_ERROR_CODE) {
// file too large
throw new UploadMaxFileSizeException(
`Upload file size limit exceeded.`
);
}
throw err;
}
let filesData: {
[key: string]: IUploadedFile[];
} = {};
for (let fileField in files) {
if (`length` in files[fileField]) {
filesData[fileField] = files[fileField] as IUploadedFile[];
} else {
filesData[fileField] = [files[fileField] as IUploadedFile];
}
}
return resolve({
...fields,
...files
} as T);
});
});
};
When I print out the result of parseFormData, I am getting the following result.
As you can see, the field field, 'users[0][photoFile]' is not parsed putting into the corresponding field of the request body object. Instead, the entire field name is string, 'users[0][photoFile]'. What is wrong with my code and how can I fix it?
I my project, file will upload to server when user upload files and get the link back
And when submit form, user avatar just is a link, not a file

Nestjs throw error and http status 400 when fileFilter in fileinterceptor finds an unsupported file extension

I am having difficulty handling an error in my application.
I want to create a FileInterceptor (multer) which checks if the file has an allowed file extension. The documentation shows this:
const allowedFileExtensions = ['.jpg', '.png'];
FileInterceptor(
'image',
{
dest: './uploads',
fileFilter: (req, file, callback) => {
const extension = path.extname(file.originalname);
if (allowedFileExtensions.includes(extension)) {
callback(null, true);
} else {
// gives the 500 error
callback(new Error('Only images are allowed'), false);
}
}
}
This kinda works. But it has two flaws in my opinion. First, it returns a 500 error:
{
"statusCode": 500,
"message": "Internal server error"
}
It would be better to return a 400 error with a error message which explains why it failed.
Second, in the console of the Nest application it shows the stacktrace. I'd rather use the logging for this instead. (The application has middleware in place to pick up NestJS errors and log them automagically.)
So what I am trying is the following:
const allowedFileExtensions = ['.jpg', '.png'];
FileInterceptor(
'image',
{
dest: './uploads',
fileFilter: (req, file, callback) => {
const extension = path.extname(file.originalname);
if (!allowedFileExtensions.includes(extension)) {
// crashes the application
throw new BadRequestException('Only images are allowed', `Bad request. Accepted file extensions are: ${allowedFileExtensions.toString()}`);
}
callback(null, true)
}
}
But this crashes the application. I am not able to upload a different image anymore after this.
Any idea?
Ah found it. The BadRequestException cannot be thrown outside the body of the function.
const allowedFileExtensions = ['.jpg', '.png'];
enum FileValidationErrors {
UNSUPPORTED_FILE_TYPE
}
#Post('/:id/upload-photo')
#UseInterceptors(
FileInterceptor(
'image',
{
dest: './uploads',
fileFilter: (req, file, callback) => {
const extension = path.extname(file.originalname);
if (allowedFileExtensions.includes(extension)) {
callback(null, true);
} else {
// provide the validation error in the request
req.fileValidationError = FileValidationErrors.UNSUPPORTED_FILE_TYPE
callback(null, false);
}
}
}
)
)
uploadSinglePhoto(
#Param('id', ParseIntPipe) id: number,
#UploadedFile() image,
#Req() req // add the request property
): Promise<UserProfileEntity> {
// check here for a potential error
if (req?.fileValidationError === FileValidationErrors.UNSUPPORTED_FILE_TYPE) {
// if so, throw the BadRequestException
throw new BadRequestException('Only images are allowed', `Bad request. Accepted file extensions are: ${allowedFileExtensions.toString()}`);
}
this.logger.verbose(`Adding image ${image}`);
const imageUrlLocation = `${image.destination.substring(1)}/${image.filename}`;
return this.userProfileService.saveUserProfilePhotoLocation(id, imageUrlLocation);
}

Intercepting in Multer Mutates Request? (NestJS)

Does multer mutates any request that has given to it? I'm currently trying to intercept the request to add this in logs.
But whenever I try to execute this code first:
const newReq = cloneDeep(request); // lodash cloneDeep
const newRes = cloneDeep(response);
const postMulterRequest: any = await new Promise((resolve, reject) => {
const multerReponse = multer().any()
multerReponse(request, newRes, err => {
if (err) reject(err)
resolve(request)
})
})
files = postMulterRequest?.files;
The #UseInterceptors(FileInterceptor('file')) becomes undefined.
I have already seen the problem, it seems like the multerReponse(request, newRes, err => { mutates the request. But I don't know what the other approach I can do to fix this. (I tried JSON Serialization, Object.assign, cloneDeep, but none of those worked)
I have tried adding newReq and newRes (cloned object) to multerResponse at first it worked. But at the second time, the thread only hangs up, and doesn't proceed to next steps. Or the multerReponse(request, newRes, err => { doesn't return anything.
The whole code looks like this and used globally (some parts of here were redacted/removed; but the main logic is still the same) :
#Injectable()
export class AuditingInterceptor implements NestInterceptor {
constructor(
#InjectModel(Auditing.name)
private readonly AuditingModel: Model<Auditing>,
) {}
async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
const { headers, method, ip, route, query, body } = request;
let bodyParam = Object.assign({}, body),
files: any;
const newReq = cloneDeep(request); // lodash cloneDeep
const newRes = cloneDeep(response);
const postMulterRequest: any = await new Promise((resolve, reject) => {
const multerReponse = multer().any();
multerReponse(newReq, newRes, (err) => {
if (err) reject(err);
resolve(newReq);
});
});
files = postMulterRequest?.files;
return next.handle().pipe(
tap(() =>
this.AuditingModel.create({
request: {
query,
bodyParam,
files,
},
timeAccessed: new Date().toISOString(),
}),
),
);
}
}
Summary of what I need to do here is I need to intercept and log the file in our DB before it gets processed in the method/endpoint that uses #UseInterceptors(FileInterceptor('file')).
I have solve this by intercepting the request using the
#Req() req
and creating a method to handle the files that was intercepted inside the FileInterceptor decorator.
Code Example:
// create logs service first to handle your queries
createLogs(file, req){
// do what you need to do with the file, and req here
const { filename } = file;
const { ip } = req
....
}
// main service
// inject the service first
constructor(#Inject(LogsService) private logsService: LogsService)
uploadHandler(file, req){
this.logsService.createLogs(file, req)
// proceed with the next steps
....
}
// controller
#Post('upload')
#UseInterceptors(FileInterceptor('file'))
testFunction(#UploadedFile() file: Express.Multer.File,, #Req req){
return this.serviceNameHere.uploadHandler(file, req);
}

DELETE method requires query parameter instead of path parameter in next.js

I am creating a CRUD in API,
but the delete does not seems to work properly.
I get a response from this
http://localhost:3000/api/admin/categories?id=1
and not from this
http://localhost:3000/api/admin/categories/1
this is the code in next.js:
export default async (req, res) => {
const {
query: { id },
method,
} = req;
switch (method) {
case "DELETE":
return res.status(200).json({
success: true,
id: id,
});
}
in React:
axios.delete(`http://localhost:3000/api/admin/categories/`, {id: 1})
The same situation is also with "PUT" method
Folder Directory:
api
|
---admin
--------categories
--------index.js
If you want to create API Routes such as DELETE, PUT ( they require /id as path parameter), you should create a separate file in that folder.
Like this:
api
|
---admin
--------categories
--------index.js
--------[id].js
And in the [id].js file:
export default function handler(req, res) {
const { id } = req.query;
if (req.method === "DELETE") {
res.end(`Category: ${id}`);
}
}

Download File with loopback 4

I want to download a file from a loopback 4 based server. My current situation is, that i can access the file with fs.readFileSync, but it's only working for text-files. If i want to download pdf or zip files it's not working.
This is what i have so far:
export class FileController
{
constructor(
#repository(FileRepository) public fileRepository: FileRepository
){}
#get('/files/download/{id}')
async download(#param.path.number('id') id: number): Promise<string>
{
const file = await this.fileRepository.findById(id);
const filepath = file.FilePath;
if(!fs.existsSync(filepath))
{
throw new HttpErrors.NotFound(`The File #${id} can not be delivered, because the file is missing.`);
}
else
{
// #todo set headers for content type, length and caching
return fs.readFileSync(filepath,'utf8');
}
}
}
If i inject RestBindings.Http.RESPONSE into the constructor, I'm able to access the response object and might edit the headers using the setHeader-Method, but with no affect.
What do i have to do to:
pass the file content correctly to the client
set the headers to tell the browser the correct file meta data
Using this.response.download():
return await new Promise((resolve: any, reject: any) => {
// your logic ...
this.response.download(filepath, (err: any) => {
if (err) reject(err);
resolve();
});
});

Resources