⚡ (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'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { createCustomDomainQuery } from '../queries/createCustomDomainQuery'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
|
||||
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])$/
|
||||
@ -46,6 +46,24 @@ export const CustomDomainModal = ({
|
||||
})
|
||||
|
||||
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(() => {
|
||||
if (inputValue === '' || !isOpen) return
|
||||
@ -62,15 +80,7 @@ export const CustomDomainModal = ({
|
||||
|
||||
const onAddDomainClick = async () => {
|
||||
if (!hostnameRegex.test(inputValue)) return
|
||||
setIsLoading(true)
|
||||
const { error } = await createCustomDomainQuery(workspaceId, {
|
||||
name: inputValue,
|
||||
})
|
||||
setIsLoading(false)
|
||||
if (error)
|
||||
return showToast({ title: error.name, description: error.message })
|
||||
onNewDomain(inputValue)
|
||||
onClose()
|
||||
mutate({ name: inputValue, workspaceId })
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
|
@ -15,8 +15,7 @@ import React, { useState } from 'react'
|
||||
import { CustomDomainModal } from './CustomDomainModal'
|
||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { useCustomDomains } from '../hooks/useCustomDomains'
|
||||
import { deleteCustomDomainQuery } from '../queries/deleteCustomDomainQuery'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
|
||||
type Props = Omit<MenuButtonProps, 'type'> & {
|
||||
currentCustomDomain?: string
|
||||
@ -32,10 +31,36 @@ export const CustomDomainsDropdown = ({
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const { workspace } = useWorkspace()
|
||||
const { showToast } = useToast()
|
||||
const { customDomains, mutate } = useCustomDomains({
|
||||
workspaceId: workspace?.id,
|
||||
onError: (error) =>
|
||||
showToast({ title: error.name, description: error.message }),
|
||||
const { data, refetch } = trpc.customDomains.listCustomDomains.useQuery(
|
||||
{
|
||||
workspaceId: workspace?.id as string,
|
||||
},
|
||||
{
|
||||
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) => () =>
|
||||
@ -45,27 +70,14 @@ export const CustomDomainsDropdown = ({
|
||||
(domainName: string) => async (e: React.MouseEvent) => {
|
||||
if (!workspace) return
|
||||
e.stopPropagation()
|
||||
setIsDeleting(domainName)
|
||||
const { error } = await deleteCustomDomainQuery(workspace.id, domainName)
|
||||
setIsDeleting('')
|
||||
if (error)
|
||||
return showToast({ title: error.name, description: error.message })
|
||||
mutate({
|
||||
customDomains: (customDomains ?? []).filter(
|
||||
(cd) => cd.name !== domainName
|
||||
),
|
||||
name: domainName,
|
||||
workspaceId: workspace.id,
|
||||
})
|
||||
}
|
||||
|
||||
const handleNewDomain = (domain: string) => {
|
||||
if (!workspace) return
|
||||
mutate({
|
||||
customDomains: [
|
||||
...(customDomains ?? []),
|
||||
{ name: domain, workspaceId: workspace?.id },
|
||||
],
|
||||
})
|
||||
handleMenuItemClick(domain)()
|
||||
const handleNewDomain = (name: string) => {
|
||||
onCustomDomainSelect(name)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -92,7 +104,7 @@ export const CustomDomainsDropdown = ({
|
||||
</MenuButton>
|
||||
<MenuList maxW="500px" shadow="lg">
|
||||
<Stack maxH={'35vh'} overflowY="scroll" spacing="0">
|
||||
{(customDomains ?? []).map((customDomain) => (
|
||||
{(data?.customDomains ?? []).map((customDomain) => (
|
||||
<Button
|
||||
role="menuitem"
|
||||
minH="40px"
|
||||
@ -107,6 +119,7 @@ export const CustomDomainsDropdown = ({
|
||||
>
|
||||
{customDomain.name}
|
||||
<IconButton
|
||||
as="span"
|
||||
icon={<TrashIcon />}
|
||||
aria-label="Remove domain"
|
||||
size="xs"
|
||||
|
@ -34,7 +34,7 @@ test('should be able to connect custom domain', async ({ page }) => {
|
||||
'Enter'
|
||||
)
|
||||
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 page.click('button >> text=Add my 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 { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
@ -33,7 +33,7 @@ export const SharePage = () => {
|
||||
const { showToast } = useToast()
|
||||
|
||||
const handlePublicIdChange = async (publicId: string) => {
|
||||
updateTypebot({ updates: { publicId } })
|
||||
updateTypebot({ updates: { publicId }, save: true })
|
||||
}
|
||||
|
||||
const publicId = typebot
|
||||
@ -50,7 +50,7 @@ export const SharePage = () => {
|
||||
}
|
||||
|
||||
const handleCustomDomainChange = (customDomain: string | null) =>
|
||||
updateTypebot({ updates: { customDomain } })
|
||||
updateTypebot({ updates: { customDomain }, save: true })
|
||||
|
||||
const checkIfPathnameIsValid = (pathname: string) => {
|
||||
const isCorrectlyFormatted =
|
||||
@ -113,8 +113,8 @@ export const SharePage = () => {
|
||||
onPathnameChange={handlePathnameChange}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<TrashIcon />}
|
||||
aria-label="Remove custom domain"
|
||||
icon={<CloseIcon />}
|
||||
aria-label="Remove custom URL"
|
||||
size="xs"
|
||||
onClick={() => handleCustomDomainChange(null)}
|
||||
/>
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
} from '../helpers/sanitizers'
|
||||
import { isWriteTypebotForbidden } from '../helpers/isWriteTypebotForbidden'
|
||||
import { isCloudProdInstance } from '@/helpers/isCloudProdInstance'
|
||||
import { Prisma } from '@typebot.io/prisma'
|
||||
|
||||
export const updateTypebot = authenticatedProcedure
|
||||
.meta({
|
||||
@ -134,9 +135,13 @@ export const updateTypebot = authenticatedProcedure
|
||||
folderId: typebot.folderId,
|
||||
variables: typebot.variables,
|
||||
edges: typebot.edges,
|
||||
resultsTablePreferences: typebot.resultsTablePreferences ?? undefined,
|
||||
publicId: typebot.publicId ?? undefined,
|
||||
customDomain: typebot.customDomain ?? undefined,
|
||||
resultsTablePreferences:
|
||||
typebot.resultsTablePreferences === null
|
||||
? Prisma.DbNull
|
||||
: typebot.resultsTablePreferences,
|
||||
publicId: typebot.publicId === null ? null : typebot.publicId,
|
||||
customDomain:
|
||||
typebot.customDomain === null ? null : typebot.customDomain,
|
||||
isClosed: typebot.isClosed,
|
||||
},
|
||||
})
|
||||
|
@ -2,7 +2,7 @@ import { MemberInWorkspace, User } from '@typebot.io/prisma'
|
||||
|
||||
export const isWriteWorkspaceForbidden = (
|
||||
workspace: {
|
||||
members: MemberInWorkspace[]
|
||||
members: Pick<MemberInWorkspace, 'userId' | 'role'>[]
|
||||
},
|
||||
user: Pick<User, 'email' | 'id'>
|
||||
) => {
|
||||
|
@ -11,6 +11,7 @@ import { workspaceRouter } from '@/features/workspace/api/router'
|
||||
import { router } from '../../trpc'
|
||||
import { analyticsRouter } from '@/features/analytics/api/router'
|
||||
import { collaboratorsRouter } from '@/features/collaboration/api/router'
|
||||
import { customDomainsRouter } from '@/features/customDomains/api/router'
|
||||
|
||||
export const trpcRouter = router({
|
||||
getAppVersionProcedure,
|
||||
@ -25,6 +26,7 @@ export const trpcRouter = router({
|
||||
credentials: credentialsRouter,
|
||||
theme: themeRouter,
|
||||
collaborators: collaboratorsRouter,
|
||||
customDomains: customDomainsRouter,
|
||||
})
|
||||
|
||||
export type AppRouter = typeof trpcRouter
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
notAuthenticated,
|
||||
} from '@typebot.io/lib/api'
|
||||
|
||||
// TODO: delete
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const user = await getAuthenticatedUser(req, res)
|
||||
if (!user) return notAuthenticated(res)
|
||||
|
@ -16375,9 +16375,6 @@
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"isClosed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"resultsTablePreferences": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -16415,6 +16412,9 @@
|
||||
"customDomain": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"isClosed": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@typebot.io/react",
|
||||
"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",
|
||||
"types": "dist/index.d.ts",
|
||||
"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) =>
|
||||
/^([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) =>
|
||||
/^(?:(?:[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