fix(gallery): guest upload honours general_max_files_per_upload + i18n placeholder interpolates (#613)
Zszywany reported on v3.44.0 (Ubuntu, Postgres 15): with Settings →
General → "Max Files per Upload" set to 10, an event configured with
allow_user_uploads + a guest selecting 16 files in the gallery's
"Upload Photos" modal succeeded silently — admin uploads to the same
event correctly refused with "Upload limit reached". On top of that,
the gallery modal's "fileRequirements" hint literally rendered
`{{limit}}` instead of the configured number.
Two separate misses for the guest path, both fixed here:
1. **Backend enforcement** — `backend/src/routes/gallery.js:1641` had
`limits: { fileSize: 50MB, files: 10 }` and `.array('photos', 10)`
hardcoded. The admin path at adminPhotos.js:131 has always resolved
files-per-batch via `getMaxFilesPerUpload()` (cached 60s read of
`general_max_files_per_upload`); guest path just never used it.
Mirror the admin: `const maxFilesPerUpload = await getMaxFilesPerUpload()`
and feed multer both `limits.files` AND the `.array(...)` cap. The
50MB per-file size is a separate concern from this issue and stays
as-is for now.
2. **i18n interpolation missing on the guest modal** —
`UserPhotoUpload.tsx:203` called `t('upload.fileRequirements')` with
no arguments. The translation string at `en.json:160` is
"JPEG, PNG or WebP (max 50MB per file, {{limit}} files per upload)"
— `{{limit}}` is unbound, so i18next emits it literally. The admin
variant `PhotoUpload.tsx:414` correctly passes
`{ limit: maxFilesPerUpload }`.
Also wired up the same client-side count guard the admin component
uses: addFiles refuses additions past the limit (`upload.limitReached`)
and warns on partial-truncate (`upload.someFilesSkipped`). Backend
enforces too, but the client guard saves a 4MB+ multipart POST when
the user is clearly over.
To surface the setting on the guest side, `general_max_files_per_upload`
joins the public-settings whitelist + projection (publicSettings.js)
and the `PublicSettings` TS interface gets the new field. Default
fallback (500, matching `uploadSettings.js` DEFAULT_MAX_FILES_PER_UPLOAD)
in both backend projection and frontend reader so an install that's
never set the value renders a sensible number rather than "undefined".
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user