Skip to content

Commit

Permalink
⚡ (customDomain) Add configuration modal for domain verification
Browse files Browse the repository at this point in the history
Closes #742
  • Loading branch information
baptisteArno committed Sep 18, 2023
1 parent 21ad061 commit 322c48c
Show file tree
Hide file tree
Showing 11 changed files with 447 additions and 8 deletions.
8 changes: 8 additions & 0 deletions apps/builder/src/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -635,3 +635,11 @@ export const ChevronLastIcon = (props: IconProps) => (
<path d="M17 6v12" />
</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>
)
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const deleteCustomDomain = authenticatedProcedure
.meta({
openapi: {
method: 'DELETE',
path: '/custom-domains',
path: '/custom-domains/{name}',
protect: true,
summary: 'Delete custom domain',
tags: ['Custom domains'],
Expand Down
2 changes: 2 additions & 0 deletions apps/builder/src/features/customDomains/api/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { router } from '@/helpers/server/trpc'
import { createCustomDomain } from './createCustomDomain'
import { deleteCustomDomain } from './deleteCustomDomain'
import { listCustomDomains } from './listCustomDomains'
import { verifyCustomDomain } from './verifyCustomDomain'

export const customDomainsRouter = router({
createCustomDomain,
deleteCustomDomain,
listCustomDomains,
verifyCustomDomain,
})
131 changes: 131 additions & 0 deletions apps/builder/src/features/customDomains/api/verifyCustomDomain.ts
Original file line number Diff line number Diff line change
@@ -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())
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,21 @@ 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])$/

type CustomDomainModalProps = {
type Props = {
workspaceId: string
isOpen: boolean
onClose: () => void
domain?: string
onNewDomain: (customDomain: string) => void
}

export const CustomDomainModal = ({
export const CreateCustomDomainModal = ({
workspaceId,
isOpen,
onClose,
onNewDomain,
domain = '',
}: CustomDomainModalProps) => {
}: Props) => {
const inputRef = useRef<HTMLInputElement>(null)
const [isLoading, setIsLoading] = useState(false)
const [inputValue, setInputValue] = useState(domain)
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from '@chakra-ui/react'
import { ChevronLeftIcon, PlusIcon, TrashIcon } from '@/components/icons'
import React, { useState } from 'react'
import { CustomDomainModal } from './CustomDomainModal'
import { CreateCustomDomainModal } from './CreateCustomDomainModal'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { useToast } from '@/hooks/useToast'
import { trpc } from '@/lib/trpc'
Expand Down Expand Up @@ -83,7 +83,7 @@ export const CustomDomainsDropdown = ({
return (
<Menu isLazy placement="bottom-start" matchWidth>
{workspace?.id && (
<CustomDomainModal
<CreateCustomDomainModal
workspaceId={workspace.id}
isOpen={isOpen}
onClose={onClose}
Expand Down
Loading

4 comments on commit 322c48c

@vercel
Copy link

@vercel vercel bot commented on 322c48c Sep 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 322c48c Sep 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

builder-v2 – ./apps/builder

builder-v2-git-main-typebot-io.vercel.app
builder-v2-typebot-io.vercel.app
app.typebot.io

@vercel
Copy link

@vercel vercel bot commented on 322c48c Sep 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

docs – ./apps/docs

docs-git-main-typebot-io.vercel.app
docs.typebot.io
docs-typebot-io.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 322c48c Sep 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

viewer-v2 – ./apps/viewer

acordo-certo.com
app-liberado.pro
ask.pemantau.org
batepapo.digital
bot.contakit.com
bot.imovfast.com
bot.piccinato.co
chat.sifucrm.com
chat.syncwin.com
chatonlineja.com
clo.closeer.work
cockroach.cr8.ai
desafioem21d.com
digitando.online
faqs.nigerias.io
feiraodehoje.com
georgemarttt.com
go.chatbotcv.com
haymanevents.com
kw.wpwakanda.com
localamor.online
lojamundobox.com
my.skillbrow.com
myrentalhost.com
silvercop.com.br
silvercop.online
stan.vselise.com
typebot.aloe.bot
vidalimentar.com
voicehelp.cr8.ai
web.bjogador.com
webwhatsapp.work
whatisappweb.com
www.pantaflow.ai
zap.fundviser.in
analistamines.com
app.bouclidom.com
app.chatforms.net
appbotcontato.com
appmillion.online
averdadehoje.site
bot.cerograsa.com
bot.chatbotcv.com
bot.hostnation.de
bot.ketoolife.com
bot.maitempah.com
bot.phuonghub.com
bot.reviewamp.com
bot.reviewzer.com
bot.uluhub.com.br
chat.daftarjer.com
chat.hand-made.one
chat.tuanpakya.com
chat.webisharp.com
chatbotforthat.com
descobrindotudo.me
dicanatural.online
digitalhelp.com.au
draraquelnutri.com
drcarlosyoshi.site
goalsettingbot.com
viewer-v2-typebot-io.vercel.app
mdb.assessoria.fernanda.progenbr.com
mdb.assessoria.jbatista.progenbr.com
mdb.assessoria.mauricio.progenbr.com
mdb.evento.autocadastro.progenbr.com
form.shopmercedesbenzsouthorlando.com
mdb.evento.equipeinterna.progenbr.com
bot.studiotecnicoimmobiliaremerelli.it
mdb.assessoria.boaventura.progenbr.com
mdb.assessoria.jtrebesqui.progenbr.com
pesquisa.escolamodacomproposito.com.br
anamnese.clinicaramosodontologia.com.br
gabinete.baleia.formulario.progenbr.com
mdb.assessoria.carreirinha.progenbr.com
chrome-os-inquiry-system.itschromeos.com
mdb.assessoria.paulomarques.progenbr.com
viewer-v2-git-main-typebot-io.vercel.app
main-menu-for-itschromeos.itschromeos.com
mdb.assessoria.qrcode.ademir.progenbr.com
mdb.assessoria.qrcode.arthur.progenbr.com
mdb.assessoria.qrcode.danilo.progenbr.com
mdb.assessoria.qrcode.marcao.progenbr.com
mdb.assessoria.qrcode.marcio.progenbr.com
mdb.assessoria.qrcode.aloisio.progenbr.com
mdb.assessoria.qrcode.girotto.progenbr.com
mdb.assessoria.qrcode.marinho.progenbr.com
mdb.assessoria.qrcode.rodrigo.progenbr.com
mdb.assessoria.carlosalexandre.progenbr.com
mdb.assessoria.qrcode.desideri.progenbr.com
mdb.assessoria.qrcode.fernanda.progenbr.com
mdb.assessoria.qrcode.jbatista.progenbr.com
mdb.assessoria.qrcode.mauricio.progenbr.com
mdb.assessoria.fernanda.regional.progenbr.com
mdb.assessoria.qrcode.boaventura.progenbr.com
mdb.assessoria.qrcode.jtrebesqui.progenbr.com
mdb.assessoria.qrcode.carreirinha.progenbr.com
mdb.assessoria.qrcode.paulomarques.progenbr.com
mdb.assessoria.qrcode.carlosalexandre.progenbr.com
mdb.assessoria.qrcode.fernanda.regional.progenbr.com

Please sign in to comment.