BasicAuthSettingsForm.tsx350 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { PermissionAction } from '@supabase/shared-types/out/constants'
3import { useParams } from 'common'
4import { ExternalLink } from 'lucide-react'
5import Link from 'next/link'
6import { useEffect } from 'react'
7import { useForm } from 'react-hook-form'
8import { toast } from 'sonner'
9import {
10 Alert,
11 AlertDescription,
12 AlertTitle,
13 Button,
14 Card,
15 CardContent,
16 CardFooter,
17 Form,
18 FormControl,
19 FormField,
20 Switch,
21 WarningIcon,
22} from 'ui'
23import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
24import {
25 PageSection,
26 PageSectionContent,
27 PageSectionMeta,
28 PageSectionSummary,
29 PageSectionTitle,
30} from 'ui-patterns/PageSection'
31import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
32import * as z from 'zod'
33
34import { NO_REQUIRED_CHARACTERS } from './Auth.constants'
35import AlertError from '@/components/ui/AlertError'
36import { InlineLink } from '@/components/ui/InlineLink'
37import NoPermission from '@/components/ui/NoPermission'
38import { useAuthConfigQuery } from '@/data/auth/auth-config-query'
39import { useAuthConfigUpdateMutation } from '@/data/auth/auth-config-update-mutation'
40import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
41import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
42import { DOCS_URL } from '@/lib/constants'
43
44const schema = z.object({
45 DISABLE_SIGNUP: z.boolean(),
46 EXTERNAL_ANONYMOUS_USERS_ENABLED: z.boolean(),
47 SECURITY_MANUAL_LINKING_ENABLED: z.boolean(),
48 MAILER_AUTOCONFIRM: z.boolean(),
49 SITE_URL: z.string().min(1, 'Must have a Site URL'),
50})
51
52export const BasicAuthSettingsForm = () => {
53 const { ref: projectRef } = useParams()
54 const showManualLinking = useIsFeatureEnabled('authentication:show_manual_linking')
55
56 const {
57 data: authConfig,
58 error: authConfigError,
59 isError,
60 isSuccess,
61 isPending: isLoading,
62 } = useAuthConfigQuery({ projectRef })
63 const { mutate: updateAuthConfig, isPending: isUpdatingConfig } = useAuthConfigUpdateMutation()
64
65 const { can: canReadConfig, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
66 PermissionAction.READ,
67 'custom_config_gotrue'
68 )
69 const { can: canUpdateConfig } = useAsyncCheckPermissions(
70 PermissionAction.UPDATE,
71 'custom_config_gotrue'
72 )
73
74 const form = useForm({
75 resolver: zodResolver(schema as any),
76 defaultValues: {
77 DISABLE_SIGNUP: true,
78 EXTERNAL_ANONYMOUS_USERS_ENABLED: false,
79 SECURITY_MANUAL_LINKING_ENABLED: false,
80 MAILER_AUTOCONFIRM: true,
81 SITE_URL: '',
82 },
83 })
84 const { isDirty } = form.formState
85
86 useEffect(() => {
87 if (authConfig) {
88 form.reset({
89 DISABLE_SIGNUP: !authConfig.DISABLE_SIGNUP,
90 EXTERNAL_ANONYMOUS_USERS_ENABLED: authConfig.EXTERNAL_ANONYMOUS_USERS_ENABLED,
91 SECURITY_MANUAL_LINKING_ENABLED: authConfig.SECURITY_MANUAL_LINKING_ENABLED,
92 // The backend uses false to represent that email confirmation is required
93 MAILER_AUTOCONFIRM: !authConfig.MAILER_AUTOCONFIRM,
94 SITE_URL: authConfig.SITE_URL,
95 })
96 }
97 }, [authConfig])
98
99 const onSubmit = (values: any) => {
100 const payload = { ...values }
101 payload.DISABLE_SIGNUP = !values.DISABLE_SIGNUP
102 // The backend uses empty string to represent no required characters in the password
103 if (payload.PASSWORD_REQUIRED_CHARACTERS === NO_REQUIRED_CHARACTERS) {
104 payload.PASSWORD_REQUIRED_CHARACTERS = ''
105 }
106
107 // The backend uses false to represent that email confirmation is required
108 payload.MAILER_AUTOCONFIRM = !values.MAILER_AUTOCONFIRM
109
110 updateAuthConfig(
111 { projectRef: projectRef!, config: payload },
112 {
113 onError: (error) => {
114 toast.error(`Failed to update settings: ${error?.message}`)
115 },
116 onSuccess: () => {
117 toast.success('Successfully updated settings')
118 },
119 }
120 )
121 }
122
123 return (
124 <PageSection>
125 <PageSectionMeta>
126 <PageSectionSummary>
127 <PageSectionTitle>User Signups</PageSectionTitle>
128 </PageSectionSummary>
129 </PageSectionMeta>
130 <PageSectionContent>
131 {isError && (
132 <AlertError
133 error={authConfigError}
134 subject="Failed to retrieve auth configuration for hooks"
135 />
136 )}
137
138 {isPermissionsLoaded && !canReadConfig && (
139 <div className="mt-8">
140 <NoPermission resourceText="view auth configuration settings" />
141 </div>
142 )}
143
144 {isLoading && (
145 <Card>
146 <CardContent className="py-6">
147 <ShimmeringLoader />
148 </CardContent>
149 <CardContent className="py-6">
150 <ShimmeringLoader />
151 </CardContent>
152 <CardContent className="py-6">
153 <ShimmeringLoader />
154 </CardContent>
155 <CardContent className="py-7">
156 <ShimmeringLoader />
157 </CardContent>
158 <CardContent className="py-7"></CardContent>
159 </Card>
160 )}
161
162 {isSuccess && (
163 <Form {...form}>
164 <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
165 <Card>
166 <CardContent>
167 <FormField
168 control={form.control}
169 name="DISABLE_SIGNUP"
170 render={({ field }) => (
171 <FormItemLayout
172 layout="flex-row-reverse"
173 label="Allow new users to sign up"
174 description="If this is disabled, new users will not be able to sign up to your application"
175 >
176 <FormControl>
177 <Switch
178 checked={field.value}
179 onCheckedChange={field.onChange}
180 disabled={!canUpdateConfig}
181 />
182 </FormControl>
183 </FormItemLayout>
184 )}
185 />
186 </CardContent>
187 {showManualLinking && (
188 <CardContent>
189 <FormField
190 control={form.control}
191 name="SECURITY_MANUAL_LINKING_ENABLED"
192 render={({ field }) => (
193 <FormItemLayout
194 layout="flex-row-reverse"
195 label="Allow manual linking"
196 description={
197 <>
198 Enable{' '}
199 <InlineLink
200 className="text-foreground-light hover:text-foreground"
201 href={`${DOCS_URL}/guides/auth/auth-identity-linking#manual-linking-beta`}
202 >
203 manual linking APIs
204 </InlineLink>{' '}
205 for your project
206 </>
207 }
208 >
209 <FormControl>
210 <Switch
211 checked={field.value}
212 onCheckedChange={field.onChange}
213 disabled={!canUpdateConfig}
214 />
215 </FormControl>
216 </FormItemLayout>
217 )}
218 />
219 </CardContent>
220 )}
221 <CardContent>
222 <FormField
223 control={form.control}
224 name="EXTERNAL_ANONYMOUS_USERS_ENABLED"
225 render={({ field }) => (
226 <FormItemLayout
227 layout="flex-row-reverse"
228 label="Allow anonymous sign-ins"
229 description={
230 <>
231 Enable{' '}
232 <InlineLink
233 className="text-foreground-light hover:text-foreground"
234 href={`${DOCS_URL}/guides/auth/auth-anonymous`}
235 >
236 anonymous sign-ins
237 </InlineLink>{' '}
238 for your project
239 </>
240 }
241 >
242 <FormControl>
243 <Switch
244 checked={field.value}
245 onCheckedChange={field.onChange}
246 disabled={!canUpdateConfig}
247 />
248 </FormControl>
249 </FormItemLayout>
250 )}
251 />
252
253 {form.watch('EXTERNAL_ANONYMOUS_USERS_ENABLED') && (
254 <Alert
255 className="flex w-full items-center justify-between mt-4"
256 variant="warning"
257 >
258 <WarningIcon />
259 <div>
260 <AlertTitle>
261 Anonymous users will use the{' '}
262 <code className="text-code-inline">authenticated</code> role when signing
263 in
264 </AlertTitle>
265 <AlertDescription className="flex flex-col gap-y-3">
266 <p>
267 As a result, anonymous users will be subjected to RLS policies that
268 apply to the <code className="text-code-inline">public</code> and{' '}
269 <code className="text-code-inline">authenticated</code> roles. We
270 strongly advise{' '}
271 <Link
272 href={`/project/${projectRef}/auth/policies`}
273 className="text-foreground underline"
274 >
275 reviewing your RLS policies
276 </Link>{' '}
277 to ensure that access to your data is restricted where required.
278 </p>
279 <Button asChild type="default" className="w-min" icon={<ExternalLink />}>
280 <Link href={`${DOCS_URL}/guides/auth/auth-anonymous#access-control`}>
281 View access control docs
282 </Link>
283 </Button>
284 </AlertDescription>
285 </div>
286 </Alert>
287 )}
288
289 {!authConfig?.SECURITY_CAPTCHA_ENABLED &&
290 form.watch('EXTERNAL_ANONYMOUS_USERS_ENABLED') && (
291 <Alert className="mt-4">
292 <WarningIcon />
293 <AlertTitle>
294 We highly recommend{' '}
295 <InlineLink href={`/project/${projectRef}/auth/protection`}>
296 enabling captcha
297 </InlineLink>{' '}
298 for anonymous sign-ins
299 </AlertTitle>
300 <AlertDescription>
301 This will prevent potential abuse on sign-ins which may bloat your
302 database and incur costs for monthly active users (MAU)
303 </AlertDescription>
304 </Alert>
305 )}
306 </CardContent>
307 <CardContent>
308 <FormField
309 control={form.control}
310 name="MAILER_AUTOCONFIRM"
311 render={({ field }) => (
312 <FormItemLayout
313 layout="flex-row-reverse"
314 label="Confirm email"
315 description="Users will need to confirm their email address before signing in for the first time"
316 >
317 <FormControl>
318 <Switch
319 checked={field.value}
320 onCheckedChange={field.onChange}
321 disabled={!canUpdateConfig}
322 />
323 </FormControl>
324 </FormItemLayout>
325 )}
326 />
327 </CardContent>
328 <CardFooter className="justify-end space-x-2">
329 {isDirty && (
330 <Button type="default" onClick={() => form.reset()}>
331 Cancel
332 </Button>
333 )}
334 <Button
335 type="primary"
336 htmlType="submit"
337 disabled={!canUpdateConfig || isUpdatingConfig || !isDirty}
338 loading={isUpdatingConfig}
339 >
340 Save changes
341 </Button>
342 </CardFooter>
343 </Card>
344 </form>
345 </Form>
346 )}
347 </PageSectionContent>
348 </PageSection>
349 )
350}