|
|
|
@ -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<void> => {
|
|
|
|
|
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<PublicTypebot, 'groups' | 'typebotId'>
|
|
|
|
|
|
|
|
|
|
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<PublicTypebot, 'groups' | 'typebotId'>,
|
|
|
|
|
blockId: string
|
|
|
|
|
): Promise<FileInputBlock | null> => {
|
|
|
|
|
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<PublicTypebot, 'groups'>[]
|
|
|
|
|
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
|