⚡ (customDomains) Fix custom domain update feedback
This commit is contained in:
@@ -0,0 +1,85 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { customDomainSchema } from '@typebot.io/schemas/features/customDomains'
|
||||||
|
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden copy'
|
||||||
|
import got, { HTTPError } from 'got'
|
||||||
|
|
||||||
|
export const createCustomDomain = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'POST',
|
||||||
|
path: '/custom-domains',
|
||||||
|
protect: true,
|
||||||
|
summary: 'Create custom domain',
|
||||||
|
tags: ['Custom domains'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
workspaceId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
customDomain: customDomainSchema.pick({
|
||||||
|
name: true,
|
||||||
|
createdAt: true,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(async ({ input: { workspaceId, name }, ctx: { user } }) => {
|
||||||
|
const workspace = await prisma.workspace.findFirst({
|
||||||
|
where: { id: workspaceId },
|
||||||
|
select: {
|
||||||
|
members: {
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
role: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!workspace || isWriteWorkspaceForbidden(workspace, user))
|
||||||
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'No workspaces found' })
|
||||||
|
|
||||||
|
const existingCustomDomain = await prisma.customDomain.findFirst({
|
||||||
|
where: { name },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (existingCustomDomain)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'CONFLICT',
|
||||||
|
message: 'Custom domain already registered',
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createDomainOnVercel(name)
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
if (err instanceof HTTPError && err.response.statusCode !== 409)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'INTERNAL_SERVER_ERROR',
|
||||||
|
message: 'Failed to create custom domain on Vercel',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const customDomain = await prisma.customDomain.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return { customDomain }
|
||||||
|
})
|
||||||
|
|
||||||
|
const createDomainOnVercel = (name: string) =>
|
||||||
|
got.post({
|
||||||
|
url: `https://api.vercel.com/v10/projects/${process.env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME}/domains?teamId=${process.env.VERCEL_TEAM_ID}`,
|
||||||
|
headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
|
||||||
|
json: { name },
|
||||||
|
})
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden copy'
|
||||||
|
import got from 'got'
|
||||||
|
|
||||||
|
export const deleteCustomDomain = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'DELETE',
|
||||||
|
path: '/custom-domains',
|
||||||
|
protect: true,
|
||||||
|
summary: 'Delete custom domain',
|
||||||
|
tags: ['Custom domains'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
workspaceId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
message: z.literal('success'),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(async ({ input: { workspaceId, name }, ctx: { user } }) => {
|
||||||
|
const workspace = await prisma.workspace.findFirst({
|
||||||
|
where: { id: workspaceId },
|
||||||
|
select: {
|
||||||
|
members: {
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
role: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!workspace || isWriteWorkspaceForbidden(workspace, user))
|
||||||
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'No workspaces found' })
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteDomainOnVercel(name)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'INTERNAL_SERVER_ERROR',
|
||||||
|
message: 'Failed to delete domain on Vercel',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await prisma.customDomain.deleteMany({
|
||||||
|
where: {
|
||||||
|
name,
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return { message: 'success' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteDomainOnVercel = (name: string) =>
|
||||||
|
got.delete({
|
||||||
|
url: `https://api.vercel.com/v9/projects/${process.env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME}/domains/${name}?teamId=${process.env.VERCEL_TEAM_ID}`,
|
||||||
|
headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
|
||||||
|
})
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
|
||||||
|
import { customDomainSchema } from '@typebot.io/schemas/features/customDomains'
|
||||||
|
|
||||||
|
export const listCustomDomains = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'GET',
|
||||||
|
path: '/custom-domains',
|
||||||
|
protect: true,
|
||||||
|
summary: 'List custom domains',
|
||||||
|
tags: ['Custom domains'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
workspaceId: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
customDomains: z.array(
|
||||||
|
customDomainSchema.pick({
|
||||||
|
name: true,
|
||||||
|
createdAt: true,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
|
||||||
|
const workspace = await prisma.workspace.findFirst({
|
||||||
|
where: { id: workspaceId },
|
||||||
|
select: {
|
||||||
|
members: {
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
customDomains: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!workspace || isReadWorkspaceFobidden(workspace, user))
|
||||||
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'No workspaces found' })
|
||||||
|
|
||||||
|
const descSortedCustomDomains = workspace.customDomains.sort(
|
||||||
|
(a, b) => b.createdAt.getTime() - a.createdAt.getTime()
|
||||||
|
)
|
||||||
|
|
||||||
|
return { customDomains: descSortedCustomDomains }
|
||||||
|
})
|
||||||
10
apps/builder/src/features/customDomains/api/router.ts
Normal file
10
apps/builder/src/features/customDomains/api/router.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { router } from '@/helpers/server/trpc'
|
||||||
|
import { createCustomDomain } from './createCustomDomain'
|
||||||
|
import { deleteCustomDomain } from './deleteCustomDomain'
|
||||||
|
import { listCustomDomains } from './listCustomDomains'
|
||||||
|
|
||||||
|
export const customDomainsRouter = router({
|
||||||
|
createCustomDomain,
|
||||||
|
deleteCustomDomain,
|
||||||
|
listCustomDomains,
|
||||||
|
})
|
||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { useToast } from '@/hooks/useToast'
|
import { useToast } from '@/hooks/useToast'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { createCustomDomainQuery } from '../queries/createCustomDomainQuery'
|
import { trpc } from '@/lib/trpc'
|
||||||
|
|
||||||
const hostnameRegex =
|
const hostnameRegex =
|
||||||
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/
|
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/
|
||||||
@@ -46,6 +46,24 @@ export const CustomDomainModal = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
|
const { mutate } = trpc.customDomains.createCustomDomain.useMutation({
|
||||||
|
onMutate: () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showToast({
|
||||||
|
title: 'Error while creating custom domain',
|
||||||
|
description: error.message,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
setIsLoading(false)
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
onNewDomain(data.customDomain.name)
|
||||||
|
onClose()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inputValue === '' || !isOpen) return
|
if (inputValue === '' || !isOpen) return
|
||||||
@@ -62,15 +80,7 @@ export const CustomDomainModal = ({
|
|||||||
|
|
||||||
const onAddDomainClick = async () => {
|
const onAddDomainClick = async () => {
|
||||||
if (!hostnameRegex.test(inputValue)) return
|
if (!hostnameRegex.test(inputValue)) return
|
||||||
setIsLoading(true)
|
mutate({ name: inputValue, workspaceId })
|
||||||
const { error } = await createCustomDomainQuery(workspaceId, {
|
|
||||||
name: inputValue,
|
|
||||||
})
|
|
||||||
setIsLoading(false)
|
|
||||||
if (error)
|
|
||||||
return showToast({ title: error.name, description: error.message })
|
|
||||||
onNewDomain(inputValue)
|
|
||||||
onClose()
|
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ import React, { useState } from 'react'
|
|||||||
import { CustomDomainModal } from './CustomDomainModal'
|
import { CustomDomainModal } from './CustomDomainModal'
|
||||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||||
import { useToast } from '@/hooks/useToast'
|
import { useToast } from '@/hooks/useToast'
|
||||||
import { useCustomDomains } from '../hooks/useCustomDomains'
|
import { trpc } from '@/lib/trpc'
|
||||||
import { deleteCustomDomainQuery } from '../queries/deleteCustomDomainQuery'
|
|
||||||
|
|
||||||
type Props = Omit<MenuButtonProps, 'type'> & {
|
type Props = Omit<MenuButtonProps, 'type'> & {
|
||||||
currentCustomDomain?: string
|
currentCustomDomain?: string
|
||||||
@@ -32,10 +31,36 @@ export const CustomDomainsDropdown = ({
|
|||||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
const { workspace } = useWorkspace()
|
const { workspace } = useWorkspace()
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
const { customDomains, mutate } = useCustomDomains({
|
const { data, refetch } = trpc.customDomains.listCustomDomains.useQuery(
|
||||||
workspaceId: workspace?.id,
|
{
|
||||||
onError: (error) =>
|
workspaceId: workspace?.id as string,
|
||||||
showToast({ title: error.name, description: error.message }),
|
},
|
||||||
|
{
|
||||||
|
enabled: !!workspace?.id,
|
||||||
|
onError: (error) => {
|
||||||
|
showToast({
|
||||||
|
title: 'Error while fetching custom domains',
|
||||||
|
description: error.message,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const { mutate } = trpc.customDomains.deleteCustomDomain.useMutation({
|
||||||
|
onMutate: ({ name }) => {
|
||||||
|
setIsDeleting(name)
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showToast({
|
||||||
|
title: 'Error while deleting custom domain',
|
||||||
|
description: error.message,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
setIsDeleting('')
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
refetch()
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleMenuItemClick = (customDomain: string) => () =>
|
const handleMenuItemClick = (customDomain: string) => () =>
|
||||||
@@ -45,27 +70,14 @@ export const CustomDomainsDropdown = ({
|
|||||||
(domainName: string) => async (e: React.MouseEvent) => {
|
(domainName: string) => async (e: React.MouseEvent) => {
|
||||||
if (!workspace) return
|
if (!workspace) return
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setIsDeleting(domainName)
|
|
||||||
const { error } = await deleteCustomDomainQuery(workspace.id, domainName)
|
|
||||||
setIsDeleting('')
|
|
||||||
if (error)
|
|
||||||
return showToast({ title: error.name, description: error.message })
|
|
||||||
mutate({
|
mutate({
|
||||||
customDomains: (customDomains ?? []).filter(
|
name: domainName,
|
||||||
(cd) => cd.name !== domainName
|
workspaceId: workspace.id,
|
||||||
),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNewDomain = (domain: string) => {
|
const handleNewDomain = (name: string) => {
|
||||||
if (!workspace) return
|
onCustomDomainSelect(name)
|
||||||
mutate({
|
|
||||||
customDomains: [
|
|
||||||
...(customDomains ?? []),
|
|
||||||
{ name: domain, workspaceId: workspace?.id },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
handleMenuItemClick(domain)()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -92,7 +104,7 @@ export const CustomDomainsDropdown = ({
|
|||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuList maxW="500px" shadow="lg">
|
<MenuList maxW="500px" shadow="lg">
|
||||||
<Stack maxH={'35vh'} overflowY="scroll" spacing="0">
|
<Stack maxH={'35vh'} overflowY="scroll" spacing="0">
|
||||||
{(customDomains ?? []).map((customDomain) => (
|
{(data?.customDomains ?? []).map((customDomain) => (
|
||||||
<Button
|
<Button
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
minH="40px"
|
minH="40px"
|
||||||
@@ -107,6 +119,7 @@ export const CustomDomainsDropdown = ({
|
|||||||
>
|
>
|
||||||
{customDomain.name}
|
{customDomain.name}
|
||||||
<IconButton
|
<IconButton
|
||||||
|
as="span"
|
||||||
icon={<TrashIcon />}
|
icon={<TrashIcon />}
|
||||||
aria-label="Remove domain"
|
aria-label="Remove domain"
|
||||||
size="xs"
|
size="xs"
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ test('should be able to connect custom domain', async ({ page }) => {
|
|||||||
'Enter'
|
'Enter'
|
||||||
)
|
)
|
||||||
await expect(page.locator('text="custom-path"')).toBeVisible()
|
await expect(page.locator('text="custom-path"')).toBeVisible()
|
||||||
await page.click('[aria-label="Remove custom domain"]')
|
await page.click('[aria-label="Remove custom URL"]')
|
||||||
await expect(page.locator('text=sub.yolozeeer.com')).toBeHidden()
|
await expect(page.locator('text=sub.yolozeeer.com')).toBeHidden()
|
||||||
await page.click('button >> text=Add my domain')
|
await page.click('button >> text=Add my domain')
|
||||||
await page.click('[aria-label="Remove domain"]')
|
await page.click('[aria-label="Remove domain"]')
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import { CustomDomain } from '@typebot.io/prisma'
|
|
||||||
import { stringify } from 'qs'
|
|
||||||
import { fetcher } from '@/helpers/fetcher'
|
|
||||||
import useSWR from 'swr'
|
|
||||||
|
|
||||||
export const useCustomDomains = ({
|
|
||||||
workspaceId,
|
|
||||||
onError,
|
|
||||||
}: {
|
|
||||||
workspaceId?: string
|
|
||||||
onError: (error: Error) => void
|
|
||||||
}) => {
|
|
||||||
const { data, error, mutate } = useSWR<
|
|
||||||
{ customDomains: Omit<CustomDomain, 'createdAt'>[] },
|
|
||||||
Error
|
|
||||||
>(
|
|
||||||
workspaceId ? `/api/customDomains?${stringify({ workspaceId })}` : null,
|
|
||||||
fetcher
|
|
||||||
)
|
|
||||||
if (error) onError(error)
|
|
||||||
return {
|
|
||||||
customDomains: data?.customDomains,
|
|
||||||
isLoading: !error && !data,
|
|
||||||
mutate,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { CustomDomain, Credentials } from '@typebot.io/prisma'
|
|
||||||
import { stringify } from 'qs'
|
|
||||||
import { sendRequest } from '@typebot.io/lib'
|
|
||||||
|
|
||||||
export const createCustomDomainQuery = async (
|
|
||||||
workspaceId: string,
|
|
||||||
customDomain: Omit<CustomDomain, 'createdAt' | 'workspaceId'>
|
|
||||||
) =>
|
|
||||||
sendRequest<{
|
|
||||||
credentials: Credentials
|
|
||||||
}>({
|
|
||||||
url: `/api/customDomains?${stringify({ workspaceId })}`,
|
|
||||||
method: 'POST',
|
|
||||||
body: customDomain,
|
|
||||||
})
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { Credentials } from '@typebot.io/prisma'
|
|
||||||
import { stringify } from 'qs'
|
|
||||||
import { sendRequest } from '@typebot.io/lib'
|
|
||||||
|
|
||||||
export const deleteCustomDomainQuery = async (
|
|
||||||
workspaceId: string,
|
|
||||||
customDomain: string
|
|
||||||
) =>
|
|
||||||
sendRequest<{
|
|
||||||
credentials: Credentials
|
|
||||||
}>({
|
|
||||||
url: `/api/customDomains/${customDomain}?${stringify({ workspaceId })}`,
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TrashIcon } from '@/components/icons'
|
import { CloseIcon } from '@/components/icons'
|
||||||
import { Seo } from '@/components/Seo'
|
import { Seo } from '@/components/Seo'
|
||||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||||
import { useToast } from '@/hooks/useToast'
|
import { useToast } from '@/hooks/useToast'
|
||||||
@@ -33,7 +33,7 @@ export const SharePage = () => {
|
|||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
|
|
||||||
const handlePublicIdChange = async (publicId: string) => {
|
const handlePublicIdChange = async (publicId: string) => {
|
||||||
updateTypebot({ updates: { publicId } })
|
updateTypebot({ updates: { publicId }, save: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicId = typebot
|
const publicId = typebot
|
||||||
@@ -50,7 +50,7 @@ export const SharePage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleCustomDomainChange = (customDomain: string | null) =>
|
const handleCustomDomainChange = (customDomain: string | null) =>
|
||||||
updateTypebot({ updates: { customDomain } })
|
updateTypebot({ updates: { customDomain }, save: true })
|
||||||
|
|
||||||
const checkIfPathnameIsValid = (pathname: string) => {
|
const checkIfPathnameIsValid = (pathname: string) => {
|
||||||
const isCorrectlyFormatted =
|
const isCorrectlyFormatted =
|
||||||
@@ -113,8 +113,8 @@ export const SharePage = () => {
|
|||||||
onPathnameChange={handlePathnameChange}
|
onPathnameChange={handlePathnameChange}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<TrashIcon />}
|
icon={<CloseIcon />}
|
||||||
aria-label="Remove custom domain"
|
aria-label="Remove custom URL"
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={() => handleCustomDomainChange(null)}
|
onClick={() => handleCustomDomainChange(null)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from '../helpers/sanitizers'
|
} from '../helpers/sanitizers'
|
||||||
import { isWriteTypebotForbidden } from '../helpers/isWriteTypebotForbidden'
|
import { isWriteTypebotForbidden } from '../helpers/isWriteTypebotForbidden'
|
||||||
import { isCloudProdInstance } from '@/helpers/isCloudProdInstance'
|
import { isCloudProdInstance } from '@/helpers/isCloudProdInstance'
|
||||||
|
import { Prisma } from '@typebot.io/prisma'
|
||||||
|
|
||||||
export const updateTypebot = authenticatedProcedure
|
export const updateTypebot = authenticatedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@@ -134,9 +135,13 @@ export const updateTypebot = authenticatedProcedure
|
|||||||
folderId: typebot.folderId,
|
folderId: typebot.folderId,
|
||||||
variables: typebot.variables,
|
variables: typebot.variables,
|
||||||
edges: typebot.edges,
|
edges: typebot.edges,
|
||||||
resultsTablePreferences: typebot.resultsTablePreferences ?? undefined,
|
resultsTablePreferences:
|
||||||
publicId: typebot.publicId ?? undefined,
|
typebot.resultsTablePreferences === null
|
||||||
customDomain: typebot.customDomain ?? undefined,
|
? Prisma.DbNull
|
||||||
|
: typebot.resultsTablePreferences,
|
||||||
|
publicId: typebot.publicId === null ? null : typebot.publicId,
|
||||||
|
customDomain:
|
||||||
|
typebot.customDomain === null ? null : typebot.customDomain,
|
||||||
isClosed: typebot.isClosed,
|
isClosed: typebot.isClosed,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { MemberInWorkspace, User } from '@typebot.io/prisma'
|
|||||||
|
|
||||||
export const isWriteWorkspaceForbidden = (
|
export const isWriteWorkspaceForbidden = (
|
||||||
workspace: {
|
workspace: {
|
||||||
members: MemberInWorkspace[]
|
members: Pick<MemberInWorkspace, 'userId' | 'role'>[]
|
||||||
},
|
},
|
||||||
user: Pick<User, 'email' | 'id'>
|
user: Pick<User, 'email' | 'id'>
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { workspaceRouter } from '@/features/workspace/api/router'
|
|||||||
import { router } from '../../trpc'
|
import { router } from '../../trpc'
|
||||||
import { analyticsRouter } from '@/features/analytics/api/router'
|
import { analyticsRouter } from '@/features/analytics/api/router'
|
||||||
import { collaboratorsRouter } from '@/features/collaboration/api/router'
|
import { collaboratorsRouter } from '@/features/collaboration/api/router'
|
||||||
|
import { customDomainsRouter } from '@/features/customDomains/api/router'
|
||||||
|
|
||||||
export const trpcRouter = router({
|
export const trpcRouter = router({
|
||||||
getAppVersionProcedure,
|
getAppVersionProcedure,
|
||||||
@@ -25,6 +26,7 @@ export const trpcRouter = router({
|
|||||||
credentials: credentialsRouter,
|
credentials: credentialsRouter,
|
||||||
theme: themeRouter,
|
theme: themeRouter,
|
||||||
collaborators: collaboratorsRouter,
|
collaborators: collaboratorsRouter,
|
||||||
|
customDomains: customDomainsRouter,
|
||||||
})
|
})
|
||||||
|
|
||||||
export type AppRouter = typeof trpcRouter
|
export type AppRouter = typeof trpcRouter
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
notAuthenticated,
|
notAuthenticated,
|
||||||
} from '@typebot.io/lib/api'
|
} from '@typebot.io/lib/api'
|
||||||
|
|
||||||
|
// TODO: delete
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
const user = await getAuthenticatedUser(req, res)
|
const user = await getAuthenticatedUser(req, res)
|
||||||
if (!user) return notAuthenticated(res)
|
if (!user) return notAuthenticated(res)
|
||||||
|
|||||||
@@ -16375,9 +16375,6 @@
|
|||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"isClosed": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"resultsTablePreferences": {
|
"resultsTablePreferences": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -16415,6 +16412,9 @@
|
|||||||
"customDomain": {
|
"customDomain": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
},
|
||||||
|
"isClosed": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
@@ -31137,6 +31137,205 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/custom-domains": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "customDomains-createCustomDomain",
|
||||||
|
"summary": "Create custom domain",
|
||||||
|
"tags": [
|
||||||
|
"Workspace",
|
||||||
|
"Custom domains"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Authorization": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"workspaceId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"workspaceId",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"customDomain": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"createdAt"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"customDomain"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"$ref": "#/components/responses/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"operationId": "customDomains-deleteCustomDomain",
|
||||||
|
"summary": "Delete custom domain",
|
||||||
|
"tags": [
|
||||||
|
"Workspace",
|
||||||
|
"Custom domains"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Authorization": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "workspaceId",
|
||||||
|
"in": "query",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"in": "query",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"success"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"message"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"$ref": "#/components/responses/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"get": {
|
||||||
|
"operationId": "customDomains-listCustomDomains",
|
||||||
|
"summary": "List custom domains",
|
||||||
|
"tags": [
|
||||||
|
"Workspace",
|
||||||
|
"Custom domains"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Authorization": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "workspaceId",
|
||||||
|
"in": "query",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"customDomains": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"createdAt"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"customDomains"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"$ref": "#/components/responses/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/react",
|
"name": "@typebot.io/react",
|
||||||
"version": "0.1.17",
|
"version": "0.1.17",
|
||||||
"description": "Convenient library to display typebots on your Next.js website",
|
"description": "Convenient library to display typebots on your React app",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
10
packages/schemas/features/customDomains.ts
Normal file
10
packages/schemas/features/customDomains.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { CustomDomain as CustomDomainInDb } from '@typebot.io/prisma'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
const domainNameRegex = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/
|
||||||
|
|
||||||
|
export const customDomainSchema = z.object({
|
||||||
|
name: z.string().refine((name) => domainNameRegex.test(name)),
|
||||||
|
workspaceId: z.string(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
}) satisfies z.ZodType<CustomDomainInDb>
|
||||||
@@ -39,7 +39,7 @@ const resultsTablePreferencesSchema = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const isPathNameCompatible = (str: string) =>
|
const isPathNameCompatible = (str: string) =>
|
||||||
/^([a-z0-9]+-[a-z0-9]*)*$/.test(str) || /^[a-z0-9]*$/.test(str)
|
/^([a-zA-Z0-9]+(-|.)[a-zA-z0-9]*)*$/.test(str) || /^[a-zA-Z0-9]*$/.test(str)
|
||||||
|
|
||||||
const isDomainNameWithPathNameCompatible = (str: string) =>
|
const isDomainNameWithPathNameCompatible = (str: string) =>
|
||||||
/^(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(?:\/[\w-\/]*)?)$/.test(
|
/^(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(?:\/[\w-\/]*)?)$/.test(
|
||||||
|
|||||||
Reference in New Issue
Block a user