CustomDomainsConfigureHostname.tsx175 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { PermissionAction } from '@supabase/shared-types/out/constants'
3import { useParams } from 'common'
4import { useForm } from 'react-hook-form'
5import {
6 Button,
7 Card,
8 CardContent,
9 CardFooter,
10 CardHeader,
11 CardTitle,
12 Form,
13 FormControl,
14 FormField,
15 Input,
16} from 'ui'
17import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
18import { z } from 'zod'
19
20import CopyButton from '@/components/ui/CopyButton'
21import { DocsButton } from '@/components/ui/DocsButton'
22import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
23import { useCheckCNAMERecordMutation } from '@/data/custom-domains/check-cname-mutation'
24import { useCustomDomainCreateMutation } from '@/data/custom-domains/custom-domains-create-mutation'
25import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
26import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
27import { DOCS_URL } from '@/lib/constants'
28
29const schema = z.object({
30 domain: z.string().trim().min(1, 'A value for your custom domain is required'),
31})
32
33export const CustomDomainsConfigureHostname = () => {
34 const { ref } = useParams()
35 const { data: project } = useSelectedProjectQuery()
36
37 const { mutate: checkCNAMERecord, isPending: isCheckingRecord } = useCheckCNAMERecordMutation()
38 const { mutate: createCustomDomain, isPending: isCreating } = useCustomDomainCreateMutation()
39 const { data: settings } = useProjectSettingsV2Query({ projectRef: ref })
40
41 const endpoint = settings?.app_config?.endpoint
42 const { can: canConfigureCustomDomain } = useAsyncCheckPermissions(
43 PermissionAction.UPDATE,
44 'projects',
45 {
46 resource: {
47 project_id: project?.id,
48 },
49 }
50 )
51
52 const form = useForm<z.infer<typeof schema>>({
53 resolver: zodResolver(schema as any),
54 defaultValues: {
55 domain: '',
56 },
57 mode: 'onSubmit',
58 reValidateMode: 'onBlur',
59 })
60
61 const onCreateCustomDomain = async (values: z.infer<typeof schema>) => {
62 if (!ref) return console.error('Project ref is required')
63
64 checkCNAMERecord(
65 { domain: values.domain.trim() },
66 {
67 onSuccess: () => {
68 createCustomDomain({ projectRef: ref, customDomain: values.domain.trim() })
69 },
70 }
71 )
72 }
73
74 const domain = form.watch('domain')
75 const trimmedDomain = domain.trim()
76 const isSubmitting = isCheckingRecord || isCreating
77
78 return (
79 <Form {...form}>
80 <form onSubmit={form.handleSubmit(onCreateCustomDomain)}>
81 <Card>
82 <CardHeader className="flex flex-row items-center justify-between space-y-0 gap-4">
83 <CardTitle>Add a custom domain</CardTitle>
84 <DocsButton href={`${DOCS_URL}/guides/platform/custom-domains`} />
85 </CardHeader>
86 <CardContent>
87 <div className="space-y-4">
88 <FormField
89 control={form.control}
90 name="domain"
91 render={({ field }) => (
92 <FormItemLayout
93 layout="flex-row-reverse"
94 label="Custom domain"
95 description="Enter the subdomain you want to use."
96 className="[&>div]:md:w-1/2"
97 >
98 <FormControl>
99 <Input
100 {...field}
101 placeholder="subdomain.example.com"
102 disabled={!canConfigureCustomDomain || isSubmitting}
103 autoComplete="off"
104 />
105 </FormControl>
106 </FormItemLayout>
107 )}
108 />
109 </div>
110 </CardContent>
111 <CardContent>
112 <h4 className="text-sm mb-1">Configure a CNAME record</h4>
113 <p className="text-sm text-foreground-light">
114 Set up a CNAME record for{' '}
115 {domain ? <code className="text-code-inline">{domain}</code> : 'your custom domain'}{' '}
116 resolving to{' '}
117 {endpoint ? (
118 <span className="inline-flex items-center gap-x-1">
119 <code className="text-code-inline">{endpoint}</code>
120 <CopyButton
121 iconOnly
122 type="text"
123 className="h-5 w-5 min-w-0 p-0 [&_svg]:h-3 [&_svg]:w-3"
124 text={endpoint}
125 />
126 </span>
127 ) : (
128 "your project's API URL"
129 )}{' '}
130 with as low a TTL as possible. If you're using Cloudflare as your DNS provider,
131 disable the proxy option.
132 <br />
133 {trimmedDomain.includes('.') ? (
134 <>
135 Some DNS providers expect only the subdomain label{' '}
136 <code className="text-code-inline">{trimmedDomain.split('.')[0]}</code>, while
137 others accept the full hostname{' '}
138 <code className="text-code-inline whitespace-nowrap">{trimmedDomain}</code>.
139 </>
140 ) : (
141 'Some DNS providers expect only the subdomain label, while others accept the full hostname.'
142 )}
143 </p>
144 </CardContent>
145
146 <CardFooter className="justify-end space-x-2">
147 {form.formState.isDirty && (
148 <Button
149 type="default"
150 disabled={isSubmitting}
151 onClick={() => form.reset({ domain: '' })}
152 >
153 Cancel
154 </Button>
155 )}
156 <Button
157 type="primary"
158 htmlType="submit"
159 loading={isSubmitting}
160 disabled={!form.formState.isDirty || isSubmitting || !canConfigureCustomDomain}
161 >
162 Add
163 </Button>
164 </CardFooter>
165 </Card>
166
167 {!canConfigureCustomDomain && (
168 <p className="text-xs text-foreground-light">
169 You need additional permissions to update your project's custom domain settings.
170 </p>
171 )}
172 </form>
173 </Form>
174 )
175}