diff --git a/apps/builder/components/share/ShareContent.tsx b/apps/builder/components/share/ShareContent.tsx index e3e20071e..ba4c7a91d 100644 --- a/apps/builder/components/share/ShareContent.tsx +++ b/apps/builder/components/share/ShareContent.tsx @@ -22,16 +22,22 @@ import { getViewerUrl, isDefined, isNotDefined } from 'utils' import { CustomDomainsDropdown } from './customDomain/CustomDomainsDropdown' import { EditableUrl } from './EditableUrl' import { integrationsList } from './integrations/EmbedButton' +import { isPublicDomainAvailableQuery } from './queries/isPublicDomainAvailableQuery' export const ShareContent = () => { const { workspace } = useWorkspace() const { typebot, updateTypebot } = useTypebot() const { showToast } = useToast() - const handlePublicIdChange = (publicId: string) => { + const handlePublicIdChange = async (publicId: string) => { if (publicId === typebot?.publicId) return if (publicId.length < 4) return showToast({ description: 'ID must be longer than 4 characters' }) + + const { data } = await isPublicDomainAvailableQuery(publicId) + if (!data?.isAvailable) + return showToast({ description: 'ID is already taken' }) + updateTypebot({ publicId }) } diff --git a/apps/builder/components/share/queries/isPublicDomainAvailableQuery.ts b/apps/builder/components/share/queries/isPublicDomainAvailableQuery.ts new file mode 100644 index 000000000..3d4a74332 --- /dev/null +++ b/apps/builder/components/share/queries/isPublicDomainAvailableQuery.ts @@ -0,0 +1,7 @@ +import { sendRequest } from 'utils' + +export const isPublicDomainAvailableQuery = (publicId: string) => + sendRequest<{ isAvailable: boolean }>({ + method: 'GET', + url: `/api/publicIdAvailable?publicId=${publicId}`, + }) diff --git a/apps/builder/pages/api/publicIdAvailable.ts b/apps/builder/pages/api/publicIdAvailable.ts new file mode 100644 index 000000000..72578c184 --- /dev/null +++ b/apps/builder/pages/api/publicIdAvailable.ts @@ -0,0 +1,19 @@ +import { withSentry } from '@sentry/nextjs' +import prisma from 'libs/prisma' +import { NextApiRequest, NextApiResponse } from 'next' +import { getAuthenticatedUser } from 'services/api/utils' +import { badRequest, methodNotAllowed, notAuthenticated } from 'utils/api' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const user = await getAuthenticatedUser(req) + if (!user) return notAuthenticated(res) + if (req.method === 'GET') { + const publicId = req.query.publicId as string | undefined + if (!publicId) return badRequest(res, 'publicId is required') + const exists = await prisma.typebot.count({ where: { publicId } }) + return res.send({ isAvailable: Boolean(!exists) }) + } + return methodNotAllowed(res) +} + +export default withSentry(handler) diff --git a/apps/builder/playwright/tests/share.spec.ts b/apps/builder/playwright/tests/share.spec.ts new file mode 100644 index 000000000..ad529ff27 --- /dev/null +++ b/apps/builder/playwright/tests/share.spec.ts @@ -0,0 +1,35 @@ +import test, { expect } from '@playwright/test' +import cuid from 'cuid' +import { defaultTextInputOptions, InputBlockType } from 'models' +import { createTypebots } from 'utils/playwright/databaseActions' +import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers' + +test('should not be able to submit taken url ID', async ({ page }) => { + const takenTypebotId = cuid() + const typebotId = cuid() + await createTypebots([ + { + id: takenTypebotId, + ...parseDefaultGroupWithBlock({ + type: InputBlockType.TEXT, + options: defaultTextInputOptions, + }), + publicId: 'taken-url-id', + }, + ]) + await createTypebots([ + { + id: typebotId, + ...parseDefaultGroupWithBlock({ + type: InputBlockType.TEXT, + options: defaultTextInputOptions, + }), + publicId: typebotId + '-public', + }, + ]) + await page.goto(`/typebots/${typebotId}/share`) + await page.getByText(`${typebotId}-public`).click() + await page.getByRole('textbox').fill('taken-url-id') + await page.getByRole('textbox').press('Enter') + await expect(page.getByText('ID is already taken').nth(0)).toBeVisible() +}) diff --git a/packages/utils/playwright/databaseActions.ts b/packages/utils/playwright/databaseActions.ts index 6b777a7be..02b1d3257 100644 --- a/packages/utils/playwright/databaseActions.ts +++ b/packages/utils/playwright/databaseActions.ts @@ -159,7 +159,7 @@ export const createTypebots = async (partialTypebots: Partial[]) => { return { ...typebot, id: typebotId, - publicId: typebotId + '-public', + publicId: typebot.publicId ?? typebotId + '-public', } }) await prisma.typebot.createMany({ @@ -167,7 +167,7 @@ export const createTypebots = async (partialTypebots: Partial[]) => { }) return prisma.publicTypebot.createMany({ data: typebotsWithId.map((t) => - parseTypebotToPublicTypebot(t.id + '-public', parseTestTypebot(t)) + parseTypebotToPublicTypebot(t.publicId, parseTestTypebot(t)) ), }) }