diff --git a/backend/src/routes/gallery.js b/backend/src/routes/gallery.js index 444712fc..31bd918b 100644 --- a/backend/src/routes/gallery.js +++ b/backend/src/routes/gallery.js @@ -1627,7 +1627,7 @@ router.post('/:eventId/upload', verifyGalleryAccess, async (req, res) => { // Import multer and photo processing const multer = require('multer'); - const { getAllowedMimeTypes } = require('../services/uploadSettings'); + const { getAllowedMimeTypes, getMaxFilesPerUpload } = require('../services/uploadSettings'); const { validateFileType } = require('../utils/fileSecurityUtils'); // Resolve allowed MIME types from settings @@ -1638,11 +1638,26 @@ router.post('/:eventId/upload', verifyGalleryAccess, async (req, res) => { allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp']; } + // #613 — per-batch file count was hardcoded to 10 here, so the admin's + // Settings → General → "Max Files per Upload" value silently didn't + // apply to guest uploads (only admin uploads honoured it via + // adminPhotos.js:131). Zszywany reported uploading 16 files succeeded + // even with the limit set to 10. Mirror the admin path: resolve from + // settings (cached for 60s in the service) and feed multer both + // `limits.files` and the `.array(...)` cap. Fall back to the service's + // default if the read fails. + let maxFilesPerUpload; + try { + maxFilesPerUpload = await getMaxFilesPerUpload(); + } catch { + maxFilesPerUpload = 500; + } + const upload = multer({ dest: tempUploadDir, limits: { - fileSize: 50 * 1024 * 1024, // 50MB - files: 10 // Max 10 files at once + fileSize: 50 * 1024 * 1024, // 50MB per file (separate concern from #613) + files: maxFilesPerUpload }, fileFilter: (req, file, cb) => { if (validateFileType(file.originalname, file.mimetype, allowedMimeTypes)) { @@ -1651,7 +1666,7 @@ router.post('/:eventId/upload', verifyGalleryAccess, async (req, res) => { cb(new Error('Invalid file type')); } } - }).array('photos', 10); + }).array('photos', maxFilesPerUpload); // Handle upload upload(req, res, async (err) => { diff --git a/backend/src/routes/publicSettings.js b/backend/src/routes/publicSettings.js index 050adc9e..9137807e 100644 --- a/backend/src/routes/publicSettings.js +++ b/backend/src/routes/publicSettings.js @@ -18,7 +18,15 @@ router.get('/', async (req, res) => { 'event_default_require_password', 'event_default_feedback_enabled', 'gallery_show_filter_bar', - 'event_phone_field_enabled' + 'event_phone_field_enabled', + // #613 — guest upload UI needs to know the per-batch file + // count limit so it can render a real number in the + // "fileRequirements" hint (`{{limit}}` was showing literal) + // and short-circuit before posting too many files. The + // backend route also enforces it via getMaxFilesPerUpload, + // but a client-side guard saves a 4MB+ round-trip when the + // limit is small. + 'general_max_files_per_upload' ]); }) .select('setting_key', 'setting_value'); @@ -145,6 +153,17 @@ router.get('/', async (req, res) => { gallery_show_filter_bar: settingsObject.gallery_show_filter_bar !== false, // Upload settings (safe to expose - needed for client-side validation) allowed_file_types: settingsObject.general_allowed_file_types || 'jpg,jpeg,png,webp', + // #613 — per-batch file count limit. The guest UserPhotoUpload modal + // reads this both to render the "{{limit}} files per upload" hint + // (showed `{{limit}}` literal before this) and to refuse a batch + // before posting it. Backend route enforces the same value via + // getMaxFilesPerUpload(), so the client-side check is purely a UX + // optimisation. Default mirrors uploadSettings.js + // DEFAULT_MAX_FILES_PER_UPLOAD so the UI shows a sensible number + // even on installs that have never explicitly set the value. + general_max_files_per_upload: Number.isFinite(Number(settingsObject.general_max_files_per_upload)) + ? Number(settingsObject.general_max_files_per_upload) + : 500, // SEO meta tag flags (safe to expose - these are intended for crawlers) seo_meta_noindex: settingsObject.seo_meta_noindex === true, seo_meta_nofollow: settingsObject.seo_meta_nofollow === true, diff --git a/frontend/src/components/gallery/UserPhotoUpload.tsx b/frontend/src/components/gallery/UserPhotoUpload.tsx index 791aa624..955f3b18 100644 --- a/frontend/src/components/gallery/UserPhotoUpload.tsx +++ b/frontend/src/components/gallery/UserPhotoUpload.tsx @@ -32,6 +32,17 @@ export const UserPhotoUpload: React.FC = ({ const { data: publicSettings } = usePublicSettings(); + // #613 — guest upload UI was hardcoded to behave as if the limit was + // unlimited (no client-side guard) and the fileRequirements hint + // rendered `{{limit}}` literally because t() was called with no + // interpolation argument. publicSettings now surfaces + // general_max_files_per_upload (default 500 from publicSettings.js) + // so we can render a real number and refuse oversized batches before + // they hit the backend. Backend route enforces the same value too. + const maxFilesPerUpload = Number.isFinite(Number(publicSettings?.general_max_files_per_upload)) + ? Number(publicSettings?.general_max_files_per_upload) + : 500; + const allowedMimeTypes = useMemo( () => extensionsToMimeTypes(publicSettings?.allowed_file_types), [publicSettings?.allowed_file_types] @@ -57,6 +68,20 @@ export const UserPhotoUpload: React.FC = ({ return true; }); if (validFiles.length === 0) return; + + // #613 — per-batch file count guard. Mirrors what the admin's + // PhotoUpload component does. Backend also enforces, so this is + // purely UX (saves a multi-MB POST when the user clearly went over). + const remaining = Math.max(0, maxFilesPerUpload - files.length); + if (remaining === 0) { + toast.error(t('upload.limitReached', { limit: maxFilesPerUpload })); + return; + } + if (validFiles.length > remaining) { + toast.warning(t('upload.someFilesSkipped', { allowed: remaining, limit: maxFilesPerUpload })); + setFiles((prev) => [...prev, ...validFiles.slice(0, remaining)]); + return; + } setFiles((prev) => [...prev, ...validFiles]); }; @@ -200,7 +225,10 @@ export const UserPhotoUpload: React.FC = ({ {t('upload.clickToUpload')}

- {t('upload.fileRequirements')} + {/* #613 — pass { limit } so `{{limit}}` interpolates + with the real number from settings instead of + rendering literally. */} + {t('upload.fileRequirements', { limit: maxFilesPerUpload })}