CustomDomainVerify.tsx200 lines · main
| 1 | import { useParams } from 'common' |
| 2 | import { AlertCircle, RefreshCw } from 'lucide-react' |
| 3 | import { useEffect } from 'react' |
| 4 | import { toast } from 'sonner' |
| 5 | import { Alert, AlertDescription, AlertTitle, Button, WarningIcon } from 'ui' |
| 6 | import { Admonition } from 'ui-patterns/admonition' |
| 7 | |
| 8 | import { DNSRecord } from './DNSRecord' |
| 9 | import { DNSTableHeaders } from './DNSTableHeaders' |
| 10 | import { DocsButton } from '@/components/ui/DocsButton' |
| 11 | import { InlineLink } from '@/components/ui/InlineLink' |
| 12 | import Panel from '@/components/ui/Panel' |
| 13 | import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' |
| 14 | import { useCustomDomainDeleteMutation } from '@/data/custom-domains/custom-domains-delete-mutation' |
| 15 | import { useCustomDomainsQuery } from '@/data/custom-domains/custom-domains-query' |
| 16 | import { useCustomDomainReverifyQuery } from '@/data/custom-domains/custom-domains-reverify-query' |
| 17 | import { DOCS_URL } from '@/lib/constants' |
| 18 | |
| 19 | export const CustomDomainVerify = () => { |
| 20 | const { ref: projectRef } = useParams() |
| 21 | |
| 22 | const { data: settings } = useProjectSettingsV2Query({ projectRef }) |
| 23 | |
| 24 | const { data: customDomainData } = useCustomDomainsQuery({ projectRef }) |
| 25 | const customDomain = customDomainData?.customDomain |
| 26 | const isSSLCertificateDeploying = |
| 27 | customDomain?.ssl.status !== undefined && customDomain.ssl.txt_name === undefined |
| 28 | |
| 29 | const { mutate: deleteCustomDomain, isPending: isDeleting } = useCustomDomainDeleteMutation({ |
| 30 | onSuccess: () => { |
| 31 | toast.success( |
| 32 | 'Custom domain setup cancelled successfully. It may take a few seconds before your custom domain is fully removed, so you may need to refresh your browser.' |
| 33 | ) |
| 34 | }, |
| 35 | }) |
| 36 | |
| 37 | const { |
| 38 | data: reverifyData, |
| 39 | refetch: refetchReverify, |
| 40 | isFetching: isReverifyLoading, |
| 41 | isError: isReverifyError, |
| 42 | error: reverifyError, |
| 43 | } = useCustomDomainReverifyQuery( |
| 44 | { projectRef }, |
| 45 | { |
| 46 | // Poll every 10 seconds if the SSL certificate is being deployed |
| 47 | refetchInterval: isSSLCertificateDeploying && !isDeleting ? 10000 : false, |
| 48 | } |
| 49 | ) |
| 50 | |
| 51 | useEffect(() => { |
| 52 | if (isReverifyError) { |
| 53 | toast.error(reverifyError?.message) |
| 54 | } |
| 55 | }, [isReverifyError, reverifyError]) |
| 56 | |
| 57 | const isNotVerifiedYet = reverifyData?.status === '2_initiated' |
| 58 | |
| 59 | const hasCAAErrors = customDomain?.ssl.validation_errors?.reduce( |
| 60 | (acc, error) => acc || error.message.includes('caa_error'), |
| 61 | false |
| 62 | ) |
| 63 | const isValidating = (customDomain?.ssl.txt_name ?? '') === '' |
| 64 | |
| 65 | const onReverifyCustomDomain = () => { |
| 66 | if (!projectRef) return console.error('Project ref is required') |
| 67 | refetchReverify() |
| 68 | } |
| 69 | |
| 70 | const onCancelCustomDomain = async () => { |
| 71 | if (!projectRef) return console.error('Project ref is required') |
| 72 | deleteCustomDomain({ projectRef }) |
| 73 | } |
| 74 | |
| 75 | return ( |
| 76 | <> |
| 77 | <Panel.Content className="space-y-6"> |
| 78 | <div> |
| 79 | <h4 className="text-foreground mb-2"> |
| 80 | Configure TXT verification for your custom domain{' '} |
| 81 | <code className="text-code-inline">{customDomain?.hostname}</code> |
| 82 | </h4> |
| 83 | <p className="text-sm text-foreground-light"> |
| 84 | Set the following TXT record(s) in your DNS provider, then click verify to confirm your |
| 85 | control over the domain. Records which have been successfully verified will be removed |
| 86 | from this list below. |
| 87 | </p> |
| 88 | {!isValidating && ( |
| 89 | <div className="mt-4 mb-2"> |
| 90 | <Admonition |
| 91 | type="note" |
| 92 | title={ |
| 93 | isNotVerifiedYet |
| 94 | ? 'Unable to verify records from DNS provider yet.' |
| 95 | : 'Please note that it may take up to 24 hours for the DNS records to propagate.' |
| 96 | } |
| 97 | > |
| 98 | <p> |
| 99 | You may also visit{' '} |
| 100 | <InlineLink href={`https://whatsmydns.net/#TXT/${customDomain?.ssl.txt_name}`}> |
| 101 | here |
| 102 | </InlineLink>{' '} |
| 103 | to check if your DNS has been propagated successfully before clicking verify. |
| 104 | </p> |
| 105 | {isNotVerifiedYet && ( |
| 106 | <p className="mt-1"> |
| 107 | Some registrars will require you to remove the domain name when creating DNS |
| 108 | records. As an example, to create a record for{' '} |
| 109 | <code className="text-code-inline">foo.app.example.com</code>, you would need to |
| 110 | create an entry for <code className="text-code-inline">foo.app</code>. |
| 111 | </p> |
| 112 | )} |
| 113 | </Admonition> |
| 114 | </div> |
| 115 | )} |
| 116 | </div> |
| 117 | |
| 118 | {hasCAAErrors && ( |
| 119 | <Alert> |
| 120 | <WarningIcon /> |
| 121 | <AlertTitle>Certificate Authority Authentication (CAA) error</AlertTitle> |
| 122 | <AlertDescription> |
| 123 | Please add a CAA record allowing "digicert.com" to issue certificates for{' '} |
| 124 | <code className="text-code-inline">{customDomain?.hostname}</code>. For example:{' '} |
| 125 | <code className="text-code-inline">0 issue "digicert.com"</code> |
| 126 | </AlertDescription> |
| 127 | </Alert> |
| 128 | )} |
| 129 | |
| 130 | {customDomain?.ssl.status === 'validation_timed_out' ? ( |
| 131 | <Alert> |
| 132 | <WarningIcon /> |
| 133 | <AlertTitle>Validation timed out</AlertTitle> |
| 134 | <AlertDescription> |
| 135 | Please click "Verify" again to retry the validation of the records |
| 136 | </AlertDescription> |
| 137 | </Alert> |
| 138 | ) : ( |
| 139 | <div className="space-y-2"> |
| 140 | <DNSTableHeaders display={customDomain?.ssl.txt_name ?? ''} /> |
| 141 | |
| 142 | {customDomain?.verification_errors?.includes( |
| 143 | 'custom hostname does not CNAME to this zone.' |
| 144 | ) && ( |
| 145 | <DNSRecord |
| 146 | type="CNAME" |
| 147 | name={customDomain?.hostname} |
| 148 | value={settings?.app_config?.endpoint ?? 'Loading...'} |
| 149 | /> |
| 150 | )} |
| 151 | |
| 152 | {!isValidating && customDomain?.ssl.status === 'pending_validation' && ( |
| 153 | <DNSRecord |
| 154 | type="TXT" |
| 155 | name={customDomain?.ssl.txt_name ?? 'Loading...'} |
| 156 | value={customDomain?.ssl.txt_value ?? 'Loading...'} |
| 157 | /> |
| 158 | )} |
| 159 | |
| 160 | {customDomain?.ssl.status === 'pending_deployment' && ( |
| 161 | <div className="flex items-center justify-center space-x-2 py-8"> |
| 162 | <AlertCircle size={16} strokeWidth={1.5} /> |
| 163 | <p className="text-sm text-foreground-light"> |
| 164 | SSL certificate is being deployed. Please wait a few minutes and try again. |
| 165 | </p> |
| 166 | </div> |
| 167 | )} |
| 168 | </div> |
| 169 | )} |
| 170 | </Panel.Content> |
| 171 | |
| 172 | <div className="border-t border-muted" /> |
| 173 | |
| 174 | <Panel.Content> |
| 175 | <div className="flex items-center justify-between"> |
| 176 | <DocsButton href={`${DOCS_URL}/guides/platform/custom-domains`} /> |
| 177 | <div className="flex items-center space-x-2"> |
| 178 | <Button |
| 179 | type="default" |
| 180 | onClick={onCancelCustomDomain} |
| 181 | loading={isDeleting} |
| 182 | className="self-end" |
| 183 | > |
| 184 | Cancel |
| 185 | </Button> |
| 186 | <Button |
| 187 | icon={<RefreshCw />} |
| 188 | onClick={onReverifyCustomDomain} |
| 189 | loading={!isValidating && isReverifyLoading} |
| 190 | disabled={isDeleting || isReverifyLoading || isValidating} |
| 191 | className="self-end" |
| 192 | > |
| 193 | Verify |
| 194 | </Button> |
| 195 | </div> |
| 196 | </div> |
| 197 | </Panel.Content> |
| 198 | </> |
| 199 | ) |
| 200 | } |