Skip to content

Commit

Permalink
fix(team): 🛂 Improve collab permissions
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno committed Mar 29, 2022
1 parent 8d6330f commit bb194b1
Show file tree
Hide file tree
Showing 14 changed files with 101 additions and 56 deletions.
38 changes: 6 additions & 32 deletions apps/builder/pages/api/typebots/[typebotId].ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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 } },
Expand Down Expand Up @@ -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,
Expand All @@ -62,38 +62,12 @@ 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 })
}
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)
5 changes: 3 additions & 2 deletions apps/builder/pages/api/typebots/[typebotId]/blocks.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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 })
Expand Down
3 changes: 2 additions & 1 deletion apps/builder/pages/api/typebots/[typebotId]/collaborators.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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({
Expand Down
Original file line number Diff line number Diff line change
@@ -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({
Expand Down
14 changes: 12 additions & 2 deletions apps/builder/pages/api/typebots/[typebotId]/invitations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 }
Expand All @@ -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({
Expand Down
Original file line number Diff line number Diff line change
@@ -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({
Expand Down
11 changes: 6 additions & 5 deletions apps/builder/pages/api/typebots/[typebotId]/results.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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,
},
Expand All @@ -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 })
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
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'

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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
7 changes: 4 additions & 3 deletions apps/builder/pages/api/typebots/[typebotId]/results/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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,
},
})
Expand Down
7 changes: 6 additions & 1 deletion apps/builder/pages/api/typebots/[typebotId]/webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
})
Expand Down
9 changes: 8 additions & 1 deletion apps/builder/playwright/tests/collaboration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -18,6 +22,7 @@ test.beforeAll(async () => {
}),
},
])
await createResults({ typebotId })
})

test.describe('Typebot owner', () => {
Expand Down Expand Up @@ -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()
})
})
33 changes: 33 additions & 0 deletions apps/builder/services/api/dbRules.ts
Original file line number Diff line number Diff line change
@@ -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')
Loading

3 comments on commit bb194b1

@vercel
Copy link

@vercel vercel bot commented on bb194b1 Mar 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on bb194b1 Mar 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

builder-v2 – ./apps/builder

app.typebot.io
builder-v2-git-main-typebot-io.vercel.app
builder-v2-typebot-io.vercel.app

@vercel
Copy link

@vercel vercel bot commented on bb194b1 Mar 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.