fix(team): 🛂 Improve collab permissions
This commit is contained in:
@ -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)
|
||||
|
@ -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 })
|
||||
|
@ -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({
|
||||
|
@ -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({
|
||||
|
@ -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({
|
||||
|
@ -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({
|
||||
|
@ -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 })
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
@ -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 },
|
||||
})
|
||||
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
33
apps/builder/services/api/dbRules.ts
Normal file
33
apps/builder/services/api/dbRules.ts
Normal file
@ -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')
|
@ -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
|
||||
|
Reference in New Issue
Block a user