diff --git a/apps/web/app/api/v1/management/storage/route.ts b/apps/web/app/api/v1/management/storage/route.ts index 89ce2fdd16..9c3a67e52b 100644 --- a/apps/web/app/api/v1/management/storage/route.ts +++ b/apps/web/app/api/v1/management/storage/route.ts @@ -1,3 +1,6 @@ +import { NextRequest } from "next/server"; +import { logger } from "@formbricks/logger"; +import { TUploadPublicFileRequest, ZUploadPublicFileRequest } from "@formbricks/types/storage"; import { checkAuth } from "@/app/api/v1/management/storage/lib/utils"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; @@ -5,9 +8,6 @@ import { TApiV1Authentication, withV1ApiWrapper } from "@/app/lib/api/with-api-l import { rateLimitConfigs } from "@/modules/core/rate-limit/rate-limit-configs"; import { getSignedUrlForUpload } from "@/modules/storage/service"; import { getErrorResponseFromStorageError } from "@/modules/storage/utils"; -import { NextRequest } from "next/server"; -import { logger } from "@formbricks/logger"; -import { TUploadPublicFileRequest, ZUploadPublicFileRequest } from "@formbricks/types/storage"; // api endpoint for getting a signed url for uploading a public file // uploaded files will be public, anyone can access the file @@ -52,7 +52,16 @@ export const POST = withV1ApiWrapper({ }; } - const signedUrlResponse = await getSignedUrlForUpload(fileName, environmentId, fileType, "public"); + const MAX_PUBLIC_FILE_SIZE_MB = 5; + const maxFileUploadSize = MAX_PUBLIC_FILE_SIZE_MB * 1024 * 1024; + + const signedUrlResponse = await getSignedUrlForUpload( + fileName, + environmentId, + fileType, + "public", + maxFileUploadSize + ); if (!signedUrlResponse.ok) { logger.error({ error: signedUrlResponse.error }, "Error getting signed url for upload"); diff --git a/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.test.tsx b/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.test.tsx index 2f8a08719b..37b872ebd9 100644 --- a/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.test.tsx +++ b/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.test.tsx @@ -1,15 +1,15 @@ -import { - removeOrganizationEmailLogoUrlAction, - sendTestEmailAction, - updateOrganizationEmailLogoUrlAction, -} from "@/modules/ee/whitelabel/email-customization/actions"; -import { handleFileUpload } from "@/modules/storage/file-upload"; import "@testing-library/jest-dom/vitest"; import { cleanup, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { TOrganization } from "@formbricks/types/organizations"; import { TUser } from "@formbricks/types/user"; +import { + removeOrganizationEmailLogoUrlAction, + sendTestEmailAction, + updateOrganizationEmailLogoUrlAction, +} from "@/modules/ee/whitelabel/email-customization/actions"; +import { handleFileUpload } from "@/modules/storage/file-upload"; import { EmailCustomizationSettings } from "./email-customization-settings"; vi.mock("@/lib/constants", () => ({ @@ -107,7 +107,6 @@ describe("EmailCustomizationSettings", () => { const saveButton = screen.getAllByRole("button", { name: /save/i }); await user.click(saveButton[0]); - // The component calls `uploadFile` then `updateOrganizationEmailLogoUrlAction` expect(handleFileUpload).toHaveBeenCalledWith(testFile, "env-123", ["jpeg", "png", "jpg", "webp"]); expect(updateOrganizationEmailLogoUrlAction).toHaveBeenCalledWith({ organizationId: "org-123", diff --git a/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.tsx b/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.tsx index 4f539c578c..be9b61bddd 100644 --- a/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.tsx +++ b/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.tsx @@ -1,5 +1,14 @@ "use client"; +import { useTranslate } from "@tolgee/react"; +import { RepeatIcon, Trash2Icon } from "lucide-react"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import React, { useRef, useState } from "react"; +import { toast } from "react-hot-toast"; +import { TOrganization } from "@formbricks/types/organizations"; +import { TAllowedFileExtension } from "@formbricks/types/storage"; +import { TUser } from "@formbricks/types/user"; import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard"; import { cn } from "@/lib/cn"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; @@ -15,15 +24,6 @@ import { Uploader } from "@/modules/ui/components/file-input/components/uploader import { showStorageNotConfiguredToast } from "@/modules/ui/components/storage-not-configured-toast/lib/utils"; import { Muted, P, Small } from "@/modules/ui/components/typography"; import { ModalButton, UpgradePrompt } from "@/modules/ui/components/upgrade-prompt"; -import { useTranslate } from "@tolgee/react"; -import { RepeatIcon, Trash2Icon } from "lucide-react"; -import Image from "next/image"; -import { useRouter } from "next/navigation"; -import React, { useRef, useState } from "react"; -import { toast } from "react-hot-toast"; -import { TOrganization } from "@formbricks/types/organizations"; -import { TAllowedFileExtension } from "@formbricks/types/storage"; -import { TUser } from "@formbricks/types/user"; const allowedFileExtensions: TAllowedFileExtension[] = ["jpeg", "png", "jpg", "webp"]; diff --git a/apps/web/modules/projects/settings/look/components/edit-logo.tsx b/apps/web/modules/projects/settings/look/components/edit-logo.tsx index 0e83e4dc7d..1de0d745eb 100644 --- a/apps/web/modules/projects/settings/look/components/edit-logo.tsx +++ b/apps/web/modules/projects/settings/look/components/edit-logo.tsx @@ -1,5 +1,10 @@ "use client"; +import { Project } from "@prisma/client"; +import { useTranslate } from "@tolgee/react"; +import Image from "next/image"; +import { ChangeEvent, useRef, useState } from "react"; +import toast from "react-hot-toast"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { updateProjectAction } from "@/modules/projects/settings/actions"; import { handleFileUpload } from "@/modules/storage/file-upload"; @@ -11,11 +16,6 @@ import { DeleteDialog } from "@/modules/ui/components/delete-dialog"; import { FileInput } from "@/modules/ui/components/file-input"; import { Input } from "@/modules/ui/components/input"; import { showStorageNotConfiguredToast } from "@/modules/ui/components/storage-not-configured-toast/lib/utils"; -import { Project } from "@prisma/client"; -import { useTranslate } from "@tolgee/react"; -import Image from "next/image"; -import { ChangeEvent, useRef, useState } from "react"; -import toast from "react-hot-toast"; interface EditLogoProps { project: Project; @@ -151,6 +151,7 @@ export const EditLogo = ({ project, environmentId, isReadOnly, isStorageConfigur setIsEditing(true); }} disabled={isReadOnly} + maxSizeInMB={5} isStorageConfigured={isStorageConfigured} /> )} diff --git a/apps/web/modules/storage/file-upload.ts b/apps/web/modules/storage/file-upload.ts index 617a8505cb..bf0a60633a 100644 --- a/apps/web/modules/storage/file-upload.ts +++ b/apps/web/modules/storage/file-upload.ts @@ -1,7 +1,7 @@ export enum FileUploadError { NO_FILE = "No file provided or invalid file type. Expected a File or Blob.", INVALID_FILE_TYPE = "Please upload an image file.", - FILE_SIZE_EXCEEDED = "File size must be less than 10 MB.", + FILE_SIZE_EXCEEDED = "File size must be less than 5 MB.", UPLOAD_FAILED = "Upload failed. Please try again.", INVALID_FILE_NAME = "Invalid file name. Please rename your file and try again.", } @@ -36,7 +36,9 @@ export const handleFileUpload = async ( const bufferBytes = fileBuffer.byteLength; const bufferKB = bufferBytes / 1024; - if (bufferKB > 10240) { + const MAX_FILE_SIZE_MB = 5; + const maxSizeInKB = MAX_FILE_SIZE_MB * 1024; + if (bufferKB > maxSizeInKB) { return { error: FileUploadError.FILE_SIZE_EXCEEDED, url: "", diff --git a/apps/web/modules/survey/components/question-form-input/index.tsx b/apps/web/modules/survey/components/question-form-input/index.tsx index 783f88651c..25bea4eb2f 100644 --- a/apps/web/modules/survey/components/question-form-input/index.tsx +++ b/apps/web/modules/survey/components/question-form-input/index.tsx @@ -346,6 +346,7 @@ export const QuestionFormInput = ({ fileUrl={getFileUrl()} videoUrl={getVideoUrl()} isVideoAllowed={true} + maxSizeInMB={5} isStorageConfigured={isStorageConfigured} /> )} diff --git a/apps/web/modules/survey/editor/components/image-survey-bg.test.tsx b/apps/web/modules/survey/editor/components/image-survey-bg.test.tsx index f00049de9d..1249a9dc0c 100644 --- a/apps/web/modules/survey/editor/components/image-survey-bg.test.tsx +++ b/apps/web/modules/survey/editor/components/image-survey-bg.test.tsx @@ -1,6 +1,6 @@ -import { UploadImageSurveyBg } from "@/modules/survey/editor/components/image-survey-bg"; import { cleanup, render, screen } from "@testing-library/react"; import { afterEach, describe, expect, test, vi } from "vitest"; +import { UploadImageSurveyBg } from "@/modules/survey/editor/components/image-survey-bg"; // Create a ref to store the props passed to FileInput const mockFileInputProps: any = { current: null }; @@ -44,7 +44,7 @@ describe("UploadImageSurveyBg", () => { allowedFileExtensions: ["png", "jpeg", "jpg", "webp", "heic"], environmentId: mockEnvironmentId, fileUrl: mockBackground, - maxSizeInMB: 2, + maxSizeInMB: 5, }); }); @@ -197,7 +197,7 @@ describe("UploadImageSurveyBg", () => { expect(mockHandleBgChange).not.toHaveBeenCalled(); }); - test("should not call handleBgChange when a file exceeding 2MB size limit is uploaded", () => { + test("should not call handleBgChange when a file exceeding 5MB size limit is uploaded", () => { render( { // Verify FileInput was rendered with correct maxSizeInMB prop expect(screen.getByTestId("file-input-mock")).toBeInTheDocument(); - expect(mockFileInputProps.current?.maxSizeInMB).toBe(2); + expect(mockFileInputProps.current?.maxSizeInMB).toBe(5); // Get the onFileUpload function from the props passed to FileInput const onFileUpload = mockFileInputProps.current?.onFileUpload; diff --git a/apps/web/modules/survey/editor/components/image-survey-bg.tsx b/apps/web/modules/survey/editor/components/image-survey-bg.tsx index 2320ae5845..570d029c1e 100644 --- a/apps/web/modules/survey/editor/components/image-survey-bg.tsx +++ b/apps/web/modules/survey/editor/components/image-survey-bg.tsx @@ -28,7 +28,7 @@ export const UploadImageSurveyBg = ({ } }} fileUrl={background} - maxSizeInMB={2} + maxSizeInMB={5} isStorageConfigured={isStorageConfigured} /> diff --git a/apps/web/modules/survey/editor/components/picture-selection-form.tsx b/apps/web/modules/survey/editor/components/picture-selection-form.tsx index d3f83c2344..6906aaff27 100644 --- a/apps/web/modules/survey/editor/components/picture-selection-form.tsx +++ b/apps/web/modules/survey/editor/components/picture-selection-form.tsx @@ -1,12 +1,5 @@ "use client"; -import { cn } from "@/lib/cn"; -import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils"; -import { QuestionFormInput } from "@/modules/survey/components/question-form-input"; -import { Button } from "@/modules/ui/components/button"; -import { FileInput } from "@/modules/ui/components/file-input"; -import { Label } from "@/modules/ui/components/label"; -import { Switch } from "@/modules/ui/components/switch"; import { useAutoAnimate } from "@formkit/auto-animate/react"; import { createId } from "@paralleldrive/cuid2"; import { useTranslate } from "@tolgee/react"; @@ -14,6 +7,13 @@ import { PlusIcon } from "lucide-react"; import type { JSX } from "react"; import { TSurvey, TSurveyPictureSelectionQuestion } from "@formbricks/types/surveys/types"; import { TUserLocale } from "@formbricks/types/user"; +import { cn } from "@/lib/cn"; +import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils"; +import { QuestionFormInput } from "@/modules/survey/components/question-form-input"; +import { Button } from "@/modules/ui/components/button"; +import { FileInput } from "@/modules/ui/components/file-input"; +import { Label } from "@/modules/ui/components/label"; +import { Switch } from "@/modules/ui/components/switch"; interface PictureSelectionFormProps { localSurvey: TSurvey; @@ -141,6 +141,7 @@ export const PictureSelectionForm = ({ onFileUpload={handleFileInputChanges} fileUrl={question?.choices?.map((choice) => choice.imageUrl)} multiple={true} + maxSizeInMB={5} isStorageConfigured={isStorageConfigured} /> diff --git a/apps/web/modules/ui/components/file-input/index.tsx b/apps/web/modules/ui/components/file-input/index.tsx index c9ed4ed6ae..409ee03c07 100644 --- a/apps/web/modules/ui/components/file-input/index.tsx +++ b/apps/web/modules/ui/components/file-input/index.tsx @@ -1,16 +1,16 @@ "use client"; -import { cn } from "@/lib/cn"; -import { FileUploadError, handleFileUpload } from "@/modules/storage/file-upload"; -import { LoadingSpinner } from "@/modules/ui/components/loading-spinner"; -import { OptionsSwitch } from "@/modules/ui/components/options-switch"; -import { showStorageNotConfiguredToast } from "@/modules/ui/components/storage-not-configured-toast/lib/utils"; import { useTranslate } from "@tolgee/react"; import { FileIcon, XIcon } from "lucide-react"; import Image from "next/image"; import { useEffect, useState } from "react"; import toast from "react-hot-toast"; import { TAllowedFileExtension } from "@formbricks/types/storage"; +import { cn } from "@/lib/cn"; +import { FileUploadError, handleFileUpload } from "@/modules/storage/file-upload"; +import { LoadingSpinner } from "@/modules/ui/components/loading-spinner"; +import { OptionsSwitch } from "@/modules/ui/components/options-switch"; +import { showStorageNotConfiguredToast } from "@/modules/ui/components/storage-not-configured-toast/lib/utils"; import { Uploader } from "./components/uploader"; import { VideoSettings } from "./components/video-settings"; import { getAllowedFiles } from "./lib/utils"; diff --git a/apps/web/modules/ui/components/file-input/lib/utils.test.ts b/apps/web/modules/ui/components/file-input/lib/utils.test.ts index 5485d0625c..813a90be8d 100644 --- a/apps/web/modules/ui/components/file-input/lib/utils.test.ts +++ b/apps/web/modules/ui/components/file-input/lib/utils.test.ts @@ -51,7 +51,7 @@ describe("File Input Utils", () => { expect(result).toHaveLength(1); expect(result[0].name).toBe("test.txt"); - expect(toast.error).toHaveBeenCalledWith(expect.stringContaining("Unsupported file types: test.doc")); + expect(toast.error).toHaveBeenCalledWith(expect.stringContaining("Unsupported file type.")); }); test("should filter out files exceeding size limit", async () => { @@ -64,7 +64,7 @@ describe("File Input Utils", () => { expect(result).toHaveLength(1); expect(result[0].name).toBe("small.txt"); - expect(toast.error).toHaveBeenCalledWith(expect.stringContaining("Files exceeding size limit (5 MB)")); + expect(toast.error).toHaveBeenCalledWith(expect.stringContaining("File exceeds 5 MB size limit.")); }); test("should convert HEIC files to JPEG", async () => { diff --git a/apps/web/modules/ui/components/file-input/lib/utils.ts b/apps/web/modules/ui/components/file-input/lib/utils.ts index cbd28f7cdb..e80eebfb51 100644 --- a/apps/web/modules/ui/components/file-input/lib/utils.ts +++ b/apps/web/modules/ui/components/file-input/lib/utils.ts @@ -63,10 +63,21 @@ export const getAllowedFiles = async ( let toastMessage = ""; if (sizeExceedFiles.length > 0) { - toastMessage += `Files exceeding size limit (${maxSizeInMB} MB): ${sizeExceedFiles.join(", ")}. `; + if (sizeExceedFiles.length === 1) { + toastMessage += `File exceeds ${maxSizeInMB} MB size limit.`; + } else { + toastMessage += `${sizeExceedFiles.length} files exceed ${maxSizeInMB} MB size limit.`; + } } if (unsupportedExtensionFiles.length > 0) { - toastMessage += `Unsupported file types: ${unsupportedExtensionFiles.join(", ")}.`; + if (toastMessage) { + toastMessage += " "; + } + if (unsupportedExtensionFiles.length === 1) { + toastMessage += `Unsupported file type.`; + } else { + toastMessage += `${unsupportedExtensionFiles.length} files have unsupported types.`; + } } if (toastMessage) { toast.error(toastMessage); diff --git a/docs/api-reference/openapi.json b/docs/api-reference/openapi.json index a7090768f8..40931bc78e 100644 --- a/docs/api-reference/openapi.json +++ b/docs/api-reference/openapi.json @@ -5675,7 +5675,7 @@ }, "/api/v1/management/storage": { "post": { - "description": "API endpoint for uploading public files. Uploaded files are public and accessible by anyone. This endpoint requires authentication. It accepts a JSON body with fileName, fileType, environmentId, and optionally allowedFileExtensions to restrict file types. On success, it returns a signed URL for uploading the file to S3.", + "description": "API endpoint for uploading public files. Uploaded files are public and accessible by anyone. This endpoint requires authentication and enforces a hard limit of 5 MB for all uploads. It accepts a JSON body with fileName, fileType, environmentId, and optionally allowedFileExtensions to restrict file types. On success, it returns a signed URL for uploading the file to S3.", "parameters": [ { "example": "{{apiKey}}", diff --git a/docs/xm-and-surveys/core-features/email-customization.mdx b/docs/xm-and-surveys/core-features/email-customization.mdx index c8ae3b1803..f14bd02c40 100644 --- a/docs/xm-and-surveys/core-features/email-customization.mdx +++ b/docs/xm-and-surveys/core-features/email-customization.mdx @@ -22,7 +22,7 @@ Email branding is a white-label feature that allows you to customize the email t ![Email Customization Settings](/images/xm-and-surveys/core-features/email-customization/email-customization-card.webp) -3. Upload a logo of your company. +3. Upload a logo of your company. Logos must be 5 MB or less. 4. Click on the **Save** button. ![Updated Logo](/images/xm-and-surveys/core-features/email-customization/updated-logo.webp) diff --git a/docs/xm-and-surveys/core-features/styling-theme.mdx b/docs/xm-and-surveys/core-features/styling-theme.mdx index 12ee42d0d4..3df460223f 100644 --- a/docs/xm-and-surveys/core-features/styling-theme.mdx +++ b/docs/xm-and-surveys/core-features/styling-theme.mdx @@ -49,7 +49,7 @@ In the left side bar, you find the `Configuration` page. On this page you find t - **Color**: Pick any color for the background - **Animation**: Add dynamic animations to enhance user experience.. -- **Upload**: Use a custom uploaded image for a personalized touch +- **Upload**: Use a custom uploaded image for a personalized touch. Images must be 5 MB or less. - Image: Choose from Unsplash's extensive gallery. Note that these images will have a link and mention of the author & Unsplash on the bottom right to give them the credit for their awesome work! - **Background Overlay**: Adjust the background's opacity @@ -63,7 +63,7 @@ Customize your survey with your brand's logo. ![Choose a link survey template](/images/xm-and-surveys/core-features/styling-theme/step-four.webp) -2. Upload your logo +2. Upload your logo. Logos must be 5 MB or less. ![Choose a link survey template](/images/xm-and-surveys/core-features/styling-theme/step-five.webp) diff --git a/docs/xm-and-surveys/surveys/general-features/add-image-or-video-question.mdx b/docs/xm-and-surveys/surveys/general-features/add-image-or-video-question.mdx index 24cf4c7445..9a194ba881 100644 --- a/docs/xm-and-surveys/surveys/general-features/add-image-or-video-question.mdx +++ b/docs/xm-and-surveys/surveys/general-features/add-image-or-video-question.mdx @@ -16,7 +16,7 @@ Click the icon on the right side of the question to add an image or video: ![Access Question settings](/images/xm-and-surveys/surveys/general-features/add-image-or-video-question/add-image-or-video-to-question.webp) -Upload an image by clicking the upload icon or dragging the file: +Upload an image by clicking the upload icon or dragging the file. Images must be 5 MB or less: ![Overview of adding image to question](/images/xm-and-surveys/surveys/general-features/add-image-or-video-question/add-image-or-video-to-question-image.webp) diff --git a/docs/xm-and-surveys/surveys/question-type/select-picture.mdx b/docs/xm-and-surveys/surveys/question-type/select-picture.mdx index 64b2e0348d..27162bf2b1 100644 --- a/docs/xm-and-surveys/surveys/question-type/select-picture.mdx +++ b/docs/xm-and-surveys/surveys/question-type/select-picture.mdx @@ -40,7 +40,7 @@ Provide an optional description with further instructions. ### Images -Images can be uploaded via click or drag and drop. At least two images are required. +Images can be uploaded via click or drag and drop. At least two images are required. Each image must be 5 MB or less. ### Allow Multi Select