+ {data.featureLabel && (
+ {data.featureLabel}
+ )}
{features.map((feature, index) => (
{feature}
diff --git a/apps/landing-page/components/common/TableCells.tsx b/apps/landing-page/components/common/TableCells.tsx
index 3c4a79c859d..59709259a39 100644
--- a/apps/landing-page/components/common/TableCells.tsx
+++ b/apps/landing-page/components/common/TableCells.tsx
@@ -1,11 +1,11 @@
-import { CheckIcon } from 'assets/icons/CheckIcon'
+import { CheckCircleIcon } from 'assets/icons/CheckCircleIcon'
import { CloseIcon } from 'assets/icons/CloseIcon'
import { Td, Text } from '@chakra-ui/react'
import React, { ReactNode } from 'react'
export const Yes = (props: { children?: ReactNode }) => (
-
+
{props.children && (
{props.children}
diff --git a/apps/landing-page/pages/about.tsx b/apps/landing-page/pages/about.tsx
index 271a707122e..1f5d8d90ca7 100644
--- a/apps/landing-page/pages/about.tsx
+++ b/apps/landing-page/pages/about.tsx
@@ -64,7 +64,7 @@ const AboutPage = () => {
You can use the tool for free but your forms will contain a
"Made with Typebot" small badge that potentially gets people to know
about the product. If you want to remove it and also have access to
- other advanced features, you have to subscribe for $30 per month.
+ other advanced features, you have to subscribe for $39 per month.
If you have any questions, feel free to reach out to me at{' '}
diff --git a/apps/landing-page/pages/pricing.tsx b/apps/landing-page/pages/pricing.tsx
index 92287f2712a..89f2df81476 100644
--- a/apps/landing-page/pages/pricing.tsx
+++ b/apps/landing-page/pages/pricing.tsx
@@ -14,15 +14,26 @@ import { Header } from 'components/common/Header/Header'
import { NextChakraLink } from 'components/common/nextChakraAdapters/NextChakraLink'
import { SocialMetaTags } from 'components/common/SocialMetaTags'
import { BackgroundPolygons } from 'components/Homepage/Hero/BackgroundPolygons'
+import { PlanComparisonTables } from 'components/PricingPage/PlanComparisonTables'
import { PricingCard } from 'components/PricingPage/PricingCard'
import { ActionButton } from 'components/PricingPage/PricingCard/ActionButton'
import { useEffect, useState } from 'react'
const Pricing = () => {
- const [price, setPrice] = useState<'$30' | '25€' | ''>('')
+ const [price, setPrice] = useState<{
+ personalPro: '$39' | '39€' | ''
+ team: '$99' | '99€' | ''
+ }>({
+ personalPro: '',
+ team: '',
+ })
useEffect(() => {
- setPrice(navigator.languages.find((l) => l.includes('fr')) ? '25€' : '$30')
+ setPrice(
+ navigator.languages.find((l) => l.includes('fr'))
+ ? { personalPro: '39€', team: '99€' }
+ : { personalPro: '$39', team: '$99' }
+ )
}, [])
return (
@@ -49,12 +60,12 @@ const Pricing = () => {
px={[4, 0]}
mt={[20, 32]}
w="full"
- maxW="900px"
+ maxW="1200px"
>
{
href="https://app.typebot.io/register"
_hover={{ textDecor: 'none' }}
>
- Try now
+ Get started
}
/>
{
borderColor="orange.200"
button={
@@ -99,10 +110,38 @@ const Pricing = () => {
}
/>
+
+ Subscribe now
+
+ }
+ />
-
- Frequently asked questions
-
+
+
+ Compare plans & features
+
+
+
+ Frequently asked questions
+
+
@@ -114,39 +153,6 @@ const Pricing = () => {
const Faq = () => {
return (
-
-
-
-
- How can I use Typebot with my team?
-
-
-
-
-
- Typebot allows you to invite your colleagues to collaborate on any of
- your typebot. You can give him access as a reader or an editor. Your
- colleague's account can be a free account.
-
- I'm working on a better solution for teams with shared workspaces and
- other team-oriented features.
-
-
-
-
-
-
-
- How many seats will I have with the Pro plan?
-
-
-
-
-
- You'll have only one seat. You can invite your colleagues to
- collaborate on your typebots even though they have a free account.
-
-
diff --git a/apps/viewer/pages/api/typebots.ts b/apps/viewer/pages/api/typebots.ts
index d7b928aea95..30478620090 100644
--- a/apps/viewer/pages/api/typebots.ts
+++ b/apps/viewer/pages/api/typebots.ts
@@ -9,7 +9,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await authenticateUser(req)
if (!user) return res.status(401).json({ message: 'Not authenticated' })
const typebots = await prisma.typebot.findMany({
- where: { ownerId: user.id },
+ where: { workspace: { members: { some: { userId: user.id } } } },
select: { name: true, publishedTypebotId: true, id: true },
})
return res.send({ typebots })
diff --git a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/sampleResult.ts b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/sampleResult.ts
index 2ea8d11cc3e..30759b88288 100644
--- a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/sampleResult.ts
+++ b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/sampleResult.ts
@@ -6,13 +6,16 @@ import { parseSampleResult } from 'services/api/webhooks'
import { methodNotAllowed } from 'utils'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
+ const user = await authenticateUser(req)
+ if (!user) return res.status(401).json({ message: 'Not authenticated' })
if (req.method === 'GET') {
- const user = await authenticateUser(req)
- if (!user) return res.status(401).json({ message: 'Not authenticated' })
const typebotId = req.query.typebotId.toString()
const stepId = req.query.blockId.toString()
- const typebot = (await prisma.typebot.findUnique({
- where: { id_ownerId: { id: typebotId, ownerId: user.id } },
+ const typebot = (await prisma.typebot.findFirst({
+ where: {
+ id: typebotId,
+ workspace: { members: { some: { userId: user.id } } },
+ },
})) as unknown as Typebot | undefined
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
const step = typebot.blocks
diff --git a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/sampleResult.ts b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/sampleResult.ts
index 9b067ef3b41..ebcb1256332 100644
--- a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/sampleResult.ts
+++ b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/sampleResult.ts
@@ -6,13 +6,16 @@ import { parseSampleResult } from 'services/api/webhooks'
import { methodNotAllowed } from 'utils'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
+ const user = await authenticateUser(req)
+ if (!user) return res.status(401).json({ message: 'Not authenticated' })
if (req.method === 'GET') {
- const user = await authenticateUser(req)
- if (!user) return res.status(401).json({ message: 'Not authenticated' })
const typebotId = req.query.typebotId.toString()
const blockId = req.query.blockId.toString()
- const typebot = (await prisma.typebot.findUnique({
- where: { id_ownerId: { id: typebotId, ownerId: user.id } },
+ const typebot = (await prisma.typebot.findFirst({
+ where: {
+ id: typebotId,
+ workspace: { members: { some: { userId: user.id } } },
+ },
})) as unknown as Typebot | undefined
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
const linkedTypebots = await getLinkedTypebots(typebot, user)
diff --git a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/subscribeWebhook.ts b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/subscribeWebhook.ts
index f66a927521d..efc6b501182 100644
--- a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/subscribeWebhook.ts
+++ b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/subscribeWebhook.ts
@@ -6,9 +6,9 @@ import { authenticateUser } from 'services/api/utils'
import { byId, methodNotAllowed } from 'utils'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
+ const user = await authenticateUser(req)
+ if (!user) return res.status(401).json({ message: 'Not authenticated' })
if (req.method === 'POST') {
- const user = await authenticateUser(req)
- if (!user) return res.status(401).json({ message: 'Not authenticated' })
const body = req.body as Record
if (!('url' in body))
return res.status(403).send({ message: 'url is missing in body' })
@@ -16,8 +16,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const typebotId = req.query.typebotId.toString()
const blockId = req.query.blockId.toString()
const stepId = req.query.stepId.toString()
- const typebot = (await prisma.typebot.findUnique({
- where: { id_ownerId: { id: typebotId, ownerId: user.id } },
+ const typebot = (await prisma.typebot.findFirst({
+ where: {
+ id: typebotId,
+ workspace: { members: { some: { userId: user.id } } },
+ },
})) as unknown as Typebot | undefined
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
try {
diff --git a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/unsubscribeWebhook.ts b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/unsubscribeWebhook.ts
index 5015610415c..3b1c16465f1 100644
--- a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/unsubscribeWebhook.ts
+++ b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/unsubscribeWebhook.ts
@@ -6,14 +6,17 @@ import { authenticateUser } from 'services/api/utils'
import { byId, methodNotAllowed } from 'utils'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
+ const user = await authenticateUser(req)
+ if (!user) return res.status(401).json({ message: 'Not authenticated' })
if (req.method === 'POST') {
- const user = await authenticateUser(req)
- if (!user) return res.status(401).json({ message: 'Not authenticated' })
const typebotId = req.query.typebotId.toString()
const blockId = req.query.blockId.toString()
const stepId = req.query.stepId.toString()
- const typebot = (await prisma.typebot.findUnique({
- where: { id_ownerId: { id: typebotId, ownerId: user.id } },
+ const typebot = (await prisma.typebot.findFirst({
+ where: {
+ id: typebotId,
+ workspace: { members: { some: { userId: user.id } } },
+ },
})) as unknown as Typebot | undefined
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
try {
diff --git a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/subscribeWebhook.ts b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/subscribeWebhook.ts
index fba8c2f3453..1d3318cc4f8 100644
--- a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/subscribeWebhook.ts
+++ b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/subscribeWebhook.ts
@@ -6,17 +6,20 @@ import { authenticateUser } from 'services/api/utils'
import { byId, methodNotAllowed } from 'utils'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
+ const user = await authenticateUser(req)
+ if (!user) return res.status(401).json({ message: 'Not authenticated' })
if (req.method === 'POST') {
- const user = await authenticateUser(req)
- if (!user) return res.status(401).json({ message: 'Not authenticated' })
const body = req.body as Record
if (!('url' in body))
return res.status(403).send({ message: 'url is missing in body' })
const { url } = body
const typebotId = req.query.typebotId.toString()
const stepId = req.query.blockId.toString()
- const typebot = (await prisma.typebot.findUnique({
- where: { id_ownerId: { id: typebotId, ownerId: user.id } },
+ const typebot = (await prisma.typebot.findFirst({
+ where: {
+ id: typebotId,
+ workspace: { members: { some: { userId: user.id } } },
+ },
})) as unknown as Typebot | undefined
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
try {
diff --git a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/unsubscribeWebhook.ts b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/unsubscribeWebhook.ts
index f0bdd480ca7..f9f0de7b66a 100644
--- a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/unsubscribeWebhook.ts
+++ b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/unsubscribeWebhook.ts
@@ -6,13 +6,16 @@ import { authenticateUser } from 'services/api/utils'
import { byId, methodNotAllowed } from 'utils'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
+ const user = await authenticateUser(req)
+ if (!user) return res.status(401).json({ message: 'Not authenticated' })
if (req.method === 'POST') {
- const user = await authenticateUser(req)
- if (!user) return res.status(401).json({ message: 'Not authenticated' })
const typebotId = req.query.typebotId.toString()
const stepId = req.query.blockId.toString()
- const typebot = (await prisma.typebot.findUnique({
- where: { id_ownerId: { id: typebotId, ownerId: user.id } },
+ const typebot = (await prisma.typebot.findFirst({
+ where: {
+ id: typebotId,
+ workspace: { members: { some: { userId: user.id } } },
+ },
})) as unknown as Typebot | undefined
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
try {
diff --git a/apps/viewer/pages/api/typebots/[typebotId]/results.ts b/apps/viewer/pages/api/typebots/[typebotId]/results.ts
index 54e9503b091..9ca4c4ebd9e 100644
--- a/apps/viewer/pages/api/typebots/[typebotId]/results.ts
+++ b/apps/viewer/pages/api/typebots/[typebotId]/results.ts
@@ -9,13 +9,14 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await authenticateUser(req)
if (!user) return res.status(401).json({ message: 'Not authenticated' })
const typebotId = req.query.typebotId.toString()
- const typebot = await prisma.typebot.findUnique({
- where: { id_ownerId: { id: typebotId, ownerId: user.id } },
- })
- if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
const limit = Number(req.query.limit)
const results = (await prisma.result.findMany({
- where: { typebotId: typebot.id },
+ where: {
+ typebot: {
+ id: typebotId,
+ workspace: { members: { some: { userId: user.id } } },
+ },
+ },
orderBy: { createdAt: 'desc' },
take: limit,
include: { answers: true },
diff --git a/apps/viewer/pages/api/typebots/[typebotId]/webhookBlocks.ts b/apps/viewer/pages/api/typebots/[typebotId]/webhookBlocks.ts
index 804e3978373..8fe9c7cf0bd 100644
--- a/apps/viewer/pages/api/typebots/[typebotId]/webhookBlocks.ts
+++ b/apps/viewer/pages/api/typebots/[typebotId]/webhookBlocks.ts
@@ -10,8 +10,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await authenticateUser(req)
if (!user) return res.status(401).json({ message: 'Not authenticated' })
const typebotId = req.query.typebotId.toString()
- const typebot = await prisma.typebot.findUnique({
- where: { id_ownerId: { id: typebotId, ownerId: user.id } },
+ const typebot = await prisma.typebot.findFirst({
+ where: {
+ id: typebotId,
+ workspace: { members: { some: { userId: user.id } } },
+ },
select: { blocks: true, webhooks: true },
})
const emptyWebhookSteps = (typebot?.blocks as Block[]).reduce<
diff --git a/apps/viewer/pages/api/typebots/[typebotId]/webhookSteps.ts b/apps/viewer/pages/api/typebots/[typebotId]/webhookSteps.ts
index 8e1721caaf7..3dee0b9b87b 100644
--- a/apps/viewer/pages/api/typebots/[typebotId]/webhookSteps.ts
+++ b/apps/viewer/pages/api/typebots/[typebotId]/webhookSteps.ts
@@ -10,8 +10,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await authenticateUser(req)
if (!user) return res.status(401).json({ message: 'Not authenticated' })
const typebotId = req.query.typebotId.toString()
- const typebot = await prisma.typebot.findUnique({
- where: { id_ownerId: { id: typebotId, ownerId: user.id } },
+ const typebot = await prisma.typebot.findFirst({
+ where: {
+ id: typebotId,
+ workspace: { members: { some: { userId: user.id } } },
+ },
select: { blocks: true, webhooks: true },
})
const emptyWebhookSteps = (typebot?.blocks as Block[]).reduce<
diff --git a/apps/viewer/playwright/services/database.ts b/apps/viewer/playwright/services/database.ts
index fa0274f197a..f0c4b61db09 100644
--- a/apps/viewer/playwright/services/database.ts
+++ b/apps/viewer/playwright/services/database.ts
@@ -8,16 +8,21 @@ import {
Typebot,
Webhook,
} from 'models'
-import { PrismaClient } from 'db'
+import { PrismaClient, WorkspaceRole } from 'db'
import { readFileSync } from 'fs'
import { encrypt } from 'utils'
const prisma = new PrismaClient()
+const proWorkspaceId = 'proWorkspaceViewer'
+
export const teardownDatabase = async () => {
try {
- await prisma.user.delete({
- where: { id: 'proUser' },
+ await prisma.workspace.deleteMany({
+ where: { members: { some: { userId: { in: ['proUser'] } } } },
+ })
+ await prisma.user.deleteMany({
+ where: { id: { in: ['proUser'] } },
})
} catch (err) {
console.error(err)
@@ -34,6 +39,17 @@ export const createUser = () =>
email: 'user@email.com',
name: 'User',
apiToken: 'userToken',
+ workspaces: {
+ create: {
+ role: WorkspaceRole.ADMIN,
+ workspace: {
+ create: {
+ id: proWorkspaceId,
+ name: 'Pro workspace',
+ },
+ },
+ },
+ },
},
})
@@ -81,6 +97,7 @@ const parseTestTypebot = (partialTypebot: Partial): Typebot => ({
folderId: null,
name: 'My typebot',
ownerId: 'proUser',
+ workspaceId: proWorkspaceId,
icon: null,
theme: defaultTheme,
settings: defaultSettings,
@@ -143,6 +160,7 @@ export const importTypebotInDatabase = async (
const typebot: any = {
...JSON.parse(readFileSync(path).toString()),
...updates,
+ workspaceId: proWorkspaceId,
ownerId: 'proUser',
}
await prisma.typebot.create({
@@ -203,6 +221,7 @@ export const createSmtpCredentials = (
name: smtpData.from.email as string,
type: CredentialsType.SMTP,
ownerId: 'proUser',
+ workspaceId: proWorkspaceId,
},
})
}
diff --git a/packages/db/prisma/migrations/20220513223344_add_workspaces/migration.sql b/packages/db/prisma/migrations/20220513223344_add_workspaces/migration.sql
new file mode 100644
index 00000000000..3f2a9597f49
--- /dev/null
+++ b/packages/db/prisma/migrations/20220513223344_add_workspaces/migration.sql
@@ -0,0 +1,84 @@
+-- CreateEnum
+CREATE TYPE "WorkspaceRole" AS ENUM ('ADMIN', 'MEMBER', 'GUEST');
+
+-- AlterEnum
+ALTER TYPE "CollaborationType" ADD VALUE 'FULL_ACCESS';
+
+-- AlterEnum
+ALTER TYPE "Plan" ADD VALUE 'TEAM';
+
+-- AlterTable
+ALTER TABLE "Credentials" ADD COLUMN "workspaceId" TEXT,
+ALTER COLUMN "ownerId" DROP NOT NULL;
+
+-- AlterTable
+ALTER TABLE "CustomDomain" ADD COLUMN "workspaceId" TEXT,
+ALTER COLUMN "ownerId" DROP NOT NULL;
+
+-- AlterTable
+ALTER TABLE "DashboardFolder" ADD COLUMN "workspaceId" TEXT,
+ALTER COLUMN "ownerId" DROP NOT NULL;
+
+-- AlterTable
+ALTER TABLE "Typebot" ADD COLUMN "workspaceId" TEXT,
+ALTER COLUMN "ownerId" DROP NOT NULL;
+
+-- AlterTable
+ALTER TABLE "User" ALTER COLUMN "plan" DROP NOT NULL;
+
+-- CreateTable
+CREATE TABLE "Workspace" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "icon" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "plan" "Plan" NOT NULL DEFAULT E'FREE',
+ "stripeId" TEXT,
+
+ CONSTRAINT "Workspace_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "MemberInWorkspace" (
+ "userId" TEXT NOT NULL,
+ "workspaceId" TEXT NOT NULL,
+ "role" "WorkspaceRole" NOT NULL
+);
+
+-- CreateTable
+CREATE TABLE "WorkspaceInvitation" (
+ "id" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "email" TEXT NOT NULL,
+ "workspaceId" TEXT NOT NULL,
+ "type" "WorkspaceRole" NOT NULL,
+
+ CONSTRAINT "WorkspaceInvitation_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Workspace_stripeId_key" ON "Workspace"("stripeId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "MemberInWorkspace_userId_workspaceId_key" ON "MemberInWorkspace"("userId", "workspaceId");
+
+-- AddForeignKey
+ALTER TABLE "MemberInWorkspace" ADD CONSTRAINT "MemberInWorkspace_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "MemberInWorkspace" ADD CONSTRAINT "MemberInWorkspace_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "WorkspaceInvitation" ADD CONSTRAINT "WorkspaceInvitation_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "CustomDomain" ADD CONSTRAINT "CustomDomain_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Credentials" ADD CONSTRAINT "Credentials_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "DashboardFolder" ADD CONSTRAINT "DashboardFolder_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Typebot" ADD CONSTRAINT "Typebot_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma
index 721b1010c0f..9faf129d01b 100644
--- a/packages/db/prisma/schema.prisma
+++ b/packages/db/prisma/schema.prisma
@@ -50,7 +50,7 @@ model User {
sessions Session[]
typebots Typebot[]
folders DashboardFolder[]
- plan Plan @default(FREE)
+ plan Plan? @default(FREE)
stripeId String? @unique
credentials Credentials[]
customDomains CustomDomain[]
@@ -59,6 +59,47 @@ model User {
company String?
onboardingCategories String[]
graphNavigation GraphNavigation?
+ workspaces MemberInWorkspace[]
+}
+
+model Workspace {
+ id String @id @default(cuid())
+ name String
+ icon String?
+ members MemberInWorkspace[]
+ folders DashboardFolder[]
+ typebots Typebot[]
+ createdAt DateTime @default(now())
+ plan Plan @default(FREE)
+ stripeId String? @unique
+ customDomains CustomDomain[]
+ credentials Credentials[]
+ invitations WorkspaceInvitation[]
+}
+
+model MemberInWorkspace {
+ userId String
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ workspaceId String
+ workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
+ role WorkspaceRole
+
+ @@unique([userId, workspaceId])
+}
+
+enum WorkspaceRole {
+ ADMIN
+ MEMBER
+ GUEST
+}
+
+model WorkspaceInvitation {
+ id String @id @default(cuid())
+ createdAt DateTime @default(now())
+ email String
+ workspaceId String
+ workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
+ type WorkspaceRole
}
enum GraphNavigation {
@@ -67,21 +108,25 @@ enum GraphNavigation {
}
model CustomDomain {
- name String @id
- createdAt DateTime @default(now())
- ownerId String
- owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
+ name String @id
+ createdAt DateTime @default(now())
+ ownerId String?
+ owner User? @relation(fields: [ownerId], references: [id], onDelete: Cascade)
+ workspaceId String?
+ workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
}
model Credentials {
- id String @id @default(cuid())
- createdAt DateTime @default(now())
- ownerId String
- owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
- data String // Encrypted data
- name String
- type String
- iv String
+ id String @id @default(cuid())
+ createdAt DateTime @default(now())
+ ownerId String?
+ owner User? @relation(fields: [ownerId], references: [id], onDelete: Cascade)
+ workspaceId String?
+ workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
+ data String // Encrypted data
+ name String
+ type String
+ iv String
@@unique([name, type, ownerId])
}
@@ -89,6 +134,7 @@ model Credentials {
enum Plan {
FREE
PRO
+ TEAM
LIFETIME
OFFERED
}
@@ -106,12 +152,14 @@ model DashboardFolder {
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
name String
- owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
- ownerId String
+ owner User? @relation(fields: [ownerId], references: [id], onDelete: Cascade)
+ ownerId String?
parentFolderId String?
parentFolder DashboardFolder? @relation("ParentChild", fields: [parentFolderId], references: [id])
childrenFolder DashboardFolder[] @relation("ParentChild")
typebots Typebot[]
+ workspaceId String?
+ workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
@@unique([id, ownerId])
}
@@ -122,8 +170,8 @@ model Typebot {
updatedAt DateTime @default(now()) @updatedAt
icon String?
name String
- ownerId String
- owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
+ ownerId String?
+ owner User? @relation(fields: [ownerId], references: [id], onDelete: Cascade)
publishedTypebotId String?
publishedTypebot PublicTypebot?
results Result[]
@@ -139,6 +187,8 @@ model Typebot {
collaborators CollaboratorsOnTypebots[]
invitations Invitation[]
webhooks Webhook[]
+ workspaceId String?
+ workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
@@unique([id, ownerId])
}
@@ -166,6 +216,7 @@ model CollaboratorsOnTypebots {
enum CollaborationType {
READ
WRITE
+ FULL_ACCESS
}
model PublicTypebot {
diff --git a/packages/scripts/.env.local.example b/packages/scripts/.env.local.example
index ea6bd64078b..71c1af2d4d1 100644
--- a/packages/scripts/.env.local.example
+++ b/packages/scripts/.env.local.example
@@ -1 +1,2 @@
-DATABASE_URL=postgresql://postgres:@localhost:5432/typebot
\ No newline at end of file
+DATABASE_URL=postgresql://postgres:typebot@localhost:5432/typebot
+ENCRYPTION_SECRET=
\ No newline at end of file
diff --git a/packages/scripts/index.ts b/packages/scripts/index.ts
index 58cdf2461ee..1abf960f24a 100644
--- a/packages/scripts/index.ts
+++ b/packages/scripts/index.ts
@@ -1,5 +1,5 @@
-import { PrismaClient } from 'db'
import path from 'path'
+import { migrateWorkspace } from './workspaceMigration'
require('dotenv').config({
path: path.join(
@@ -8,7 +8,8 @@ require('dotenv').config({
),
})
-const prisma = new PrismaClient()
-const main = async () => {}
+const main = async () => {
+ await migrateWorkspace()
+}
main().then()
diff --git a/packages/scripts/package.json b/packages/scripts/package.json
index ad3199513a2..64c1b1ac7c8 100644
--- a/packages/scripts/package.json
+++ b/packages/scripts/package.json
@@ -6,7 +6,8 @@
"private": true,
"scripts": {
"start:local": "ts-node index.ts",
- "start:prod": "NODE_ENV=production ts-node index.ts"
+ "start:prod": "NODE_ENV=production ts-node index.ts",
+ "start:workspaces:migration": "ts-node workspaceMigration.ts"
},
"devDependencies": {
"db": "*",
diff --git a/packages/scripts/workspaceMigration.ts b/packages/scripts/workspaceMigration.ts
new file mode 100644
index 00000000000..a106b7f474a
--- /dev/null
+++ b/packages/scripts/workspaceMigration.ts
@@ -0,0 +1,85 @@
+import { Plan, PrismaClient, WorkspaceRole } from 'db'
+import path from 'path'
+
+const prisma = new PrismaClient()
+
+export const migrateWorkspace = async () => {
+ const users = await prisma.user.findMany({
+ where: { workspaces: { none: {} } },
+ include: {
+ folders: true,
+ typebots: true,
+ credentials: true,
+ customDomains: true,
+ CollaboratorsOnTypebots: {
+ include: { typebot: { select: { workspaceId: true } } },
+ },
+ },
+ })
+ let i = 1
+ for (const user of users) {
+ console.log('Updating', user.email, `(${i}/${users.length})`)
+ i += 1
+ const newWorkspace = await prisma.workspace.create({
+ data: {
+ name: user.name ? `${user.name}'s workspace` : 'My workspace',
+ members: { create: { userId: user.id, role: WorkspaceRole.ADMIN } },
+ stripeId: user.stripeId,
+ plan: user.plan ?? Plan.FREE,
+ },
+ })
+ await prisma.credentials.updateMany({
+ where: { id: { in: user.credentials.map((c) => c.id) } },
+ data: { workspaceId: newWorkspace.id, ownerId: null },
+ })
+ await prisma.customDomain.updateMany({
+ where: {
+ name: { in: user.customDomains.map((c) => c.name) },
+ ownerId: user.id,
+ },
+ data: { workspaceId: newWorkspace.id, ownerId: null },
+ })
+ await prisma.dashboardFolder.updateMany({
+ where: {
+ id: { in: user.folders.map((c) => c.id) },
+ },
+ data: { workspaceId: newWorkspace.id, ownerId: null },
+ })
+ await prisma.typebot.updateMany({
+ where: {
+ id: { in: user.typebots.map((c) => c.id) },
+ },
+ data: { workspaceId: newWorkspace.id, ownerId: null },
+ })
+ for (const collab of user.CollaboratorsOnTypebots) {
+ if (!collab.typebot.workspaceId) continue
+ await prisma.memberInWorkspace.upsert({
+ where: {
+ userId_workspaceId: {
+ userId: user.id,
+ workspaceId: collab.typebot.workspaceId,
+ },
+ },
+ create: {
+ role: WorkspaceRole.GUEST,
+ userId: user.id,
+ workspaceId: collab.typebot.workspaceId,
+ },
+ update: {},
+ })
+ }
+ }
+}
+
+require('dotenv').config({
+ path: path.join(
+ __dirname,
+ process.env.NODE_ENV === 'production' ? '.env.production' : '.env.local'
+ ),
+})
+
+const main = async () => {
+ await migrateWorkspace()
+}
+
+main().then()
|