🛂 (fileUpload) Improve file upload size limit enforcement
Closes #799, closes #797
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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 (
|
||||
<Stack spacing={6}>
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
<HStack>
|
||||
<NumberInput
|
||||
label={'Size limit:'}
|
||||
defaultValue={options.sizeLimit ?? 10}
|
||||
onValueChange={handleSizeLimitChange}
|
||||
withVariableButton={false}
|
||||
/>
|
||||
<Text>MB</Text>
|
||||
</HStack>
|
||||
|
||||
<Stack>
|
||||
<FormLabel mb="0">Placeholder:</FormLabel>
|
||||
<CodeEditor
|
||||
|
||||
@@ -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
|
||||
@@ -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 && (
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}`,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user