From 1fce1f1831f44c2bc677ffe5448b12a05f58c9d4 Mon Sep 17 00:00:00 2001 From: seaznCode Date: Sat, 29 Nov 2025 13:50:48 +0100 Subject: [PATCH] feat: enhance image upload validation and management in CoffeeController --- controller/admin/CoffeeController.js | 43 +++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/controller/admin/CoffeeController.js b/controller/admin/CoffeeController.js index 868d1e3..14d73bb 100644 --- a/controller/admin/CoffeeController.js +++ b/controller/admin/CoffeeController.js @@ -36,6 +36,14 @@ exports.create = async (req, res) => { let original_filename = null; let uploadedKey = null; if (req.file) { + const allowedMime = ['image/jpeg','image/png','image/webp']; + if (!allowedMime.includes(req.file.mimetype)) { + return res.status(400).json({ error: 'Invalid image type. Allowed: JPG, PNG, WebP' }); + } + const maxBytes = 10 * 1024 * 1024; // 10MB + if (req.file.size > maxBytes) { + return res.status(400).json({ error: 'Image exceeds 10MB limit' }); + } const s3 = new S3Client({ region: process.env.EXOSCALE_REGION, endpoint: process.env.EXOSCALE_ENDPOINT, @@ -50,6 +58,7 @@ exports.create = async (req, res) => { Key: key, Body: req.file.buffer, ContentType: req.file.mimetype, + ACL: 'public-read' })); object_storage_id = key; original_filename = req.file.originalname; @@ -103,11 +112,20 @@ exports.update = async (req, res) => { const currency = req.body.currency; const is_featured = req.body.is_featured === undefined ? undefined : (req.body.is_featured === 'true' || req.body.is_featured === true); const state = req.body.state === undefined ? undefined : (req.body.state === 'false' || req.body.state === false ? false : true); + const removePicture = req.body.removePicture === 'true'; let object_storage_id; let original_filename; let uploadedKey = null; if (req.file) { + const allowedMime = ['image/jpeg','image/png','image/webp']; + if (!allowedMime.includes(req.file.mimetype)) { + return res.status(400).json({ error: 'Invalid image type. Allowed: JPG, PNG, WebP' }); + } + const maxBytes = 10 * 1024 * 1024; // 10MB + if (req.file.size > maxBytes) { + return res.status(400).json({ error: 'Image exceeds 10MB limit' }); + } const s3 = new S3Client({ region: process.env.EXOSCALE_REGION, endpoint: process.env.EXOSCALE_ENDPOINT, @@ -117,7 +135,7 @@ exports.update = async (req, res) => { }, }); const key = `coffee/products/${Date.now()}_${req.file.originalname}`; - await s3.send(new PutObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: key, Body: req.file.buffer, ContentType: req.file.mimetype })); + await s3.send(new PutObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: key, Body: req.file.buffer, ContentType: req.file.mimetype, ACL: 'public-read' })); object_storage_id = key; original_filename = req.file.originalname; uploadedKey = key; @@ -127,6 +145,29 @@ exports.update = async (req, res) => { const current = await CoffeeService.get(id); if (!current) return res.status(404).json({ error: 'Not found' }); + // If removePicture requested and no new file uploaded, clear existing object_storage_id + if (removePicture && !object_storage_id) { + object_storage_id = null; + original_filename = null; + // Delete previous object if exists + if (current && current.object_storage_id) { + try { + const s3del = new S3Client({ + region: process.env.EXOSCALE_REGION, + endpoint: process.env.EXOSCALE_ENDPOINT, + credentials: { + accessKeyId: process.env.EXOSCALE_ACCESS_KEY, + secretAccessKey: process.env.EXOSCALE_SECRET_KEY, + }, + }); + await s3del.send(new DeleteObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: current.object_storage_id })); + logger.info('[CoffeeController.update] removed existing picture', { id }); + } catch (delErr) { + logger.warn('[CoffeeController.update] remove existing picture failed', { id, msg: delErr.message }); + } + } + } + const updated = await CoffeeService.update(id, { title: title ?? current.title, description: description ?? current.description,