feat(editor): 🔒️ Add verification on backend for file input deployment
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|||||||
@@ -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 },
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user