mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 16:16:21 -06:00
fix: restricts management file uploads size to be less than 5MB (#6669)
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"];
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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: "",
|
||||
|
||||
@@ -346,6 +346,7 @@ export const QuestionFormInput = ({
|
||||
fileUrl={getFileUrl()}
|
||||
videoUrl={getVideoUrl()}
|
||||
isVideoAllowed={true}
|
||||
maxSizeInMB={5}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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(
|
||||
<UploadImageSurveyBg
|
||||
environmentId={mockEnvironmentId}
|
||||
@@ -209,7 +209,7 @@ describe("UploadImageSurveyBg", () => {
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -28,7 +28,7 @@ export const UploadImageSurveyBg = ({
|
||||
}
|
||||
}}
|
||||
fileUrl={background}
|
||||
maxSizeInMB={2}
|
||||
maxSizeInMB={5}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -22,7 +22,7 @@ Email branding is a white-label feature that allows you to customize the email t
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
2. Upload your logo
|
||||
2. Upload your logo. Logos must be 5 MB or less.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Click the icon on the right side of the question to add an image or video:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user