⚡ (customDomain) Add configuration modal for domain verification
Closes #742
This commit is contained in:
@@ -635,3 +635,11 @@ export const ChevronLastIcon = (props: IconProps) => (
|
|||||||
<path d="M17 6v12" />
|
<path d="M17 6v12" />
|
||||||
</Icon>
|
</Icon>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const XCircleIcon = (props: IconProps) => (
|
||||||
|
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="m15 9-6 6" />
|
||||||
|
<path d="m9 9 6 6" />
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const deleteCustomDomain = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
path: '/custom-domains',
|
path: '/custom-domains/{name}',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Delete custom domain',
|
summary: 'Delete custom domain',
|
||||||
tags: ['Custom domains'],
|
tags: ['Custom domains'],
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import { router } from '@/helpers/server/trpc'
|
|||||||
import { createCustomDomain } from './createCustomDomain'
|
import { createCustomDomain } from './createCustomDomain'
|
||||||
import { deleteCustomDomain } from './deleteCustomDomain'
|
import { deleteCustomDomain } from './deleteCustomDomain'
|
||||||
import { listCustomDomains } from './listCustomDomains'
|
import { listCustomDomains } from './listCustomDomains'
|
||||||
|
import { verifyCustomDomain } from './verifyCustomDomain'
|
||||||
|
|
||||||
export const customDomainsRouter = router({
|
export const customDomainsRouter = router({
|
||||||
createCustomDomain,
|
createCustomDomain,
|
||||||
deleteCustomDomain,
|
deleteCustomDomain,
|
||||||
listCustomDomains,
|
listCustomDomains,
|
||||||
|
verifyCustomDomain,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { DomainConfigResponse, DomainVerificationResponse } from '../types'
|
||||||
|
import {
|
||||||
|
DomainResponse,
|
||||||
|
DomainVerificationStatus,
|
||||||
|
domainResponseSchema,
|
||||||
|
domainVerificationStatusSchema,
|
||||||
|
} from '@typebot.io/schemas/features/customDomains'
|
||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { env } from '@typebot.io/env'
|
||||||
|
|
||||||
|
export const verifyCustomDomain = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'GET',
|
||||||
|
path: '/custom-domains/{name}/verify',
|
||||||
|
protect: true,
|
||||||
|
summary: 'Verify domain config',
|
||||||
|
tags: ['Custom domains'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
workspaceId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
status: domainVerificationStatusSchema,
|
||||||
|
domainJson: domainResponseSchema,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.query(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' })
|
||||||
|
|
||||||
|
let status: DomainVerificationStatus = 'Valid Configuration'
|
||||||
|
|
||||||
|
const [domainJson, configJson] = await Promise.all([
|
||||||
|
getDomainResponse(name),
|
||||||
|
getConfigResponse(name),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (domainJson?.error?.code === 'not_found') {
|
||||||
|
status = 'Domain Not Found'
|
||||||
|
} else if (domainJson.error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'INTERNAL_SERVER_ERROR',
|
||||||
|
message: domainJson.error.message,
|
||||||
|
})
|
||||||
|
} else if (!domainJson.verified) {
|
||||||
|
status = 'Pending Verification'
|
||||||
|
const verificationJson = await verifyDomain(name)
|
||||||
|
|
||||||
|
if (verificationJson && verificationJson.verified) {
|
||||||
|
status = 'Valid Configuration'
|
||||||
|
}
|
||||||
|
} else if (configJson.misconfigured) {
|
||||||
|
status = 'Invalid Configuration'
|
||||||
|
} else {
|
||||||
|
status = 'Valid Configuration'
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
domainJson,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const getDomainResponse = async (
|
||||||
|
domain: string
|
||||||
|
): Promise<DomainResponse & { error: { code: string; message: string } }> => {
|
||||||
|
return await fetch(
|
||||||
|
`https://api.vercel.com/v9/projects/${env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME}/domains/${domain}?teamId=${env.VERCEL_TEAM_ID}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${env.VERCEL_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
).then((res) => {
|
||||||
|
return res.json()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getConfigResponse = async (
|
||||||
|
domain: string
|
||||||
|
): Promise<DomainConfigResponse> => {
|
||||||
|
return await fetch(
|
||||||
|
`https://api.vercel.com/v6/domains/${domain}/config?teamId=${env.VERCEL_TEAM_ID}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${env.VERCEL_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
).then((res) => res.json())
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyDomain = async (
|
||||||
|
domain: string
|
||||||
|
): Promise<DomainVerificationResponse> => {
|
||||||
|
return await fetch(
|
||||||
|
`https://api.vercel.com/v9/projects/${env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME}/domains/${domain}/verify?teamId=${env.VERCEL_TEAM_ID}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${env.VERCEL_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
).then((res) => res.json())
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ 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])$/
|
||||||
|
|
||||||
type CustomDomainModalProps = {
|
type Props = {
|
||||||
workspaceId: string
|
workspaceId: string
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
@@ -30,13 +30,13 @@ type CustomDomainModalProps = {
|
|||||||
onNewDomain: (customDomain: string) => void
|
onNewDomain: (customDomain: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CustomDomainModal = ({
|
export const CreateCustomDomainModal = ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
onNewDomain,
|
onNewDomain,
|
||||||
domain = '',
|
domain = '',
|
||||||
}: CustomDomainModalProps) => {
|
}: Props) => {
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [inputValue, setInputValue] = useState(domain)
|
const [inputValue, setInputValue] = useState(domain)
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalBody,
|
||||||
|
HStack,
|
||||||
|
ModalFooter,
|
||||||
|
Button,
|
||||||
|
Text,
|
||||||
|
Box,
|
||||||
|
Code,
|
||||||
|
Stack,
|
||||||
|
Alert,
|
||||||
|
AlertIcon,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { XCircleIcon } from '@/components/icons'
|
||||||
|
import { trpc } from '@/lib/trpc'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
workspaceId: string
|
||||||
|
isOpen: boolean
|
||||||
|
domain: string
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomDomainConfigModal = ({
|
||||||
|
workspaceId,
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
domain,
|
||||||
|
}: Props) => {
|
||||||
|
const { data, error } = trpc.customDomains.verifyCustomDomain.useQuery({
|
||||||
|
name: domain,
|
||||||
|
workspaceId,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { domainJson, status } = data ?? {}
|
||||||
|
|
||||||
|
if (!status || status === 'Valid Configuration' || !domainJson) return null
|
||||||
|
|
||||||
|
if ('error' in domainJson) return null
|
||||||
|
|
||||||
|
const subdomain = getSubdomain(domainJson.name, domainJson.apexName)
|
||||||
|
|
||||||
|
const recordType = subdomain ? 'CNAME' : 'A'
|
||||||
|
|
||||||
|
const txtVerification =
|
||||||
|
(status === 'Pending Verification' &&
|
||||||
|
domainJson.verification?.find((x) => x.type === 'TXT')) ||
|
||||||
|
null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} size="xl">
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>
|
||||||
|
<HStack>
|
||||||
|
<XCircleIcon stroke="red.500" />
|
||||||
|
<Text fontSize="lg" fontWeight="semibold">
|
||||||
|
{status}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody>
|
||||||
|
{txtVerification ? (
|
||||||
|
<Stack spacing="4">
|
||||||
|
<Text>
|
||||||
|
Please set the following <Code>TXT</Code> record on{' '}
|
||||||
|
<Text as="span" fontWeight="bold">
|
||||||
|
{domainJson.apexName}
|
||||||
|
</Text>{' '}
|
||||||
|
to prove ownership of{' '}
|
||||||
|
<Text as="span" fontWeight="bold">
|
||||||
|
{domainJson.name}
|
||||||
|
</Text>
|
||||||
|
:
|
||||||
|
</Text>
|
||||||
|
<HStack
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="flex-start"
|
||||||
|
spacing="6"
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Text fontWeight="bold">Type</Text>
|
||||||
|
<Text fontSize="sm" fontFamily="mono">
|
||||||
|
{txtVerification.type}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Text fontWeight="bold">Name</Text>
|
||||||
|
<Text fontSize="sm" fontFamily="mono">
|
||||||
|
{txtVerification.domain.slice(
|
||||||
|
0,
|
||||||
|
txtVerification.domain.length -
|
||||||
|
domainJson.apexName.length -
|
||||||
|
1
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Text fontWeight="bold">Value</Text>
|
||||||
|
<Text fontSize="sm" fontFamily="mono">
|
||||||
|
<Box text-overflow="ellipsis" white-space="nowrap">
|
||||||
|
{txtVerification.value}
|
||||||
|
</Box>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</HStack>
|
||||||
|
<Alert status="warning">
|
||||||
|
<AlertIcon />
|
||||||
|
<Text>
|
||||||
|
If you are using this domain for another site, setting this
|
||||||
|
TXT record will transfer domain ownership away from that site
|
||||||
|
and break it. Please exercise caution when setting this
|
||||||
|
record.
|
||||||
|
</Text>
|
||||||
|
</Alert>
|
||||||
|
</Stack>
|
||||||
|
) : status === 'Unknown Error' ? (
|
||||||
|
<Text mb="5" fontSize="sm">
|
||||||
|
{error?.message}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<Text>
|
||||||
|
To configure your{' '}
|
||||||
|
{recordType === 'A' ? 'apex domain' : 'subdomain'} (
|
||||||
|
<Box as="span" fontWeight="bold">
|
||||||
|
{recordType === 'A' ? domainJson.apexName : domainJson.name}
|
||||||
|
</Box>
|
||||||
|
), set the following {recordType} record on your DNS provider to
|
||||||
|
continue:
|
||||||
|
</Text>
|
||||||
|
<HStack justifyContent="space-between">
|
||||||
|
<Stack>
|
||||||
|
<Text fontWeight="bold">Type</Text>
|
||||||
|
<Text fontFamily="mono" fontSize="sm">
|
||||||
|
{recordType}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Text fontWeight="bold">Name</Text>
|
||||||
|
<Text fontFamily="mono" fontSize="sm">
|
||||||
|
{recordType === 'A' ? '@' : subdomain ?? 'www'}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Text fontWeight="bold">Value</Text>
|
||||||
|
<Text fontFamily="mono" fontSize="sm">
|
||||||
|
{recordType === 'A'
|
||||||
|
? '76.76.21.21'
|
||||||
|
: `cname.vercel-dns.com`}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Text fontWeight="bold">TTL</Text>
|
||||||
|
<Text fontFamily="mono" fontSize="sm">
|
||||||
|
86400
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</HStack>
|
||||||
|
<Alert fontSize="sm">
|
||||||
|
<AlertIcon />
|
||||||
|
<Text>
|
||||||
|
Note: for TTL, if <Code>86400</Code> is not available, set the
|
||||||
|
highest value possible. Also, domain propagation can take up
|
||||||
|
to an hour.
|
||||||
|
</Text>
|
||||||
|
</Alert>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter as={HStack}>
|
||||||
|
<Button onClick={onClose}>Close</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSubdomain = (name: string, apexName: string) => {
|
||||||
|
if (name === apexName) return null
|
||||||
|
return name.slice(0, name.length - apexName.length - 1)
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { ChevronLeftIcon, PlusIcon, TrashIcon } from '@/components/icons'
|
import { ChevronLeftIcon, PlusIcon, TrashIcon } from '@/components/icons'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { CustomDomainModal } from './CustomDomainModal'
|
import { CreateCustomDomainModal } from './CreateCustomDomainModal'
|
||||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||||
import { useToast } from '@/hooks/useToast'
|
import { useToast } from '@/hooks/useToast'
|
||||||
import { trpc } from '@/lib/trpc'
|
import { trpc } from '@/lib/trpc'
|
||||||
@@ -83,7 +83,7 @@ export const CustomDomainsDropdown = ({
|
|||||||
return (
|
return (
|
||||||
<Menu isLazy placement="bottom-start" matchWidth>
|
<Menu isLazy placement="bottom-start" matchWidth>
|
||||||
{workspace?.id && (
|
{workspace?.id && (
|
||||||
<CustomDomainModal
|
<CreateCustomDomainModal
|
||||||
workspaceId={workspace.id}
|
workspaceId={workspace.id}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { XCircleIcon } from '@/components/icons'
|
||||||
|
import { trpc } from '@/lib/trpc'
|
||||||
|
import { useToast } from '@/hooks/useToast'
|
||||||
|
import { Flex, Tooltip, useDisclosure } from '@chakra-ui/react'
|
||||||
|
import { CustomDomainConfigModal } from './CustomDomainConfigModal'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
domain: string
|
||||||
|
workspaceId: string
|
||||||
|
}
|
||||||
|
export default function DomainStatusIcon({ domain, workspaceId }: Props) {
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
|
const { showToast } = useToast()
|
||||||
|
const { data, isLoading } = trpc.customDomains.verifyCustomDomain.useQuery(
|
||||||
|
{
|
||||||
|
name: domain,
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onError: (err) => {
|
||||||
|
showToast({ description: err.message })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isLoading || data?.status === 'Valid Configuration') return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip label={data?.status}>
|
||||||
|
<Flex onClick={onOpen} cursor="pointer">
|
||||||
|
<XCircleIcon stroke="red.500" />
|
||||||
|
</Flex>
|
||||||
|
</Tooltip>
|
||||||
|
<CustomDomainConfigModal
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
isOpen={isOpen}
|
||||||
|
domain={domain}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
27
apps/builder/src/features/customDomains/types.ts
Normal file
27
apps/builder/src/features/customDomains/types.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// Copied from https://github.com/vercel/platforms/blob/main/lib/types.ts
|
||||||
|
|
||||||
|
// From https://vercel.com/docs/rest-api/endpoints#get-a-domain-s-configuration
|
||||||
|
export interface DomainConfigResponse {
|
||||||
|
configuredBy?: ('CNAME' | 'A' | 'http') | null
|
||||||
|
acceptedChallenges?: ('dns-01' | 'http-01')[]
|
||||||
|
misconfigured: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// From https://vercel.com/docs/rest-api/endpoints#verify-project-domain
|
||||||
|
export interface DomainVerificationResponse {
|
||||||
|
name: string
|
||||||
|
apexName: string
|
||||||
|
projectId: string
|
||||||
|
redirect?: string | null
|
||||||
|
redirectStatusCode?: (307 | 301 | 302 | 308) | null
|
||||||
|
gitBranch?: string | null
|
||||||
|
updatedAt?: number
|
||||||
|
createdAt?: number
|
||||||
|
verified: boolean
|
||||||
|
verification?: {
|
||||||
|
type: string
|
||||||
|
domain: string
|
||||||
|
value: string
|
||||||
|
reason: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CloseIcon } from '@/components/icons'
|
import { TrashIcon } 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'
|
||||||
@@ -26,6 +26,7 @@ import { TypebotHeader } from '@/features/editor/components/TypebotHeader'
|
|||||||
import { parseDefaultPublicId } from '../helpers/parseDefaultPublicId'
|
import { parseDefaultPublicId } from '../helpers/parseDefaultPublicId'
|
||||||
import { useI18n } from '@/locales'
|
import { useI18n } from '@/locales'
|
||||||
import { env } from '@typebot.io/env'
|
import { env } from '@typebot.io/env'
|
||||||
|
import DomainStatusIcon from '@/features/customDomains/components/DomainStatusIcon'
|
||||||
|
|
||||||
export const SharePage = () => {
|
export const SharePage = () => {
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
@@ -113,11 +114,17 @@ export const SharePage = () => {
|
|||||||
onPathnameChange={handlePathnameChange}
|
onPathnameChange={handlePathnameChange}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<CloseIcon />}
|
icon={<TrashIcon />}
|
||||||
aria-label="Remove custom URL"
|
aria-label="Remove custom URL"
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={() => handleCustomDomainChange(null)}
|
onClick={() => handleCustomDomainChange(null)}
|
||||||
/>
|
/>
|
||||||
|
{workspace?.id && (
|
||||||
|
<DomainStatusIcon
|
||||||
|
domain={typebot.customDomain.split('/')[0]}
|
||||||
|
workspaceId={workspace.id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
)}
|
)}
|
||||||
{isNotDefined(typebot?.customDomain) &&
|
{isNotDefined(typebot?.customDomain) &&
|
||||||
|
|||||||
@@ -1,6 +1,40 @@
|
|||||||
import { CustomDomain as CustomDomainInDb } from '@typebot.io/prisma'
|
import { CustomDomain as CustomDomainInDb } from '@typebot.io/prisma'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const domainVerificationStatusSchema = z.enum([
|
||||||
|
'Valid Configuration',
|
||||||
|
'Invalid Configuration',
|
||||||
|
'Domain Not Found',
|
||||||
|
'Pending Verification',
|
||||||
|
'Unknown Error',
|
||||||
|
])
|
||||||
|
export type DomainVerificationStatus = z.infer<
|
||||||
|
typeof domainVerificationStatusSchema
|
||||||
|
>
|
||||||
|
|
||||||
|
export const domainResponseSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
apexName: z.string(),
|
||||||
|
projectId: z.string(),
|
||||||
|
redirect: z.string().nullable(),
|
||||||
|
redirectStatusCode: z.number().nullable(),
|
||||||
|
gitBranch: z.string().nullable(),
|
||||||
|
updatedAt: z.number().nullable(),
|
||||||
|
createdAt: z.number().nullable(),
|
||||||
|
verified: z.boolean(),
|
||||||
|
verification: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
type: z.string(),
|
||||||
|
domain: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
reason: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
export type DomainResponse = z.infer<typeof domainResponseSchema>
|
||||||
|
|
||||||
const domainNameRegex = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/
|
const domainNameRegex = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/
|
||||||
|
|
||||||
export const customDomainSchema = z.object({
|
export const customDomainSchema = z.object({
|
||||||
|
|||||||
Reference in New Issue
Block a user