diff --git a/apps/builder/src/components/ImageUploadContent/UploadButton.tsx b/apps/builder/src/components/ImageUploadContent/UploadButton.tsx index 9eb63ad6c8..f73e00369e 100644 --- a/apps/builder/src/components/ImageUploadContent/UploadButton.tsx +++ b/apps/builder/src/components/ImageUploadContent/UploadButton.tsx @@ -26,9 +26,15 @@ export const UploadButton = ({ setIsUploading(false) }, onSuccess: async (data) => { + if (!file) return + const formData = new FormData() + Object.entries(data.formData).forEach(([key, value]) => { + formData.append(key, value) + }) + formData.append('file', file) const upload = await fetch(data.presignedUrl, { - method: 'PUT', - body: file, + method: 'POST', + body: formData, }) if (!upload.ok) { diff --git a/apps/builder/src/features/billing/components/UsageProgressBars.tsx b/apps/builder/src/features/billing/components/UsageProgressBars.tsx index 30b5c9fdc6..5388885c00 100644 --- a/apps/builder/src/features/billing/components/UsageProgressBars.tsx +++ b/apps/builder/src/features/billing/components/UsageProgressBars.tsx @@ -9,12 +9,11 @@ import { Tooltip, } from '@chakra-ui/react' import { AlertIcon } from '@/components/icons' -import { Plan, Workspace } from '@typebot.io/prisma' +import { Workspace } from '@typebot.io/prisma' import React from 'react' import { parseNumberWithCommas } from '@typebot.io/lib' -import { getChatsLimit, getStorageLimit } from '@typebot.io/lib/pricing' +import { getChatsLimit } from '@typebot.io/lib/pricing' import { defaultQueryOptions, trpc } from '@/lib/trpc' -import { storageToReadable } from '../helpers/storageToReadable' import { useScopedI18n } from '@/locales' type Props = { @@ -30,19 +29,12 @@ export const UsageProgressBars = ({ workspace }: Props) => { defaultQueryOptions ) const totalChatsUsed = data?.totalChatsUsed ?? 0 - const totalStorageUsed = data?.totalStorageUsed ?? 0 const workspaceChatsLimit = getChatsLimit(workspace) - const workspaceStorageLimit = getStorageLimit(workspace) - const workspaceStorageLimitGigabites = - workspaceStorageLimit * 1024 * 1024 * 1024 const chatsPercentage = Math.round( (totalChatsUsed / workspaceChatsLimit) * 100 ) - const storagePercentage = Math.round( - (totalStorageUsed / workspaceStorageLimitGigabites) * 100 - ) return ( @@ -103,63 +95,6 @@ export const UsageProgressBars = ({ workspace }: Props) => { colorScheme={totalChatsUsed >= workspaceChatsLimit ? 'red' : 'blue'} /> - {workspace.plan !== Plan.FREE && ( - - - - - {scopedT('storage.heading')} - - {storagePercentage >= 80 && ( - - {scopedT('storage.alert.soonReach')} -
-
- {scopedT('storage.alert.updatePlan')} - - } - > - - - -
- )} -
- - - {storageToReadable(totalStorageUsed)} - - - /{' '} - {workspaceStorageLimit === -1 - ? scopedT('unlimited') - : `${workspaceStorageLimit} GB`} - - -
- = workspaceStorageLimitGigabites - ? 'red' - : 'blue' - } - rounded="full" - hasStripe - isIndeterminate={isLoading} - /> -
- )} ) } diff --git a/apps/builder/src/features/blocks/inputs/fileUpload/components/FileInputSettings.tsx b/apps/builder/src/features/blocks/inputs/fileUpload/components/FileInputSettings.tsx index d68fa464c7..49e336c1de 100644 --- a/apps/builder/src/features/blocks/inputs/fileUpload/components/FileInputSettings.tsx +++ b/apps/builder/src/features/blocks/inputs/fileUpload/components/FileInputSettings.tsx @@ -1,8 +1,8 @@ -import { FormLabel, HStack, Stack, Text } from '@chakra-ui/react' +import { FormLabel, Stack } from '@chakra-ui/react' import { CodeEditor } from '@/components/inputs/CodeEditor' import { FileInputOptions, Variable } from '@typebot.io/schemas' import React from 'react' -import { TextInput, NumberInput } from '@/components/inputs' +import { TextInput } from '@/components/inputs' import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput' @@ -24,9 +24,6 @@ export const FileInputSettings = ({ options, onOptionsChange }: Props) => { const handleVariableChange = (variable?: Variable) => onOptionsChange({ ...options, variableId: variable?.id }) - const handleSizeLimitChange = (sizeLimit?: number) => - onOptionsChange({ ...options, sizeLimit }) - const handleRequiredChange = (isRequired: boolean) => onOptionsChange({ ...options, isRequired }) @@ -48,16 +45,6 @@ export const FileInputSettings = ({ options, onOptionsChange }: Props) => { initialValue={options.isMultipleAllowed} onCheckChange={handleMultipleFilesChange} /> - - - MB - - Placeholder: { workspace?.plan, ]) - const storageLimitPercentage = useMemo(() => { - if (!usageData?.totalStorageUsed || !workspace?.plan) return 0 - return Math.round( - (usageData.totalStorageUsed / - 1024 / - 1024 / - 1024 / - getStorageLimit({ - additionalStorageIndex: workspace.additionalStorageIndex, - plan: workspace.plan, - customStorageLimit: workspace.customStorageLimit, - })) * - 100 - ) - }, [ - usageData?.totalStorageUsed, - workspace?.additionalStorageIndex, - workspace?.customStorageLimit, - workspace?.plan, - ]) - return ( <> {chatsLimitPercentage > ALERT_CHATS_PERCENT_THRESHOLD && ( @@ -74,22 +52,6 @@ export const UsageAlertBanners = ({ workspace }: Props) => { /> )} - {storageLimitPercentage > ALERT_STORAGE_PERCENT_THRESHOLD && ( - - - Your workspace collected{' '} - {storageLimitPercentage}% of your total storage - allowed. Upgrade your plan or delete some existing results to - continue collecting files from your user beyond this limit. - - } - buttonLabel="Upgrade" - /> - - )} ) } diff --git a/apps/builder/src/features/upload/api/generateUploadUrl.ts b/apps/builder/src/features/upload/api/generateUploadUrl.ts index c03d40a462..f568340df3 100644 --- a/apps/builder/src/features/upload/api/generateUploadUrl.ts +++ b/apps/builder/src/features/upload/api/generateUploadUrl.ts @@ -2,7 +2,7 @@ import { authenticatedProcedure } from '@/helpers/server/trpc' import { z } from 'zod' import { env } from '@typebot.io/env' import { TRPCError } from '@trpc/server' -import { generatePresignedUrl } from '@typebot.io/lib/s3/generatePresignedUrl' +import { generatePresignedPostPolicy } from '@typebot.io/lib/s3/generatePresignedPostPolicy' import prisma from '@/lib/prisma' import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden' import { isWriteTypebotForbidden } from '@/features/typebot/helpers/isWriteTypebotForbidden' @@ -54,6 +54,7 @@ export const generateUploadUrl = authenticatedProcedure .output( z.object({ presignedUrl: z.string(), + formData: z.record(z.string(), z.any()), fileUrl: z.string(), }) ) @@ -76,16 +77,17 @@ export const generateUploadUrl = authenticatedProcedure uploadProps: filePathProps, }) - const presignedUrl = await generatePresignedUrl({ + const presignedPostPolicy = await generatePresignedPostPolicy({ fileType, filePath, }) return { - presignedUrl, + presignedUrl: presignedPostPolicy.postURL, + formData: presignedPostPolicy.formData, fileUrl: env.S3_PUBLIC_CUSTOM_DOMAIN ? `${env.S3_PUBLIC_CUSTOM_DOMAIN}/${filePath}` - : presignedUrl.split('?')[0], + : `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`, } }) diff --git a/apps/builder/src/pages/api/storage/upload-url.ts b/apps/builder/src/pages/api/storage/upload-url.ts index a6cce8897c..49fbe854cc 100644 --- a/apps/builder/src/pages/api/storage/upload-url.ts +++ b/apps/builder/src/pages/api/storage/upload-url.ts @@ -5,7 +5,7 @@ import { methodNotAllowed, notAuthenticated, } from '@typebot.io/lib/api' -import { generatePresignedUrl } from '@typebot.io/lib/s3/generatePresignedUrl' +import { generatePresignedPostPolicy } from '@typebot.io/lib/s3/generatePresignedPostPolicy' import { env } from '@typebot.io/env' const handler = async ( @@ -25,9 +25,15 @@ const handler = async ( const filePath = req.query.filePath as string | undefined const fileType = req.query.fileType as string | undefined if (!filePath || !fileType) return badRequest(res) - const presignedUrl = await generatePresignedUrl({ fileType, filePath }) + const presignedPostPolicy = await generatePresignedPostPolicy({ + fileType, + filePath, + }) - return res.status(200).send({ presignedUrl }) + return res.status(200).send({ + presignedUrl: `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`, + formData: presignedPostPolicy.formData, + }) } return methodNotAllowed(res) } diff --git a/apps/docs/docs/editor/blocks/inputs/file-upload.mdx b/apps/docs/docs/editor/blocks/inputs/file-upload.mdx index 2f1df61067..3730255b56 100644 --- a/apps/docs/docs/editor/blocks/inputs/file-upload.mdx +++ b/apps/docs/docs/editor/blocks/inputs/file-upload.mdx @@ -29,4 +29,6 @@ The File upload input block allows you to collect files from your user. The placeholder accepts [HTML](https://en.wikipedia.org/wiki/HTML). -Note that there is a 10MB fixed limit per file. +## Size limit + +There is a 10MB fixed limit per uploaded file. If you want your respondents to upload larger files, you should ask them to upload their files to a cloud storage service (e.g. Google Drive, Dropbox, etc.) and share the link with you. diff --git a/apps/docs/docs/self-hosting/configuration.md b/apps/docs/docs/self-hosting/configuration.md index 4bfed6133f..819b2fcb0a 100644 --- a/apps/docs/docs/self-hosting/configuration.md +++ b/apps/docs/docs/self-hosting/configuration.md @@ -11,18 +11,19 @@ Parameters marked with are required. ## General -| Parameter | Default | Description | -| --------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| DATABASE_URL | | The database URL | -| ENCRYPTION_SECRET | | A 256-bit key used to encrypt sensitive data. It is strongly recommended to [generate](https://www.allkeysgenerator.com/Random/Security-Encryption-Key-Generator.aspx) a new one. The secret should be the same between builder and viewer. | -| NEXTAUTH_URL | | The builder base URL. Should be the publicly accessible URL (i.e. `https://typebot.domain.com`) | -| NEXT_PUBLIC_VIEWER_URL | | The viewer base URL. Should be the publicly accessible URL (i.e. `https://bot.domain.com`) | -| ADMIN_EMAIL | | The email that will get an `UNLIMITED` plan on user creation. The associated user will be able to bypass database rules. | -| NEXTAUTH_URL_INTERNAL | | The internal builder base URL. You have to set it only when `NEXTAUTH_URL` can't be reached by your builder container / server. For a docker deployment, you should set it to `http://localhost:3000`. | -| DEFAULT_WORKSPACE_PLAN | FREE | Default workspace plan on user creation or when a user creates a new workspace. Possible values are `FREE`, `STARTER`, `PRO`, `LIFETIME`, `UNLIMITED`. The default plan for admin user is `UNLIMITED` | -| DISABLE_SIGNUP | false | Disable new user sign ups. Invited users are still able to sign up. | -| NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID | | Typebot ID used for the onboarding. Onboarding page is skipped if not provided. | -| DEBUG | false | If enabled, the server will print valuable logs to debug config issues. | +| Parameter | Default | Description | +| ------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| DATABASE_URL | | The database URL | +| ENCRYPTION_SECRET | | A 256-bit key used to encrypt sensitive data. It is strongly recommended to [generate](https://www.allkeysgenerator.com/Random/Security-Encryption-Key-Generator.aspx) a new one. The secret should be the same between builder and viewer. | +| NEXTAUTH_URL | | The builder base URL. Should be the publicly accessible URL (i.e. `https://typebot.domain.com`) | +| NEXT_PUBLIC_VIEWER_URL | | The viewer base URL. Should be the publicly accessible URL (i.e. `https://bot.domain.com`) | +| ADMIN_EMAIL | | The email that will get an `UNLIMITED` plan on user creation. The associated user will be able to bypass database rules. | +| NEXTAUTH_URL_INTERNAL | | The internal builder base URL. You have to set it only when `NEXTAUTH_URL` can't be reached by your builder container / server. For a docker deployment, you should set it to `http://localhost:3000`. | +| DEFAULT_WORKSPACE_PLAN | FREE | Default workspace plan on user creation or when a user creates a new workspace. Possible values are `FREE`, `STARTER`, `PRO`, `LIFETIME`, `UNLIMITED`. The default plan for admin user is `UNLIMITED` | +| DISABLE_SIGNUP | false | Disable new user sign ups. Invited users are still able to sign up. | +| NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID | | Typebot ID used for the onboarding. Onboarding page is skipped if not provided. | +| DEBUG | false | If enabled, the server will print valuable logs to debug config issues. | +| NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE | | Limits the size of each file that can be uploaded in the bots (i.e. Set `10` to limit the file upload to 10MB) | ## Email (Auth, notifications) diff --git a/apps/viewer/src/features/blocks/inputs/fileUpload/api/deprecated/getUploadUrl.ts b/apps/viewer/src/features/blocks/inputs/fileUpload/api/deprecated/getUploadUrl.ts index 0599975157..b9b11582ca 100644 --- a/apps/viewer/src/features/blocks/inputs/fileUpload/api/deprecated/getUploadUrl.ts +++ b/apps/viewer/src/features/blocks/inputs/fileUpload/api/deprecated/getUploadUrl.ts @@ -10,7 +10,7 @@ import { } from '@typebot.io/schemas' import { byId, isDefined } from '@typebot.io/lib' import { z } from 'zod' -import { generatePresignedUrl } from '@typebot.io/lib/s3/generatePresignedUrl' +import { generatePresignedPostPolicy } from '@typebot.io/lib/s3/generatePresignedPostPolicy' import { env } from '@typebot.io/env' export const getUploadUrl = publicProcedure @@ -34,6 +34,7 @@ export const getUploadUrl = publicProcedure .output( z.object({ presignedUrl: z.string(), + formData: z.record(z.string(), z.any()), hasReachedStorageLimit: z.boolean(), }) ) @@ -61,13 +62,15 @@ export const getUploadUrl = publicProcedure message: 'File upload block not found', }) - const presignedUrl = await generatePresignedUrl({ + const presignedPostPolicy = await generatePresignedPostPolicy({ fileType, filePath, + maxFileSize: env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE, }) return { - presignedUrl, + presignedUrl: `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`, + formData: presignedPostPolicy.formData, hasReachedStorageLimit: false, } }) diff --git a/apps/viewer/src/features/blocks/inputs/fileUpload/api/generateUploadUrl.ts b/apps/viewer/src/features/blocks/inputs/fileUpload/api/generateUploadUrl.ts index 1c66a0bb61..175f679392 100644 --- a/apps/viewer/src/features/blocks/inputs/fileUpload/api/generateUploadUrl.ts +++ b/apps/viewer/src/features/blocks/inputs/fileUpload/api/generateUploadUrl.ts @@ -2,8 +2,9 @@ import { publicProcedure } from '@/helpers/server/trpc' import prisma from '@/lib/prisma' import { TRPCError } from '@trpc/server' import { z } from 'zod' -import { generatePresignedUrl } from '@typebot.io/lib/s3/generatePresignedUrl' +import { generatePresignedPostPolicy } from '@typebot.io/lib/s3/generatePresignedPostPolicy' import { env } from '@typebot.io/env' +import { InputBlockType, publicTypebotSchema } from '@typebot.io/schemas' export const generateUploadUrl = publicProcedure .meta({ @@ -28,6 +29,7 @@ export const generateUploadUrl = publicProcedure .output( z.object({ presignedUrl: z.string(), + formData: z.record(z.string(), z.any()), fileUrl: z.string(), }) ) @@ -44,6 +46,7 @@ export const generateUploadUrl = publicProcedure typebotId: filePathProps.typebotId, }, select: { + groups: true, typebot: { select: { workspaceId: true, @@ -62,15 +65,30 @@ export const generateUploadUrl = publicProcedure const filePath = `public/workspaces/${workspaceId}/typebots/${filePathProps.typebotId}/results/${filePathProps.resultId}/${filePathProps.fileName}` - const presignedUrl = await generatePresignedUrl({ + const fileUploadBlock = publicTypebotSchema._def.schema.shape.groups + .parse(publicTypebot.groups) + .flatMap((group) => group.blocks) + .find((block) => block.id === filePathProps.blockId) + + if (fileUploadBlock?.type !== InputBlockType.FILE) + throw new TRPCError({ + code: 'BAD_REQUEST', + message: "Can't find file upload block", + }) + + const presignedPostPolicy = await generatePresignedPostPolicy({ fileType, filePath, + maxFileSize: + fileUploadBlock.options.sizeLimit ?? + env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE, }) return { - presignedUrl, + presignedUrl: presignedPostPolicy.postURL, + formData: presignedPostPolicy.formData, fileUrl: env.S3_PUBLIC_CUSTOM_DOMAIN ? `${env.S3_PUBLIC_CUSTOM_DOMAIN}/${filePath}` - : presignedUrl.split('?')[0], + : `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`, } }) diff --git a/apps/viewer/src/features/blocks/inputs/fileUpload/fileUpload.spec.ts b/apps/viewer/src/features/blocks/inputs/fileUpload/fileUpload.spec.ts index c8db4ad584..0eb4d8a1bd 100644 --- a/apps/viewer/src/features/blocks/inputs/fileUpload/fileUpload.spec.ts +++ b/apps/viewer/src/features/blocks/inputs/fileUpload/fileUpload.spec.ts @@ -93,25 +93,4 @@ test.describe('Storage limit is reached', () => { fakeStorage: THREE_GIGABYTES, }) }) - - test("shouldn't upload anything if limit has been reached", async ({ - page, - }) => { - await page.goto(`/${typebotId}-public`) - await page - .locator(`input[type="file"]`) - .setInputFiles([ - getTestAsset('typebots/api.json'), - getTestAsset('typebots/fileUpload.json'), - getTestAsset('typebots/hugeGroup.json'), - ]) - await expect(page.locator(`text="3"`)).toBeVisible() - await page.locator('text="Upload 3 files"').click() - await expect(page.locator(`text="3 files uploaded"`)).toBeVisible() - await page.evaluate(() => - window.localStorage.setItem('workspaceId', 'starterWorkspace') - ) - await page.goto(`${env.NEXTAUTH_URL}/typebots/${typebotId}/results`) - await expect(page.locator('text="150%"')).toBeVisible() - }) }) diff --git a/apps/viewer/src/features/chat/queries/upsertAnswer.ts b/apps/viewer/src/features/chat/queries/upsertAnswer.ts index 76a5c8ba33..49a359a938 100644 --- a/apps/viewer/src/features/chat/queries/upsertAnswer.ts +++ b/apps/viewer/src/features/chat/queries/upsertAnswer.ts @@ -1,8 +1,6 @@ import prisma from '@/lib/prisma' -import { isNotDefined } from '@typebot.io/lib' import { Prisma } from '@typebot.io/prisma' -import { InputBlock, InputBlockType, SessionState } from '@typebot.io/schemas' -import got from 'got' +import { InputBlock, SessionState } from '@typebot.io/schemas' type Props = { answer: Omit @@ -11,12 +9,9 @@ type Props = { itemId?: string state: SessionState } -export const upsertAnswer = async ({ answer, reply, block, state }: Props) => { +export const upsertAnswer = async ({ answer, block, state }: Props) => { const resultId = state.typebotsQueue[0].resultId if (!resultId) return - if (reply.includes('http') && block.type === InputBlockType.FILE) { - answer.storageUsed = await computeStorageUsed(reply) - } const where = { resultId, blockId: block.id, @@ -33,7 +28,6 @@ export const upsertAnswer = async ({ answer, reply, block, state }: Props) => { where, data: { content: answer.content, - storageUsed: answer.storageUsed, itemId: answer.itemId, }, }) @@ -41,18 +35,3 @@ export const upsertAnswer = async ({ answer, reply, block, state }: Props) => { data: [{ ...answer, resultId }], }) } - -const computeStorageUsed = async (reply: string) => { - let storageUsed = 0 - const fileUrls = reply.split(', ') - const hasReachedStorageLimit = fileUrls[0] === null - if (!hasReachedStorageLimit) { - for (const url of fileUrls) { - const { headers } = await got(url) - const size = headers['content-length'] - if (isNotDefined(size)) continue - storageUsed += parseInt(size, 10) - } - } - return storageUsed -} diff --git a/packages/deprecated/bot-engine/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts b/packages/deprecated/bot-engine/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts index f793aa6d9e..682f5cff35 100644 --- a/packages/deprecated/bot-engine/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts +++ b/packages/deprecated/bot-engine/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts @@ -23,6 +23,7 @@ export const uploadFiles = async ({ i += 1 const { data } = await sendRequest<{ presignedUrl: string + formData: Record hasReachedStorageLimit: boolean }>( `${basePath}/storage/upload-url?filePath=${encodeURIComponent( @@ -35,9 +36,14 @@ export const uploadFiles = async ({ const url = data.presignedUrl if (data.hasReachedStorageLimit) urls.push(null) else { - const upload = await fetch(url, { - method: 'PUT', - body: file, + const formData = new FormData() + Object.entries(data.formData).forEach(([key, value]) => { + formData.append(key, value) + }) + formData.append('file', file) + const upload = await fetch(data.presignedUrl, { + method: 'POST', + body: formData, }) if (!upload.ok) continue diff --git a/packages/embeds/js/src/features/blocks/inputs/fileUpload/components/FileUploadForm.tsx b/packages/embeds/js/src/features/blocks/inputs/fileUpload/components/FileUploadForm.tsx index 455848d760..3bcd29e056 100644 --- a/packages/embeds/js/src/features/blocks/inputs/fileUpload/components/FileUploadForm.tsx +++ b/packages/embeds/js/src/features/blocks/inputs/fileUpload/components/FileUploadForm.tsx @@ -7,6 +7,7 @@ import { Button } from '@/components/Button' import { Spinner } from '@/components/Spinner' import { uploadFiles } from '../helpers/uploadFiles' import { guessApiHost } from '@/utils/guessApiHost' +import { getRuntimeVariable } from '@typebot.io/env/getRuntimeVariable' type Props = { context: BotContext @@ -25,15 +26,14 @@ export const FileUploadForm = (props: Props) => { const onNewFiles = (files: FileList) => { setErrorMessage(undefined) const newFiles = Array.from(files) + const sizeLimit = + props.block.options.sizeLimit ?? + getRuntimeVariable('NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE') if ( - newFiles.some( - (file) => - file.size > (props.block.options.sizeLimit ?? 10) * 1024 * 1024 - ) + sizeLimit && + newFiles.some((file) => file.size > sizeLimit * 1024 * 1024) ) - return setErrorMessage( - `A file is larger than ${props.block.options.sizeLimit ?? 10}MB` - ) + return setErrorMessage(`A file is larger than ${sizeLimit}MB`) if (!props.block.options.isMultipleAllowed && files) return startSingleFileUpload(newFiles[0]) setSelectedFiles([...selectedFiles(), ...newFiles]) diff --git a/packages/embeds/js/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts b/packages/embeds/js/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts index c0e5af889a..2792739c64 100644 --- a/packages/embeds/js/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts +++ b/packages/embeds/js/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts @@ -28,6 +28,7 @@ export const uploadFiles = async ({ i += 1 const { data } = await sendRequest<{ presignedUrl: string + formData: Record fileUrl: string }>({ method: 'POST', @@ -40,9 +41,14 @@ export const uploadFiles = async ({ if (!data?.presignedUrl) continue else { + const formData = new FormData() + Object.entries(data.formData).forEach(([key, value]) => { + formData.append(key, value) + }) + formData.append('file', file) const upload = await fetch(data.presignedUrl, { - method: 'PUT', - body: file, + method: 'POST', + body: formData, }) if (!upload.ok) continue diff --git a/packages/env/env.ts b/packages/env/env.ts index 6f6abfdc96..072db6d20c 100644 --- a/packages/env/env.ts +++ b/packages/env/env.ts @@ -35,6 +35,7 @@ const baseEnv = { .transform((string) => string.split(',')), NEXT_PUBLIC_VIEWER_INTERNAL_URL: z.string().url().optional(), NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID: z.string().min(1).optional(), + NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE: z.coerce.number().optional(), }, runtimeEnv: { NEXT_PUBLIC_E2E_TEST: getRuntimeVariable('NEXT_PUBLIC_E2E_TEST'), @@ -45,6 +46,9 @@ const baseEnv = { NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID: getRuntimeVariable( 'NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID' ), + NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE: getRuntimeVariable( + 'NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE' + ), }, } const githubEnv = { diff --git a/packages/lib/s3/generatePresignedPostPolicy.ts b/packages/lib/s3/generatePresignedPostPolicy.ts new file mode 100644 index 0000000000..e514fefc34 --- /dev/null +++ b/packages/lib/s3/generatePresignedPostPolicy.ts @@ -0,0 +1,40 @@ +import { env } from '@typebot.io/env' +import { Client, PostPolicyResult } from 'minio' + +type Props = { + filePath: string + fileType?: string + maxFileSize?: number +} + +const tenMinutes = 10 * 60 + +export const generatePresignedPostPolicy = async ({ + filePath, + fileType, + maxFileSize, +}: Props): Promise => { + if (!env.S3_ENDPOINT || !env.S3_ACCESS_KEY || !env.S3_SECRET_KEY) + throw new Error( + 'S3 not properly configured. Missing one of those variables: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY' + ) + + const minioClient = new Client({ + endPoint: env.S3_ENDPOINT, + port: env.S3_PORT, + useSSL: env.S3_SSL, + accessKey: env.S3_ACCESS_KEY, + secretKey: env.S3_SECRET_KEY, + region: env.S3_REGION, + }) + + const postPolicy = minioClient.newPostPolicy() + if (maxFileSize) + postPolicy.setContentLengthRange(0, maxFileSize * 1024 * 1024) + postPolicy.setKey(filePath) + postPolicy.setBucket(env.S3_BUCKET) + postPolicy.setExpires(new Date(Date.now() + tenMinutes)) + if (fileType) postPolicy.setContentType(fileType) + + return minioClient.presignedPostPolicy(postPolicy) +} diff --git a/packages/lib/s3/generatePresignedUrl.ts b/packages/lib/s3/generatePresignedUrl.ts deleted file mode 100644 index 2003cf8057..0000000000 --- a/packages/lib/s3/generatePresignedUrl.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { env } from '@typebot.io/env' -import { Client } from 'minio' - -type GeneratePresignedUrlProps = { - filePath: string - fileType?: string -} - -const tenMinutes = 10 * 60 - -export const generatePresignedUrl = async ({ - filePath, - fileType, -}: GeneratePresignedUrlProps): Promise => { - if (!env.S3_ENDPOINT || !env.S3_ACCESS_KEY || !env.S3_SECRET_KEY) - throw new Error( - 'S3 not properly configured. Missing one of those variables: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY' - ) - - const minioClient = new Client({ - endPoint: env.S3_ENDPOINT, - port: env.S3_PORT, - useSSL: env.S3_SSL, - accessKey: env.S3_ACCESS_KEY, - secretKey: env.S3_SECRET_KEY, - region: env.S3_REGION, - }) - - return minioClient.presignedUrl('PUT', env.S3_BUCKET, filePath, tenMinutes, { - 'Content-Type': fileType, - }) -} diff --git a/packages/schemas/features/blocks/inputs/file.ts b/packages/schemas/features/blocks/inputs/file.ts index 46a455834d..5fd624670b 100644 --- a/packages/schemas/features/blocks/inputs/file.ts +++ b/packages/schemas/features/blocks/inputs/file.ts @@ -12,7 +12,7 @@ export const fileInputOptionsSchema = optionBaseSchema.merge( clear: z.string().optional(), skip: z.string().optional(), }), - sizeLimit: z.number().optional(), + sizeLimit: z.number().optional().describe('Deprecated'), }) )