mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-30 03:33:48 -05:00
fix: moves storage api management endpoint to use payload instead of … (#5348)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { handleFileUpload } from "@/app/lib/fileUpload";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { LoadingSpinner } from "@/modules/ui/components/loading-spinner";
|
||||
import { OptionsSwitch } from "@/modules/ui/components/options-switch";
|
||||
@@ -11,7 +12,7 @@ import toast from "react-hot-toast";
|
||||
import { TAllowedFileExtension } from "@formbricks/types/common";
|
||||
import { Uploader } from "./components/uploader";
|
||||
import { VideoSettings } from "./components/video-settings";
|
||||
import { getAllowedFiles, uploadFile } from "./lib/utils";
|
||||
import { getAllowedFiles } from "./lib/utils";
|
||||
|
||||
const allowedFileTypesForPreview = ["png", "jpeg", "jpg", "webp"];
|
||||
const isImage = (name: string) => {
|
||||
@@ -21,7 +22,7 @@ const isImage = (name: string) => {
|
||||
interface FileInputProps {
|
||||
id: string;
|
||||
allowedFileExtensions: TAllowedFileExtension[];
|
||||
environmentId: string | undefined;
|
||||
environmentId: string;
|
||||
onFileUpload: (uploadedUrl: string[] | undefined, fileType: "image" | "video") => void;
|
||||
fileUrl?: string | string[];
|
||||
videoUrl?: string;
|
||||
@@ -78,14 +79,11 @@ export const FileInput = ({
|
||||
allowedFiles.map((file) => ({ url: URL.createObjectURL(file), name: file.name, uploaded: false }))
|
||||
);
|
||||
|
||||
const uploadedFiles = await Promise.allSettled(
|
||||
allowedFiles.map((file) => uploadFile(file, allowedFileExtensions, environmentId))
|
||||
const uploadedFiles = await Promise.all(
|
||||
allowedFiles.map((file) => handleFileUpload(file, environmentId, allowedFileExtensions))
|
||||
);
|
||||
|
||||
if (
|
||||
uploadedFiles.length < allowedFiles.length ||
|
||||
uploadedFiles.some((file) => file.status === "rejected")
|
||||
) {
|
||||
if (uploadedFiles.length < allowedFiles.length || uploadedFiles.some((file) => file.error)) {
|
||||
if (uploadedFiles.length === 0) {
|
||||
toast.error(t("common.no_files_uploaded"));
|
||||
} else {
|
||||
@@ -95,8 +93,8 @@ export const FileInput = ({
|
||||
|
||||
const uploadedUrls: string[] = [];
|
||||
uploadedFiles.forEach((file) => {
|
||||
if (file.status === "fulfilled") {
|
||||
uploadedUrls.push(encodeURI(file.value.url));
|
||||
if (file.url) {
|
||||
uploadedUrls.push(encodeURI(file.url));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -147,14 +145,11 @@ export const FileInput = ({
|
||||
...allowedFiles.map((file) => ({ url: URL.createObjectURL(file), name: file.name, uploaded: false })),
|
||||
]);
|
||||
|
||||
const uploadedFiles = await Promise.allSettled(
|
||||
allowedFiles.map((file) => uploadFile(file, allowedFileExtensions, environmentId))
|
||||
const uploadedFiles = await Promise.all(
|
||||
allowedFiles.map((file) => handleFileUpload(file, environmentId, allowedFileExtensions))
|
||||
);
|
||||
|
||||
if (
|
||||
uploadedFiles.length < allowedFiles.length ||
|
||||
uploadedFiles.some((file) => file.status === "rejected")
|
||||
) {
|
||||
if (uploadedFiles.length < allowedFiles.length || uploadedFiles.some((file) => file.error)) {
|
||||
if (uploadedFiles.length === 0) {
|
||||
toast.error(t("common.no_files_uploaded"));
|
||||
} else {
|
||||
@@ -164,8 +159,8 @@ export const FileInput = ({
|
||||
|
||||
const uploadedUrls: string[] = [];
|
||||
uploadedFiles.forEach((file) => {
|
||||
if (file.status === "fulfilled") {
|
||||
uploadedUrls.push(encodeURI(file.value.url));
|
||||
if (file.url) {
|
||||
uploadedUrls.push(encodeURI(file.url));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { toast } from "react-hot-toast";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { convertHeicToJpegAction } from "./actions";
|
||||
import { checkForYoutubePrivacyMode, getAllowedFiles } from "./utils";
|
||||
|
||||
// Mock FileReader
|
||||
class MockFileReader {
|
||||
onload: (() => void) | null = null;
|
||||
onerror: ((error: any) => void) | null = null;
|
||||
result: string | null = null;
|
||||
|
||||
readAsDataURL() {
|
||||
// Simulate asynchronous read
|
||||
setTimeout(() => {
|
||||
this.result = "data:text/plain;base64,dGVzdA=="; // base64 for "test"
|
||||
if (this.onload) {
|
||||
this.onload();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Mock global FileReader
|
||||
global.FileReader = MockFileReader as any;
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
toast: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./actions", () => ({
|
||||
convertHeicToJpegAction: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("File Input Utils", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("getAllowedFiles", () => {
|
||||
test("should filter out files with unsupported extensions", async () => {
|
||||
const files = [
|
||||
new File(["test"], "test.txt", { type: "text/plain" }),
|
||||
new File(["test"], "test.doc", { type: "application/msword" }),
|
||||
];
|
||||
|
||||
const result = await getAllowedFiles(files, ["txt"], 5);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].name).toBe("test.txt");
|
||||
expect(toast.error).toHaveBeenCalledWith(expect.stringContaining("Unsupported file types: test.doc"));
|
||||
});
|
||||
|
||||
test("should filter out files exceeding size limit", async () => {
|
||||
const files = [
|
||||
new File(["x".repeat(6 * 1024 * 1024)], "large.txt", { type: "text/plain" }),
|
||||
new File(["test"], "small.txt", { type: "text/plain" }),
|
||||
];
|
||||
|
||||
const result = await getAllowedFiles(files, ["txt"], 5);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].name).toBe("small.txt");
|
||||
expect(toast.error).toHaveBeenCalledWith(expect.stringContaining("Files exceeding size limit (5 MB)"));
|
||||
});
|
||||
|
||||
test("should convert HEIC files to JPEG", async () => {
|
||||
const heicFile = new File(["test"], "test.heic", { type: "image/heic" });
|
||||
const mockConvertedFile = new File(["converted"], "test.jpg", { type: "image/jpeg" });
|
||||
|
||||
vi.mocked(convertHeicToJpegAction).mockResolvedValue({
|
||||
data: mockConvertedFile,
|
||||
});
|
||||
|
||||
const result = await getAllowedFiles([heicFile], ["heic", "jpg"], 5);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].name).toBe("test.jpg");
|
||||
expect(result[0].type).toBe("image/jpeg");
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkForYoutubePrivacyMode", () => {
|
||||
test("should return true for youtube-nocookie.com URLs", () => {
|
||||
const url = "https://www.youtube-nocookie.com/watch?v=test";
|
||||
expect(checkForYoutubePrivacyMode(url)).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false for regular youtube.com URLs", () => {
|
||||
const url = "https://www.youtube.com/watch?v=test";
|
||||
expect(checkForYoutubePrivacyMode(url)).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false for invalid URLs", () => {
|
||||
const url = "not-a-url";
|
||||
expect(checkForYoutubePrivacyMode(url)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,96 +4,6 @@ import { toast } from "react-hot-toast";
|
||||
import { TAllowedFileExtension } from "@formbricks/types/common";
|
||||
import { convertHeicToJpegAction } from "./actions";
|
||||
|
||||
export const uploadFile = async (
|
||||
file: File | Blob,
|
||||
allowedFileExtensions: string[] | undefined,
|
||||
environmentId: string | undefined
|
||||
) => {
|
||||
try {
|
||||
if (!(file instanceof Blob) || !(file instanceof File)) {
|
||||
throw new Error(`Invalid file type. Expected Blob or File, but received ${typeof file}`);
|
||||
}
|
||||
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
|
||||
const bufferBytes = fileBuffer.byteLength;
|
||||
const bufferKB = bufferBytes / 1024;
|
||||
|
||||
if (bufferKB > 10240) {
|
||||
const err = new Error("File size is greater than 10MB");
|
||||
err.name = "FileTooLargeError";
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
fileName: file.name,
|
||||
fileType: file.type,
|
||||
allowedFileExtensions: allowedFileExtensions,
|
||||
environmentId: environmentId,
|
||||
};
|
||||
|
||||
const response = await fetch("/api/v1/management/storage", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Upload failed with status: ${response.status}`);
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
|
||||
const { data } = json;
|
||||
const { signedUrl, fileUrl, signingData, presignedFields, updatedFileName } = data;
|
||||
|
||||
let requestHeaders: Record<string, string> = {};
|
||||
|
||||
if (signingData) {
|
||||
const { signature, timestamp, uuid } = signingData;
|
||||
|
||||
requestHeaders = {
|
||||
"X-File-Type": file.type,
|
||||
"X-File-Name": encodeURIComponent(updatedFileName),
|
||||
"X-Environment-ID": environmentId ?? "",
|
||||
"X-Signature": signature,
|
||||
"X-Timestamp": String(timestamp),
|
||||
"X-UUID": uuid,
|
||||
};
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
if (presignedFields) {
|
||||
Object.keys(presignedFields).forEach((key) => {
|
||||
formData.append(key, presignedFields[key]);
|
||||
});
|
||||
}
|
||||
|
||||
formData.append("file", file);
|
||||
|
||||
const uploadResponse = await fetch(signedUrl, {
|
||||
method: "POST",
|
||||
...(signingData ? { headers: requestHeaders } : {}),
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error(`Upload failed with status: ${uploadResponse.status}`);
|
||||
}
|
||||
|
||||
return {
|
||||
uploaded: true,
|
||||
url: fileUrl,
|
||||
};
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const isFileSizeExceed = (fileSizeInMB: number, maxSizeInMB?: number) => {
|
||||
if (maxSizeInMB && fileSizeInMB > maxSizeInMB) {
|
||||
return true;
|
||||
@@ -169,6 +79,7 @@ export const checkForYoutubePrivacyMode = (url: string): boolean => {
|
||||
const parsedUrl = new URL(url);
|
||||
return parsedUrl.host === "www.youtube-nocookie.com";
|
||||
} catch (e) {
|
||||
console.error("Invalid URL", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user