diff --git a/backend/src/routes/adminSettings.js b/backend/src/routes/adminSettings.js index a46bc64d..5183a6bb 100644 --- a/backend/src/routes/adminSettings.js +++ b/backend/src/routes/adminSettings.js @@ -25,7 +25,7 @@ const { resetSecurityConfigCache } = require('../utils/authSecurity'); const { errorResponse } = require('../utils/routeHelpers'); const logger = require('../utils/logger'); const router = express.Router(); -const { clearMaxFilesPerUploadCache, MAX_ALLOWED_FILES_PER_UPLOAD } = require('../services/uploadSettings'); +const { clearMaxFilesPerUploadCache, MAX_ALLOWED_FILES_PER_UPLOAD, clearMaxFileSizeCache, MAX_ALLOWED_FILE_SIZE_MB } = require('../services/uploadSettings'); const watermarkService = require('../services/watermarkService'); const watermarkGeneratorService = require('../services/watermarkGeneratorService'); @@ -1095,6 +1095,24 @@ router.put('/general', adminAuth, requirePermission('settings.edit'), async (req settings.general_max_files_per_upload = normalizedValue; } + // Per-file size limit (MB). Validate/clamp on save, mirroring the count + // above, so an out-of-range value can't be persisted — otherwise the public + // endpoint would advertise the raw value while getMaxFileSizeMb() normalizes + // it, and the guest UI would reject files the backend actually accepts. + if (Object.prototype.hasOwnProperty.call(settings, 'general_max_file_size_mb')) { + uploadLimitTouched = true; + const rawValue = Number(settings.general_max_file_size_mb); + const normalizedValue = Number.isFinite(rawValue) ? Math.floor(rawValue) : NaN; + + if (!Number.isInteger(normalizedValue) || normalizedValue < 1 || normalizedValue > MAX_ALLOWED_FILE_SIZE_MB) { + return res.status(400).json({ + error: `general_max_file_size_mb must be an integer between 1 and ${MAX_ALLOWED_FILE_SIZE_MB}` + }); + } + + settings.general_max_file_size_mb = normalizedValue; + } + if (publicSiteKeysTouched) { if (Object.prototype.hasOwnProperty.call(settings, 'general_public_site_custom_css')) { settings.general_public_site_custom_css = sanitizeCss(settings.general_public_site_custom_css || ''); @@ -1151,6 +1169,7 @@ router.put('/general', adminAuth, requirePermission('settings.edit'), async (req } if (uploadLimitTouched) { clearMaxFilesPerUploadCache(); + clearMaxFileSizeCache(); } if (Object.prototype.hasOwnProperty.call(settings, 'general_short_gallery_urls')) { clearShareLinkSettingsCache(); diff --git a/frontend/src/services/publicSettings.service.ts b/frontend/src/services/publicSettings.service.ts index 1ce62b08..f7e461fe 100644 --- a/frontend/src/services/publicSettings.service.ts +++ b/frontend/src/services/publicSettings.service.ts @@ -82,6 +82,9 @@ export interface PublicSettings { // modal can render the real number in `upload.fileRequirements` and refuse // oversized batches client-side. Backend enforces the same value too. general_max_files_per_upload?: number; + // #613 follow-up — per-file size limit (MB), surfaced so the guest upload + // modal shows the real limit and guards client-side. Backend enforces it too. + general_max_file_size_mb?: number; // Event field requirements event_require_customer_name?: boolean; event_require_customer_email?: boolean;