diff --git a/src/middlewares/upload.js b/src/middlewares/upload.js new file mode 100644 index 0000000..125457e --- /dev/null +++ b/src/middlewares/upload.js @@ -0,0 +1,80 @@ +import multer from 'multer'; +import path from 'path'; +import fs from 'fs'; +import crypto from 'crypto'; +import config from '../config/index.js'; + +const uploadDir = path.resolve(process.cwd(), config.uploadDir); +if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); +} + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + let userPath = req.body.path || ''; + + userPath = path.normalize(userPath).replace(/^(\.\.[\/\\])+/, ''); + + const targetPath = path.join(uploadDir, userPath); + + if (!targetPath.startsWith(uploadDir)) { + return cb(new Error('Invalid path.')); + } + + if (!fs.existsSync(targetPath)) { + fs.mkdirSync(targetPath, { recursive: true }); + } + + cb(null, targetPath); + }, + + filename: (req, file, cb) => { + const randomName = crypto.randomBytes(16).toString('hex'); + const ext = path.extname(file.originalname); + cb(null, randomName + ext); + } +}); + +const fileFilter = (req, file, cb) => { + const allowedExtensions = /jpeg|jpg|png|gif/; + const allowedMimeTypes = /image\/jpeg|image\/jpg|image\/png|image\/gif/; + + const extname = allowedExtensions.test(path.extname(file.originalname).toLowerCase()); + const mimetype = allowedMimeTypes.test(file.mimetype); + + if (extname && mimetype) { + cb(null, true); + } else { + cb(new multer.MulterError('LIMIT_UNEXPECTED_FILE', file.fieldname)); + } +}; + +const multerUpload = multer({ + storage, + fileFilter, + limits: { fileSize: 1024 * 1024 * 1024 } // 1GB +}); + +const uploadMiddleware = (fieldName, multiple = false) => (req, res, next) => { + const uploader = multiple ? multerUpload.array(fieldName) : multerUpload.single(fieldName); + + uploader(req, res, (err) => { + if (err instanceof multer.MulterError) { + return res.status(400).json({ error: err.message }); + } else if (err) { + return res.status(500).json({ error: 'Internal server error during file upload.' }); + } + + if (!multiple && !req.file) { + return res.status(400).json({ error: `No file uploaded under field "${fieldName}"` }); + } + const result = req.file + + next(result); + }); +}; + +export { + fileFilter, + uploadMiddleware +}; diff --git a/src/routes/index.js b/src/routes/index.js index 61246b2..3647689 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -15,7 +15,7 @@ import { sendOkResponse, sendResponseNoContent, } from '../utils/responses.js' -import { latitudeRange, longitudeRange, uuid, varChar } from '../validators/expressValidator/customValidators.js' +import { json, latitudeRange, longitudeRange, uuid, varChar } from '../validators/expressValidator/customValidators.js' import { integerRange } from '../validators/expressValidator/customValidators.js' import {payloadExpressValidator} from '../validators/expressValidator/payloadExpressValidator.js' import { authorizePermission, setToken, authenticateToken, refreshAuthenticate} from '../middlewares/auth.js' @@ -86,8 +86,8 @@ export default(config) => { (req, res, next) => indexController(req, res, next), (result, req, res, next) => addLinks(result, req, res, next, hasAddLinks, linkRoutes), (result, req, res, _) => sendOkResponse(result, req, res) - ) - + ); + // User Routes /** * @name get/users @@ -974,7 +974,8 @@ export default(config) => { varChar('address').optional({ nullable: true, values: 'falsy' }), varChar('description').optional({ nullable: true, values: 'falsy' }), latitudeRange('latitude'), - longitudeRange('longitude') + longitudeRange('longitude'), + json('images').optional({ nullable: true, values: 'falsy' }), ], (req, res, next) => payloadExpressValidator(req, res, next, config), (req, res, next) => insertPlaceController(req, res, next, config), @@ -1012,7 +1013,8 @@ export default(config) => { varChar('address').optional({ nullable: true, values: 'falsy' }), varChar('description').optional({ nullable: true, values: 'falsy' }), latitudeRange('latitude').optional({ nullable: false, values: 'falsy' }), - longitudeRange('longitude').optional({nullable: false, values: 'falsy'}) + longitudeRange('longitude').optional({nullable: false, values: 'falsy'}), + json('images').optional({ nullable: true, values: 'falsy' }), ], (req, res, next) => payloadExpressValidator(req, res, next, config), (req, res, next) => modifyPlaceController(req, res, next, config), @@ -1221,6 +1223,7 @@ export default(config) => { uuid('user_uuid'), uuid('report_type_uuid').optional({ nullable: false, values: 'falsy' }), integerRange('rating', {min: 1, max: 3}), + json('images').optional({ nullable: true, values: 'falsy' }), varChar('description').optional({ nullable: true, values: 'falsy' }), ], (req, res, next) => payloadExpressValidator(req, res, next, config), @@ -1258,6 +1261,7 @@ export default(config) => { uuid('user_uuid').optional({ nullable: false, values: 'falsy' }), uuid('report_type_uuid').optional({ nullable: false, values: 'falsy' }), integerRange('rating', {min: 1, max: 3}).optional({ nullable: false, values: 'falsy' }), + json('images').optional({ nullable: true, values: 'falsy' }), varChar('description').optional({ nullable: true, values: 'falsy' }), ], (req, res, next) => payloadExpressValidator(req, res, next, config), diff --git a/src/routes/uploadRoutes.js b/src/routes/uploadRoutes.js new file mode 100644 index 0000000..1542ca0 --- /dev/null +++ b/src/routes/uploadRoutes.js @@ -0,0 +1,29 @@ +import { uploadMiddleware } from '../middlewares/upload.js'; // tu middleware multer +import { Router } from 'express' + +export default(config) => { + /** + * Express router to mount user related functions on. + * @type {Object} + * @const + * @namespace placeRouter + */ + const router = Router() + const hasAddLinks = config.environment !== 'production' + + // Single file upload route + router.post( + '/upload', + uploadMiddleware('file'), + (result, req, res, next) => addLinks(result, req, res, next, hasAddLinks, linkRoutes), + (result, req, res, _) => sendOkResponse(result, req, res)); + + // Multiple file upload route + router.post( + '/upload-multiple', + uploadMiddleware('file', true), + (result, req, res, next) => addLinks(result, req, res, next, hasAddLinks, linkRoutes), + (result, req, res, _) => sendOkResponse(result, req, res) + ) + +}