Skip to content

Commit

Permalink
🛂 (fileUpload) Improve file upload size limit enforcement
Browse files Browse the repository at this point in the history
Closes #799, closes #797
  • Loading branch information
baptisteArno committed Sep 19, 2023
1 parent f626c98 commit bb13c2b
Show file tree
Hide file tree
Showing 19 changed files with 143 additions and 239 deletions.
10 changes: 8 additions & 2 deletions apps/builder/src/components/ImageUploadContent/UploadButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
69 changes: 2 additions & 67 deletions apps/builder/src/features/billing/components/UsageProgressBars.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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 (
<Stack spacing={6}>
Expand Down Expand Up @@ -103,63 +95,6 @@ export const UsageProgressBars = ({ workspace }: Props) => {
colorScheme={totalChatsUsed >= workspaceChatsLimit ? 'red' : 'blue'}
/>
</Stack>
{workspace.plan !== Plan.FREE && (
<Stack spacing={3}>
<Flex justifyContent="space-between">
<HStack>
<Heading fontSize="xl" as="h3">
{scopedT('storage.heading')}
</Heading>
{storagePercentage >= 80 && (
<Tooltip
placement="top"
rounded="md"
p="3"
label={
<Text>
{scopedT('storage.alert.soonReach')}
<br />
<br />
{scopedT('storage.alert.updatePlan')}
</Text>
}
>
<span>
<AlertIcon color="orange.500" />
</span>
</Tooltip>
)}
</HStack>
<HStack>
<Skeleton
fontWeight="bold"
isLoaded={!isLoading}
h={isLoading ? '5px' : 'auto'}
>
{storageToReadable(totalStorageUsed)}
</Skeleton>
<Text>
/{' '}
{workspaceStorageLimit === -1
? scopedT('unlimited')
: `${workspaceStorageLimit} GB`}
</Text>
</HStack>
</Flex>
<Progress
value={storagePercentage}
h="5px"
colorScheme={
totalStorageUsed >= workspaceStorageLimitGigabites
? 'red'
: 'blue'
}
rounded="full"
hasStripe
isIndeterminate={isLoading}
/>
</Stack>
)}
</Stack>
)
}
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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 })

Expand All @@ -48,16 +45,6 @@ export const FileInputSettings = ({ options, onOptionsChange }: Props) => {
initialValue={options.isMultipleAllowed}
onCheckChange={handleMultipleFilesChange}
/>
<HStack>
<NumberInput
label={'Size limit:'}
defaultValue={options.sizeLimit ?? 10}
onValueChange={handleSizeLimitChange}
withVariableButton={false}
/>
<Text>MB</Text>
</HStack>

<Stack>
<FormLabel mb="0">Placeholder:</FormLabel>
<CodeEditor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import { trpc } from '@/lib/trpc'
import { Flex } from '@chakra-ui/react'
import { Workspace } from '@typebot.io/schemas'
import { useMemo } from 'react'
import { getChatsLimit, getStorageLimit } from '@typebot.io/lib/pricing'
import { getChatsLimit } from '@typebot.io/lib/pricing'

const ALERT_CHATS_PERCENT_THRESHOLD = 80
const ALERT_STORAGE_PERCENT_THRESHOLD = 80

type Props = {
workspace: Workspace
Expand Down Expand Up @@ -35,27 +34,6 @@ export const UsageAlertBanners = ({ workspace }: Props) => {
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 && (
Expand All @@ -74,22 +52,6 @@ export const UsageAlertBanners = ({ workspace }: Props) => {
/>
</Flex>
)}
{storageLimitPercentage > ALERT_STORAGE_PERCENT_THRESHOLD && (
<Flex p="4">
<UnlockPlanAlertInfo
status="warning"
contentLabel={
<>
Your workspace collected{' '}
<strong>{storageLimitPercentage}%</strong> 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"
/>
</Flex>
)}
</>
)
}
10 changes: 6 additions & 4 deletions apps/builder/src/features/upload/api/generateUploadUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -54,6 +54,7 @@ export const generateUploadUrl = authenticatedProcedure
.output(
z.object({
presignedUrl: z.string(),
formData: z.record(z.string(), z.any()),
fileUrl: z.string(),
})
)
Expand All @@ -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}`,
}
})

Expand Down
12 changes: 9 additions & 3 deletions apps/builder/src/pages/api/storage/upload-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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)
}
Expand Down
4 changes: 3 additions & 1 deletion apps/docs/docs/editor/blocks/inputs/file-upload.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
25 changes: 13 additions & 12 deletions apps/docs/docs/self-hosting/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,19 @@ Parameters marked with <Asterix/> are required.

## General

| Parameter | Default | Description |
| --------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| DATABASE_URL <Asterix/> | | The database URL |
| ENCRYPTION_SECRET <Asterix/> | | 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 <Asterix/> | | The builder base URL. Should be the publicly accessible URL (i.e. `https://typebot.domain.com`) |
| NEXT_PUBLIC_VIEWER_URL <Asterix/> | | 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 <Asterix/> | | The database URL |
| ENCRYPTION_SECRET <Asterix/> | | 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 <Asterix/> | | The builder base URL. Should be the publicly accessible URL (i.e. `https://typebot.domain.com`) |
| NEXT_PUBLIC_VIEWER_URL <Asterix/> | | 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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -34,6 +34,7 @@ export const getUploadUrl = publicProcedure
.output(
z.object({
presignedUrl: z.string(),
formData: z.record(z.string(), z.any()),
hasReachedStorageLimit: z.boolean(),
})
)
Expand Down Expand Up @@ -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,
}
})
Expand Down
Loading

0 comments on commit bb13c2b

Please sign in to comment.