// Shared multer middleware for the two admin image-upload routes: // POST /admin/posts/upload (posts.router.js) and POST /admin/uploads // (uploads.router.js). It lived inline in admin.routes.js while both routes did; // the PR 3 split put them in different files, so the config moved here rather // than being duplicated — one upload directory, one mimetype allowlist. // // Kept in this directory on purpose: UPLOAD_DIR is resolved relative to // __dirname, so moving the file to another folder would silently repoint the // upload directory. const path = require('path') const fs = require('fs') const crypto = require('crypto') const multer = require('multer') const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(__dirname, '..', '..', '..', '..', 'uploads') fs.mkdirSync(UPLOAD_DIR, { recursive: true }) // Whitelisted image mimetypes → the extension we store the file under. The // stored extension is derived from this map (keyed by the accepted mimetype), // never from originalname — so a spoofed `Content-Type: image/png` paired with // `originalname: x.html` can never land an executable .html file in /uploads. const MIME_EXT = { 'image/png': '.png', 'image/jpeg': '.jpg', 'image/gif': '.gif', 'image/webp': '.webp', 'image/avif': '.avif', } const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, UPLOAD_DIR), filename: (req, file, cb) => { const ext = MIME_EXT[file.mimetype] || '' cb(null, `${Date.now()}-${crypto.randomBytes(8).toString('hex')}${ext}`) }, }) const upload = multer({ storage, limits: { fileSize: 8 * 1024 * 1024 }, fileFilter: (req, file, cb) => { // Single source of truth: only mimetypes we can map to a safe extension pass. if (MIME_EXT[file.mimetype]) cb(null, true) else cb(new Error('Only image uploads are allowed')) }, }) module.exports = { upload, UPLOAD_DIR, MIME_EXT }