Merge pull request #614 from the-luap/fix/guest-upload-limits-613
fix(gallery): guest upload honours general_max_files_per_upload + i18n placeholder interpolates (#613)
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -32,6 +32,17 @@ export const UserPhotoUpload: React.FC<UserPhotoUploadProps> = ({
|
||||
|
||||
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<UserPhotoUploadProps> = ({
|
||||
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<UserPhotoUploadProps> = ({
|
||||
{t('upload.clickToUpload')}
|
||||
</p>
|
||||
<p className="text-xs text-muted-theme">
|
||||
{t('upload.fileRequirements')}
|
||||
{/* #613 — pass { limit } so `{{limit}}` interpolates
|
||||
with the real number from settings instead of
|
||||
rendering literally. */}
|
||||
{t('upload.fileRequirements', { limit: maxFilesPerUpload })}
|
||||
</p>
|
||||
<input
|
||||
type="file"
|
||||
|
||||
@@ -70,6 +70,10 @@ export interface PublicSettings {
|
||||
umami_share_url: string | null;
|
||||
// Upload settings
|
||||
allowed_file_types?: string;
|
||||
// #613 — per-batch file count limit, surfaced so the guest UserPhotoUpload
|
||||
// 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;
|
||||
// Event field requirements
|
||||
event_require_customer_name?: boolean;
|
||||
event_require_customer_email?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user