Skip to content

Commit

Permalink
⚡ (fileUpload) New visibility option: "Public", "Private" or "Auto" (#…
Browse files Browse the repository at this point in the history
…1196)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced file visibility options for uploaded files, allowing users
to set files as public or private.
- Added a new API endpoint for retrieving temporary URLs for files,
enhancing file accessibility.
- Expanded file upload documentation to include information on file
visibility settings.
- Updated URL validation to support URLs with port numbers and
"http://localhost".
- **Enhancements**
- Improved media download functionality by replacing the `got` library
with a custom `downloadMedia` function.
- Enhanced bot flow continuation and session start logic to support a
wider range of reply types, including WhatsApp media messages.
- **Bug Fixes**
- Adjusted file path and URL construction in the `generateUploadUrl`
function to correctly reflect file visibility settings.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
baptisteArno authored Jan 30, 2024
1 parent 515fcaf commit 6215cfb
Show file tree
Hide file tree
Showing 17 changed files with 305 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import React from 'react'
import { TextInput } from '@/components/inputs'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { defaultFileInputOptions } from '@typebot.io/schemas/features/blocks/inputs/file/constants'
import {
defaultFileInputOptions,
fileVisibilityOptions,
} from '@typebot.io/schemas/features/blocks/inputs/file/constants'
import { useTranslate } from '@tolgee/react'
import { DropdownList } from '@/components/DropdownList'

type Props = {
options: FileInputBlock['options']
Expand Down Expand Up @@ -37,6 +41,10 @@ export const FileInputSettings = ({ options, onOptionsChange }: Props) => {
const updateSkipButtonLabel = (skip: string) =>
onOptionsChange({ ...options, labels: { ...options?.labels, skip } })

const updateVisibility = (
visibility: (typeof fileVisibilityOptions)[number]
) => onOptionsChange({ ...options, visibility })

return (
<Stack spacing={4}>
<SwitchWithLabel
Expand Down Expand Up @@ -91,6 +99,13 @@ export const FileInputSettings = ({ options, onOptionsChange }: Props) => {
onChange={updateSkipButtonLabel}
withVariableButton={false}
/>
<DropdownList
label="Visibility:"
moreInfoTooltip='This setting determines who can see the uploaded files. "Public" means that anyone who has the link can see the files. "Private" means that only a members of this workspace can see the files.'
currentItem={options?.visibility}
onItemSelect={updateVisibility}
items={fileVisibilityOptions}
/>
<Stack>
<FormLabel mb="0" htmlFor="variable">
{options?.isMultipleAllowed
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import prisma from '@typebot.io/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
import {
badRequest,
methodNotAllowed,
notAuthenticated,
notFound,
} from '@typebot.io/lib/api'
import { getFileTempUrl } from '@typebot.io/lib/s3/getFileTempUrl'
import { isReadTypebotForbidden } from '@/features/typebot/helpers/isReadTypebotForbidden'

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {
const user = await getAuthenticatedUser(req, res)
if (!user) return notAuthenticated(res)

const typebotId = req.query.typebotId as string
const resultId = req.query.resultId as string
const fileName = req.query.fileName as string

if (!fileName) return badRequest(res, 'fileName missing not found')

const typebot = await prisma.typebot.findFirst({
where: {
id: typebotId,
},
select: {
whatsAppCredentialsId: true,
collaborators: {
select: {
userId: true,
},
},
workspace: {
select: {
id: true,
isSuspended: true,
isPastDue: true,
members: {
select: {
userId: true,
},
},
},
},
},
})

if (!typebot?.workspace || (await isReadTypebotForbidden(typebot, user)))
return notFound(res, 'Workspace not found')

if (!typebot) return notFound(res, 'Typebot not found')

const tmpUrl = await getFileTempUrl({
key: `private/workspaces/${typebot.workspace.id}/typebots/${typebotId}/results/${resultId}/${fileName}`,
})

if (!tmpUrl) return notFound(res, 'File not found')

return res.redirect(tmpUrl)
}
return methodNotAllowed(res)
}

export default handler
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ import {
} from '@typebot.io/lib/api'
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
import { WhatsAppCredentials } from '@typebot.io/schemas/features/whatsapp'
import got from 'got'
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
import { env } from '@typebot.io/env'
import { downloadMedia } from '@typebot.io/bot-engine/whatsapp/downloadMedia'

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {
Expand Down Expand Up @@ -61,25 +60,15 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
credentials.iv
)) as WhatsAppCredentials['data']

const { body } = await got.get({
url: `${env.WHATSAPP_CLOUD_API_URL}/v17.0/${mediaId}`,
headers: {
Authorization: `Bearer ${credentialsData.systemUserAccessToken}`,
},
const { file, mimeType } = await downloadMedia({
mediaId,
systemUserAccessToken: credentialsData.systemUserAccessToken,
})

const parsedBody = JSON.parse(body) as { url: string; mime_type: string }

const buffer = await got(parsedBody.url, {
headers: {
Authorization: `Bearer ${credentialsData.systemUserAccessToken}`,
},
}).buffer()

res.setHeader('Content-Type', parsedBody.mime_type)
res.setHeader('Content-Type', mimeType)
res.setHeader('Cache-Control', 'public, max-age=86400')

return res.send(buffer)
return res.send(file)
}
return methodNotAllowed(res)
}
Expand Down
8 changes: 8 additions & 0 deletions apps/docs/editor/blocks/inputs/file-upload.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,11 @@ The placeholder accepts [HTML](https://en.wikipedia.org/wiki/HTML).
## 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.

## Visibility

This option allows you to choose between generating public URLs for the uploaded files or keeping them private. If you choose to keep the files private, you will be able to see the file only if you are logged in to your Typebot account.

Note that if you choose to keep the files private, you will not be able to use the file URL with other blocks like Attachment in the Send email block or others. These services won't be able to read the files.

By default, this option is set to `Auto`. This means that the files will be public if uploaded from the web runtime but private if uploaded from the WhatsApp runtime.
16 changes: 16 additions & 0 deletions apps/docs/openapi/builder.json
Original file line number Diff line number Diff line change
Expand Up @@ -24342,6 +24342,14 @@
},
"sizeLimit": {
"type": "number"
},
"visibility": {
"type": "string",
"enum": [
"Auto",
"Public",
"Private"
]
}
}
}
Expand Down Expand Up @@ -27033,6 +27041,14 @@
"type": "string"
}
}
},
"visibility": {
"type": "string",
"enum": [
"Auto",
"Public",
"Private"
]
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions apps/docs/openapi/viewer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7418,6 +7418,14 @@
},
"sizeLimit": {
"type": "number"
},
"visibility": {
"type": "string",
"enum": [
"Auto",
"Public",
"Private"
]
}
}
}
Expand Down Expand Up @@ -10534,6 +10542,14 @@
"type": "string"
}
}
},
"visibility": {
"type": "string",
"enum": [
"Auto",
"Public",
"Private"
]
}
}
}
Expand Down
21 changes: 14 additions & 7 deletions apps/viewer/src/features/fileUpload/api/generateUploadUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,6 @@ export const generateUploadUrl = publicProcedure
message: "Can't find workspaceId",
})

const resultId = session.state.typebotsQueue[0].resultId

const filePath = `public/workspaces/${workspaceId}/typebots/${typebotId}/results/${resultId}/${filePathProps.fileName}`

if (session.state.currentBlockId === undefined)
throw new TRPCError({
code: 'BAD_REQUEST',
Expand All @@ -163,6 +159,14 @@ export const generateUploadUrl = publicProcedure
message: "Can't find file upload block",
})

const resultId = session.state.typebotsQueue[0].resultId

const filePath = `${
fileUploadBlock.options?.visibility === 'Private' ? 'private' : 'public'
}/workspaces/${workspaceId}/typebots/${typebotId}/results/${resultId}/${
filePathProps.fileName
}`

const presignedPostPolicy = await generatePresignedPostPolicy({
fileType,
filePath,
Expand All @@ -175,8 +179,11 @@ export const generateUploadUrl = publicProcedure
return {
presignedUrl: presignedPostPolicy.postURL,
formData: presignedPostPolicy.formData,
fileUrl: env.S3_PUBLIC_CUSTOM_DOMAIN
? `${env.S3_PUBLIC_CUSTOM_DOMAIN}/${filePath}`
: `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`,
fileUrl:
fileUploadBlock.options?.visibility === 'Private'
? `${env.NEXTAUTH_URL}/api/typebots/${typebotId}/results/${resultId}/${filePathProps.fileName}`
: env.S3_PUBLIC_CUSTOM_DOMAIN
? `${env.S3_PUBLIC_CUSTOM_DOMAIN}/${filePath}`
: `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`,
}
})
5 changes: 3 additions & 2 deletions packages/bot-engine/blocks/inputs/url/validateUrl.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const urlRegex =
/^(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})$/
/^(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]:[0-9]*\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]:[0-9]*\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})$/

export const validateUrl = (url: string) => urlRegex.test(url)
export const validateUrl = (url: string) =>
url.startsWith('http://localhost') || urlRegex.test(url)
Loading

0 comments on commit 6215cfb

Please sign in to comment.