import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import Link from 'next/link' import { useEffect } from 'react' import { SubmitHandler, useForm, useWatch } from 'react-hook-form' import { toast } from 'sonner' import { Badge, Button, Card, CardContent, CardFooter, Form, FormControl, FormField, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { PageSection, PageSectionContent, PageSectionMeta, PageSectionSummary, PageSectionTitle, } from 'ui-patterns/PageSection' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import * as z from 'zod' import { AlertError } from '@/components/ui/AlertError' import { InlineLink } from '@/components/ui/InlineLink' import { NoPermission } from '@/components/ui/NoPermission' import { useAuthConfigQuery } from '@/data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from '@/data/auth/auth-config-update-mutation' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { DOCS_URL } from '@/lib/constants' const CAPTCHA_PROVIDERS = [ { key: 'hcaptcha', label: 'hCaptcha' }, { key: 'turnstile', label: 'Turnstile by Cloudflare' }, ] type CaptchaProviders = 'hcaptcha' | 'turnstile' const baseSchema = z.object({ PASSWORD_HIBP_ENABLED: z.boolean().optional(), }) const captchaEnabledSchema = z .object({ SECURITY_CAPTCHA_ENABLED: z.literal(true), SECURITY_CAPTCHA_SECRET: z.string().min(1, 'Must have a Captcha secret'), SECURITY_CAPTCHA_PROVIDER: z.enum(['hcaptcha', 'turnstile'], { required_error: 'Captcha provider must be either hcaptcha or turnstile', }), }) .merge(baseSchema) const captchaDisabledSchema = z .object({ SECURITY_CAPTCHA_ENABLED: z.literal(false), SECURITY_CAPTCHA_SECRET: z.string().optional(), SECURITY_CAPTCHA_PROVIDER: z.enum(['hcaptcha', 'turnstile']).optional(), }) .merge(baseSchema) const formSchema = z.discriminatedUnion('SECURITY_CAPTCHA_ENABLED', [ captchaEnabledSchema, captchaDisabledSchema, ]) type FormSchema = z.infer export const ProtectionAuthSettingsForm = () => { const { ref: projectRef } = useParams() const { data: authConfig, error: authConfigError, isError, isPending: isLoading, } = useAuthConfigQuery({ projectRef }) const { mutate: updateAuthConfig, isPending: isUpdatingConfig } = useAuthConfigUpdateMutation({ onError: (error) => { toast.error(`Failed to update settings: ${error?.message}`) }, onSuccess: () => { toast.success('Successfully updated settings') }, }) const { can: canReadConfig } = useAsyncCheckPermissions( PermissionAction.READ, 'custom_config_gotrue' ) const { can: canUpdateConfig } = useAsyncCheckPermissions( PermissionAction.UPDATE, 'custom_config_gotrue' ) const protectionForm = useForm({ resolver: zodResolver(formSchema), defaultValues: { SECURITY_CAPTCHA_ENABLED: false, SECURITY_CAPTCHA_SECRET: '', SECURITY_CAPTCHA_PROVIDER: 'hcaptcha', PASSWORD_HIBP_ENABLED: false, }, }) const { isDirty } = protectionForm.formState useEffect(() => { if (authConfig && !isUpdatingConfig) { const SECURITY_CAPTCHA_PROVIDER = (authConfig.SECURITY_CAPTCHA_PROVIDER || 'hcaptcha') as CaptchaProviders protectionForm.reset({ SECURITY_CAPTCHA_ENABLED: authConfig.SECURITY_CAPTCHA_ENABLED, SECURITY_CAPTCHA_SECRET: authConfig.SECURITY_CAPTCHA_SECRET || '', SECURITY_CAPTCHA_PROVIDER, PASSWORD_HIBP_ENABLED: authConfig.PASSWORD_HIBP_ENABLED || false, }) } }, [authConfig, isUpdatingConfig]) const onSubmitProtection: SubmitHandler> = (values) => { updateAuthConfig({ projectRef: projectRef!, config: values }) } const SECURITY_CAPTCHA_ENABLED = useWatch({ name: 'SECURITY_CAPTCHA_ENABLED', control: protectionForm.control, }) if (isError) { return ( ) } if (!canReadConfig) { return ( ) } if (isLoading) { return ( ) } return ( Bot and Abuse Protection
( )} /> {SECURITY_CAPTCHA_ENABLED && ( <> { const selectedProvider = CAPTCHA_PROVIDERS.find( (x) => x.key === field.value ) return ( How to set up {selectedProvider?.label}? ) }} /> ( )} /> )} (
{field.value ? 'Enabled' : 'Disabled'}
)} />
{isDirty && ( )}
) }