2
0

feat(editor): 🔒️ Add verification on backend for file input deployment

This commit is contained in:
Baptiste Arnaud
2022-06-13 08:21:48 +02:00
parent 910b871556
commit 14afd2249e
7 changed files with 112 additions and 17 deletions

View File

@@ -34,6 +34,7 @@ export const PublishButton = () => {
publishedTypebot, publishedTypebot,
restorePublishedTypebot, restorePublishedTypebot,
typebot, typebot,
isSavingLoading,
} = useTypebot() } = useTypebot()
const hasInputFile = typebot?.groups const hasInputFile = typebot?.groups
@@ -73,7 +74,7 @@ export const PublishButton = () => {
> >
<Button <Button
colorScheme="blue" colorScheme="blue"
isLoading={isPublishing} isLoading={isPublishing || isSavingLoading}
isDisabled={isPublished} isDisabled={isPublished}
onClick={handlePublishClick} onClick={handlePublishClick}
borderRightRadius={publishedTypebot && !isPublished ? 0 : undefined} borderRightRadius={publishedTypebot && !isPublished ? 0 : undefined}

View File

@@ -187,10 +187,12 @@ export const TypebotContext = ({
} }
const savePublishedTypebot = async (newPublishedTypebot: PublicTypebot) => { const savePublishedTypebot = async (newPublishedTypebot: PublicTypebot) => {
if (!localTypebot) return
setIsPublishing(true) setIsPublishing(true)
const { error } = await updatePublishedTypebot( const { error } = await updatePublishedTypebot(
newPublishedTypebot.id, newPublishedTypebot.id,
newPublishedTypebot newPublishedTypebot,
localTypebot.workspaceId
) )
setIsPublishing(false) setIsPublishing(false)
if (error) if (error)
@@ -303,10 +305,13 @@ export const TypebotContext = ({
}) })
} else { } else {
setIsPublishing(true) setIsPublishing(true)
const { data, error } = await createPublishedTypebot({ const { data, error } = await createPublishedTypebot(
...parseTypebotToPublicTypebot(newLocalTypebot), {
id: publishedTypebotId, ...parseTypebotToPublicTypebot(newLocalTypebot),
}) id: publishedTypebotId,
},
localTypebot.workspaceId
)
setIsPublishing(false) setIsPublishing(false)
if (error) if (error)
return showToast({ title: error.name, description: error.message }) return showToast({ title: error.name, description: error.message })

View File

@@ -1,16 +1,29 @@
import { withSentry } from '@sentry/nextjs' import { withSentry } from '@sentry/nextjs'
import prisma from 'libs/prisma' import prisma from 'libs/prisma'
import { InputBlockType, PublicTypebot } from 'models'
import { NextApiRequest, NextApiResponse } from 'next' import { NextApiRequest, NextApiResponse } from 'next'
import { canPublishFileInput } from 'services/api/dbRules'
import { getAuthenticatedUser } from 'services/api/utils' import { getAuthenticatedUser } from 'services/api/utils'
import { methodNotAllowed, notAuthenticated } from 'utils' import { badRequest, methodNotAllowed, notAuthenticated } from 'utils'
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req) const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res) if (!user) return notAuthenticated(res)
try { try {
if (req.method === 'POST') { if (req.method === 'POST') {
const data = const workspaceId = req.query.workspaceId as string | undefined
if (!workspaceId) return badRequest(res, 'workspaceId is required')
const data = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as PublicTypebot
const typebotContainsFileInput = data.groups
.flatMap((g) => g.blocks)
.some((b) => b.type === InputBlockType.FILE)
if (
typebotContainsFileInput &&
!(await canPublishFileInput({ userId: user.id, workspaceId, res }))
)
return
const typebot = await prisma.publicTypebot.create({ const typebot = await prisma.publicTypebot.create({
data: { ...data }, data: { ...data },
}) })

View File

@@ -1,16 +1,31 @@
import { withSentry } from '@sentry/nextjs' import { withSentry } from '@sentry/nextjs'
import prisma from 'libs/prisma' import prisma from 'libs/prisma'
import { InputBlockType, PublicTypebot } from 'models'
import { NextApiRequest, NextApiResponse } from 'next' import { NextApiRequest, NextApiResponse } from 'next'
import { canPublishFileInput } from 'services/api/dbRules'
import { getAuthenticatedUser } from 'services/api/utils' import { getAuthenticatedUser } from 'services/api/utils'
import { methodNotAllowed, notAuthenticated } from 'utils' import { badRequest, methodNotAllowed, notAuthenticated } from 'utils'
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req) const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res) if (!user) return notAuthenticated(res)
const id = req.query.id.toString() const id = req.query.id as string
const workspaceId = req.query.workspaceId as string | undefined
if (req.method === 'PUT') { if (req.method === 'PUT') {
const data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body const data = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as PublicTypebot
if (!workspaceId) return badRequest(res, 'workspaceId is required')
const typebotContainsFileInput = data.groups
.flatMap((g) => g.blocks)
.some((b) => b.type === InputBlockType.FILE)
if (
typebotContainsFileInput &&
!(await canPublishFileInput({ userId: user.id, workspaceId, res }))
)
return
const typebots = await prisma.publicTypebot.update({ const typebots = await prisma.publicTypebot.update({
where: { id }, where: { id },
data, data,

View File

@@ -1,6 +1,7 @@
import test, { expect } from '@playwright/test' import test, { expect } from '@playwright/test'
import { import {
createTypebots, createTypebots,
freeWorkspaceId,
parseDefaultGroupWithBlock, parseDefaultGroupWithBlock,
} from '../../services/database' } from '../../services/database'
import { defaultFileInputOptions, InputBlockType } from 'models' import { defaultFileInputOptions, InputBlockType } from 'models'
@@ -8,6 +9,8 @@ import { typebotViewer } from '../../services/selectorUtils'
import cuid from 'cuid' import cuid from 'cuid'
import path from 'path' import path from 'path'
test.describe.configure({ mode: 'parallel' })
test('options should work', async ({ page }) => { test('options should work', async ({ page }) => {
const typebotId = cuid() const typebotId = cuid()
await createTypebots([ await createTypebots([
@@ -49,3 +52,31 @@ test('options should work', async ({ page }) => {
typebotViewer(page).locator(`text="3 files uploaded"`) typebotViewer(page).locator(`text="3 files uploaded"`)
).toBeVisible() ).toBeVisible()
}) })
test.describe('Free workspace', () => {
test.use({
storageState: path.join(__dirname, '../../freeUser.json'),
})
test("shouldn't be able to publish typebot", async ({ page }) => {
const typebotId = cuid()
await createTypebots([
{
id: typebotId,
...parseDefaultGroupWithBlock({
type: InputBlockType.FILE,
options: defaultFileInputOptions,
}),
workspaceId: freeWorkspaceId,
},
])
await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text="Collect file"')
await page.click('text="Allow multiple files?"')
await page.click('text="Publish"')
await expect(
page.locator(
'text="You need to upgrade your plan in order to use file input blocks"'
)
).toBeVisible()
})
})

View File

@@ -1,5 +1,7 @@
import { CollaborationType, Prisma, User, WorkspaceRole } from 'db' import { CollaborationType, Plan, Prisma, User, WorkspaceRole } from 'db'
import { isNotEmpty } from 'utils' import prisma from 'libs/prisma'
import { NextApiResponse } from 'next'
import { forbidden, isNotEmpty } from 'utils'
const parseWhereFilter = ( const parseWhereFilter = (
typebotIds: string[] | string, typebotIds: string[] | string,
@@ -51,3 +53,27 @@ export const canEditGuests = (user: User, typebotId: string) => ({
}, },
}, },
}) })
export const canPublishFileInput = async ({
userId,
workspaceId,
res,
}: {
userId: string
workspaceId: string
res: NextApiResponse
}) => {
const workspace = await prisma.workspace.findFirst({
where: { id: workspaceId, members: { some: { userId } } },
select: { plan: true },
})
if (!workspace) {
forbidden(res, 'workspace not found')
return false
}
if (workspace?.plan === Plan.FREE) {
forbidden(res, 'You need to upgrade your plan to use this feature')
return false
}
return true
}

View File

@@ -37,19 +37,23 @@ export const parsePublicTypebotToTypebot = (
workspaceId: existingTypebot.workspaceId, workspaceId: existingTypebot.workspaceId,
}) })
export const createPublishedTypebot = async (typebot: PublicTypebot) => export const createPublishedTypebot = async (
typebot: PublicTypebot,
workspaceId: string
) =>
sendRequest<PublicTypebot>({ sendRequest<PublicTypebot>({
url: `/api/publicTypebots`, url: `/api/publicTypebots?workspaceId=${workspaceId}`,
method: 'POST', method: 'POST',
body: typebot, body: typebot,
}) })
export const updatePublishedTypebot = async ( export const updatePublishedTypebot = async (
id: string, id: string,
typebot: Omit<PublicTypebot, 'id'> typebot: Omit<PublicTypebot, 'id'>,
workspaceId: string
) => ) =>
sendRequest({ sendRequest({
url: `/api/publicTypebots/${id}`, url: `/api/publicTypebots/${id}?workspaceId=${workspaceId}`,
method: 'PUT', method: 'PUT',
body: typebot, body: typebot,
}) })