Openstatus www.openstatus.dev

fix(dashboard): custom domain configuration (#1433)

* fix: custom domain configuration

* chore: refresh config button

* chore: delete legacy code

* wip:

* wip:

authored by

Maximilian Kaske and committed by
GitHub
c145c32a 311f362d

+117 -469
+44 -51
apps/dashboard/src/components/domains/domain-configuration.tsx
··· 1 1 "use client"; 2 2 3 + import { Badge } from "@/components/ui/badge"; 3 4 import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 4 5 import { cn } from "@/lib/utils"; 5 6 ··· 30 31 31 32 // FIXME: add loading state! 32 33 export default function DomainConfiguration({ domain }: { domain: string }) { 33 - const { status, domainJson } = useDomainStatus(domain); 34 + const { status, domainJson, isLoading } = useDomainStatus(domain); 34 35 35 36 if (!status || !domainJson) return null; 36 37 ··· 55 56 return ( 56 57 <div> 57 58 <div className="mb-4 flex items-center space-x-2"> 58 - <DomainStatusIcon status={status} /> 59 + <DomainStatusIcon status={status} loading={isLoading} /> 59 60 <p className="font-semibold">{status}</p> 61 + <Badge variant="secondary">{domain}</Badge> 60 62 </div> 61 63 {txtVerification ? ( 62 64 <> ··· 107 109 CNAME Record{subdomain && " (recommended)"} 108 110 </TabsTrigger> 109 111 </TabsList> 110 - <TabsContent value="A"> 111 - <div className="my-3 text-left"> 112 - <p className="my-5 text-sm"> 113 - To configure your apex domain ( 114 - <InlineSnippet>{domainJson.apexName}</InlineSnippet> 115 - ), set the following A record on your DNS provider to 116 - continue: 117 - </p> 118 - <div className="flex items-center justify-start space-x-10 rounded-md bg-muted p-2"> 119 - <div> 120 - <p className="font-bold text-sm">Type</p> 121 - <p className="mt-2 font-mono text-sm">A</p> 122 - </div> 123 - <div> 124 - <p className="font-bold text-sm">Name</p> 125 - <p className="mt-2 font-mono text-sm">@</p> 126 - </div> 127 - <div> 128 - <p className="font-bold text-sm">Value</p> 129 - <p className="mt-2 font-mono text-sm">76.76.21.21</p> 130 - </div> 131 - <div> 132 - <p className="font-bold text-sm">TTL</p> 133 - <p className="mt-2 font-mono text-sm">86400</p> 134 - </div> 112 + <TabsContent value="A" className="space-y-2"> 113 + <p className="text-sm"> 114 + To configure your apex domain ( 115 + <InlineSnippet>{domainJson.apexName}</InlineSnippet> 116 + ), set the following A record on your DNS provider to continue: 117 + </p> 118 + <div className="flex items-center justify-start space-x-10 rounded-md bg-muted p-2"> 119 + <div> 120 + <p className="font-bold text-sm">Type</p> 121 + <p className="mt-2 font-mono text-sm">A</p> 122 + </div> 123 + <div> 124 + <p className="font-bold text-sm">Name</p> 125 + <p className="mt-2 font-mono text-sm">@</p> 126 + </div> 127 + <div> 128 + <p className="font-bold text-sm">Value</p> 129 + <p className="mt-2 font-mono text-sm">76.76.21.21</p> 130 + </div> 131 + <div> 132 + <p className="font-bold text-sm">TTL</p> 133 + <p className="mt-2 font-mono text-sm">86400</p> 135 134 </div> 136 135 </div> 137 - <div> 138 - <p className="font-bold text-sm">Value</p> 139 - <p className="mt-2 font-mono text-sm">cname.vercel-dns.com</p> 140 - <div className="flex items-center justify-start space-x-10 rounded-md bg-muted p-2"> 141 - <div> 142 - <p className="font-bold text-sm">Type</p> 143 - <p className="mt-2 font-mono text-sm">CNAME</p> 144 - </div> 145 - <div> 146 - <p className="font-bold text-sm">Name</p> 147 - <p className="mt-2 font-mono text-sm"> 148 - {subdomain ?? "www"} 149 - </p> 150 - </div> 151 - <div> 152 - <p className="font-bold text-sm">Value</p> 153 - <p className="mt-2 font-mono text-sm"> 154 - {"cname.vercel-dns.com"} 155 - </p> 156 - </div> 157 - <div> 158 - <p className="font-bold text-sm">TTL</p> 159 - <p className="mt-2 font-mono text-sm">86400</p> 160 - </div> 136 + </TabsContent> 137 + <TabsContent value="CNAME"> 138 + <div className="flex items-center justify-start space-x-10 rounded-md bg-muted p-2"> 139 + <div> 140 + <p className="font-bold text-sm">Type</p> 141 + <p className="mt-2 font-mono text-sm">CNAME</p> 142 + </div> 143 + <div> 144 + <p className="font-bold text-sm">Name</p> 145 + <p className="mt-2 font-mono text-sm">{subdomain ?? "www"}</p> 146 + </div> 147 + <div> 148 + <p className="font-bold text-sm">Value</p> 149 + <p className="mt-2 font-mono text-sm">cname.vercel-dns.com</p> 150 + </div> 151 + <div> 152 + <p className="font-bold text-sm">TTL</p> 153 + <p className="mt-2 font-mono text-sm">86400</p> 161 154 </div> 162 155 </div> 163 156 </TabsContent>
+4 -1
apps/dashboard/src/components/domains/domain-status-icon.tsx
··· 12 12 loading?: boolean; 13 13 }) { 14 14 return loading ? ( 15 - <LoaderCircle className="text-background" stroke="currentColor" /> 15 + <LoaderCircle 16 + className="animate-spin text-muted-foreground" 17 + stroke="currentColor" 18 + /> 16 19 ) : status === "Valid Configuration" ? ( 17 20 <CheckCircle2 18 21 fill="#22c55e"
+33 -17
apps/dashboard/src/components/domains/use-domain-status.ts
··· 2 2 3 3 import { useTRPC } from "@/lib/trpc/client"; 4 4 import { useQuery } from "@tanstack/react-query"; 5 - import { useEffect } from "react"; 5 + import { useCallback } from "react"; 6 6 7 - export function useDomainStatus(domain: string) { 7 + export function useDomainStatus(domain?: string) { 8 8 const trpc = useTRPC(); 9 - const { data: domainJson, refetch: refetchDomain } = useQuery( 10 - trpc.domain.getDomainResponse.queryOptions({ domain }), 11 - ); 12 - const { data: configJson, refetch: refetchConfig } = useQuery( 13 - trpc.domain.getConfigResponse.queryOptions({ domain }), 14 - ); 15 - const { data: verificationJson, refetch: refetchVerification } = useQuery( 9 + const { 10 + data: domainJson, 11 + refetch: refetchDomain, 12 + isLoading: isLoadingDomain, 13 + isRefetching: isRefetchingDomain, 14 + } = useQuery(trpc.domain.getDomainResponse.queryOptions({ domain })); 15 + const { 16 + data: configJson, 17 + refetch: refetchConfig, 18 + isLoading: isLoadingConfig, 19 + isRefetching: isRefetchingConfig, 20 + } = useQuery(trpc.domain.getConfigResponse.queryOptions({ domain })); 21 + const { 22 + data: verificationJson, 23 + refetch: refetchVerification, 24 + isLoading: isLoadingVerification, 25 + isRefetching: isRefetchingVerification, 26 + } = useQuery( 16 27 trpc.domain.verifyDomain.queryOptions( 17 28 { domain }, 18 29 { enabled: !domainJson?.verified }, 19 30 ), 20 31 ); 21 32 22 - // NOTE: refetch every 5 seconds to check for the status 23 - useEffect(() => { 24 - const interval = setInterval(() => { 25 - refetchDomain(); 26 - refetchConfig(); 27 - refetchVerification(); 28 - }, 5000); 29 - return () => clearInterval(interval); 33 + const refreshAll = useCallback(() => { 34 + refetchDomain(); 35 + refetchConfig(); 36 + refetchVerification(); 30 37 }, [refetchDomain, refetchConfig, refetchVerification]); 31 38 32 39 let status: DomainVerificationStatusProps = "Valid Configuration"; ··· 52 59 } else { 53 60 status = "Valid Configuration"; 54 61 } 62 + 55 63 return { 56 64 status, 57 65 domainJson, 66 + refresh: refreshAll, 67 + isLoading: 68 + isLoadingDomain || 69 + isLoadingConfig || 70 + isLoadingVerification || 71 + isRefetchingDomain || 72 + isRefetchingConfig || 73 + isRefetchingVerification, 58 74 }; 59 75 }
+23 -4
apps/dashboard/src/components/forms/status-page/form-custom-domain.tsx
··· 23 23 24 24 import { Link } from "@/components/common/link"; 25 25 import DomainConfiguration from "@/components/domains/domain-configuration"; 26 + import { useDomainStatus } from "@/components/domains/use-domain-status"; 26 27 import { Form, FormField, FormItem, FormMessage } from "@/components/ui/form"; 27 28 import { isTRPCClientError } from "@trpc/client"; 28 29 import type React from "react"; 29 - import { useTransition } from "react"; 30 + import { useEffect, useTransition } from "react"; 30 31 import { toast } from "sonner"; 31 32 32 33 const schema = z.object({ ··· 52 53 }, 53 54 }); 54 55 const [isPending, startTransition] = useTransition(); 56 + const { refresh, isLoading } = useDomainStatus(defaultValues?.domain); 55 57 56 58 function submitAction(values: FormValues) { 57 59 if (isPending) return; ··· 76 78 }); 77 79 } 78 80 81 + // NOTE: poll every 30 seconds to check for the status 82 + useEffect(() => { 83 + const interval = setInterval(() => refresh(), 30_000); 84 + return () => clearInterval(interval); 85 + }, [refresh]); 86 + 79 87 return ( 80 88 <Form {...form}> 81 89 <form onSubmit={form.handleSubmit(submitAction)} {...props}> ··· 133 141 </Link> 134 142 </Button> 135 143 ) : ( 136 - <Button type="submit" disabled={isPending}> 137 - {isPending ? "Submitting..." : "Submit"} 138 - </Button> 144 + <div className="flex items-center gap-2"> 145 + <Button 146 + type="button" 147 + variant="ghost" 148 + disabled={isPending || isLoading} 149 + onClick={refresh} 150 + className="hidden sm:block" 151 + > 152 + {isLoading ? "Refreshing..." : "Refresh Configuration"} 153 + </Button> 154 + <Button type="submit" disabled={isPending}> 155 + {isPending ? "Submitting..." : "Submit"} 156 + </Button> 157 + </div> 139 158 )} 140 159 </FormCardFooter> 141 160 </FormCard>
-163
apps/web/src/components/domains/domain-configuration.tsx
··· 1 - "use client"; 2 - 3 - import { Tabs, TabsContent, TabsList, TabsTrigger } from "@openstatus/ui"; 4 - 5 - import { useDomainStatus } from "@/hooks/use-domain-status"; 6 - import { getSubdomain } from "@/lib/domains"; 7 - import { cn } from "@/lib/utils"; 8 - import DomainStatusIcon from "./domain-status-icon"; 9 - 10 - export const InlineSnippet = ({ 11 - className, 12 - children, 13 - }: { 14 - className?: string; 15 - children?: string; 16 - }) => { 17 - return ( 18 - <span 19 - className={cn( 20 - "inline-block rounded-md bg-muted px-1 py-0.5 font-mono", 21 - className, 22 - )} 23 - > 24 - {children} 25 - </span> 26 - ); 27 - }; 28 - export default function DomainConfiguration({ domain }: { domain: string }) { 29 - const domainStatus = useDomainStatus(domain); 30 - const { status, domainJson } = domainStatus || {}; 31 - 32 - if (!status || status === "Valid Configuration" || !domainJson) return null; 33 - 34 - const subdomain = 35 - domainJson?.name && domainJson?.apexName 36 - ? getSubdomain(domainJson.name, domainJson.apexName) 37 - : null; 38 - 39 - const txtVerification = 40 - (status === "Pending Verification" && 41 - domainJson?.verification?.find((x) => x.type === "TXT")) || 42 - null; 43 - 44 - return ( 45 - <div> 46 - <div className="mb-4 flex items-center space-x-2"> 47 - <DomainStatusIcon status={status} /> 48 - <p className="font-semibold text-lg">{status}</p> 49 - </div> 50 - {txtVerification ? ( 51 - <> 52 - <p className="text-sm"> 53 - Please set the following TXT record on{" "} 54 - <InlineSnippet>{domainJson.apexName}</InlineSnippet> to prove 55 - ownership of <InlineSnippet>{domainJson.name}</InlineSnippet>: 56 - </p> 57 - <div className="my-5 flex items-start justify-start space-x-10 rounded-md bg-muted p-2"> 58 - <div> 59 - <p className="font-bold text-sm">Type</p> 60 - <p className="mt-2 font-mono text-sm">{txtVerification.type}</p> 61 - </div> 62 - <div> 63 - <p className="font-bold text-sm">Name</p> 64 - <p className="mt-2 font-mono text-sm"> 65 - {txtVerification.domain.slice( 66 - 0, 67 - txtVerification.domain.length - 68 - (domainJson?.apexName?.length || 0) - 69 - 1, 70 - )} 71 - </p> 72 - </div> 73 - <div> 74 - <p className="font-bold text-sm">Value</p> 75 - <p className="mt-2 font-mono text-sm"> 76 - <span className="text-ellipsis">{txtVerification.value}</span> 77 - </p> 78 - </div> 79 - </div> 80 - <p className="text-muted-foreground text-sm"> 81 - Warning: if you are using this domain for another site, setting this 82 - TXT record will transfer domain ownership away from that site and 83 - break it. Please exercise caution when setting this record. 84 - </p> 85 - </> 86 - ) : status === "Unknown Error" ? ( 87 - <p className="mb-5 text-sm">{domainJson?.error?.message}</p> 88 - ) : ( 89 - <> 90 - <Tabs defaultValue={subdomain ? "CNAME" : "A"}> 91 - <TabsList> 92 - <TabsTrigger value="A"> 93 - A Record{!subdomain && " (recommended)"} 94 - </TabsTrigger> 95 - <TabsTrigger value="CNAME"> 96 - CNAME Record{subdomain && " (recommended)"} 97 - </TabsTrigger> 98 - </TabsList> 99 - <TabsContent value="A"> 100 - <div className="my-3 text-left"> 101 - <p className="my-5 text-sm"> 102 - To configure your apex domain ( 103 - <InlineSnippet>{domainJson.apexName}</InlineSnippet> 104 - ), set the following A record on your DNS provider to 105 - continue: 106 - </p> 107 - <div className="flex items-center justify-start space-x-10 rounded-md bg-muted p-2"> 108 - <div> 109 - <p className="font-bold text-sm">Type</p> 110 - <p className="mt-2 font-mono text-sm">A</p> 111 - </div> 112 - <div> 113 - <p className="font-bold text-sm">Name</p> 114 - <p className="mt-2 font-mono text-sm">@</p> 115 - </div> 116 - <div> 117 - <p className="font-bold text-sm">Value</p> 118 - <p className="mt-2 font-mono text-sm">76.76.21.21</p> 119 - </div> 120 - <div> 121 - <p className="font-bold text-sm">TTL</p> 122 - <p className="mt-2 font-mono text-sm">86400</p> 123 - </div> 124 - </div> 125 - </div> 126 - <div> 127 - <p className="font-bold text-sm">Value</p> 128 - <p className="mt-2 font-mono text-sm">cname.vercel-dns.com</p> 129 - <div className="flex items-center justify-start space-x-10 rounded-md bg-muted p-2"> 130 - <div> 131 - <p className="font-bold text-sm">Type</p> 132 - <p className="mt-2 font-mono text-sm">CNAME</p> 133 - </div> 134 - <div> 135 - <p className="font-bold text-sm">Name</p> 136 - <p className="mt-2 font-mono text-sm"> 137 - {subdomain ?? "www"} 138 - </p> 139 - </div> 140 - <div> 141 - <p className="font-bold text-sm">Value</p> 142 - <p className="mt-2 font-mono text-sm"> 143 - {"cname.vercel-dns.com"} 144 - </p> 145 - </div> 146 - <div> 147 - <p className="font-bold text-sm">TTL</p> 148 - <p className="mt-2 font-mono text-sm">86400</p> 149 - </div> 150 - </div> 151 - </div> 152 - </TabsContent> 153 - </Tabs> 154 - <p className="muted-foreground mt-5 text-sm"> 155 - Note: for TTL, if <InlineSnippet>86400</InlineSnippet> is not 156 - available, set the highest value possible. Also, domain propagation 157 - can take up to an hour. 158 - </p> 159 - </> 160 - )} 161 - </div> 162 - ); 163 - }
-33
apps/web/src/components/domains/domain-status-icon.tsx
··· 1 - "use client"; 2 - 3 - import { AlertCircle, CheckCircle2, XCircle } from "lucide-react"; 4 - 5 - import type { DomainVerificationStatusProps } from "@openstatus/api/src/router/domain"; 6 - 7 - import { LoadingAnimation } from "../loading-animation"; 8 - 9 - export default function DomainStatusIcon({ 10 - status, 11 - loading, 12 - }: { 13 - status: DomainVerificationStatusProps; 14 - loading?: boolean; 15 - }) { 16 - return loading ? ( 17 - <LoadingAnimation /> 18 - ) : status === "Valid Configuration" ? ( 19 - <CheckCircle2 20 - fill="#22c55e" 21 - stroke="currentColor" 22 - className="text-background" 23 - /> 24 - ) : status === "Pending Verification" ? ( 25 - <AlertCircle 26 - fill="#eab308" 27 - stroke="currentColor" 28 - className="text-background" 29 - /> 30 - ) : ( 31 - <XCircle fill="#ef4444" stroke="currentColor" className="text-background" /> 32 - ); 33 - }
-135
apps/web/src/components/forms/custom-domain-form.tsx
··· 1 - "use client"; 2 - 3 - import { zodResolver } from "@hookform/resolvers/zod"; 4 - import { useRouter } from "next/navigation"; 5 - import { useTransition } from "react"; 6 - import { useForm } from "react-hook-form"; 7 - import type * as z from "zod"; 8 - 9 - import { selectPageSchema } from "@openstatus/db/src/schema"; 10 - import { 11 - Button, 12 - Form, 13 - FormControl, 14 - FormDescription, 15 - FormField, 16 - FormItem, 17 - FormLabel, 18 - FormMessage, 19 - InputWithAddons, 20 - } from "@openstatus/ui"; 21 - 22 - import { useDomainStatus } from "@/hooks/use-domain-status"; 23 - import { toast, toastAction } from "@/lib/toast"; 24 - import { api } from "@/trpc/client"; 25 - import DomainConfiguration from "../domains/domain-configuration"; 26 - import DomainStatusIcon from "../domains/domain-status-icon"; 27 - import { LoadingAnimation } from "../loading-animation"; 28 - 29 - const customDomain = selectPageSchema.pick({ 30 - customDomain: true, 31 - id: true, 32 - }); 33 - 34 - type Schema = z.infer<typeof customDomain>; 35 - 36 - // TODO: check 37 - 38 - export function CustomDomainForm({ defaultValues }: { defaultValues: Schema }) { 39 - const form = useForm<Schema>({ 40 - resolver: zodResolver(customDomain), 41 - defaultValues, 42 - }); 43 - const router = useRouter(); 44 - const [isPending, startTransition] = useTransition(); 45 - const domainStatus = useDomainStatus(defaultValues?.customDomain); 46 - const { status } = domainStatus || {}; 47 - 48 - async function onSubmit(data: Schema) { 49 - startTransition(async () => { 50 - try { 51 - if (data.customDomain.toLowerCase().includes("openstatus")) { 52 - toast.error("Domain cannot contain 'openstatus'"); 53 - return; 54 - } 55 - 56 - await api.page.addCustomDomain.mutate({ 57 - customDomain: data.customDomain, 58 - pageId: defaultValues?.id, 59 - }); 60 - 61 - if (data.customDomain && !defaultValues.customDomain) { 62 - await api.domain.addDomainToVercel.mutate({ 63 - domain: data.customDomain, 64 - }); 65 - // if changed, remove old domain and add new one 66 - } else if ( 67 - defaultValues.customDomain && 68 - data.customDomain !== defaultValues.customDomain 69 - ) { 70 - await api.domain.removeDomainFromVercelProject.mutate({ 71 - domain: defaultValues.customDomain, 72 - }); 73 - await api.domain.addDomainToVercel.mutate({ 74 - domain: data.customDomain, 75 - }); 76 - // if removed 77 - } else if (data.customDomain === "") { 78 - await api.domain.removeDomainFromVercelProject.mutate({ 79 - domain: defaultValues.customDomain, 80 - }); 81 - } 82 - toastAction("saved"); 83 - router.refresh(); 84 - } catch { 85 - toastAction("error"); 86 - } 87 - }); 88 - } 89 - 90 - return ( 91 - <Form {...form}> 92 - <form 93 - onSubmit={form.handleSubmit(onSubmit)} 94 - className="grid w-full grid-cols-1 items-center gap-6 sm:grid-cols-6" 95 - > 96 - <FormField 97 - control={form.control} 98 - name="customDomain" 99 - render={({ field }) => ( 100 - <FormItem className="sm:col-span-4"> 101 - <FormLabel>Custom Domain</FormLabel> 102 - <FormControl> 103 - <div className="flex items-center space-x-3"> 104 - <InputWithAddons 105 - placeholder="status.documenso.com" 106 - leading="https://" 107 - {...field} 108 - /> 109 - <div className="h-full w-7"> 110 - {/* TODO: add loading state */} 111 - {status ? <DomainStatusIcon status={status} /> : null} 112 - </div> 113 - </div> 114 - </FormControl> 115 - <FormDescription> 116 - The custom domain for your status page. 117 - </FormDescription> 118 - <FormMessage /> 119 - </FormItem> 120 - )} 121 - /> 122 - <div className="sm:col-span-full"> 123 - <Button className="w-full sm:w-auto" size="lg"> 124 - {!isPending ? "Confirm" : <LoadingAnimation />} 125 - </Button> 126 - </div> 127 - <div className="sm:col-span-5"> 128 - {defaultValues?.customDomain ? ( 129 - <DomainConfiguration domain={defaultValues.customDomain} /> 130 - ) : null} 131 - </div> 132 - </form> 133 - </Form> 134 - ); 135 - }
-62
apps/web/src/hooks/use-domain-status.ts
··· 1 - import { useEffect, useState } from "react"; 2 - 3 - import type { 4 - DomainResponse, 5 - DomainVerificationStatusProps, 6 - } from "@openstatus/api/src/router/domain"; 7 - 8 - import { api } from "@/trpc/client"; 9 - 10 - export function useDomainStatus(domain: string) { 11 - const [domainVerification, setDomainVerification] = useState<{ 12 - status: DomainVerificationStatusProps; 13 - domainJson: DomainResponse & { error?: { code: string; message: string } }; 14 - }>(); 15 - 16 - useEffect(() => { 17 - async function checkDomain() { 18 - const data = await verifyDomain(domain); 19 - setDomainVerification(data); 20 - } 21 - checkDomain(); // first render! 22 - setInterval(checkDomain, 5000); 23 - // clearInterval(myInterval) 24 - }, [domain]); 25 - 26 - return domainVerification; 27 - } 28 - 29 - export async function verifyDomain(domain: string) { 30 - let status: DomainVerificationStatusProps = "Valid Configuration"; 31 - const [domainJson, configJson] = await Promise.all([ 32 - api.domain.getDomainResponse.query({ domain }), 33 - api.domain.getConfigResponse.query({ domain }), 34 - ]); 35 - 36 - if (domainJson?.error?.code === "not_found") { 37 - // domain not found on Vercel project 38 - status = "Domain Not Found"; 39 - 40 - // unknown error 41 - } else if (domainJson.error) { 42 - status = "Unknown Error"; 43 - 44 - // if domain is not verified, we try to verify now 45 - } else if (!domainJson.verified) { 46 - status = "Pending Verification"; 47 - const verificationJson = await api.domain.verifyDomain.query({ domain }); 48 - 49 - // domain was just verified 50 - if (verificationJson?.verified) { 51 - status = "Valid Configuration"; 52 - } 53 - } else if (configJson.misconfigured) { 54 - status = "Invalid Configuration"; 55 - } else { 56 - status = "Valid Configuration"; 57 - } 58 - return { 59 - status, 60 - domainJson, 61 - }; 62 - }
+13 -3
packages/api/src/router/domain.ts
··· 106 106 // return await data.json(); 107 107 // }), 108 108 getDomainResponse: protectedProcedure 109 - .input(z.object({ domain: z.string() })) 109 + .input(z.object({ domain: z.string().optional() })) 110 110 .query(async (opts) => { 111 + if (!opts.input.domain) { 112 + return null; 113 + } 111 114 const data = await fetch( 112 115 `https://api.vercel.com/v9/projects/${env.PROJECT_ID_VERCEL}/domains/${opts.input.domain}?teamId=${env.TEAM_ID_VERCEL}`, 113 116 { ··· 129 132 .optional(), 130 133 }) 131 134 .parse(json); 135 + console.log({ result }); 132 136 return result; 133 137 }), 134 138 getConfigResponse: protectedProcedure 135 - .input(z.object({ domain: z.string() })) 139 + .input(z.object({ domain: z.string().optional() })) 136 140 .query(async (opts) => { 141 + if (!opts.input.domain) { 142 + return null; 143 + } 137 144 const data = await fetch( 138 145 `https://api.vercel.com/v6/domains/${opts.input.domain}/config?teamId=${env.TEAM_ID_VERCEL}`, 139 146 { ··· 149 156 return result; 150 157 }), 151 158 verifyDomain: protectedProcedure 152 - .input(z.object({ domain: z.string() })) 159 + .input(z.object({ domain: z.string().optional() })) 153 160 .query(async (opts) => { 161 + if (!opts.input.domain) { 162 + return null; 163 + } 154 164 const data = await fetch( 155 165 `https://api.vercel.com/v9/projects/${env.PROJECT_ID_VERCEL}/domains/${opts.input.domain}/verify?teamId=${env.TEAM_ID_VERCEL}`, 156 166 {