🐛 (share) Restrict public ID to non-existant only
This commit is contained in:
@ -22,16 +22,22 @@ import { getViewerUrl, isDefined, isNotDefined } from 'utils'
|
|||||||
import { CustomDomainsDropdown } from './customDomain/CustomDomainsDropdown'
|
import { CustomDomainsDropdown } from './customDomain/CustomDomainsDropdown'
|
||||||
import { EditableUrl } from './EditableUrl'
|
import { EditableUrl } from './EditableUrl'
|
||||||
import { integrationsList } from './integrations/EmbedButton'
|
import { integrationsList } from './integrations/EmbedButton'
|
||||||
|
import { isPublicDomainAvailableQuery } from './queries/isPublicDomainAvailableQuery'
|
||||||
|
|
||||||
export const ShareContent = () => {
|
export const ShareContent = () => {
|
||||||
const { workspace } = useWorkspace()
|
const { workspace } = useWorkspace()
|
||||||
const { typebot, updateTypebot } = useTypebot()
|
const { typebot, updateTypebot } = useTypebot()
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
|
|
||||||
const handlePublicIdChange = (publicId: string) => {
|
const handlePublicIdChange = async (publicId: string) => {
|
||||||
if (publicId === typebot?.publicId) return
|
if (publicId === typebot?.publicId) return
|
||||||
if (publicId.length < 4)
|
if (publicId.length < 4)
|
||||||
return showToast({ description: 'ID must be longer than 4 characters' })
|
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 })
|
updateTypebot({ publicId })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
import { sendRequest } from 'utils'
|
||||||
|
|
||||||
|
export const isPublicDomainAvailableQuery = (publicId: string) =>
|
||||||
|
sendRequest<{ isAvailable: boolean }>({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/api/publicIdAvailable?publicId=${publicId}`,
|
||||||
|
})
|
19
apps/builder/pages/api/publicIdAvailable.ts
Normal file
19
apps/builder/pages/api/publicIdAvailable.ts
Normal file
@ -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)
|
35
apps/builder/playwright/tests/share.spec.ts
Normal file
35
apps/builder/playwright/tests/share.spec.ts
Normal file
@ -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()
|
||||||
|
})
|
@ -159,7 +159,7 @@ export const createTypebots = async (partialTypebots: Partial<Typebot>[]) => {
|
|||||||
return {
|
return {
|
||||||
...typebot,
|
...typebot,
|
||||||
id: typebotId,
|
id: typebotId,
|
||||||
publicId: typebotId + '-public',
|
publicId: typebot.publicId ?? typebotId + '-public',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await prisma.typebot.createMany({
|
await prisma.typebot.createMany({
|
||||||
@ -167,7 +167,7 @@ export const createTypebots = async (partialTypebots: Partial<Typebot>[]) => {
|
|||||||
})
|
})
|
||||||
return prisma.publicTypebot.createMany({
|
return prisma.publicTypebot.createMany({
|
||||||
data: typebotsWithId.map((t) =>
|
data: typebotsWithId.map((t) =>
|
||||||
parseTypebotToPublicTypebot(t.id + '-public', parseTestTypebot(t))
|
parseTypebotToPublicTypebot(t.publicId, parseTestTypebot(t))
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user