🐛 (collaboration) Fix a database rule preventing collaborators to edit a bot
Also check if new user has invitations when signup is disabled Closes #265
This commit is contained in:
@ -0,0 +1,52 @@
|
||||
import { Invitation, PrismaClient, WorkspaceRole } from 'db'
|
||||
|
||||
export type InvitationWithWorkspaceId = Invitation & {
|
||||
typebot: {
|
||||
workspaceId: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const convertInvitationsToCollaborations = async (
|
||||
p: PrismaClient,
|
||||
{ id, email }: { id: string; email: string },
|
||||
invitations: InvitationWithWorkspaceId[]
|
||||
) => {
|
||||
await p.collaboratorsOnTypebots.createMany({
|
||||
data: invitations.map((invitation) => ({
|
||||
typebotId: invitation.typebotId,
|
||||
type: invitation.type,
|
||||
userId: id,
|
||||
})),
|
||||
})
|
||||
const workspaceInvitations = invitations.reduce<InvitationWithWorkspaceId[]>(
|
||||
(acc, invitation) =>
|
||||
acc.some(
|
||||
(inv) => inv.typebot.workspaceId === invitation.typebot.workspaceId
|
||||
)
|
||||
? acc
|
||||
: [...acc, invitation],
|
||||
[]
|
||||
)
|
||||
for (const invitation of workspaceInvitations) {
|
||||
if (!invitation.typebot.workspaceId) continue
|
||||
await p.memberInWorkspace.upsert({
|
||||
where: {
|
||||
userId_workspaceId: {
|
||||
userId: id,
|
||||
workspaceId: invitation.typebot.workspaceId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
userId: id,
|
||||
workspaceId: invitation.typebot.workspaceId,
|
||||
role: WorkspaceRole.GUEST,
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
}
|
||||
return p.invitation.deleteMany({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
})
|
||||
}
|
22
apps/builder/src/features/auth/api/getNewUserInvitations.ts
Normal file
22
apps/builder/src/features/auth/api/getNewUserInvitations.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { PrismaClient, WorkspaceInvitation } from 'db'
|
||||
import { InvitationWithWorkspaceId } from './convertInvitationsToCollaborations'
|
||||
|
||||
export const getNewUserInvitations = async (
|
||||
p: PrismaClient,
|
||||
email: string
|
||||
): Promise<{
|
||||
invitations: InvitationWithWorkspaceId[]
|
||||
workspaceInvitations: WorkspaceInvitation[]
|
||||
}> => {
|
||||
const [invitations, workspaceInvitations] = await p.$transaction([
|
||||
p.invitation.findMany({
|
||||
where: { email },
|
||||
include: { typebot: { select: { workspaceId: true } } },
|
||||
}),
|
||||
p.workspaceInvitation.findMany({
|
||||
where: { email },
|
||||
}),
|
||||
])
|
||||
|
||||
return { invitations, workspaceInvitations }
|
||||
}
|
@ -1 +1,4 @@
|
||||
export * from './convertInvitationsToCollaborations'
|
||||
export * from './getAuthenticatedUser'
|
||||
export * from './getNewUserInvitations'
|
||||
export * from './joinWorkspaces'
|
||||
|
20
apps/builder/src/features/auth/api/joinWorkspaces.ts
Normal file
20
apps/builder/src/features/auth/api/joinWorkspaces.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { PrismaClient, WorkspaceInvitation } from 'db'
|
||||
|
||||
export const joinWorkspaces = async (
|
||||
p: PrismaClient,
|
||||
{ id, email }: { id: string; email: string },
|
||||
invitations: WorkspaceInvitation[]
|
||||
) => {
|
||||
await p.memberInWorkspace.createMany({
|
||||
data: invitations.map((invitation) => ({
|
||||
workspaceId: invitation.workspaceId,
|
||||
role: invitation.type,
|
||||
userId: id,
|
||||
})),
|
||||
})
|
||||
return p.workspaceInvitation.deleteMany({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
})
|
||||
}
|
@ -64,7 +64,7 @@ test.describe('Typebot owner', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Guest', () => {
|
||||
test.describe('Guest with read access', () => {
|
||||
test('should have shared typebots displayed', async ({ page }) => {
|
||||
const typebotId = cuid()
|
||||
const guestWorkspaceId = cuid()
|
||||
@ -121,3 +121,58 @@ test.describe('Guest', () => {
|
||||
await expect(page.locator('text="See logs" >> nth=9')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Guest with write access', () => {
|
||||
test('should have shared typebots displayed', async ({ page }) => {
|
||||
const typebotId = cuid()
|
||||
const guestWorkspaceId = cuid()
|
||||
await prisma.workspace.create({
|
||||
data: {
|
||||
id: guestWorkspaceId,
|
||||
name: 'Guest Workspace #3',
|
||||
plan: Plan.FREE,
|
||||
members: {
|
||||
createMany: {
|
||||
data: [{ role: WorkspaceRole.GUEST, userId }],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
await createTypebots([
|
||||
{
|
||||
id: typebotId,
|
||||
name: 'Guest typebot',
|
||||
workspaceId: guestWorkspaceId,
|
||||
...parseDefaultGroupWithBlock({
|
||||
type: InputBlockType.TEXT,
|
||||
options: defaultTextInputOptions,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'Another typebot',
|
||||
workspaceId: guestWorkspaceId,
|
||||
},
|
||||
])
|
||||
await prisma.collaboratorsOnTypebots.create({
|
||||
data: {
|
||||
typebotId,
|
||||
userId,
|
||||
type: CollaborationType.WRITE,
|
||||
},
|
||||
})
|
||||
await createFolder(guestWorkspaceId, 'Guest folder')
|
||||
await page.goto(`/typebots`)
|
||||
await page.click('text=Pro workspace')
|
||||
await page.click('text=Guest workspace #3')
|
||||
await expect(page.locator('text=Guest typebot')).toBeVisible()
|
||||
await expect(page.locator('text=Another typebot')).toBeHidden()
|
||||
await expect(page.locator('text=Guest folder')).toBeHidden()
|
||||
await page.click('text=Guest typebot')
|
||||
await page.click('button[aria-label="Show collaboration menu"]')
|
||||
await page.click('text=Everyone at Guest workspace')
|
||||
await expect(page.locator('text="Remove"')).toBeHidden()
|
||||
await expect(page.locator('text=John Doe')).toBeVisible()
|
||||
await page.click('text=Group #1', { force: true })
|
||||
await expect(page.locator('input[value="Group #1"]')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
@ -41,7 +41,7 @@ export const EditTypebotPage = () => {
|
||||
>
|
||||
{typebot ? (
|
||||
<GraphDndProvider>
|
||||
<BlocksSideBar />
|
||||
{!isReadOnly && <BlocksSideBar />}
|
||||
<GraphProvider isReadOnly={isReadOnly}>
|
||||
<GroupsCoordinatesProvider groups={typebot.groups}>
|
||||
<Graph flex="1" typebot={typebot} />
|
||||
|
@ -10,8 +10,9 @@ import { Provider } from 'next-auth/providers'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { CustomAdapter } from './adapter'
|
||||
import { User } from 'db'
|
||||
import { env, getAtPath, isNotEmpty } from 'utils'
|
||||
import { env, getAtPath, isDefined, isNotEmpty } from 'utils'
|
||||
import { mockedUser } from '@/features/auth'
|
||||
import { getNewUserInvitations } from '@/features/auth/api'
|
||||
|
||||
const providers: Provider[] = []
|
||||
|
||||
@ -155,15 +156,14 @@ const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
||||
}
|
||||
},
|
||||
signIn: async ({ account, user }) => {
|
||||
const userExists =
|
||||
'graphNavigation' in user && user.graphNavigation !== undefined
|
||||
if (
|
||||
!account ||
|
||||
(process.env.DISABLE_SIGNUP === 'true' &&
|
||||
!userExists &&
|
||||
user.email !== process.env.ADMIN_EMAIL)
|
||||
)
|
||||
return false
|
||||
if (!account) return false
|
||||
const isNewUser = !('createdAt' in user && isDefined(user.createdAt))
|
||||
if (process.env.DISABLE_SIGNUP === 'true' && isNewUser && user.email) {
|
||||
const { invitations, workspaceInvitations } =
|
||||
await getNewUserInvitations(prisma, user.email)
|
||||
if (invitations.length === 0 && workspaceInvitations.length === 0)
|
||||
return false
|
||||
}
|
||||
const requiredGroups = getRequiredGroups(account.provider)
|
||||
if (requiredGroups.length > 0) {
|
||||
const userGroups = await getUserGroups(account)
|
||||
|
@ -1,38 +1,29 @@
|
||||
// Forked from https://github.com/nextauthjs/adapters/blob/main/packages/prisma/src/index.ts
|
||||
import {
|
||||
PrismaClient,
|
||||
Prisma,
|
||||
Invitation,
|
||||
WorkspaceRole,
|
||||
WorkspaceInvitation,
|
||||
Session,
|
||||
} from 'db'
|
||||
import { PrismaClient, Prisma, WorkspaceRole, Session } from 'db'
|
||||
import type { Adapter, AdapterUser } from 'next-auth/adapters'
|
||||
import cuid from 'cuid'
|
||||
import { got } from 'got'
|
||||
import { generateId } from 'utils'
|
||||
import { parseWorkspaceDefaultPlan } from '@/features/workspace'
|
||||
|
||||
type InvitationWithWorkspaceId = Invitation & {
|
||||
typebot: {
|
||||
workspaceId: string | null
|
||||
}
|
||||
}
|
||||
import {
|
||||
getNewUserInvitations,
|
||||
convertInvitationsToCollaborations,
|
||||
joinWorkspaces,
|
||||
} from '@/features/auth/api'
|
||||
|
||||
export function CustomAdapter(p: PrismaClient): Adapter {
|
||||
return {
|
||||
createUser: async (data: Omit<AdapterUser, 'id'>) => {
|
||||
const user = { id: cuid(), email: data.email as string }
|
||||
const invitations = await p.invitation.findMany({
|
||||
where: { email: user.email },
|
||||
include: { typebot: { select: { workspaceId: true } } },
|
||||
})
|
||||
const workspaceInvitations = await p.workspaceInvitation.findMany({
|
||||
where: { email: user.email },
|
||||
})
|
||||
const { invitations, workspaceInvitations } = await getNewUserInvitations(
|
||||
p,
|
||||
user.email
|
||||
)
|
||||
if (
|
||||
process.env.DISABLE_SIGNUP === 'true' &&
|
||||
process.env.ADMIN_EMAIL !== data.email
|
||||
process.env.ADMIN_EMAIL !== user.email &&
|
||||
invitations.length === 0 &&
|
||||
workspaceInvitations.length === 0
|
||||
)
|
||||
throw Error('New users are forbidden')
|
||||
const createdUser = await p.user.create({
|
||||
@ -137,67 +128,3 @@ export function CustomAdapter(p: PrismaClient): Adapter {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const convertInvitationsToCollaborations = async (
|
||||
p: PrismaClient,
|
||||
{ id, email }: { id: string; email: string },
|
||||
invitations: InvitationWithWorkspaceId[]
|
||||
) => {
|
||||
await p.collaboratorsOnTypebots.createMany({
|
||||
data: invitations.map((invitation) => ({
|
||||
typebotId: invitation.typebotId,
|
||||
type: invitation.type,
|
||||
userId: id,
|
||||
})),
|
||||
})
|
||||
const workspaceInvitations = invitations.reduce<InvitationWithWorkspaceId[]>(
|
||||
(acc, invitation) =>
|
||||
acc.some(
|
||||
(inv) => inv.typebot.workspaceId === invitation.typebot.workspaceId
|
||||
)
|
||||
? acc
|
||||
: [...acc, invitation],
|
||||
[]
|
||||
)
|
||||
for (const invitation of workspaceInvitations) {
|
||||
if (!invitation.typebot.workspaceId) continue
|
||||
await p.memberInWorkspace.upsert({
|
||||
where: {
|
||||
userId_workspaceId: {
|
||||
userId: id,
|
||||
workspaceId: invitation.typebot.workspaceId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
userId: id,
|
||||
workspaceId: invitation.typebot.workspaceId,
|
||||
role: WorkspaceRole.GUEST,
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
}
|
||||
return p.invitation.deleteMany({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const joinWorkspaces = async (
|
||||
p: PrismaClient,
|
||||
{ id, email }: { id: string; email: string },
|
||||
invitations: WorkspaceInvitation[]
|
||||
) => {
|
||||
await p.memberInWorkspace.createMany({
|
||||
data: invitations.map((invitation) => ({
|
||||
workspaceId: invitation.workspaceId,
|
||||
role: invitation.type,
|
||||
userId: id,
|
||||
})),
|
||||
})
|
||||
return p.workspaceInvitation.deleteMany({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Plan, Prisma, User, WorkspaceRole } from 'db'
|
||||
import { CollaborationType, Plan, Prisma, User, WorkspaceRole } from 'db'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { NextApiResponse } from 'next'
|
||||
import { env, isNotEmpty } from 'utils'
|
||||
@ -7,16 +7,26 @@ import { forbidden } from 'utils/api'
|
||||
export const canWriteTypebots = (
|
||||
typebotIds: string[] | string,
|
||||
user: Pick<User, 'email' | 'id'>
|
||||
): Prisma.TypebotWhereInput => ({
|
||||
id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds },
|
||||
workspace: isNotEmpty(env('E2E_TEST'))
|
||||
? undefined
|
||||
): Prisma.TypebotWhereInput =>
|
||||
isNotEmpty(env('E2E_TEST'))
|
||||
? {}
|
||||
: {
|
||||
members: {
|
||||
some: { userId: user.id, role: { not: WorkspaceRole.GUEST } },
|
||||
},
|
||||
},
|
||||
})
|
||||
id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds },
|
||||
OR: [
|
||||
{
|
||||
workspace: {
|
||||
members: {
|
||||
some: { userId: user.id, role: { not: WorkspaceRole.GUEST } },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
collaborators: {
|
||||
some: { userId: user.id, type: { not: CollaborationType.READ } },
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const canReadTypebots = (
|
||||
typebotIds: string | string[],
|
||||
|
Reference in New Issue
Block a user