Skip to content

Commit

Permalink
⚡ (customDomains) Fix custom domain update feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno committed Aug 21, 2023
1 parent dc4c19a commit c08e0cd
Show file tree
Hide file tree
Showing 19 changed files with 506 additions and 104 deletions.
85 changes: 85 additions & 0 deletions apps/builder/src/features/customDomains/api/createCustomDomain.ts
Original file line number Diff line number Diff line change
@@ -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 },
})
68 changes: 68 additions & 0 deletions apps/builder/src/features/customDomains/api/deleteCustomDomain.ts
Original file line number Diff line number Diff line change
@@ -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}` },
})
54 changes: 54 additions & 0 deletions apps/builder/src/features/customDomains/api/listCustomDomains.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 10 additions & 0 deletions apps/builder/src/features/customDomains/api/router.ts
Original file line number Diff line number Diff line change
@@ -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,
})
Original file line number Diff line number Diff line change
Expand Up @@ -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])$/
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) => () =>
Expand All @@ -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 (
Expand All @@ -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"
Expand All @@ -107,6 +119,7 @@ export const CustomDomainsDropdown = ({
>
{customDomain.name}
<IconButton
as="span"
icon={<TrashIcon />}
aria-label="Remove domain"
size="xs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"]')
Expand Down
Loading

4 comments on commit c08e0cd

@vercel
Copy link

@vercel vercel bot commented on c08e0cd Aug 21, 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

gsbulletin.com
journey.cr8.ai
kopibayane.com
panther.cr7.ai
panther.cr8.ai
pay.sifuim.com
penguin.cr8.ai
segredomeu.com
talk.gocare.io
test.bot.gives
ticketfute.com
unicorn.cr8.ai
whats-app.chat
apo.nigerias.io
app.blogely.com
apr.nigerias.io
aso.nigerias.io
blackcan.cr8.ai
blackvip.online
bot.4display.nl
bot.a6t-you.com
bot.artiweb.app
bot.devitus.com
bot.reeplai.com
bot.scayver.com
bot.tc-mail.com
carspecs.lam.ee
chat.lalmon.com
chat.sureb4.com
conversawpp.com
eventhub.com.au
feiraododia.com
fitness.riku.ai
games.klujo.com
localove.online
proscale.com.br
sellmycarbr.com
svhmapp.mprs.in
typebot.aloe.do
app-liberado.pro
ask.pemantau.org
batepapo.digital
bot.contakit.com
bot.imovfast.com
bot.piccinato.co
botc.ceox.com.br
chat.sifucrm.com
chat.syncwin.com
chatonlineja.com
clo.closeer.work
desafioem21d.com
faqs.nigerias.io
revistasaudeemdia.com
rossano.thegymgame.it
sbutton.wpwakanda.com
segredosdothreads.com
talk.convobuilder.com
terrosdoscassinos.com
test.leadbooster.help
whats.laracardoso.com
www.acesso-app.online
www.hemertonsilva.com
zillabot.saaszilla.co
815639944.21000000.one
83720273.bouclidom.com
aplicacao.bmind.com.br
apply.ansuraniphone.my
bbutton.wpwwakanda.com
bolsamaisbrasil.app.br
bot.chat-debora.online
bot.clubedotrader.club
bot.louismarcondes.com
bot.perfaceacademy.com
bot.pratikmandalia.com
bot.sucessodigital.xyz
bot.t20worldcup.com.au
bot.whatsappweb.adm.br
bot2.mycompany.reviews
bot3.mycompany.reviews
bot4.mycompany.reviews
c23111azqw.nigerias.io
chat.footballmeetup.ie
conto.barrettamario.it
dieta.barrettamario.it
felipewelington.com.br
form.bridesquadapp.com
form.searchcube.com.sg
go.orodrigoribeiro.com
help.giversforgood.com
info.clickasuransi.com
jenifferrodrigues.club
kodawariab736.skeep.it
mdb.diego.progenbr.com
michaeljackson.riku.ai
premium.kandabrand.com
psicologiacognitiva.co
report.gratirabbit.com
resume.gratirabbit.com
suporte.pedroallan.com
teambuilding.hidden.sg

@vercel
Copy link

@vercel vercel bot commented on c08e0cd Aug 21, 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 c08e0cd Aug 21, 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

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

@vercel
Copy link

@vercel vercel bot commented on c08e0cd Aug 21, 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-typebot-io.vercel.app
docs.typebot.io
docs-git-main-typebot-io.vercel.app

Please sign in to comment.