From bb194b1dbbcf6b8bfc4af64e124d98e3531e8566 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Tue, 29 Mar 2022 11:20:15 +0200 Subject: [PATCH] =?UTF-8?q?fix(team):=20=F0=9F=9B=82=20Improve=20collab=20?= =?UTF-8?q?permissions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../builder/pages/api/typebots/[typebotId].ts | 38 +++---------------- .../pages/api/typebots/[typebotId]/blocks.ts | 5 ++- .../api/typebots/[typebotId]/collaborators.ts | 3 +- .../[typebotId]/collaborators/[userId].ts | 7 +++- .../api/typebots/[typebotId]/invitations.ts | 14 ++++++- .../[typebotId]/invitations/[email].ts | 7 +++- .../pages/api/typebots/[typebotId]/results.ts | 11 +++--- .../[typebotId]/results/[resultId]/logs.ts | 6 ++- .../[typebotId]/results/answers/count.ts | 8 ++-- .../api/typebots/[typebotId]/results/stats.ts | 7 ++-- .../api/typebots/[typebotId]/webhooks.ts | 7 +++- .../playwright/tests/collaboration.spec.ts | 9 ++++- apps/builder/services/api/dbRules.ts | 33 ++++++++++++++++ .../src/components/ChatBlock/ChatBlock.tsx | 2 +- 14 files changed, 101 insertions(+), 56 deletions(-) create mode 100644 apps/builder/services/api/dbRules.ts diff --git a/apps/builder/pages/api/typebots/[typebotId].ts b/apps/builder/pages/api/typebots/[typebotId].ts index 60d92368a7..e59fa5c103 100644 --- a/apps/builder/pages/api/typebots/[typebotId].ts +++ b/apps/builder/pages/api/typebots/[typebotId].ts @@ -1,7 +1,8 @@ import { withSentry } from '@sentry/nextjs' -import { CollaborationType, Prisma, User } from 'db' +import { CollaborationType } from 'db' import prisma from 'libs/prisma' import { NextApiRequest, NextApiResponse } from 'next' +import { canReadTypebot, canWriteTypebot } from 'services/api/dbRules' import { getAuthenticatedUser } from 'services/api/utils' import { methodNotAllowed, notAuthenticated } from 'utils' @@ -12,7 +13,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const typebotId = req.query.typebotId.toString() if (req.method === 'GET') { const typebot = await prisma.typebot.findFirst({ - where: parseWhereFilter(typebotId, user, 'read'), + where: canReadTypebot(typebotId, user), include: { publishedTypebot: true, owner: { select: { email: true, name: true, image: true } }, @@ -40,17 +41,16 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { }) } - const canEditTypebot = parseWhereFilter(typebotId, user, 'write') if (req.method === 'DELETE') { const typebots = await prisma.typebot.deleteMany({ - where: canEditTypebot, + where: canWriteTypebot(typebotId, user), }) return res.send({ typebots }) } if (req.method === 'PUT') { const data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body const typebots = await prisma.typebot.updateMany({ - where: canEditTypebot, + where: canWriteTypebot(typebotId, user), data: { ...data, theme: data.theme ?? undefined, @@ -62,7 +62,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'PATCH') { const data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body const typebots = await prisma.typebot.updateMany({ - where: canEditTypebot, + where: canWriteTypebot(typebotId, user), data, }) return res.send({ typebots }) @@ -70,30 +70,4 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { return methodNotAllowed(res) } -const parseWhereFilter = ( - typebotId: string, - user: User, - type: 'read' | 'write' -): Prisma.TypebotWhereInput => ({ - OR: [ - { - id: typebotId, - ownerId: - (type === 'read' && user.email === process.env.ADMIN_EMAIL) || - process.env.NEXT_PUBLIC_E2E_TEST - ? undefined - : user.id, - }, - { - id: typebotId, - collaborators: { - some: { - userId: user.id, - type: type === 'write' ? CollaborationType.WRITE : undefined, - }, - }, - }, - ], -}) - export default withSentry(handler) diff --git a/apps/builder/pages/api/typebots/[typebotId]/blocks.ts b/apps/builder/pages/api/typebots/[typebotId]/blocks.ts index 9418536de4..1c1a3f7599 100644 --- a/apps/builder/pages/api/typebots/[typebotId]/blocks.ts +++ b/apps/builder/pages/api/typebots/[typebotId]/blocks.ts @@ -1,6 +1,7 @@ import { withSentry } from '@sentry/nextjs' import prisma from 'libs/prisma' import { NextApiRequest, NextApiResponse } from 'next' +import { canReadTypebot } from 'services/api/dbRules' import { getAuthenticatedUser } from 'services/api/utils' import { methodNotAllowed, notAuthenticated, notFound } from 'utils' @@ -9,8 +10,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (!user) return notAuthenticated(res) if (req.method === 'GET') { const typebotId = req.query.typebotId as string - const typebot = await prisma.typebot.findUnique({ - where: { id_ownerId: { id: typebotId, ownerId: user.id } }, + const typebot = await prisma.typebot.findFirst({ + where: canReadTypebot(typebotId, user), }) if (!typebot) return notFound(res) return res.send({ blocks: typebot.blocks }) diff --git a/apps/builder/pages/api/typebots/[typebotId]/collaborators.ts b/apps/builder/pages/api/typebots/[typebotId]/collaborators.ts index 540329cf73..cc57c28c6d 100644 --- a/apps/builder/pages/api/typebots/[typebotId]/collaborators.ts +++ b/apps/builder/pages/api/typebots/[typebotId]/collaborators.ts @@ -1,6 +1,7 @@ import { withSentry } from '@sentry/nextjs' import prisma from 'libs/prisma' import { NextApiRequest, NextApiResponse } from 'next' +import { canReadTypebot } from 'services/api/dbRules' import { getAuthenticatedUser } from 'services/api/utils' import { methodNotAllowed, notAuthenticated } from 'utils' @@ -10,7 +11,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const typebotId = req.query.typebotId as string if (req.method === 'GET') { const collaborators = await prisma.collaboratorsOnTypebots.findMany({ - where: { typebotId }, + where: { typebot: canReadTypebot(typebotId, user) }, include: { user: { select: { name: true, image: true, email: true } } }, }) return res.send({ diff --git a/apps/builder/pages/api/typebots/[typebotId]/collaborators/[userId].ts b/apps/builder/pages/api/typebots/[typebotId]/collaborators/[userId].ts index 7b617bdd96..8d20670829 100644 --- a/apps/builder/pages/api/typebots/[typebotId]/collaborators/[userId].ts +++ b/apps/builder/pages/api/typebots/[typebotId]/collaborators/[userId].ts @@ -1,14 +1,19 @@ import { withSentry } from '@sentry/nextjs' import prisma from 'libs/prisma' import { NextApiRequest, NextApiResponse } from 'next' +import { canWriteTypebot } from 'services/api/dbRules' import { getAuthenticatedUser } from 'services/api/utils' -import { methodNotAllowed, notAuthenticated } from 'utils' +import { forbidden, methodNotAllowed, notAuthenticated } from 'utils' const handler = async (req: NextApiRequest, res: NextApiResponse) => { const user = await getAuthenticatedUser(req) if (!user) return notAuthenticated(res) const typebotId = req.query.typebotId as string const userId = req.query.userId as string + const typebot = await prisma.typebot.findFirst({ + where: canWriteTypebot(typebotId, user), + }) + if (!typebot) return forbidden(res) if (req.method === 'PUT') { const data = req.body await prisma.collaboratorsOnTypebots.upsert({ diff --git a/apps/builder/pages/api/typebots/[typebotId]/invitations.ts b/apps/builder/pages/api/typebots/[typebotId]/invitations.ts index cc581b8c3c..deced7a4c8 100644 --- a/apps/builder/pages/api/typebots/[typebotId]/invitations.ts +++ b/apps/builder/pages/api/typebots/[typebotId]/invitations.ts @@ -3,10 +3,12 @@ import { invitationToCollaborate } from 'assets/emails/invitationToCollaborate' import { CollaborationType } from 'db' import prisma from 'libs/prisma' import { NextApiRequest, NextApiResponse } from 'next' +import { canReadTypebot, canWriteTypebot } from 'services/api/dbRules' import { sendEmailNotification } from 'services/api/emails' import { getAuthenticatedUser } from 'services/api/utils' import { badRequest, + forbidden, isNotDefined, methodNotAllowed, notAuthenticated, @@ -18,13 +20,17 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const typebotId = req.query.typebotId as string if (req.method === 'GET') { const invitations = await prisma.invitation.findMany({ - where: { typebotId }, + where: { typebotId, typebot: canReadTypebot(typebotId, user) }, }) return res.send({ invitations, }) } if (req.method === 'POST') { + const typebot = await prisma.typebot.findFirst({ + where: canWriteTypebot(typebotId, user), + }) + if (!typebot) return forbidden(res) const { email, type } = (req.body as | { email: string | undefined; type: CollaborationType | undefined } @@ -36,7 +42,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { }) if (existingUser) await prisma.collaboratorsOnTypebots.create({ - data: { type, typebotId, userId: existingUser.id }, + data: { + type, + typebotId, + userId: existingUser.id, + }, }) else await prisma.invitation.create({ diff --git a/apps/builder/pages/api/typebots/[typebotId]/invitations/[email].ts b/apps/builder/pages/api/typebots/[typebotId]/invitations/[email].ts index 010a97a361..06f881efe7 100644 --- a/apps/builder/pages/api/typebots/[typebotId]/invitations/[email].ts +++ b/apps/builder/pages/api/typebots/[typebotId]/invitations/[email].ts @@ -1,14 +1,19 @@ import { withSentry } from '@sentry/nextjs' import prisma from 'libs/prisma' import { NextApiRequest, NextApiResponse } from 'next' +import { canWriteTypebot } from 'services/api/dbRules' import { getAuthenticatedUser } from 'services/api/utils' -import { methodNotAllowed, notAuthenticated } from 'utils' +import { forbidden, methodNotAllowed, notAuthenticated } from 'utils' const handler = async (req: NextApiRequest, res: NextApiResponse) => { const user = await getAuthenticatedUser(req) if (!user) return notAuthenticated(res) const typebotId = req.query.typebotId as string const email = req.query.email as string + const typebot = await prisma.typebot.findFirst({ + where: canWriteTypebot(typebotId, user), + }) + if (!typebot) return forbidden(res) if (req.method === 'PUT') { const data = req.body await prisma.invitation.upsert({ diff --git a/apps/builder/pages/api/typebots/[typebotId]/results.ts b/apps/builder/pages/api/typebots/[typebotId]/results.ts index b13a782d31..0254a73ffb 100644 --- a/apps/builder/pages/api/typebots/[typebotId]/results.ts +++ b/apps/builder/pages/api/typebots/[typebotId]/results.ts @@ -1,6 +1,7 @@ import { withSentry } from '@sentry/nextjs' import prisma from 'libs/prisma' import { NextApiRequest, NextApiResponse } from 'next' +import { canReadTypebot, canWriteTypebot } from 'services/api/dbRules' import { getAuthenticatedUser } from 'services/api/utils' import { isFreePlan } from 'services/user/user' import { methodNotAllowed, notAuthenticated } from 'utils' @@ -21,10 +22,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } : undefined, where: { - typebotId, - typebot: { - ownerId: user.email === process.env.ADMIN_EMAIL ? undefined : user.id, - }, + typebot: canReadTypebot(typebotId, user), answers: { some: {} }, isCompleted: isFreePlan(user) ? true : undefined, }, @@ -39,7 +37,10 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const typebotId = req.query.typebotId.toString() const ids = req.query.ids as string[] const results = await prisma.result.deleteMany({ - where: { id: { in: ids }, typebotId, typebot: { ownerId: user.id } }, + where: { + id: { in: ids }, + typebot: canWriteTypebot(typebotId, user), + }, }) return res.status(200).send({ results }) } diff --git a/apps/builder/pages/api/typebots/[typebotId]/results/[resultId]/logs.ts b/apps/builder/pages/api/typebots/[typebotId]/results/[resultId]/logs.ts index ba83f54471..50e6b542ac 100644 --- a/apps/builder/pages/api/typebots/[typebotId]/results/[resultId]/logs.ts +++ b/apps/builder/pages/api/typebots/[typebotId]/results/[resultId]/logs.ts @@ -1,6 +1,7 @@ import { withSentry } from '@sentry/nextjs' import prisma from 'libs/prisma' import { NextApiRequest, NextApiResponse } from 'next' +import { canReadTypebot } from 'services/api/dbRules' import { getAuthenticatedUser } from 'services/api/utils' import { methodNotAllowed, notAuthenticated } from 'utils' @@ -8,8 +9,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const user = await getAuthenticatedUser(req) if (!user) return notAuthenticated(res) if (req.method === 'GET') { + const typebotId = req.query.typebotId as string const resultId = req.query.resultId as string - const logs = await prisma.log.findMany({ where: { resultId } }) + const logs = await prisma.log.findMany({ + where: { resultId, result: { typebot: canReadTypebot(typebotId, user) } }, + }) return res.send({ logs }) } methodNotAllowed(res) diff --git a/apps/builder/pages/api/typebots/[typebotId]/results/answers/count.ts b/apps/builder/pages/api/typebots/[typebotId]/results/answers/count.ts index 239e25a65a..faf53f3358 100644 --- a/apps/builder/pages/api/typebots/[typebotId]/results/answers/count.ts +++ b/apps/builder/pages/api/typebots/[typebotId]/results/answers/count.ts @@ -4,20 +4,18 @@ import { NextApiRequest, NextApiResponse } from 'next' import { methodNotAllowed, notAuthenticated } from 'utils' import { withSentry } from '@sentry/nextjs' import { getAuthenticatedUser } from 'services/api/utils' +import { canReadTypebot } from 'services/api/dbRules' const handler = async (req: NextApiRequest, res: NextApiResponse) => { const user = await getAuthenticatedUser(req) if (!user) return notAuthenticated(res) if (req.method === 'GET') { const typebotId = req.query.typebotId.toString() - const typebot = await prisma.typebot.findUnique({ - where: { id: typebotId }, + const typebot = await prisma.typebot.findFirst({ + where: canReadTypebot(typebotId, user), include: { publishedTypebot: true }, }) if (!typebot) return res.status(404).send({ answersCounts: [] }) - if (typebot?.ownerId !== user.id) - return res.status(403).send({ message: 'Forbidden' }) - const answersCounts: { blockId: string; totalAnswers: number }[] = await Promise.all( (typebot.publishedTypebot as unknown as PublicTypebot).blocks.map( diff --git a/apps/builder/pages/api/typebots/[typebotId]/results/stats.ts b/apps/builder/pages/api/typebots/[typebotId]/results/stats.ts index 29fc6d4f35..0968f3a6f4 100644 --- a/apps/builder/pages/api/typebots/[typebotId]/results/stats.ts +++ b/apps/builder/pages/api/typebots/[typebotId]/results/stats.ts @@ -2,6 +2,7 @@ import { withSentry } from '@sentry/nextjs' import prisma from 'libs/prisma' import { Stats } from 'models' import { NextApiRequest, NextApiResponse } from 'next' +import { canReadTypebot } from 'services/api/dbRules' import { getAuthenticatedUser } from 'services/api/utils' import { methodNotAllowed, notAuthenticated } from 'utils' @@ -14,20 +15,20 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const totalViews = await prisma.result.count({ where: { typebotId, - typebot: { ownerId: user.id }, + typebot: canReadTypebot(typebotId, user), }, }) const totalStarts = await prisma.result.count({ where: { typebotId, - typebot: { ownerId: user.id }, + typebot: canReadTypebot(typebotId, user), answers: { some: {} }, }, }) const totalCompleted = await prisma.result.count({ where: { typebotId, - typebot: { ownerId: user.id }, + typebot: canReadTypebot(typebotId, user), isCompleted: true, }, }) diff --git a/apps/builder/pages/api/typebots/[typebotId]/webhooks.ts b/apps/builder/pages/api/typebots/[typebotId]/webhooks.ts index cca69cb1dd..4c668ddefa 100644 --- a/apps/builder/pages/api/typebots/[typebotId]/webhooks.ts +++ b/apps/builder/pages/api/typebots/[typebotId]/webhooks.ts @@ -2,14 +2,19 @@ import { withSentry } from '@sentry/nextjs' import prisma from 'libs/prisma' import { defaultWebhookAttributes } from 'models' import { NextApiRequest, NextApiResponse } from 'next' +import { canWriteTypebot } from 'services/api/dbRules' import { getAuthenticatedUser } from 'services/api/utils' -import { methodNotAllowed, notAuthenticated } from 'utils' +import { forbidden, methodNotAllowed, notAuthenticated } from 'utils' const handler = async (req: NextApiRequest, res: NextApiResponse) => { const user = await getAuthenticatedUser(req) if (!user) return notAuthenticated(res) if (req.method === 'POST') { const typebotId = req.query.typebotId as string + const typebot = await prisma.typebot.findFirst({ + where: canWriteTypebot(typebotId, user), + }) + if (!typebot) return forbidden(res) const webhook = await prisma.webhook.create({ data: { typebotId, ...defaultWebhookAttributes }, }) diff --git a/apps/builder/playwright/tests/collaboration.spec.ts b/apps/builder/playwright/tests/collaboration.spec.ts index 3b1503e976..85d4a0a7f8 100644 --- a/apps/builder/playwright/tests/collaboration.spec.ts +++ b/apps/builder/playwright/tests/collaboration.spec.ts @@ -2,7 +2,11 @@ import test, { expect } from '@playwright/test' import cuid from 'cuid' import { InputStepType, defaultTextInputOptions } from 'models' import path from 'path' -import { createTypebots, parseDefaultBlockWithStep } from '../services/database' +import { + createResults, + createTypebots, + parseDefaultBlockWithStep, +} from '../services/database' const typebotId = cuid() @@ -18,6 +22,7 @@ test.beforeAll(async () => { }), }, ]) + await createResults({ typebotId }) }) test.describe('Typebot owner', () => { @@ -67,5 +72,7 @@ test.describe('Collaborator', () => { await expect(page.locator('text=Free user')).toBeVisible() await page.click('text=Block #1', { force: true }) await expect(page.locator('input[value="Block #1"]')).toBeHidden() + await page.goto(`/typebots/${typebotId}/results`) + await expect(page.locator('text="content199"')).toBeVisible() }) }) diff --git a/apps/builder/services/api/dbRules.ts b/apps/builder/services/api/dbRules.ts new file mode 100644 index 0000000000..b6669c9d7b --- /dev/null +++ b/apps/builder/services/api/dbRules.ts @@ -0,0 +1,33 @@ +import { CollaborationType, Prisma, User } from 'db' + +const parseWhereFilter = ( + typebotId: string, + user: User, + type: 'read' | 'write' +): Prisma.TypebotWhereInput => ({ + OR: [ + { + id: typebotId, + ownerId: + (type === 'read' && user.email === process.env.ADMIN_EMAIL) || + process.env.NEXT_PUBLIC_E2E_TEST + ? undefined + : user.id, + }, + { + id: typebotId, + collaborators: { + some: { + userId: user.id, + type: type === 'write' ? CollaborationType.WRITE : undefined, + }, + }, + }, + ], +}) + +export const canReadTypebot = (typebotId: string, user: User) => + parseWhereFilter(typebotId, user, 'read') + +export const canWriteTypebot = (typebotId: string, user: User) => + parseWhereFilter(typebotId, user, 'write') diff --git a/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx b/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx index 84af38c4c2..9146306243 100644 --- a/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx +++ b/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx @@ -161,7 +161,7 @@ export const ChatBlock = ({ return onBlockEnd(currentStep.outgoingEdgeId) } const nextStep = steps[processedSteps.length + startStepIndex] - if (nextStep) insertStepInStack(nextStep) + nextStep ? insertStepInStack(nextStep) : onBlockEnd() } const avatarSrc = typebot.theme.chat.hostAvatar?.url