diff --git a/RentalCar/server/controllers/documentController.js b/RentalCar/server/controllers/documentController.js index 5690e03..43f653c 100644 --- a/RentalCar/server/controllers/documentController.js +++ b/RentalCar/server/controllers/documentController.js @@ -1,6 +1,14 @@ import imagekit from "../configs/imageKit.js"; import Document from "../models/Document.js"; import User from "../models/User.js"; +import { getExtensionFromMime } from "../middleware/multer.js"; + +const ALLOWED_DOCUMENT_TYPES = [ + "DRIVING_LICENSE", + "ID_CARD", + "PASSPORT", + "OTHER", +]; export const uploadDocument = async (req, res) => { try { @@ -8,17 +16,38 @@ export const uploadDocument = async (req, res) => { const { documentType } = req.body; if (!documentType) { - return res.status(400).json({ success: false, message: "documentType is required" }); + return res.status(400).json({ + success: false, + message: "documentType is required", + }); + } + + if (!ALLOWED_DOCUMENT_TYPES.includes(documentType)) { + return res.status(400).json({ + success: false, + message: "Invalid documentType", + }); } if (!req.file) { - return res.status(400).json({ success: false, message: "PDF file is required" }); + return res.status(400).json({ + success: false, + message: "PDF file is required", + }); + } + + if (!imagekit) { + return res.status(503).json({ + success: false, + message: "Upload service is currently unavailable", + }); } - // upload PDF na ImageKit + const extension = getExtensionFromMime(req.file.mimetype) || ".pdf"; + const uploadRes = await imagekit.upload({ file: req.file.buffer.toString("base64"), - fileName: `doc_${documentType}_${Date.now()}.pdf`, + fileName: `doc_${userId}_${documentType}_${Date.now()}${extension}`, folder: "/documents", }); @@ -28,10 +57,20 @@ export const uploadDocument = async (req, res) => { fileUrl: uploadRes.url, }); - await User.findByIdAndUpdate(userId, { $push: { documents: doc._id } }); + await User.findByIdAndUpdate(userId, { + $addToSet: { documents: doc._id }, + }); - return res.json({ success: true, message: "Document uploaded", document: doc }); + return res.status(201).json({ + success: true, + message: "Document uploaded successfully", + document: doc, + }); } catch (error) { - return res.status(500).json({ success: false, message: error.message }); + console.error("uploadDocument error:", error); + return res.status(500).json({ + success: false, + message: "Failed to upload document", + }); } -}; +}; \ No newline at end of file diff --git a/RentalCar/server/controllers/ownerController.js b/RentalCar/server/controllers/ownerController.js index d2b7e03..b6c14f0 100644 --- a/RentalCar/server/controllers/ownerController.js +++ b/RentalCar/server/controllers/ownerController.js @@ -4,6 +4,8 @@ import Car from "../models/Car.js"; import User from "../models/User.js"; import mongoose from "mongoose"; +import { getExtensionFromMime } from "../middleware/multer.js"; + // API to change role of user export const changeRoleToOwner = async (req, res) => { try { @@ -45,11 +47,20 @@ export const addCar = async (req, res) => { } // upload na ImageKit - const uploadRes = await imagekit.upload({ - file: req.file.buffer.toString("base64"), - fileName: `car_${Date.now()}.png`, - folder: "/cars", - }); + if (!imagekit) { + return res.status(503).json({ + success: false, + message: "Upload service is currently unavailable", + }); + } + + const extension = getExtensionFromMime(req.file.mimetype) || ".jpg"; + + const uploadRes = await imagekit.upload({ + file: req.file.buffer.toString("base64"), + fileName: `car_${_id}_${Date.now()}${extension}`, + folder: "/cars", + }); // optimizovana slika const optimizedImageUrl = imagekit.url({ @@ -210,11 +221,20 @@ export const updateUserImage = async (req, res) => { }); } - const uploadRes = await imagekit.upload({ - file: req.file.buffer.toString("base64"), - fileName: `user_${_id}_${Date.now()}.png`, - folder: "/users", - }); + if (!imagekit) { + return res.status(503).json({ + success: false, + message: "Upload service is currently unavailable", + }); + } + + const extension = getExtensionFromMime(req.file.mimetype) || ".jpg"; + + const uploadRes = await imagekit.upload({ + file: req.file.buffer.toString("base64"), + fileName: `user_${_id}_${Date.now()}${extension}`, + folder: "/users", + }); const optimizedImageUrl = imagekit.url({ src: uploadRes.url, diff --git a/RentalCar/server/middleware/multer.js b/RentalCar/server/middleware/multer.js index b590e0c..d003b1f 100644 --- a/RentalCar/server/middleware/multer.js +++ b/RentalCar/server/middleware/multer.js @@ -1,10 +1,164 @@ import multer from "multer"; +import path from "path"; +import imagekit from "../configs/imageKit.js"; const storage = multer.memoryStorage(); -const upload = multer({ - storage, - limits: { fileSize: 5 * 1024 * 1024 }, // 5MB +const ALLOWED_DOCUMENT_MIME_TYPES = ["application/pdf"]; +const ALLOWED_DOCUMENT_EXTENSIONS = [".pdf"]; + +const ALLOWED_IMAGE_MIME_TYPES = [ + "image/jpeg", + "image/png", + "image/webp", +]; +const ALLOWED_IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp"]; + +const MAX_DOCUMENT_SIZE = 5 * 1024 * 1024; // 5MB +const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5MB + +const createFileFilter = ({ + allowedMimeTypes, + allowedExtensions, + fileLabel, +}) => { + return (req, file, cb) => { + const mimeType = (file.mimetype || "").toLowerCase(); + const extension = path.extname(file.originalname || "").toLowerCase(); + + const isMimeAllowed = allowedMimeTypes.includes(mimeType); + const isExtensionAllowed = allowedExtensions.includes(extension); + + if (!isMimeAllowed || !isExtensionAllowed) { + const error = new Error( + `Invalid ${fileLabel} type. Allowed: ${allowedExtensions.join(", ")}` + ); + error.statusCode = 400; + error.code = "INVALID_FILE_TYPE"; + return cb(error); + } + + cb(null, true); + }; +}; + +const createMemoryUpload = ({ maxFileSize, allowedMimeTypes, allowedExtensions, fileLabel }) => + multer({ + storage, + limits: { fileSize: maxFileSize }, + fileFilter: createFileFilter({ + allowedMimeTypes, + allowedExtensions, + fileLabel, + }), + }); + +export const documentUpload = createMemoryUpload({ + maxFileSize: MAX_DOCUMENT_SIZE, + allowedMimeTypes: ALLOWED_DOCUMENT_MIME_TYPES, + allowedExtensions: ALLOWED_DOCUMENT_EXTENSIONS, + fileLabel: "document", }); -export default upload; +export const imageUpload = createMemoryUpload({ + maxFileSize: MAX_IMAGE_SIZE, + allowedMimeTypes: ALLOWED_IMAGE_MIME_TYPES, + allowedExtensions: ALLOWED_IMAGE_EXTENSIONS, + fileLabel: "image", +}); + +export const applyUpload = (multerMiddleware) => { + return (req, res, next) => { + multerMiddleware(req, res, (err) => { + if (!err) return next(); + + if (err instanceof multer.MulterError) { + if (err.code === "LIMIT_FILE_SIZE") { + return res.status(413).json({ + success: false, + message: "File too large. Maximum allowed size is 5MB.", + }); + } + + return res.status(400).json({ + success: false, + message: err.message || "Upload error", + }); + } + + const statusCode = err.statusCode || 400; + return res.status(statusCode).json({ + success: false, + message: err.message || "Invalid upload", + }); + }); + }; +}; + +export const ensureFilePresent = (fieldName = "file") => { + return (req, res, next) => { + if (!req.file) { + return res.status(400).json({ + success: false, + message: `Missing required file field: ${fieldName}`, + }); + } + next(); + }; +}; + +export const ensureImageKitConfigured = (req, res, next) => { + if (!imagekit) { + return res.status(503).json({ + success: false, + message: "Upload service is currently unavailable", + }); + } + next(); +}; + +export const getExtensionFromMime = (mimeType = "") => { + const normalized = mimeType.toLowerCase(); + + switch (normalized) { + case "application/pdf": + return ".pdf"; + case "image/jpeg": + return ".jpg"; + case "image/png": + return ".png"; + case "image/webp": + return ".webp"; + default: + return ""; + } +}; + +export const isPdfMagicNumberValid = (buffer) => { + if (!buffer || buffer.length < 4) return false; + + return ( + buffer[0] === 0x25 && // % + buffer[1] === 0x50 && // P + buffer[2] === 0x44 && // D + buffer[3] === 0x46 // F + ); +}; + +export const ensureValidPdfSignature = (req, res, next) => { + if (!req.file || !req.file.buffer) { + return res.status(400).json({ + success: false, + message: "PDF file is required", + }); + } + + if (!isPdfMagicNumberValid(req.file.buffer)) { + return res.status(400).json({ + success: false, + message: "Invalid PDF file signature", + }); + } + + next(); +}; \ No newline at end of file diff --git a/RentalCar/server/routes/documentRoutes.js b/RentalCar/server/routes/documentRoutes.js index 37ed09b..e9bae9d 100644 --- a/RentalCar/server/routes/documentRoutes.js +++ b/RentalCar/server/routes/documentRoutes.js @@ -1,11 +1,15 @@ import express from "express"; -import multer from "multer"; import { protect } from "../middleware/auth.js"; import { uploadDocument } from "../controllers/documentController.js"; - +import { + applyUpload, + documentUpload, + ensureFilePresent, + ensureImageKitConfigured, + ensureValidPdfSignature, +} from "../middleware/multer.js"; const router = express.Router(); -const upload = multer({ storage: multer.memoryStorage() }); /** * @openapi @@ -19,7 +23,7 @@ const upload = multer({ storage: multer.memoryStorage() }); * /api/document/upload: * post: * tags: [Document] - * summary: Upload dokumenta (PDF/JPG/PNG) - multipart + * summary: Upload dokumenta (samo PDF) - multipart * security: * - bearerAuth: [] * requestBody: @@ -28,21 +32,33 @@ const upload = multer({ storage: multer.memoryStorage() }); * multipart/form-data: * schema: * type: object + * required: [file, documentType] * properties: * file: * type: string * format: binary - * type: + * description: PDF dokument, maksimalno 5MB + * documentType: * type: string - * example: "DRIVERS_LICENSE" + * enum: [DRIVING_LICENSE, ID_CARD, PASSPORT, OTHER] * responses: * 200: * description: Dokument uploadovan - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/DocumentUploadResponse' + * 400: + * description: Neispravan dokument ili neispravan request + * 413: + * description: Fajl je prevelik + * 503: + * description: Upload servis nije dostupan */ -router.post("/upload", protect, upload.single("file"), uploadDocument); +router.post( + "/upload", + protect, + ensureImageKitConfigured, + applyUpload(documentUpload.single("file")), + ensureFilePresent("file"), + ensureValidPdfSignature, + uploadDocument +); -export default router; +export default router; \ No newline at end of file diff --git a/RentalCar/server/routes/ownerRoutes.js b/RentalCar/server/routes/ownerRoutes.js index 15e021f..19ebaa8 100644 --- a/RentalCar/server/routes/ownerRoutes.js +++ b/RentalCar/server/routes/ownerRoutes.js @@ -1,6 +1,11 @@ import express from "express"; import { protect } from "../middleware/auth.js"; -import upload from "../middleware/multer.js"; +import { + applyUpload, + imageUpload, + ensureFilePresent, + ensureImageKitConfigured, +} from "../middleware/multer.js"; import { requireRole } from "../middleware/roles.js"; import { @@ -79,7 +84,9 @@ ownerRouter.post( "/add-car", protect, requireRole("owner", "admin"), - upload.single("image"), + ensureImageKitConfigured, + applyUpload(imageUpload.single("image")), + ensureFilePresent("image"), addCar ); @@ -251,7 +258,9 @@ ownerRouter.post( "/update-image", protect, requireRole("owner", "admin"), - upload.single("image"), + ensureImageKitConfigured, + applyUpload(imageUpload.single("image")), + ensureFilePresent("image"), updateUserImage ); export default ownerRouter;