From b4536abc2f72c2d770d6d8c67566bd4188a6c665 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Tue, 21 Mar 2023 09:11:36 +0100 Subject: [PATCH] :bug: (js) Fix upload file in linked typebot --- apps/viewer/next.config.js | 10 ++ .../inputs/fileUpload/api/getUploadUrl.ts} | 126 +++++++++++++----- apps/viewer/src/features/chat/api/router.ts | 6 - .../src/helpers/server/routers/v1/_app.ts | 6 +- 4 files changed, 104 insertions(+), 44 deletions(-) rename apps/viewer/src/{pages/api/typebots/[typebotId]/blocks/[blockId]/storage/upload-url.ts => features/blocks/inputs/fileUpload/api/getUploadUrl.ts} (59%) delete mode 100644 apps/viewer/src/features/chat/api/router.ts diff --git a/apps/viewer/next.config.js b/apps/viewer/next.config.js index 72eec35ab..9eb75d8d3 100644 --- a/apps/viewer/next.config.js +++ b/apps/viewer/next.config.js @@ -13,6 +13,16 @@ const nextConfig = { experimental: { outputFileTracingRoot: path.join(__dirname, '../../'), }, + async redirects() { + return [ + { + source: '/api/typebots/:typebotId/blocks/:blockId/storage/upload-url', + destination: + '/api/v1/typebots/:typebotId/blocks/:blockId/storage/upload-url', + permanent: true, + }, + ] + }, } const sentryWebpackPluginOptions = { diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/storage/upload-url.ts b/apps/viewer/src/features/blocks/inputs/fileUpload/api/getUploadUrl.ts similarity index 59% rename from apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/storage/upload-url.ts rename to apps/viewer/src/features/blocks/inputs/fileUpload/api/getUploadUrl.ts index 533fd8315..b3ca80625 100644 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/storage/upload-url.ts +++ b/apps/viewer/src/features/blocks/inputs/fileUpload/api/getUploadUrl.ts @@ -1,13 +1,17 @@ +import { publicProcedure } from '@/helpers/server/trpc' import prisma from '@/lib/prisma' -import { InputBlockType, PublicTypebot } from '@typebot.io/schemas' -import { NextApiRequest, NextApiResponse } from 'next' -import { - badRequest, - generatePresignedUrl, - methodNotAllowed, -} from '@typebot.io/lib/api' -import { byId, env, isDefined } from '@typebot.io/lib' +import { TRPCError } from '@trpc/server' import { getStorageLimit } from '@typebot.io/lib/pricing' +import { + FileInputBlock, + InputBlockType, + LogicBlockType, + PublicTypebot, + TypebotLinkBlock, +} from '@typebot.io/schemas' +import { byId, env, isDefined } from '@typebot.io/lib' +import { z } from 'zod' +import { generatePresignedUrl } from '@typebot.io/lib/api/storage' import { sendAlmostReachedStorageLimitEmail, sendReachedStorageLimitEmail, @@ -16,35 +20,61 @@ import { WorkspaceRole } from '@typebot.io/prisma' const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8 -const handler = async ( - req: NextApiRequest, - res: NextApiResponse -): Promise => { - res.setHeader('Access-Control-Allow-Origin', '*') - if (req.method === 'GET') { +export const getUploadUrl = publicProcedure + .meta({ + openapi: { + method: 'GET', + path: '/typebots/{typebotId}/blocks/{blockId}/storage/upload-url', + summary: 'Get upload URL for a file', + description: 'Used for the web client to get the bucket upload file.', + }, + }) + .input( + z.object({ + typebotId: z.string(), + blockId: z.string(), + filePath: z.string(), + fileType: z.string(), + }) + ) + .output( + z.object({ + presignedUrl: z.object({ + url: z.string(), + fields: z.any(), + }), + hasReachedStorageLimit: z.boolean(), + }) + ) + .query(async ({ input: { typebotId, blockId, filePath, fileType } }) => { if ( !process.env.S3_ENDPOINT || !process.env.S3_ACCESS_KEY || !process.env.S3_SECRET_KEY ) - return badRequest( - res, - 'S3 not properly configured. Missing one of those variables: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY' - ) - const filePath = req.query.filePath as string | undefined - const fileType = req.query.fileType as string | undefined - const typebotId = req.query.typebotId as string - const blockId = req.query.blockId as string - if (!filePath) return badRequest(res, 'Missing filePath or fileType') + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: + 'S3 not properly configured. Missing one of those variables: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY', + }) + const hasReachedStorageLimit = await checkIfStorageLimitReached(typebotId) - const typebot = (await prisma.publicTypebot.findFirst({ + const publicTypebot = (await prisma.publicTypebot.findFirst({ where: { typebotId }, - })) as unknown as PublicTypebot - const fileUploadBlock = typebot.groups - .flatMap((g) => g.blocks) - .find(byId(blockId)) - if (fileUploadBlock?.type !== InputBlockType.FILE) - return badRequest(res, 'Not a file upload block') + select: { + groups: true, + typebotId: true, + }, + })) as Pick + + const fileUploadBlock = await getFileUploadBlock(publicTypebot, blockId) + + if (!fileUploadBlock) + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'File upload block not found', + }) + const sizeLimit = fileUploadBlock.options.sizeLimit ? Math.min(fileUploadBlock.options.sizeLimit, 500) : 10 @@ -55,12 +85,38 @@ const handler = async ( sizeLimit: sizeLimit * 1024 * 1024, }) - return res.status(200).send({ + return { presignedUrl, hasReachedStorageLimit, - }) - } - return methodNotAllowed(res) + } + }) + +const getFileUploadBlock = async ( + publicTypebot: Pick, + blockId: string +): Promise => { + const fileUploadBlock = publicTypebot.groups + .flatMap((group) => group.blocks) + .find(byId(blockId)) + if (fileUploadBlock?.type === InputBlockType.FILE) return fileUploadBlock + const linkedTypebotIds = publicTypebot.groups + .flatMap((group) => group.blocks) + .filter((block) => block.type === LogicBlockType.TYPEBOT_LINK) + .flatMap((block) => (block as TypebotLinkBlock).options.typebotId) + .filter(isDefined) + const linkedTypebots = (await prisma.publicTypebot.findMany({ + where: { typebotId: { in: linkedTypebotIds } }, + select: { + groups: true, + }, + })) as Pick[] + const fileUploadBlockFromLinkedTypebots = linkedTypebots + .flatMap((typebot) => typebot.groups) + .flatMap((group) => group.blocks) + .find(byId(blockId)) + if (fileUploadBlockFromLinkedTypebots?.type === InputBlockType.FILE) + return fileUploadBlockFromLinkedTypebots + return null } const checkIfStorageLimitReached = async ( @@ -170,5 +226,3 @@ const sendReachStorageLimitNotification = async ({ data: { storageLimitSecondEmailSentAt: new Date() }, }) } - -export default handler diff --git a/apps/viewer/src/features/chat/api/router.ts b/apps/viewer/src/features/chat/api/router.ts deleted file mode 100644 index 787e40e10..000000000 --- a/apps/viewer/src/features/chat/api/router.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { router } from '@/helpers/server/trpc' -import { sendMessage } from './sendMessage' - -export const chatRouter = router({ - sendMessage, -}) diff --git a/apps/viewer/src/helpers/server/routers/v1/_app.ts b/apps/viewer/src/helpers/server/routers/v1/_app.ts index 2a684d9b6..b2d73a7fe 100644 --- a/apps/viewer/src/helpers/server/routers/v1/_app.ts +++ b/apps/viewer/src/helpers/server/routers/v1/_app.ts @@ -1,8 +1,10 @@ -import { chatRouter } from '@/features/chat/api/router' +import { getUploadUrl } from '@/features/blocks/inputs/fileUpload/api/getUploadUrl' +import { sendMessage } from '@/features/chat/api/sendMessage' import { router } from '../../trpc' export const appRouter = router({ - chat: chatRouter, + sendMessage, + getUploadUrl, }) export type AppRouter = typeof appRouter