diff --git a/backend/app.js b/backend/app.js index 752aee7..60253cc 100644 --- a/backend/app.js +++ b/backend/app.js @@ -40,4 +40,7 @@ app.use("/admin", adminRoutes); const usersRoutes = require("./routes/users.routes"); app.use("/users", usersRoutes); +const configRoutes = require("./routes/config.routes"); +app.use("/config", configRoutes); + module.exports = app; \ No newline at end of file diff --git a/backend/config/constants.js b/backend/config/constants.js new file mode 100644 index 0000000..811e376 --- /dev/null +++ b/backend/config/constants.js @@ -0,0 +1,15 @@ +/** + * Application-wide constants + * These are non-secret configuration values shared across the application + */ + +module.exports = { + // File upload limits + MAX_FILE_SIZE_MB: 20, + MAX_FILE_SIZE_BYTES: 20 * 1024 * 1024, + + // Image processing + MAX_IMAGE_DIMENSION: 800, + IMAGE_QUALITY: 85, + IMAGE_FORMAT: 'jpeg' +}; diff --git a/backend/controllers/config.controller.js b/backend/controllers/config.controller.js new file mode 100644 index 0000000..483e965 --- /dev/null +++ b/backend/controllers/config.controller.js @@ -0,0 +1,14 @@ +/** + * Configuration endpoints + * Public endpoints that provide application configuration to clients + */ + +const { MAX_FILE_SIZE_MB, MAX_IMAGE_DIMENSION, IMAGE_QUALITY } = require("../config/constants"); + +exports.getConfig = (req, res) => { + res.json({ + maxFileSizeMB: MAX_FILE_SIZE_MB, + maxImageDimension: MAX_IMAGE_DIMENSION, + imageQuality: IMAGE_QUALITY + }); +}; diff --git a/backend/middleware/image.js b/backend/middleware/image.js index 4289f98..2983ced 100644 --- a/backend/middleware/image.js +++ b/backend/middleware/image.js @@ -1,11 +1,12 @@ const multer = require("multer"); const sharp = require("sharp"); +const { MAX_FILE_SIZE_BYTES, MAX_IMAGE_DIMENSION, IMAGE_QUALITY } = require("../config/constants"); // Configure multer for memory storage (we'll process before saving to DB) const upload = multer({ storage: multer.memoryStorage(), limits: { - fileSize: 20 * 1024 * 1024, // 20MB max file size + fileSize: MAX_FILE_SIZE_BYTES, }, fileFilter: (req, file, cb) => { // Only accept images @@ -24,13 +25,13 @@ const processImage = async (req, res, next) => { } try { - // Compress and resize image to 800x800px, JPEG quality 85 + // Compress and resize image using constants const processedBuffer = await sharp(req.file.buffer) - .resize(800, 800, { + .resize(MAX_IMAGE_DIMENSION, MAX_IMAGE_DIMENSION, { fit: "inside", withoutEnlargement: true, }) - .jpeg({ quality: 85 }) + .jpeg({ quality: IMAGE_QUALITY }) .toBuffer(); // Attach processed image to request diff --git a/backend/routes/config.routes.js b/backend/routes/config.routes.js new file mode 100644 index 0000000..9a9e602 --- /dev/null +++ b/backend/routes/config.routes.js @@ -0,0 +1,8 @@ +const express = require("express"); +const router = express.Router(); +const configController = require("../controllers/config.controller"); + +// Public endpoint - no authentication required +router.get("/", configController.getConfig); + +module.exports = router; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c248c29..b9e72cd 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,6 +1,7 @@ import { BrowserRouter, Route, Routes } from "react-router-dom"; import { ROLES } from "./constants/roles"; import { AuthProvider } from "./context/AuthContext.jsx"; +import { ConfigProvider } from "./context/ConfigContext.jsx"; import AdminPanel from "./pages/AdminPanel.jsx"; import GroceryList from "./pages/GroceryList.jsx"; @@ -15,37 +16,39 @@ import RoleGuard from "./utils/RoleGuard.jsx"; function App() { return ( - - - + + + + - {/* Public route */} - } /> - } /> - - {/* Private routes with layout */} - - - - } - > - } /> + {/* Public route */} + } /> + } /> + {/* Private routes with layout */} - - + + + } - /> - + > + } /> - - - + + + + } + /> + + + + + + ); } diff --git a/frontend/src/api/config.js b/frontend/src/api/config.js new file mode 100644 index 0000000..2ab58cd --- /dev/null +++ b/frontend/src/api/config.js @@ -0,0 +1,10 @@ +import api from "./axios"; + +/** + * Fetch application configuration + * @returns {Promise} Configuration object with maxFileSizeMB, maxImageDimension, etc. + */ +export const getConfig = async () => { + const response = await api.get("/config"); + return response.data; +}; diff --git a/frontend/src/components/forms/ImageUploadSection.jsx b/frontend/src/components/forms/ImageUploadSection.jsx index 1f57a09..c83bae4 100644 --- a/frontend/src/components/forms/ImageUploadSection.jsx +++ b/frontend/src/components/forms/ImageUploadSection.jsx @@ -1,4 +1,5 @@ -import { useRef } from "react"; +import { useRef, useState, useContext } from "react"; +import { ConfigContext } from "../../context/ConfigContext"; import "../../styles/components/ImageUploadSection.css"; /** @@ -17,10 +18,25 @@ export default function ImageUploadSection({ }) { const cameraInputRef = useRef(null); const galleryInputRef = useRef(null); + const [sizeError, setSizeError] = useState(null); + const { config } = useContext(ConfigContext); + + const MAX_FILE_SIZE = config ? config.maxFileSizeMB * 1024 * 1024 : 20 * 1024 * 1024; + const MAX_FILE_SIZE_MB = config ? config.maxFileSizeMB : 20; const handleFileChange = (e) => { const file = e.target.files[0]; if (file) { + // Check file size + if (file.size > MAX_FILE_SIZE) { + const sizeMB = (file.size / (1024 * 1024)).toFixed(2); + setSizeError(`Image size (${sizeMB}MB) exceeds the ${MAX_FILE_SIZE_MB}MB limit. Please choose a smaller image.`); + // Reset the input + e.target.value = ''; + return; + } + // Clear any previous error + setSizeError(null); onImageChange(file); } }; @@ -36,6 +52,11 @@ export default function ImageUploadSection({ return (

{title}

+ {sizeError && ( +
+ {sizeError} +
+ )}
{!imagePreview ? (
diff --git a/frontend/src/context/ConfigContext.jsx b/frontend/src/context/ConfigContext.jsx new file mode 100644 index 0000000..bd32e28 --- /dev/null +++ b/frontend/src/context/ConfigContext.jsx @@ -0,0 +1,44 @@ +import { createContext, useState, useEffect } from 'react'; +import { getConfig } from '../api/config'; + +export const ConfigContext = createContext({ + config: null, + loading: true, +}); + +export const ConfigProvider = ({ children }) => { + const [config, setConfig] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchConfig = async () => { + try { + const data = await getConfig(); + setConfig(data); + } catch (error) { + console.error('Failed to fetch config:', error); + // Set default fallback values + setConfig({ + maxFileSizeMB: 20, + maxImageDimension: 800, + imageQuality: 85 + }); + } finally { + setLoading(false); + } + }; + + fetchConfig(); + }, []); + + const value = { + config, + loading + }; + + return ( + + {children} + + ); +}; diff --git a/frontend/src/styles/components/ImageUploadSection.css b/frontend/src/styles/components/ImageUploadSection.css index 79c1378..fd30b43 100644 --- a/frontend/src/styles/components/ImageUploadSection.css +++ b/frontend/src/styles/components/ImageUploadSection.css @@ -10,6 +10,17 @@ color: #333; } +.image-upload-error { + background: #f8d7da; + border: 1px solid #f5c2c7; + color: #842029; + padding: 0.75rem; + border-radius: 6px; + margin-bottom: 0.8rem; + font-size: 0.9em; + line-height: 1.4; +} + .image-upload-content { border: 2px dashed #ccc; border-radius: 8px;