2
0

🔒 Improve workspace API role filtering

This commit is contained in:
Baptiste Arnaud
2023-08-17 15:11:50 +02:00
parent 8810aa8ddb
commit 906845bd76
19 changed files with 288 additions and 138 deletions

View File

@ -1,15 +1,16 @@
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc' import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server' import { TRPCError } from '@trpc/server'
import { Typebot, typebotSchema } from '@typebot.io/schemas' import { LogicBlockType, typebotSchema } from '@typebot.io/schemas'
import { z } from 'zod' import { z } from 'zod'
import { getUserRoleInWorkspace } from '@/features/workspace/helpers/getUserRoleInWorkspace' import { isReadTypebotForbidden } from '@/features/typebot/helpers/isReadTypebotForbidden'
import { isDefined } from '@typebot.io/lib'
export const getLinkedTypebots = authenticatedProcedure export const getLinkedTypebots = authenticatedProcedure
.meta({ .meta({
openapi: { openapi: {
method: 'GET', method: 'GET',
path: '/linkedTypebots', path: '/typebots/{typebotId}/linkedTypebots',
protect: true, protect: true,
summary: 'Get linked typebots', summary: 'Get linked typebots',
tags: ['Typebot'], tags: ['Typebot'],
@ -17,8 +18,7 @@ export const getLinkedTypebots = authenticatedProcedure
}) })
.input( .input(
z.object({ z.object({
workspaceId: z.string(), typebotId: z.string(),
typebotIds: z.string().describe('Comma separated list of typebot ids'),
}) })
) )
.output( .output(
@ -33,20 +33,10 @@ export const getLinkedTypebots = authenticatedProcedure
), ),
}) })
) )
.query(async ({ input: { workspaceId, typebotIds }, ctx: { user } }) => { .query(async ({ input: { typebotId }, ctx: { user } }) => {
const typebotIdsArray = typebotIds.split(',') const typebot = await prisma.typebot.findFirst({
const workspace = await prisma.workspace.findUnique({
where: { id: workspaceId },
select: { members: true },
})
const userRole = getUserRoleInWorkspace(user.id, workspace?.members)
if (userRole === undefined)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' })
const typebots = (await prisma.typebot.findMany({
where: { where: {
isArchived: { not: true }, id: typebotId,
id: { in: typebotIdsArray },
workspaceId,
}, },
select: { select: {
id: true, id: true,
@ -54,18 +44,69 @@ export const getLinkedTypebots = authenticatedProcedure
variables: true, variables: true,
name: true, name: true,
createdAt: true, createdAt: true,
workspaceId: true,
collaborators: {
select: {
type: true,
userId: true,
}, },
})) as Pick<Typebot, 'id' | 'groups' | 'variables' | 'name' | 'createdAt'>[] },
},
})
// To avoid the Out of sort memory error, we sort the typebots manually if (!typebot || (await isReadTypebotForbidden(typebot, user)))
const sortedTypebots = typebots.sort((a, b) => { throw new TRPCError({ code: 'NOT_FOUND', message: 'No typebot found' })
const linkedTypebotIds =
typebotSchema.shape.groups
.parse(typebot.groups)
.flatMap((group) => group.blocks)
.reduce<string[]>(
(typebotIds, block) =>
block.type === LogicBlockType.TYPEBOT_LINK &&
isDefined(block.options.typebotId) &&
!typebotIds.includes(block.options.typebotId)
? [...typebotIds, block.options.typebotId]
: typebotIds,
[]
) ?? []
if (!linkedTypebotIds.length) return { typebots: [] }
const typebots = (
await prisma.typebot.findMany({
where: {
isArchived: { not: true },
id: { in: linkedTypebotIds },
},
select: {
id: true,
groups: true,
variables: true,
name: true,
createdAt: true,
workspaceId: true,
collaborators: {
select: {
type: true,
userId: true,
},
},
},
})
)
.filter(async (typebot) => !(await isReadTypebotForbidden(typebot, user)))
// To avoid the out of sort memory error, we sort the typebots manually
.sort((a, b) => {
return b.createdAt.getTime() - a.createdAt.getTime() return b.createdAt.getTime() - a.createdAt.getTime()
}) })
.map((typebot) => ({
if (!typebots) ...typebot,
throw new TRPCError({ code: 'NOT_FOUND', message: 'No typebots found' }) groups: typebotSchema.shape.groups.parse(typebot.groups),
variables: typebotSchema.shape.variables.parse(typebot.variables),
}))
return { return {
typebots: sortedTypebots, typebots,
} }
}) })

View File

@ -4,6 +4,7 @@ import { TypebotLinkOptions } from '@typebot.io/schemas'
import { byId } from '@typebot.io/lib' import { byId } from '@typebot.io/lib'
import { GroupsDropdown } from './GroupsDropdown' import { GroupsDropdown } from './GroupsDropdown'
import { TypebotsDropdown } from './TypebotsDropdown' import { TypebotsDropdown } from './TypebotsDropdown'
import { useEffect, useState } from 'react'
type Props = { type Props = {
options: TypebotLinkOptions options: TypebotLinkOptions
@ -11,13 +12,22 @@ type Props = {
} }
export const TypebotLinkForm = ({ options, onOptionsChange }: Props) => { export const TypebotLinkForm = ({ options, onOptionsChange }: Props) => {
const { linkedTypebots, typebot } = useTypebot() const { linkedTypebots, typebot, save } = useTypebot()
const [linkedTypebotId, setLinkedTypebotId] = useState(options.typebotId)
const handleTypebotIdChange = async (
typebotId: string | 'current' | undefined
) => onOptionsChange({ ...options, typebotId })
const handleTypebotIdChange = (typebotId: string | 'current' | undefined) =>
onOptionsChange({ ...options, typebotId })
const handleGroupIdChange = (groupId: string | undefined) => const handleGroupIdChange = (groupId: string | undefined) =>
onOptionsChange({ ...options, groupId }) onOptionsChange({ ...options, groupId })
useEffect(() => {
if (linkedTypebotId === options.typebotId) return
setLinkedTypebotId(options.typebotId)
save().then()
}, [linkedTypebotId, options.typebotId, save])
return ( return (
<Stack> <Stack>
{typebot && ( {typebot && (

View File

@ -7,6 +7,7 @@ import { openAICredentialsSchema } from '@typebot.io/schemas/features/blocks/int
import { smtpCredentialsSchema } from '@typebot.io/schemas/features/blocks/integrations/sendEmail' import { smtpCredentialsSchema } from '@typebot.io/schemas/features/blocks/integrations/sendEmail'
import { encrypt } from '@typebot.io/lib/api/encryption' import { encrypt } from '@typebot.io/lib/api/encryption'
import { z } from 'zod' import { z } from 'zod'
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden copy'
const inputShape = { const inputShape = {
data: true, data: true,
@ -44,11 +45,10 @@ export const createCredentials = authenticatedProcedure
const workspace = await prisma.workspace.findFirst({ const workspace = await prisma.workspace.findFirst({
where: { where: {
id: credentials.workspaceId, id: credentials.workspaceId,
members: { some: { userId: user.id } },
}, },
select: { id: true }, select: { id: true, members: true },
}) })
if (!workspace) if (!workspace || (await isWriteWorkspaceForbidden(workspace, user)))
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' }) throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' })
const { encryptedData, iv } = await encrypt(credentials.data) const { encryptedData, iv } = await encrypt(credentials.data)

View File

@ -2,6 +2,7 @@ import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc' import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server' import { TRPCError } from '@trpc/server'
import { z } from 'zod' import { z } from 'zod'
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden copy'
export const deleteCredentials = authenticatedProcedure export const deleteCredentials = authenticatedProcedure
.meta({ .meta({
@ -29,11 +30,10 @@ export const deleteCredentials = authenticatedProcedure
const workspace = await prisma.workspace.findFirst({ const workspace = await prisma.workspace.findFirst({
where: { where: {
id: workspaceId, id: workspaceId,
members: { some: { userId: user.id } },
}, },
select: { id: true }, select: { id: true, members: true },
}) })
if (!workspace) if (!workspace || (await isWriteWorkspaceForbidden(workspace, user)))
throw new TRPCError({ throw new TRPCError({
code: 'NOT_FOUND', code: 'NOT_FOUND',
message: 'Workspace not found', message: 'Workspace not found',

View File

@ -6,6 +6,7 @@ import { googleSheetsCredentialsSchema } from '@typebot.io/schemas/features/bloc
import { openAICredentialsSchema } from '@typebot.io/schemas/features/blocks/integrations/openai' import { openAICredentialsSchema } from '@typebot.io/schemas/features/blocks/integrations/openai'
import { smtpCredentialsSchema } from '@typebot.io/schemas/features/blocks/integrations/sendEmail' import { smtpCredentialsSchema } from '@typebot.io/schemas/features/blocks/integrations/sendEmail'
import { z } from 'zod' import { z } from 'zod'
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
export const listCredentials = authenticatedProcedure export const listCredentials = authenticatedProcedure
.meta({ .meta({
@ -35,21 +36,23 @@ export const listCredentials = authenticatedProcedure
const workspace = await prisma.workspace.findFirst({ const workspace = await prisma.workspace.findFirst({
where: { where: {
id: workspaceId, id: workspaceId,
members: { some: { userId: user.id } },
}, },
select: { id: true }, select: {
}) id: true,
if (!workspace) members: true,
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' }) credentials: {
const credentials = await prisma.credentials.findMany({
where: { where: {
type, type,
workspaceId,
}, },
select: { select: {
id: true, id: true,
name: true, name: true,
}, },
},
},
}) })
return { credentials } if (!workspace || (await isReadWorkspaceFobidden(workspace, user)))
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' })
return { credentials: workspace.credentials }
}) })

View File

@ -131,9 +131,10 @@ export const TypebotProvider = ({
{ redo, undo, flush, canRedo, canUndo, set: setLocalTypebot }, { redo, undo, flush, canRedo, canUndo, set: setLocalTypebot },
] = useUndo<Typebot>(undefined) ] = useUndo<Typebot>(undefined)
const linkedTypebotIds = const linkedTypebotIds = useMemo(
localTypebot?.groups () =>
.flatMap((b) => b.blocks) typebot?.groups
.flatMap((group) => group.blocks)
.reduce<string[]>( .reduce<string[]>(
(typebotIds, block) => (typebotIds, block) =>
block.type === LogicBlockType.TYPEBOT_LINK && block.type === LogicBlockType.TYPEBOT_LINK &&
@ -142,16 +143,16 @@ export const TypebotProvider = ({
? [...typebotIds, block.options.typebotId] ? [...typebotIds, block.options.typebotId]
: typebotIds, : typebotIds,
[] []
) ?? [] ) ?? [],
[typebot?.groups]
)
const { data: linkedTypebotsData } = trpc.getLinkedTypebots.useQuery( const { data: linkedTypebotsData } = trpc.getLinkedTypebots.useQuery(
{ {
workspaceId: localTypebot?.workspaceId as string, typebotId: typebot?.id as string,
typebotIds: linkedTypebotIds.join(','),
}, },
{ {
enabled: enabled: isDefined(typebot?.id) && linkedTypebotIds.length > 0,
isDefined(localTypebot?.workspaceId) && linkedTypebotIds.length > 0,
onError: (error) => onError: (error) =>
showToast({ showToast({
title: 'Error while fetching linkedTypebots', title: 'Error while fetching linkedTypebots',

View File

@ -16,6 +16,7 @@ import {
sanitizeSettings, sanitizeSettings,
} from '../helpers/sanitizers' } from '../helpers/sanitizers'
import { createId } from '@paralleldrive/cuid2' import { createId } from '@paralleldrive/cuid2'
import { sendTelemetryEvents } from '@typebot.io/lib/telemetry/sendTelemetryEvent'
export const createTypebot = authenticatedProcedure export const createTypebot = authenticatedProcedure
.meta({ .meta({
@ -106,7 +107,21 @@ export const createTypebot = authenticatedProcedure
}, },
}) })
return { typebot: typebotSchema.parse(newTypebot) } const parsedNewTypebot = typebotSchema.parse(newTypebot)
await sendTelemetryEvents([
{
name: 'Typebot created',
workspaceId: parsedNewTypebot.workspaceId,
typebotId: parsedNewTypebot.id,
userId: user.id,
data: {
name: newTypebot.name,
},
},
])
return { typebot: parsedNewTypebot }
}) })
const defaultGroups = () => { const defaultGroups = () => {

View File

@ -4,6 +4,7 @@ import { TRPCError } from '@trpc/server'
import { typebotSchema } from '@typebot.io/schemas' import { typebotSchema } from '@typebot.io/schemas'
import { z } from 'zod' import { z } from 'zod'
import { isWriteTypebotForbidden } from '../helpers/isWriteTypebotForbidden' import { isWriteTypebotForbidden } from '../helpers/isWriteTypebotForbidden'
import { sendTelemetryEvents } from '@typebot.io/lib/telemetry/sendTelemetryEvent'
export const publishTypebot = authenticatedProcedure export const publishTypebot = authenticatedProcedure
.meta({ .meta({
@ -41,7 +42,7 @@ export const publishTypebot = authenticatedProcedure
) )
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' }) throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
if (existingTypebot.publishedTypebot) { if (existingTypebot.publishedTypebot)
await prisma.publicTypebot.updateMany({ await prisma.publicTypebot.updateMany({
where: { where: {
id: existingTypebot.publishedTypebot.id, id: existingTypebot.publishedTypebot.id,
@ -59,16 +60,16 @@ export const publishTypebot = authenticatedProcedure
theme: typebotSchema.shape.theme.parse(existingTypebot.theme), theme: typebotSchema.shape.theme.parse(existingTypebot.theme),
}, },
}) })
return { message: 'success' } else
}
await prisma.publicTypebot.createMany({ await prisma.publicTypebot.createMany({
data: { data: {
version: existingTypebot.version, version: existingTypebot.version,
typebotId: existingTypebot.id, typebotId: existingTypebot.id,
edges: typebotSchema.shape.edges.parse(existingTypebot.edges), edges: typebotSchema.shape.edges.parse(existingTypebot.edges),
groups: typebotSchema.shape.groups.parse(existingTypebot.groups), groups: typebotSchema.shape.groups.parse(existingTypebot.groups),
settings: typebotSchema.shape.settings.parse(existingTypebot.settings), settings: typebotSchema.shape.settings.parse(
existingTypebot.settings
),
variables: typebotSchema.shape.variables.parse( variables: typebotSchema.shape.variables.parse(
existingTypebot.variables existingTypebot.variables
), ),
@ -76,5 +77,18 @@ export const publishTypebot = authenticatedProcedure
}, },
}) })
await sendTelemetryEvents([
{
name: 'Typebot published',
workspaceId: existingTypebot.workspaceId,
typebotId: existingTypebot.id,
userId: user.id,
data: {
name: existingTypebot.name,
isFirstPublish: existingTypebot.publishedTypebot ? undefined : true,
},
},
])
return { message: 'success' } return { message: 'success' }
}) })

View File

@ -1,6 +1,8 @@
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc' import { authenticatedProcedure } from '@/helpers/server/trpc'
import { z } from 'zod' import { z } from 'zod'
import { isAdminWriteWorkspaceForbidden } from '../helpers/isAdminWriteWorkspaceForbidden'
import { TRPCError } from '@trpc/server'
export const deleteWorkspace = authenticatedProcedure export const deleteWorkspace = authenticatedProcedure
.meta({ .meta({
@ -23,6 +25,14 @@ export const deleteWorkspace = authenticatedProcedure
}) })
) )
.mutation(async ({ input: { workspaceId }, ctx: { user } }) => { .mutation(async ({ input: { workspaceId }, ctx: { user } }) => {
const workspace = await prisma.workspace.findFirst({
where: { id: workspaceId },
include: { members: true },
})
if (!workspace || (await isAdminWriteWorkspaceForbidden(workspace, user)))
throw new TRPCError({ code: 'NOT_FOUND', message: 'No workspaces found' })
await prisma.workspace.deleteMany({ await prisma.workspace.deleteMany({
where: { members: { some: { userId: user.id } }, id: workspaceId }, where: { members: { some: { userId: user.id } }, id: workspaceId },
}) })

View File

@ -1,8 +1,9 @@
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc' import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server' import { TRPCError } from '@trpc/server'
import { Workspace, workspaceSchema } from '@typebot.io/schemas' import { workspaceSchema } from '@typebot.io/schemas'
import { z } from 'zod' import { z } from 'zod'
import { isReadWorkspaceFobidden } from '../helpers/isReadWorkspaceFobidden'
export const getWorkspace = authenticatedProcedure export const getWorkspace = authenticatedProcedure
.meta({ .meta({
@ -25,11 +26,12 @@ export const getWorkspace = authenticatedProcedure
}) })
) )
.query(async ({ input: { workspaceId }, ctx: { user } }) => { .query(async ({ input: { workspaceId }, ctx: { user } }) => {
const workspace = (await prisma.workspace.findFirst({ const workspace = await prisma.workspace.findFirst({
where: { members: { some: { userId: user.id } }, id: workspaceId }, where: { id: workspaceId },
})) as Workspace | null include: { members: true },
})
if (!workspace) if (!workspace || (await isReadWorkspaceFobidden(workspace, user)))
throw new TRPCError({ code: 'NOT_FOUND', message: 'No workspaces found' }) throw new TRPCError({ code: 'NOT_FOUND', message: 'No workspaces found' })
return { return {

View File

@ -1,11 +1,9 @@
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc' import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server' import { TRPCError } from '@trpc/server'
import { import { workspaceInvitationSchema } from '@typebot.io/schemas'
WorkspaceInvitation,
workspaceInvitationSchema,
} from '@typebot.io/schemas'
import { z } from 'zod' import { z } from 'zod'
import { isReadWorkspaceFobidden } from '../helpers/isReadWorkspaceFobidden'
export const listInvitationsInWorkspace = authenticatedProcedure export const listInvitationsInWorkspace = authenticatedProcedure
.meta({ .meta({
@ -28,19 +26,13 @@ export const listInvitationsInWorkspace = authenticatedProcedure
}) })
) )
.query(async ({ input: { workspaceId }, ctx: { user } }) => { .query(async ({ input: { workspaceId }, ctx: { user } }) => {
const invitations = (await prisma.workspaceInvitation.findMany({ const workspace = await prisma.workspace.findFirst({
where: { where: { id: workspaceId },
workspaceId, include: { members: true, invitations: true },
workspace: { members: { some: { userId: user.id } } },
},
select: { createdAt: true, email: true, type: true },
})) as WorkspaceInvitation[]
if (!invitations)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'No invitations found',
}) })
return { invitations } if (!workspace || (await isReadWorkspaceFobidden(workspace, user)))
throw new TRPCError({ code: 'NOT_FOUND', message: 'No workspaces found' })
return { invitations: workspace.invitations }
}) })

View File

@ -1,8 +1,9 @@
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc' import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server' import { TRPCError } from '@trpc/server'
import { WorkspaceMember, workspaceMemberSchema } from '@typebot.io/schemas' import { workspaceMemberSchema } from '@typebot.io/schemas'
import { z } from 'zod' import { z } from 'zod'
import { isReadWorkspaceFobidden } from '../helpers/isReadWorkspaceFobidden'
export const listMembersInWorkspace = authenticatedProcedure export const listMembersInWorkspace = authenticatedProcedure
.meta({ .meta({
@ -25,13 +26,25 @@ export const listMembersInWorkspace = authenticatedProcedure
}) })
) )
.query(async ({ input: { workspaceId }, ctx: { user } }) => { .query(async ({ input: { workspaceId }, ctx: { user } }) => {
const members = (await prisma.memberInWorkspace.findMany({ const workspace = await prisma.workspace.findFirst({
where: { userId: user.id, workspaceId }, where: { id: workspaceId },
include: { user: { select: { name: true, email: true, image: true } } }, include: {
})) as WorkspaceMember[] members: {
include: {
if (!members) user: true,
throw new TRPCError({ code: 'NOT_FOUND', message: 'No members found' }) },
},
return { members } },
})
if (!workspace || (await isReadWorkspaceFobidden(workspace, user)))
throw new TRPCError({ code: 'NOT_FOUND', message: 'No workspaces found' })
return {
members: workspace.members.map((member) => ({
role: member.role,
user: member.user,
workspaceId,
})),
}
}) })

View File

@ -1,7 +1,7 @@
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc' import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server' import { TRPCError } from '@trpc/server'
import { Workspace, workspaceSchema } from '@typebot.io/schemas' import { workspaceSchema } from '@typebot.io/schemas'
import { z } from 'zod' import { z } from 'zod'
export const listWorkspaces = authenticatedProcedure export const listWorkspaces = authenticatedProcedure
@ -23,10 +23,10 @@ export const listWorkspaces = authenticatedProcedure
}) })
) )
.query(async ({ ctx: { user } }) => { .query(async ({ ctx: { user } }) => {
const workspaces = (await prisma.workspace.findMany({ const workspaces = await prisma.workspace.findMany({
where: { members: { some: { userId: user.id } } }, where: { members: { some: { userId: user.id } } },
select: { name: true, id: true, icon: true, plan: true }, select: { name: true, id: true, icon: true, plan: true },
})) as Pick<Workspace, 'id' | 'name' | 'icon' | 'plan'>[] })
if (!workspaces) if (!workspaces)
throw new TRPCError({ code: 'NOT_FOUND', message: 'No workspaces found' }) throw new TRPCError({ code: 'NOT_FOUND', message: 'No workspaces found' })

View File

@ -1,8 +1,9 @@
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc' import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server' import { TRPCError } from '@trpc/server'
import { Workspace, workspaceSchema } from '@typebot.io/schemas' import { workspaceSchema } from '@typebot.io/schemas'
import { z } from 'zod' import { z } from 'zod'
import { isAdminWriteWorkspaceForbidden } from '../helpers/isAdminWriteWorkspaceForbidden'
export const updateWorkspace = authenticatedProcedure export const updateWorkspace = authenticatedProcedure
.meta({ .meta({
@ -32,13 +33,20 @@ export const updateWorkspace = authenticatedProcedure
data: updates, data: updates,
}) })
const workspace = (await prisma.workspace.findFirst({ const workspace = await prisma.workspace.findFirst({
where: { members: { some: { userId: user.id } }, id: workspaceId }, where: { members: { some: { userId: user.id } }, id: workspaceId },
})) as Workspace | null include: { members: true },
})
if (!workspace) if (!workspace)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' }) throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' })
if (await isAdminWriteWorkspaceForbidden(workspace, user))
throw new TRPCError({
code: 'FORBIDDEN',
message: 'You are not allowed to update this workspace',
})
return { return {
workspace, workspace,
} }

View File

@ -0,0 +1,13 @@
import { MemberInWorkspace, User } from '@typebot.io/prisma'
export const isAdminWriteWorkspaceForbidden = async (
workspace: {
members: MemberInWorkspace[]
},
user: Pick<User, 'email' | 'id'>
) => {
const userRole = workspace.members.find(
(member) => member.userId === user.id
)?.role
return !userRole || userRole !== 'ADMIN'
}

View File

@ -0,0 +1,15 @@
import { MemberInWorkspace, User } from '@typebot.io/prisma'
export const isReadWorkspaceFobidden = async (
workspace: {
members: MemberInWorkspace[]
},
user: Pick<User, 'email' | 'id'>
) => {
if (
process.env.ADMIN_EMAIL === user.email ||
workspace.members.find((member) => member.userId === user.id)
)
return false
return true
}

View File

@ -0,0 +1,13 @@
import { MemberInWorkspace, User } from '@typebot.io/prisma'
export const isWriteWorkspaceForbidden = async (
workspace: {
members: MemberInWorkspace[]
},
user: Pick<User, 'email' | 'id'>
) => {
const userRole = workspace.members.find(
(member) => member.userId === user.id
)?.role
return !userRole || userRole === 'GUEST'
}

View File

@ -23,12 +23,12 @@
"cz-emoji": "1.3.2-canary.2", "cz-emoji": "1.3.2-canary.2",
"husky": "^8.0.3", "husky": "^8.0.3",
"prettier": "2.8.8", "prettier": "2.8.8",
"turbo": "1.10.7" "turbo": "1.10.12"
}, },
"config": { "config": {
"commitizen": { "commitizen": {
"path": "node_modules/cz-emoji" "path": "node_modules/cz-emoji"
} }
}, },
"packageManager": "pnpm@8.6.7" "packageManager": "pnpm@8.6.12"
} }

44
pnpm-lock.yaml generated
View File

@ -21,8 +21,8 @@ importers:
specifier: 2.8.8 specifier: 2.8.8
version: 2.8.8 version: 2.8.8
turbo: turbo:
specifier: 1.10.7 specifier: 1.10.12
version: 1.10.7 version: 1.10.12
apps/builder: apps/builder:
dependencies: dependencies:
@ -23117,65 +23117,65 @@ packages:
fsevents: 2.3.2 fsevents: 2.3.2
dev: true dev: true
/turbo-darwin-64@1.10.7: /turbo-darwin-64@1.10.12:
resolution: {integrity: sha512-N2MNuhwrl6g7vGuz4y3fFG2aR1oCs0UZ5HKl8KSTn/VC2y2YIuLGedQ3OVbo0TfEvygAlF3QGAAKKtOCmGPNKA==} resolution: {integrity: sha512-vmDfGVPl5/aFenAbOj3eOx3ePNcWVUyZwYr7taRl0ZBbmv2TzjRiFotO4vrKCiTVnbqjQqAFQWY2ugbqCI1kOQ==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/turbo-darwin-arm64@1.10.7: /turbo-darwin-arm64@1.10.12:
resolution: {integrity: sha512-WbJkvjU+6qkngp7K4EsswOriO3xrNQag7YEGRtfLoDdMTk4O4QTeU6sfg2dKfDsBpTidTvEDwgIYJhYVGzrz9Q==} resolution: {integrity: sha512-3JliEESLNX2s7g54SOBqqkqJ7UhcOGkS0ywMr5SNuvF6kWVTbuUq7uBU/sVbGq8RwvK1ONlhPvJne5MUqBCTCQ==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/turbo-linux-64@1.10.7: /turbo-linux-64@1.10.12:
resolution: {integrity: sha512-x1CF2CDP1pDz/J8/B2T0hnmmOQI2+y11JGIzNP0KtwxDM7rmeg3DDTtDM/9PwGqfPotN9iVGgMiMvBuMFbsLhg==} resolution: {integrity: sha512-siYhgeX0DidIfHSgCR95b8xPee9enKSOjCzx7EjTLmPqPaCiVebRYvbOIYdQWRqiaKh9yfhUtFmtMOMScUf1gg==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/turbo-linux-arm64@1.10.7: /turbo-linux-arm64@1.10.12:
resolution: {integrity: sha512-JtnBmaBSYbs7peJPkXzXxsRGSGBmBEIb6/kC8RRmyvPAMyqF8wIex0pttsI+9plghREiGPtRWv/lfQEPRlXnNQ==} resolution: {integrity: sha512-K/ZhvD9l4SslclaMkTiIrnfcACgos79YcAo4kwc8bnMQaKuUeRpM15sxLpZp3xDjDg8EY93vsKyjaOhdFG2UbA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/turbo-windows-64@1.10.7: /turbo-windows-64@1.10.12:
resolution: {integrity: sha512-7A/4CByoHdolWS8dg3DPm99owfu1aY/W0V0+KxFd0o2JQMTQtoBgIMSvZesXaWM57z3OLsietFivDLQPuzE75w==} resolution: {integrity: sha512-7FSgSwvktWDNOqV65l9AbZwcoueAILeE4L7JvjauNASAjjbuzXGCEq5uN8AQU3U5BOFj4TdXrVmO2dX+lLu8Zg==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/turbo-windows-arm64@1.10.7: /turbo-windows-arm64@1.10.12:
resolution: {integrity: sha512-D36K/3b6+hqm9IBAymnuVgyePktwQ+F0lSXr2B9JfAdFPBktSqGmp50JNC7pahxhnuCLj0Vdpe9RqfnJw5zATA==} resolution: {integrity: sha512-gCNXF52dwom1HLY9ry/cneBPOKTBHhzpqhMylcyvJP0vp9zeMQQkt6yjYv+6QdnmELC92CtKNp2FsNZo+z0pyw==}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/turbo@1.10.7: /turbo@1.10.12:
resolution: {integrity: sha512-xm0MPM28TWx1e6TNC3wokfE5eaDqlfi0G24kmeHupDUZt5Wd0OzHFENEHMPqEaNKJ0I+AMObL6nbSZonZBV2HA==} resolution: {integrity: sha512-WM3+jTfQWnB9W208pmP4oeehZcC6JQNlydb/ZHMRrhmQa+htGhWLCzd6Q9rLe0MwZLPpSPFV2/bN5egCLyoKjQ==}
hasBin: true hasBin: true
requiresBuild: true requiresBuild: true
optionalDependencies: optionalDependencies:
turbo-darwin-64: 1.10.7 turbo-darwin-64: 1.10.12
turbo-darwin-arm64: 1.10.7 turbo-darwin-arm64: 1.10.12
turbo-linux-64: 1.10.7 turbo-linux-64: 1.10.12
turbo-linux-arm64: 1.10.7 turbo-linux-arm64: 1.10.12
turbo-windows-64: 1.10.7 turbo-windows-64: 1.10.12
turbo-windows-arm64: 1.10.7 turbo-windows-arm64: 1.10.12
dev: true dev: true
/type-check@0.3.2: /type-check@0.3.2: