From ab729561d47b7e063446fc8c0d1deb6dee5fe0fb Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Sat, 27 Jun 2026 16:32:26 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fix=20file=20upload=20advisory?= =?UTF-8?q?=20(#2544)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed deprecated viewer upload URL V1/V2 routes and handlers. - Hardened V3 upload paths by validating session-derived path segments. - Updated API docs and OpenAPI output to advertise only V3 generate-upload-url. - Added file-input upload URL test coverage and package test wiring. --- .../chat/generate-upload-url.mdx | 2 +- apps/docs/openapi/viewer.json | 249 +----------------- bun.lock | 3 + packages/blocks/fileInput/package.json | 6 + .../deprecated/handleGenerateUploadUrlV1.ts | 197 -------------- .../deprecated/handleGenerateUploadUrlV2.ts | 170 ------------ .../src/api/handleGenerateUploadUrl.test.ts | 139 ++++++++++ .../src/api/handleGenerateUploadUrl.ts | 13 +- packages/blocks/fileInput/src/api/router.ts | 48 ---- 9 files changed, 159 insertions(+), 668 deletions(-) delete mode 100644 packages/blocks/fileInput/src/api/deprecated/handleGenerateUploadUrlV1.ts delete mode 100644 packages/blocks/fileInput/src/api/deprecated/handleGenerateUploadUrlV2.ts create mode 100644 packages/blocks/fileInput/src/api/handleGenerateUploadUrl.test.ts diff --git a/apps/docs/api-reference/chat/generate-upload-url.mdx b/apps/docs/api-reference/chat/generate-upload-url.mdx index 9a6289522..3deaeea01 100644 --- a/apps/docs/api-reference/chat/generate-upload-url.mdx +++ b/apps/docs/api-reference/chat/generate-upload-url.mdx @@ -1,6 +1,6 @@ --- title: 'Generate upload URL' -openapi: POST /v2/generate-upload-url +openapi: POST /v3/generate-upload-url --- The `presignedUrl` and `formData` fields can be then used to upload the file to the S3 bucket, directly from the browser if necessary. Here is an example: diff --git a/apps/docs/openapi/viewer.json b/apps/docs/openapi/viewer.json index 824a97ccb..c49c147f8 100644 --- a/apps/docs/openapi/viewer.json +++ b/apps/docs/openapi/viewer.json @@ -3936,253 +3936,6 @@ } } }, - "/v1/generate-upload-url": { - "post": { - "operationId": "generateUploadUrlV1", - "summary": "Generate upload URL", - "description": "Used to upload anything from the client to S3 bucket", - "security": [ - { - "Authorization": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "filePathProps": { - "anyOf": [ - { - "type": "object", - "properties": { - "typebotId": { - "type": "string" - }, - "blockId": { - "type": "string" - }, - "resultId": { - "type": "string" - }, - "fileName": { - "type": "string" - } - }, - "required": [ - "typebotId", - "blockId", - "resultId", - "fileName" - ] - }, - { - "type": "object", - "properties": { - "sessionId": { - "type": "string" - }, - "fileName": { - "type": "string" - } - }, - "required": [ - "sessionId", - "fileName" - ] - } - ] - }, - "fileType": { - "type": "string" - } - }, - "required": [ - "filePathProps" - ] - } - } - } - }, - "responses": { - "200": { - "description": "Successful response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "presignedUrl": { - "type": "string" - }, - "formData": { - "type": "object", - "additionalProperties": {} - }, - "fileUrl": { - "type": "string" - } - }, - "required": [ - "presignedUrl", - "formData", - "fileUrl" - ] - } - } - } - }, - "400": { - "description": "Invalid input data", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/error.BAD_REQUEST" - } - } - } - }, - "401": { - "description": "Authorization not provided", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/error.UNAUTHORIZED" - } - } - } - }, - "403": { - "description": "Insufficient access", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/error.FORBIDDEN" - } - } - } - }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/error.INTERNAL_SERVER_ERROR" - } - } - } - } - } - } - }, - "/v2/generate-upload-url": { - "post": { - "operationId": "generateUploadUrlV2", - "summary": "Generate upload URL", - "description": "Used to upload anything from the client to S3 bucket", - "security": [ - { - "Authorization": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionId": { - "type": "string" - }, - "fileName": { - "type": "string" - }, - "fileType": { - "type": "string" - } - }, - "required": [ - "sessionId", - "fileName" - ] - } - } - } - }, - "responses": { - "200": { - "description": "Successful response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "presignedUrl": { - "type": "string" - }, - "formData": { - "type": "object", - "additionalProperties": {} - }, - "fileUrl": { - "type": "string" - } - }, - "required": [ - "presignedUrl", - "formData", - "fileUrl" - ] - } - } - } - }, - "400": { - "description": "Invalid input data", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/error.BAD_REQUEST" - } - } - } - }, - "401": { - "description": "Authorization not provided", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/error.UNAUTHORIZED" - } - } - } - }, - "403": { - "description": "Insufficient access", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/error.FORBIDDEN" - } - } - } - }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/error.INTERNAL_SERVER_ERROR" - } - } - } - } - } - } - }, "/v3/generate-upload-url": { "post": { "operationId": "generateUploadUrl", @@ -17876,4 +17629,4 @@ } } } -} \ No newline at end of file +} diff --git a/bun.lock b/bun.lock index 7623a144c..a9f7e4dfc 100644 --- a/bun.lock +++ b/bun.lock @@ -402,6 +402,9 @@ "@typebot.io/typebot": "workspace:*", "zod": "^4.3.5", }, + "devDependencies": { + "@types/bun": "^1.3.9", + }, }, "packages/blocks/inputs": { "name": "@typebot.io/blocks-inputs", diff --git a/packages/blocks/fileInput/package.json b/packages/blocks/fileInput/package.json index bed10f037..1c3a31963 100644 --- a/packages/blocks/fileInput/package.json +++ b/packages/blocks/fileInput/package.json @@ -11,6 +11,9 @@ }, "./package.json": "./package.json" }, + "scripts": { + "test": "bun test" + }, "dependencies": { "@orpc/server": "^1.13.9", "@typebot.io/groups": "workspace:*", @@ -24,5 +27,8 @@ "@typebot.io/blocks-logic": "workspace:*", "@typebot.io/typebot": "workspace:*", "zod": "^4.3.5" + }, + "devDependencies": { + "@types/bun": "^1.3.9" } } diff --git a/packages/blocks/fileInput/src/api/deprecated/handleGenerateUploadUrlV1.ts b/packages/blocks/fileInput/src/api/deprecated/handleGenerateUploadUrlV1.ts deleted file mode 100644 index e53c77d2d..000000000 --- a/packages/blocks/fileInput/src/api/deprecated/handleGenerateUploadUrlV1.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { ORPCError } from "@orpc/server"; -import { InputBlockType } from "@typebot.io/blocks-inputs/constants"; -import { getSession } from "@typebot.io/chat-session/queries/getSession"; -import { env } from "@typebot.io/env"; -import { getBlockById } from "@typebot.io/groups/helpers/getBlockById"; -import { parseGroups } from "@typebot.io/groups/helpers/parseGroups"; -import { - createUploadFileName, - resolveStoredUploadFileType, - resolveUploadContentDisposition, - resolveUploadFileType, -} from "@typebot.io/lib/s3/createUploadFilePath"; -import { generateSignedUploadProxyUrl } from "@typebot.io/lib/s3/signedUploadProxy"; -import prisma from "@typebot.io/prisma"; -import { z } from "zod"; -import { getUploadProxyBaseUrl } from "../getUploadProxyBaseUrl"; - -export const generateUploadUrlV1InputSchema = z.object({ - filePathProps: z - .object({ - typebotId: z.string(), - blockId: z.string(), - resultId: z.string(), - fileName: z.string(), - }) - .or( - z.object({ - sessionId: z.string(), - fileName: z.string(), - }), - ), - fileType: z.string().optional(), - fileSize: z.number().optional(), -}); - -export const handleGenerateUploadUrlV1 = async ({ - input: { filePathProps, fileType, fileSize }, - context, -}: { - input: z.infer; - context?: { apiOrigin?: string }; -}) => { - if (!env.S3_ENDPOINT || !env.S3_ACCESS_KEY || !env.S3_SECRET_KEY) - throw new ORPCError("INTERNAL_SERVER_ERROR", { - message: - "S3 not properly configured. Missing one of those variables: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY", - }); - - const resolvedFileType = resolveUploadFileType(fileType); - - const uploadFileName = createUploadFileName(resolvedFileType); - - if ("typebotId" in filePathProps) { - const publicTypebot = await prisma.publicTypebot.findFirst({ - where: { - typebotId: filePathProps.typebotId, - }, - select: { - version: true, - groups: true, - typebot: { - select: { - workspaceId: true, - }, - }, - }, - }); - - const workspaceId = publicTypebot?.typebot.workspaceId; - - if (!workspaceId) - throw new ORPCError("BAD_REQUEST", { - message: "Can't find workspaceId", - }); - - const fileUploadBlock = parseGroups(publicTypebot.groups, { - typebotVersion: publicTypebot.version, - }) - .flatMap((group) => group.blocks) - .find((block) => block.id === filePathProps.blockId); - - if (fileUploadBlock?.type !== InputBlockType.FILE) - throw new ORPCError("BAD_REQUEST", { - message: "Can't find file upload block", - }); - - const maxFileSize = - fileUploadBlock.options?.sizeLimit ?? - env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE; - - if (maxFileSize && fileSize && fileSize > maxFileSize * 1024 * 1024) - throw new ORPCError("BAD_REQUEST", { - message: `File size exceeds the ${maxFileSize}MB limit`, - }); - - return { - ...generateSignedUploadProxyUrl({ - baseUrl: getUploadProxyBaseUrl(context?.apiOrigin), - fileType: resolveStoredUploadFileType(resolvedFileType), - contentDisposition: resolveUploadContentDisposition(resolvedFileType), - filePath: `public/workspaces/${workspaceId}/typebots/${filePathProps.typebotId}/results/${filePathProps.resultId}/${uploadFileName}`, - maxFileSize, - }), - fileType: resolvedFileType, - }; - } - - const session = await getSession(filePathProps.sessionId); - - if (!session?.state) - throw new ORPCError("BAD_REQUEST", { - message: "Can't find session", - }); - - const typebotId = session.state.typebotsQueue[0].typebot.id; - - const publicTypebot = await prisma.publicTypebot.findFirst({ - where: { - typebotId, - }, - select: { - version: true, - groups: true, - typebot: { - select: { - workspaceId: true, - }, - }, - }, - }); - - const workspaceId = publicTypebot?.typebot.workspaceId; - - if (!workspaceId) - throw new ORPCError("BAD_REQUEST", { - message: "Can't find workspaceId", - }); - - if (session.state.currentBlockId === undefined) - throw new ORPCError("BAD_REQUEST", { - message: "Can't find currentBlockId in session state", - }); - - const { block: fileUploadBlock } = getBlockById( - session.state.currentBlockId, - parseGroups(publicTypebot.groups, { - typebotVersion: publicTypebot.version, - }), - ); - - if (fileUploadBlock?.type !== InputBlockType.FILE) - throw new ORPCError("BAD_REQUEST", { - message: "Can't find file upload block", - }); - - const resultId = session.state.typebotsQueue[0].resultId; - const visibility = - fileUploadBlock.options?.visibility === "Private" ? "private" : "public"; - - const filePath = resultId - ? `${visibility}/workspaces/${workspaceId}/typebots/${typebotId}/results/${resultId}/${uploadFileName}` - : `public/tmp/${typebotId}/${uploadFileName}`; - - const maxFileSize = - fileUploadBlock.options && "sizeLimit" in fileUploadBlock.options - ? fileUploadBlock.options.sizeLimit - : env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE; - - if (maxFileSize && fileSize && fileSize > maxFileSize * 1024 * 1024) - throw new ORPCError("BAD_REQUEST", { - message: `File size exceeds the ${maxFileSize}MB limit`, - }); - - const { - presignedUrl, - fileUrl: defaultFileUrl, - formData, - maxFileSize: maxFileSizeMB, - } = generateSignedUploadProxyUrl({ - baseUrl: getUploadProxyBaseUrl(context?.apiOrigin), - fileType: resolveStoredUploadFileType(resolvedFileType), - contentDisposition: resolveUploadContentDisposition(resolvedFileType), - filePath, - maxFileSize, - }); - - return { - presignedUrl, - formData, - fileType: resolvedFileType, - maxFileSize: maxFileSizeMB, - fileUrl: - fileUploadBlock.options?.visibility === "Private" && resultId - ? `${env.NEXTAUTH_URL}/api/typebots/${typebotId}/results/${resultId}/${uploadFileName}` - : defaultFileUrl, - }; -}; diff --git a/packages/blocks/fileInput/src/api/deprecated/handleGenerateUploadUrlV2.ts b/packages/blocks/fileInput/src/api/deprecated/handleGenerateUploadUrlV2.ts deleted file mode 100644 index 281e675b2..000000000 --- a/packages/blocks/fileInput/src/api/deprecated/handleGenerateUploadUrlV2.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { ORPCError } from "@orpc/server"; -import { InputBlockType } from "@typebot.io/blocks-inputs/constants"; -import type { FileInputBlock } from "@typebot.io/blocks-inputs/file/schema"; -import type { TextInputBlock } from "@typebot.io/blocks-inputs/text/schema"; -import { getSession } from "@typebot.io/chat-session/queries/getSession"; -import { env } from "@typebot.io/env"; -import { getBlockById } from "@typebot.io/groups/helpers/getBlockById"; -import { parseGroups } from "@typebot.io/groups/helpers/parseGroups"; -import { - createUploadFileName, - resolveStoredUploadFileType, - resolveUploadContentDisposition, - resolveUploadFileType, -} from "@typebot.io/lib/s3/createUploadFilePath"; -import { generateSignedUploadProxyUrl } from "@typebot.io/lib/s3/signedUploadProxy"; -import prisma from "@typebot.io/prisma"; -import { z } from "zod"; -import { getUploadProxyBaseUrl } from "../getUploadProxyBaseUrl"; - -export const generateUploadUrlV2InputSchema = z.object({ - sessionId: z.string(), - fileName: z.string(), - fileType: z.string().optional(), - fileSize: z.number().optional(), -}); - -export const handleGenerateUploadUrlV2 = async ({ - input: { sessionId, fileType, fileSize }, - context, -}: { - input: z.infer; - context?: { apiOrigin?: string }; -}) => { - if (!env.S3_ENDPOINT || !env.S3_ACCESS_KEY || !env.S3_SECRET_KEY) - throw new ORPCError("INTERNAL_SERVER_ERROR", { - message: - "S3 not properly configured. Missing one of those variables: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY", - }); - - const session = await getSession(sessionId); - - if (!session?.state) - throw new ORPCError("BAD_REQUEST", { - message: "Can't find session", - }); - - const typebotId = session.state.typebotsQueue[0].typebot.id; - - const isPreview = !session.state.typebotsQueue[0].resultId; - - const typebot = session.state.typebotsQueue[0].resultId - ? await getAndParsePublicTypebot(session.state.typebotsQueue[0].typebot.id) - : session.state.typebotsQueue[0].typebot; - - if (!typebot?.version) - throw new ORPCError("BAD_REQUEST", { - message: "Can't find typebot", - }); - - if (session.state.currentBlockId === undefined) - throw new ORPCError("BAD_REQUEST", { - message: "Can't find currentBlockId in session state", - }); - - const { block } = getBlockById( - session.state.currentBlockId, - parseGroups(typebot.groups, { - typebotVersion: typebot.version, - }), - ); - - if ( - !block || - (block.type !== InputBlockType.FILE && - (block.type !== InputBlockType.TEXT || - !block.options?.attachments?.isEnabled) && - (block.type !== InputBlockType.TEXT || - !block.options?.audioClip?.isEnabled)) - ) - throw new ORPCError("BAD_REQUEST", { - message: "Current block does not expect file upload", - }); - - const resolvedFileType = resolveUploadFileType(fileType); - - const { visibility, maxFileSize } = parseFileUploadParams(block); - - if (maxFileSize && fileSize && fileSize > maxFileSize * 1024 * 1024) - throw new ORPCError("BAD_REQUEST", { - message: `File size exceeds the ${maxFileSize}MB limit`, - }); - - const resultId = session.state.typebotsQueue[0].resultId; - const uploadFileName = createUploadFileName(resolvedFileType); - - const filePath = - "workspaceId" in typebot && typebot.workspaceId && resultId - ? `${visibility === "Private" ? "private" : "public"}/workspaces/${ - typebot.workspaceId - }/typebots/${typebotId}/results/${resultId}/${uploadFileName}` - : `public/tmp/${typebotId}/${uploadFileName}`; - - const { - presignedUrl, - fileUrl: defaultFileUrl, - formData, - maxFileSize: maxFileSizeMB, - } = generateSignedUploadProxyUrl({ - baseUrl: getUploadProxyBaseUrl(context?.apiOrigin), - fileType: resolveStoredUploadFileType(resolvedFileType), - contentDisposition: resolveUploadContentDisposition(resolvedFileType), - filePath, - maxFileSize, - }); - - return { - presignedUrl, - formData, - fileType: resolvedFileType, - maxFileSize: maxFileSizeMB, - fileUrl: - visibility === "Private" && !isPreview - ? `${env.NEXTAUTH_URL}/api/typebots/${typebotId}/results/${resultId}/${uploadFileName}` - : defaultFileUrl, - }; -}; - -const getAndParsePublicTypebot = async (typebotId: string) => { - const publicTypebot = await prisma.publicTypebot.findFirst({ - where: { - typebotId, - }, - select: { - version: true, - groups: true, - typebot: { - select: { - workspaceId: true, - }, - }, - }, - }); - - if (!publicTypebot) return null; - - return { ...publicTypebot, workspaceId: publicTypebot.typebot.workspaceId }; -}; - -const parseFileUploadParams = ( - block: FileInputBlock | TextInputBlock, -): { visibility: "Public" | "Private"; maxFileSize: number | undefined } => { - if (block.type === InputBlockType.FILE) { - return { - visibility: - block.options?.visibility === "Private" ? "Private" : "Public", - maxFileSize: - block.options && "sizeLimit" in block.options - ? block.options.sizeLimit - : env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE, - }; - } - - return { - visibility: - block.options?.attachments?.visibility === "Private" - ? "Private" - : "Public", - maxFileSize: env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE, - }; -}; diff --git a/packages/blocks/fileInput/src/api/handleGenerateUploadUrl.test.ts b/packages/blocks/fileInput/src/api/handleGenerateUploadUrl.test.ts new file mode 100644 index 000000000..2e120345d --- /dev/null +++ b/packages/blocks/fileInput/src/api/handleGenerateUploadUrl.test.ts @@ -0,0 +1,139 @@ +import { beforeEach, describe, expect, it, mock } from "bun:test"; +import { InputBlockType } from "@typebot.io/blocks-inputs/constants"; + +process.env.DATABASE_URL = "postgres://user:password@localhost:5432/typebot"; +process.env.ENCRYPTION_SECRET = "12345678901234567890123456789012"; +process.env.NEXTAUTH_URL = "http://localhost:3000"; +process.env.NEXT_PUBLIC_VIEWER_URL = "http://localhost:3001"; +process.env.S3_ACCESS_KEY = "minio"; +process.env.S3_BUCKET = "typebot"; +process.env.S3_ENDPOINT = "s3.example.com"; +process.env.S3_SECRET_KEY = "miniostorage"; +process.env.SKIP_ENV_CHECK = "false"; + +const getSessionMock = mock(); +const publicTypebotFindFirstMock = mock(); + +mock.module("@typebot.io/chat-session/queries/getSession", () => ({ + getSession: getSessionMock, +})); + +mock.module("@typebot.io/prisma", () => ({ + default: { + publicTypebot: { + findFirst: publicTypebotFindFirstMock, + }, + }, +})); + +const { handleGenerateUploadUrl } = await import("./handleGenerateUploadUrl"); + +const blockIdFromSession = "block-session"; +const resultIdFromSession = "result-session"; +const sessionId = "session-id"; +const typebotIdFromSession = "typebot-session"; +const workspaceIdFromPublicTypebot = "workspace-session"; + +describe("handleGenerateUploadUrl", () => { + beforeEach(() => { + getSessionMock.mockReset(); + publicTypebotFindFirstMock.mockReset(); + getSessionMock.mockResolvedValue(buildSession()); + publicTypebotFindFirstMock.mockResolvedValue(buildPublicTypebot()); + }); + + it("uses the session block for current uploads", async () => { + const response = await handleGenerateUploadUrl({ + input: { + sessionId, + blockId: "block-request", + fileName: "file.png", + fileType: "image/png", + fileSize: 1024, + }, + context: { apiOrigin: "http://localhost:3001" }, + }); + + const filePath = parseSignedUploadFilePath(response.presignedUrl); + + expect(filePath.startsWith(expectedCurrentResultPrefix())).toBe(true); + expect(filePath.includes("block-request")).toBe(false); + expect(filePath.endsWith(".png")).toBe(true); + expect(response.fileUrl.startsWith(expectedCurrentPrivateFileUrl())).toBe( + true, + ); + expect(response.maxFileSize).toBe(5); + }); +}); + +const buildSession = () => ({ + state: { + currentBlockId: blockIdFromSession, + typebotsQueue: [ + { + resultId: resultIdFromSession, + typebot: { + id: typebotIdFromSession, + }, + }, + ], + }, +}); + +const buildPublicTypebot = () => ({ + version: "5", + groups: [ + { + id: "group", + title: "Group", + graphCoordinates: { x: 0, y: 0 }, + blocks: [ + { + id: blockIdFromSession, + type: InputBlockType.FILE, + options: { + visibility: "Private", + sizeLimit: 5, + }, + }, + ], + }, + ], + typebot: { + workspaceId: workspaceIdFromPublicTypebot, + }, +}); + +const parseSignedUploadFilePath = (presignedUrl: string) => { + const token = new URL(presignedUrl).pathname.split("/").at(-1); + if (!token) throw new Error("Missing upload token"); + + const encodedPayload = decodeURIComponent(token).split(".")[0]; + if (!encodedPayload) throw new Error("Missing upload payload"); + + const payload: unknown = JSON.parse( + Buffer.from(encodedPayload, "base64url").toString("utf8"), + ); + + if ( + typeof payload !== "object" || + payload === null || + !("filePath" in payload) || + typeof payload.filePath !== "string" + ) + throw new Error("Invalid upload payload"); + + return payload.filePath; +}; + +const expectedResultPrefix = () => + `private/workspaces/${workspaceIdFromPublicTypebot}/typebots/${typebotIdFromSession}/results/${resultIdFromSession}/`; + +const expectedCurrentResultPrefix = () => + `${expectedResultPrefix()}blocks/${blockIdFromSession}/`; + +const expectedPrivateFileUrl = () => + `${process.env.NEXTAUTH_URL}/api/typebots/${typebotIdFromSession}/results/${resultIdFromSession}/`; + +const expectedCurrentPrivateFileUrl = () => + `${expectedPrivateFileUrl()}blocks/${blockIdFromSession}/`; diff --git a/packages/blocks/fileInput/src/api/handleGenerateUploadUrl.ts b/packages/blocks/fileInput/src/api/handleGenerateUploadUrl.ts index 79a5a148e..0f240a871 100644 --- a/packages/blocks/fileInput/src/api/handleGenerateUploadUrl.ts +++ b/packages/blocks/fileInput/src/api/handleGenerateUploadUrl.ts @@ -9,6 +9,7 @@ import { parseGroups } from "@typebot.io/groups/helpers/parseGroups"; import { parseAllowedFileTypesMetadata } from "@typebot.io/lib/extensionFromMimeType"; import { createUploadFileName, + parseUploadPathSegment, resolveStoredUploadFileType, resolveUploadContentDisposition, resolveUploadFileType, @@ -116,13 +117,17 @@ export const handleGenerateUploadUrl = async ({ const resultId = session.state.typebotsQueue[0].resultId; const uploadFileName = createUploadFileName(resolvedFileType); + const typebotPathSegment = parseUploadPathSegment(typebotId); + const blockPathSegment = parseUploadPathSegment(currentBlockId); const filePath = "workspaceId" in typebot && typebot.workspaceId && resultId - ? `${visibility === "Private" ? "private" : "public"}/workspaces/${ - typebot.workspaceId - }/typebots/${typebotId}/results/${resultId}/blocks/${currentBlockId}/${uploadFileName}` - : `public/tmp/typebots/${typebotId}/blocks/${currentBlockId}/${uploadFileName}`; + ? `${visibility === "Private" ? "private" : "public"}/workspaces/${parseUploadPathSegment( + typebot.workspaceId, + )}/typebots/${typebotPathSegment}/results/${parseUploadPathSegment( + resultId, + )}/blocks/${blockPathSegment}/${uploadFileName}` + : `public/tmp/typebots/${typebotPathSegment}/blocks/${blockPathSegment}/${uploadFileName}`; const { presignedUrl, diff --git a/packages/blocks/fileInput/src/api/router.ts b/packages/blocks/fileInput/src/api/router.ts index ddbfb2a0d..87eb99395 100644 --- a/packages/blocks/fileInput/src/api/router.ts +++ b/packages/blocks/fileInput/src/api/router.ts @@ -4,14 +4,6 @@ import { } from "@typebot.io/config/orpc/builder/middlewares"; import { publicProcedure } from "@typebot.io/config/orpc/viewer/middlewares"; import { z } from "zod"; -import { - generateUploadUrlV1InputSchema, - handleGenerateUploadUrlV1, -} from "./deprecated/handleGenerateUploadUrlV1"; -import { - generateUploadUrlV2InputSchema, - handleGenerateUploadUrlV2, -} from "./deprecated/handleGenerateUploadUrlV2"; import { generateUploadUrlInputSchema, handleGenerateUploadUrl, @@ -79,44 +71,4 @@ export const fileUploadViewerRouter = { }), ) .handler(handleGenerateUploadUrl), - generateUploadUrlV1Procedure: publicProcedure - .route({ - method: "POST", - path: "/v1/generate-upload-url", - summary: "Generate upload URL", - description: "Used to upload anything from the client to S3 bucket", - deprecated: true, - tags: ["File upload"], - }) - .input(generateUploadUrlV1InputSchema) - .output( - z.object({ - presignedUrl: z.string(), - formData: z.record(z.string(), z.string()), - fileType: z.string().optional(), - maxFileSize: z.number().optional(), - fileUrl: z.string(), - }), - ) - .handler(handleGenerateUploadUrlV1), - generateUploadUrlV2Procedure: publicProcedure - .route({ - method: "POST", - path: "/v2/generate-upload-url", - summary: "Generate upload URL", - description: "Used to upload anything from the client to S3 bucket", - deprecated: true, - tags: ["File upload"], - }) - .input(generateUploadUrlV2InputSchema) - .output( - z.object({ - presignedUrl: z.string(), - formData: z.record(z.string(), z.string()), - fileType: z.string().optional(), - maxFileSize: z.number().optional(), - fileUrl: z.string(), - }), - ) - .handler(handleGenerateUploadUrlV2), };