Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 47 additions & 8 deletions RentalCar/server/controllers/documentController.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,53 @@
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 {
const userId = req.user._id;
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",
});

Expand All @@ -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",
});
}
};
};
40 changes: 30 additions & 10 deletions RentalCar/server/controllers/ownerController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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,
Expand Down
162 changes: 158 additions & 4 deletions RentalCar/server/middleware/multer.js
Original file line number Diff line number Diff line change
@@ -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();
};
40 changes: 28 additions & 12 deletions RentalCar/server/routes/documentRoutes.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand All @@ -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;
Loading
Loading