From 1d4d39c649be6591290c379522fc602b765fbb86 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Mon, 13 Feb 2023 17:42:11 +0100 Subject: [PATCH] :card_file_box: (results) Improve result delete queries --- .../features/results/api/archiveResults.ts | 16 +--- .../api/procedures/deleteResultsProcedure.ts | 15 ++- .../features/typebot/api/utils/getTypebot.ts | 35 +++++++ .../api/utils/isReadTypebotForbidden.ts | 26 +++++ .../api/utils/isWriteTypebotForbidden.ts | 26 +++++ .../src/pages/api/typebots/[typebotId].ts | 96 +++++-------------- packages/db/mysql/schema.prisma | 2 +- 7 files changed, 128 insertions(+), 88 deletions(-) create mode 100644 apps/builder/src/features/typebot/api/utils/getTypebot.ts create mode 100644 apps/builder/src/features/typebot/api/utils/isReadTypebotForbidden.ts create mode 100644 apps/builder/src/features/typebot/api/utils/isWriteTypebotForbidden.ts diff --git a/apps/builder/src/features/results/api/archiveResults.ts b/apps/builder/src/features/results/api/archiveResults.ts index b9c0ad54bd5..9af1d955994 100644 --- a/apps/builder/src/features/results/api/archiveResults.ts +++ b/apps/builder/src/features/results/api/archiveResults.ts @@ -1,24 +1,16 @@ import prisma from '@/lib/prisma' -import { canWriteTypebots } from '@/utils/api/dbRules' import { deleteFiles } from '@/utils/api/storage' -import { User, Prisma } from 'db' +import { Prisma } from 'db' import { InputBlockType, Typebot } from 'models' export const archiveResults = async ({ - typebotId, - user, + typebot, resultsFilter, }: { - typebotId: string - user: User + typebot: Pick resultsFilter?: Prisma.ResultWhereInput }) => { - const typebot = await prisma.typebot.findFirst({ - where: canWriteTypebots(typebotId, user), - select: { groups: true }, - }) - if (!typebot) return { success: false } - const fileUploadBlockIds = (typebot as Typebot).groups + const fileUploadBlockIds = typebot.groups .flatMap((g) => g.blocks) .filter((b) => b.type === InputBlockType.FILE) .map((b) => b.id) diff --git a/apps/builder/src/features/results/api/procedures/deleteResultsProcedure.ts b/apps/builder/src/features/results/api/procedures/deleteResultsProcedure.ts index 31f2554d43c..2c4d840cc60 100644 --- a/apps/builder/src/features/results/api/procedures/deleteResultsProcedure.ts +++ b/apps/builder/src/features/results/api/procedures/deleteResultsProcedure.ts @@ -1,6 +1,7 @@ -import { canWriteTypebots } from '@/utils/api/dbRules' +import { getTypebot } from '@/features/typebot/api/utils/getTypebot' import { authenticatedProcedure } from '@/utils/server/trpc' import { TRPCError } from '@trpc/server' +import { Typebot } from 'models' import { z } from 'zod' import { archiveResults } from '../archiveResults' @@ -29,12 +30,20 @@ export const deleteResultsProcedure = authenticatedProcedure .mutation(async ({ input, ctx: { user } }) => { const idsArray = input.resultIds?.split(',') const { typebotId } = input - const { success } = await archiveResults({ + const typebot = (await getTypebot({ + accessLevel: 'write', typebotId, user, + select: { + groups: true, + }, + })) as Pick | null + if (!typebot) + throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' }) + const { success } = await archiveResults({ + typebot, resultsFilter: { id: (idsArray?.length ?? 0) > 0 ? { in: idsArray } : undefined, - typebot: canWriteTypebots(typebotId, user), }, }) diff --git a/apps/builder/src/features/typebot/api/utils/getTypebot.ts b/apps/builder/src/features/typebot/api/utils/getTypebot.ts new file mode 100644 index 00000000000..355e0cedfb9 --- /dev/null +++ b/apps/builder/src/features/typebot/api/utils/getTypebot.ts @@ -0,0 +1,35 @@ +import prisma from '@/lib/prisma' +import { Prisma, User } from 'db' +import { isReadTypebotForbidden } from './isReadTypebotForbidden' +import { isWriteTypebotForbidden } from './isWriteTypebotForbidden' + +type Props = { + typebotId: string + user: Pick + accessLevel: 'read' | 'write' + select?: T +} + +export const getTypebot = async ({ + typebotId, + user, + accessLevel, + select, +}: Props) => { + const typebot = await prisma.typebot.findFirst({ + where: { + id: typebotId, + }, + select: { + ...select, + workspaceId: true, + collaborators: { select: { userId: true, type: true } }, + }, + }) + if (!typebot) return null + if (accessLevel === 'read' && (await isReadTypebotForbidden(typebot, user))) + return null + if (accessLevel === 'write' && (await isWriteTypebotForbidden(typebot, user))) + return null + return typebot +} diff --git a/apps/builder/src/features/typebot/api/utils/isReadTypebotForbidden.ts b/apps/builder/src/features/typebot/api/utils/isReadTypebotForbidden.ts new file mode 100644 index 00000000000..63350595ece --- /dev/null +++ b/apps/builder/src/features/typebot/api/utils/isReadTypebotForbidden.ts @@ -0,0 +1,26 @@ +import prisma from '@/lib/prisma' +import { CollaboratorsOnTypebots, User } from 'db' +import { Typebot } from 'models' +import { isNotDefined } from 'utils' + +export const isReadTypebotForbidden = async ( + typebot: Pick & { + collaborators: Pick[] + }, + user: Pick +) => { + if ( + process.env.ADMIN_EMAIL === user.email || + typebot.collaborators.find( + (collaborator) => collaborator.userId === user.id + ) + ) + return false + const memberInWorkspace = await prisma.memberInWorkspace.findFirst({ + where: { + workspaceId: typebot.workspaceId, + userId: user.id, + }, + }) + return isNotDefined(memberInWorkspace) +} diff --git a/apps/builder/src/features/typebot/api/utils/isWriteTypebotForbidden.ts b/apps/builder/src/features/typebot/api/utils/isWriteTypebotForbidden.ts new file mode 100644 index 00000000000..6d99bbb71c9 --- /dev/null +++ b/apps/builder/src/features/typebot/api/utils/isWriteTypebotForbidden.ts @@ -0,0 +1,26 @@ +import prisma from '@/lib/prisma' +import { CollaborationType, CollaboratorsOnTypebots, User } from 'db' +import { Typebot } from 'models' +import { isNotDefined } from 'utils' + +export const isWriteTypebotForbidden = async ( + typebot: Pick & { + collaborators: Pick[] + }, + user: Pick +) => { + if ( + process.env.ADMIN_EMAIL === user.email || + typebot.collaborators.find( + (collaborator) => collaborator.userId === user.id + )?.type === CollaborationType.WRITE + ) + return false + const memberInWorkspace = await prisma.memberInWorkspace.findFirst({ + where: { + workspaceId: typebot.workspaceId, + userId: user.id, + }, + }) + return isNotDefined(memberInWorkspace) || memberInWorkspace.role === 'GUEST' +} diff --git a/apps/builder/src/pages/api/typebots/[typebotId].ts b/apps/builder/src/pages/api/typebots/[typebotId].ts index 64ab3936ee3..affdf843c82 100644 --- a/apps/builder/src/pages/api/typebots/[typebotId].ts +++ b/apps/builder/src/pages/api/typebots/[typebotId].ts @@ -1,4 +1,4 @@ -import { CollaborationType, CollaboratorsOnTypebots, Prisma, User } from 'db' +import { CollaborationType, Prisma } from 'db' import prisma from '@/lib/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { methodNotAllowed, notAuthenticated } from 'utils/api' @@ -6,7 +6,9 @@ import { getAuthenticatedUser } from '@/features/auth/api' import { archiveResults } from '@/features/results/api' import { Typebot, typebotSchema } from 'models' import { captureEvent } from '@sentry/nextjs' -import { isDefined, omit } from 'utils' +import { omit } from 'utils' +import { getTypebot } from '@/features/typebot/api/utils/getTypebot' +import { isReadTypebotForbidden } from '@/features/typebot/api/utils/isReadTypebotForbidden' const handler = async (req: NextApiRequest, res: NextApiResponse) => { const user = await getAuthenticatedUser(req) @@ -25,7 +27,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { webhooks: true, }, }) - if (!typebot || !(await canReadTypebots(typebot, user))) + if (!typebot || (await isReadTypebotForbidden(typebot, user))) return res.status(404).send({ typebot: null }) const { publishedTypebot, collaborators, webhooks, ...restOfTypebot } = @@ -42,18 +44,17 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } if (req.method === 'DELETE') { - const typebot = await prisma.typebot.findFirst({ - where: { id: typebotId }, + const typebot = (await getTypebot({ + accessLevel: 'write', + user, + typebotId, select: { - workspaceId: true, - collaborators: { select: { userId: true, type: true } }, + groups: true, }, - }) - if (!typebot || !(await canWriteTypebots(typebot, user))) - return res.status(404).send({ typebot: null }) + })) as Pick | null + if (!typebot) return res.status(404).send({ typebot: null }) const { success } = await archiveResults({ - typebotId, - user, + typebot, resultsFilter: { typebotId }, }) if (!success) return res.status(500).send({ success: false }) @@ -84,18 +85,17 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { }) } - const typebot = await prisma.typebot.findFirst({ - where: { id: typebotId }, + const typebot = await getTypebot({ + accessLevel: 'write', + typebotId, + user, select: { updatedAt: true, - workspaceId: true, - collaborators: { select: { userId: true, type: true } }, }, }) - if (!typebot || !(await canWriteTypebots(typebot, user))) - return res.status(404).send({ message: 'Typebot not found' }) + if (!typebot) return res.status(404).send({ message: 'Typebot not found' }) - if (typebot.updatedAt > new Date(data.updatedAt)) + if ((typebot.updatedAt as Date) > new Date(data.updatedAt)) return res.send({ message: 'Found newer version of typebot in database' }) const typebots = await prisma.typebot.updateMany({ where: { id: typebotId }, @@ -110,16 +110,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } if (req.method === 'PATCH') { - const typebot = await prisma.typebot.findFirst({ - where: { id: typebotId }, - select: { - updatedAt: true, - workspaceId: true, - collaborators: { select: { userId: true, type: true } }, - }, + const typebot = await getTypebot({ + accessLevel: 'write', + typebotId, + user, }) - if (!typebot || !(await canWriteTypebots(typebot, user))) - return res.status(404).send({ message: 'Typebot not found' }) + if (!typebot) return res.status(404).send({ message: 'Typebot not found' }) const data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body const typebots = await prisma.typebot.updateMany({ where: { id: typebotId }, @@ -130,50 +126,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { return methodNotAllowed(res) } -const canReadTypebots = async ( - typebot: Pick & { - collaborators: Pick[] - }, - user: Pick -) => { - if ( - process.env.ADMIN_EMAIL === user.email || - typebot.collaborators.find( - (collaborator) => collaborator.userId === user.id - ) - ) - return true - const memberInWorkspace = await prisma.memberInWorkspace.findFirst({ - where: { - workspaceId: typebot.workspaceId, - userId: user.id, - }, - }) - return isDefined(memberInWorkspace) -} - -const canWriteTypebots = async ( - typebot: Pick & { - collaborators: Pick[] - }, - user: Pick -) => { - if ( - process.env.ADMIN_EMAIL === user.email || - typebot.collaborators.find( - (collaborator) => collaborator.userId === user.id - )?.type === CollaborationType.WRITE - ) - return true - const memberInWorkspace = await prisma.memberInWorkspace.findFirst({ - where: { - workspaceId: typebot.workspaceId, - userId: user.id, - }, - }) - return memberInWorkspace && memberInWorkspace?.role !== 'GUEST' -} - // TODO: Remove in a month const removeOldProperties = (data: unknown) => { if (data && typeof data === 'object' && 'publishedTypebotId' in data) { diff --git a/packages/db/mysql/schema.prisma b/packages/db/mysql/schema.prisma index 9e22844625a..45d94f355a8 100644 --- a/packages/db/mysql/schema.prisma +++ b/packages/db/mysql/schema.prisma @@ -135,7 +135,7 @@ model Credentials { id String @id @default(cuid()) createdAt DateTime @default(now()) workspaceId String - data String + data String @db.Text name String type String iv String