From e42e6f0daa1d4326ec8d6d28e51ca5ad3c10651d Mon Sep 17 00:00:00 2001 From: juanzgc Date: Thu, 6 Nov 2025 09:57:24 -0500 Subject: [PATCH] fix(dashboard): Limit file uploads to 1MB (#13981) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary **What** — What changes are introduced in this PR? Limit file uploads to 1MB **Why** — Why are these changes relevant or necessary? Prevent large file uploads in the Admin **How** — How have these changes been implemented? Set size limits on the file uploads **Testing** — How have these changes been tested, or how can the reviewer test the feature? --- ## Checklist Please ensure the following before requesting a review: - [ ] I have added a **changeset** for this PR - Every non-breaking change should be marked as a **patch** - To add a changeset, run `yarn changeset` and follow the prompts - [ ] The changes are covered by relevant **tests** - [ ] I have verified the code works as intended locally - [ ] I have linked the related issue(s) if applicable --- ## Additional Context CLOSES CORE-1270 --- > [!NOTE] > Adds a 1MB default file size limit to `FileUpload`, surfaces size/type rejections in media forms via new i18n, and allows unlimited size for product import. > > - **Components**: > - `components/common/file-upload/file-upload.tsx`: > - Add `maxFileSize` (default 1MB) and size validation; return `rejectedFiles` alongside valid files. > - **Products › Media Upload**: > - `upload-media-form-item.tsx`: > - Handle `rejectedFiles` and set form errors for invalid type and oversized files. > - **Products › Import**: > - `upload-import.tsx`: Use `maxFileSize={Infinity}` to disable size limit for CSV import. > - **i18n**: > - Schema and EN translations: add `products.media.fileTooLarge` message. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c8c67f4d329f8767e99694649bf0b3fe4cf400e9. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../common/file-upload/file-upload.tsx | 29 +++++++++++++---- .../src/i18n/translations/$schema.json | 3 ++ .../dashboard/src/i18n/translations/en.json | 1 + .../upload-media-form-item.tsx | 32 +++++++++++++++---- .../components/upload-import.tsx | 1 + 5 files changed, 53 insertions(+), 13 deletions(-) diff --git a/packages/admin/dashboard/src/components/common/file-upload/file-upload.tsx b/packages/admin/dashboard/src/components/common/file-upload/file-upload.tsx index f05c45acf7..41600abf26 100644 --- a/packages/admin/dashboard/src/components/common/file-upload/file-upload.tsx +++ b/packages/admin/dashboard/src/components/common/file-upload/file-upload.tsx @@ -8,21 +8,30 @@ export interface FileType { file: File } +export interface RejectedFile { + file: File + reason: "size" | "format" +} + export interface FileUploadProps { label: string multiple?: boolean hint?: string hasError?: boolean formats: string[] - onUploaded: (files: FileType[]) => void + maxFileSize?: number // in bytes, defaults to 1MB. Set to Infinity to disable. + onUploaded: (files: FileType[], rejectedFiles?: RejectedFile[]) => void } +const DEFAULT_MAX_FILE_SIZE = 1024 * 1024 // 1MB + export const FileUpload = ({ label, hint, multiple = true, hasError, formats, + maxFileSize = DEFAULT_MAX_FILE_SIZE, onUploaded, }: FileUploadProps) => { const [isDragOver, setIsDragOver] = useState(false) @@ -65,18 +74,26 @@ export const FileUpload = ({ } const fileList = Array.from(files) - const fileObj = fileList.map((file) => { - const id = Math.random().toString(36).substring(7) + const validFiles: FileType[] = [] + const rejectedFiles: RejectedFile[] = [] + const normalizedMaxFileSize = Math.min(maxFileSize, Infinity) + fileList.forEach((file) => { + if (file.size > normalizedMaxFileSize) { + rejectedFiles.push({ file, reason: "size" }) + return + } + + const id = Math.random().toString(36).substring(7) const previewUrl = URL.createObjectURL(file) - return { + validFiles.push({ id: id, url: previewUrl, file, - } + }) }) - onUploaded(fileObj) + onUploaded(validFiles, rejectedFiles) } const handleDrop = (event: DragEvent) => { diff --git a/packages/admin/dashboard/src/i18n/translations/$schema.json b/packages/admin/dashboard/src/i18n/translations/$schema.json index 388de5849c..f413a59c52 100644 --- a/packages/admin/dashboard/src/i18n/translations/$schema.json +++ b/packages/admin/dashboard/src/i18n/translations/$schema.json @@ -1935,6 +1935,9 @@ "invalidFileType": { "type": "string" }, + "fileTooLarge": { + "type": "string" + }, "failedToUpload": { "type": "string" }, diff --git a/packages/admin/dashboard/src/i18n/translations/en.json b/packages/admin/dashboard/src/i18n/translations/en.json index 90a06cbe3a..905fc257a4 100644 --- a/packages/admin/dashboard/src/i18n/translations/en.json +++ b/packages/admin/dashboard/src/i18n/translations/en.json @@ -513,6 +513,7 @@ "uploadImagesLabel": "Upload images", "uploadImagesHint": "Drag and drop images here or click to upload.", "invalidFileType": "'{{name}}' is not a supported file type. Supported file types are: {{types}}.", + "fileTooLarge": "'{{name}}' exceeds the maximum file size of {{size}}. Please upload a smaller file.", "failedToUpload": "Failed to upload the added media. Please try again.", "deleteWarning_one": "You are about to delete {{count}} image. This action cannot be undone.", "deleteWarning_other": "You are about to delete {{count}} images. This action cannot be undone.", diff --git a/packages/admin/dashboard/src/routes/products/common/components/upload-media-form-item/upload-media-form-item.tsx b/packages/admin/dashboard/src/routes/products/common/components/upload-media-form-item/upload-media-form-item.tsx index 2d23cb8f74..81c6ec407d 100644 --- a/packages/admin/dashboard/src/routes/products/common/components/upload-media-form-item/upload-media-form-item.tsx +++ b/packages/admin/dashboard/src/routes/products/common/components/upload-media-form-item/upload-media-form-item.tsx @@ -5,6 +5,7 @@ import { z } from "zod" import { FileType, FileUpload, + RejectedFile, } from "../../../../../components/common/file-upload" import { Form } from "../../../../../components/common/form" import { MediaSchema } from "../../../product-create/constants" @@ -47,11 +48,13 @@ export const UploadMediaFormItem = ({ const { t } = useTranslation() const hasInvalidFiles = useCallback( - (fileList: FileType[]) => { + (fileList: FileType[] = [], rejectedFiles: RejectedFile[] = []) => { const invalidFile = fileList.find( - (f) => !SUPPORTED_FORMATS.includes(f.file.type) + (f) => !SUPPORTED_FORMATS.includes(f?.file?.type) ) + let hasInvalidFile = false; + if (invalidFile) { form.setError("media", { type: "invalid_file", @@ -61,22 +64,37 @@ export const UploadMediaFormItem = ({ }), }) - return true + hasInvalidFile = true; } - return false + const fileSizeRejections = rejectedFiles.filter((f) => f?.reason === "size") + + if (fileSizeRejections.length) { + const fileNames = fileSizeRejections.map((f) => f.file.name).join(", ") + form.setError("media", { + type: "file_too_large", + message: t("products.media.fileTooLarge", { + name: fileNames, + size: "1MB", + }), + }) + + hasInvalidFile = true; + } + + return hasInvalidFile; }, [form, t] ) const onUploaded = useCallback( - (files: FileType[]) => { + (files: FileType[] = [], rejectedFiles: RejectedFile[] = []) => { form.clearErrors("media") - if (hasInvalidFiles(files)) { + if (hasInvalidFiles(files, rejectedFiles)) { return } - files.forEach((f) => append({ ...f, isThumbnail: false })) + files?.forEach((f) => append({ ...f, isThumbnail: false })) }, [form, append, hasInvalidFiles] ) diff --git a/packages/admin/dashboard/src/routes/products/product-import/components/upload-import.tsx b/packages/admin/dashboard/src/routes/products/product-import/components/upload-import.tsx index 447157405c..b7618b5e8f 100644 --- a/packages/admin/dashboard/src/routes/products/product-import/components/upload-import.tsx +++ b/packages/admin/dashboard/src/routes/products/product-import/components/upload-import.tsx @@ -48,6 +48,7 @@ export const UploadImport = ({ } onUploaded(files[0].file) }} + maxFileSize={Infinity} /> {error && (