import { yupResolver } from '@hookform/resolvers/yup' import { PermissionAction } from '@supabase/shared-types/out/constants' import { Eye, EyeOff } from 'lucide-react' import { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { boolean, number, object, string } from 'yup' import { useParams } from 'common' import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold' 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 { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, Card, CardContent, CardFooter, FormControl_Shadcn_, FormField_Shadcn_, Form_Shadcn_, Input_Shadcn_, PrePostTab, SelectContent_Shadcn_, SelectItem_Shadcn_, SelectTrigger_Shadcn_, SelectValue_Shadcn_, Select_Shadcn_, Switch, WarningIcon, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { NO_REQUIRED_CHARACTERS } from '../Auth.constants' const CAPTCHA_PROVIDERS = [ { key: 'hcaptcha', label: 'hCaptcha' }, { key: 'turnstile', label: 'Turnstile by Cloudflare' }, ] const schema = object({ DISABLE_SIGNUP: boolean().required(), EXTERNAL_ANONYMOUS_USERS_ENABLED: boolean().required(), SECURITY_MANUAL_LINKING_ENABLED: boolean().required(), SITE_URL: string().required('Must have a Site URL'), SECURITY_CAPTCHA_ENABLED: boolean().required(), SECURITY_CAPTCHA_SECRET: string().when('SECURITY_CAPTCHA_ENABLED', { is: true, then: (schema) => schema.required('Must have a Captcha secret'), }), SECURITY_CAPTCHA_PROVIDER: string().when('SECURITY_CAPTCHA_ENABLED', { is: true, then: (schema) => schema .oneOf(['hcaptcha', 'turnstile']) .required('Captcha provider must be either hcaptcha or turnstile'), }), SESSIONS_TIMEBOX: number().min(0, 'Must be a positive number'), SESSIONS_INACTIVITY_TIMEOUT: number().min(0, 'Must be a positive number'), SESSIONS_SINGLE_PER_USER: boolean(), PASSWORD_MIN_LENGTH: number().min(6, 'Must be greater or equal to 6.'), PASSWORD_REQUIRED_CHARACTERS: string(), PASSWORD_HIBP_ENABLED: boolean(), }) const ProtectionAuthSettingsForm = () => { const { ref: projectRef } = useParams() const { data: authConfig, error: authConfigError, isLoading, isError, } = useAuthConfigQuery({ projectRef }) const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation() const [isUpdatingProtection, setIsUpdatingProtection] = useState(false) const [hidden, setHidden] = useState(true) const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') const protectionForm = useForm({ resolver: yupResolver(schema), defaultValues: { DISABLE_SIGNUP: true, EXTERNAL_ANONYMOUS_USERS_ENABLED: false, SECURITY_MANUAL_LINKING_ENABLED: false, SITE_URL: '', SECURITY_CAPTCHA_ENABLED: false, SECURITY_CAPTCHA_SECRET: '', SECURITY_CAPTCHA_PROVIDER: 'hcaptcha', SESSIONS_TIMEBOX: 0, SESSIONS_INACTIVITY_TIMEOUT: 0, SESSIONS_SINGLE_PER_USER: false, PASSWORD_MIN_LENGTH: 6, PASSWORD_REQUIRED_CHARACTERS: NO_REQUIRED_CHARACTERS, PASSWORD_HIBP_ENABLED: false, }, }) useEffect(() => { if (authConfig && !isUpdatingProtection) { protectionForm.reset({ DISABLE_SIGNUP: !authConfig.DISABLE_SIGNUP, EXTERNAL_ANONYMOUS_USERS_ENABLED: authConfig.EXTERNAL_ANONYMOUS_USERS_ENABLED || false, SECURITY_MANUAL_LINKING_ENABLED: authConfig.SECURITY_MANUAL_LINKING_ENABLED || false, SITE_URL: authConfig.SITE_URL || '', SECURITY_CAPTCHA_ENABLED: authConfig.SECURITY_CAPTCHA_ENABLED || false, SECURITY_CAPTCHA_SECRET: authConfig.SECURITY_CAPTCHA_SECRET || '', SECURITY_CAPTCHA_PROVIDER: authConfig.SECURITY_CAPTCHA_PROVIDER || 'hcaptcha', SESSIONS_TIMEBOX: authConfig.SESSIONS_TIMEBOX || 0, SESSIONS_INACTIVITY_TIMEOUT: authConfig.SESSIONS_INACTIVITY_TIMEOUT || 0, SESSIONS_SINGLE_PER_USER: authConfig.SESSIONS_SINGLE_PER_USER || false, PASSWORD_MIN_LENGTH: authConfig.PASSWORD_MIN_LENGTH || 6, PASSWORD_REQUIRED_CHARACTERS: authConfig.PASSWORD_REQUIRED_CHARACTERS || NO_REQUIRED_CHARACTERS, PASSWORD_HIBP_ENABLED: authConfig.PASSWORD_HIBP_ENABLED || false, }) } }, [authConfig, isUpdatingProtection]) const onSubmitProtection = (values: any) => { setIsUpdatingProtection(true) const payload = { ...values } payload.DISABLE_SIGNUP = !values.DISABLE_SIGNUP // The backend uses empty string to represent no required characters in the password if (payload.PASSWORD_REQUIRED_CHARACTERS === NO_REQUIRED_CHARACTERS) { payload.PASSWORD_REQUIRED_CHARACTERS = '' } updateAuthConfig( { projectRef: projectRef!, config: payload }, { onError: (error) => { toast.error(`Failed to update settings: ${error?.message}`) setIsUpdatingProtection(false) }, onSuccess: () => { toast.success('Successfully updated settings') setIsUpdatingProtection(false) }, } ) } if (isError) { return ( Failed to retrieve auth configuration {authConfigError.message} ) } if (!canReadConfig) { return } return ( Bot and Abuse Protection
( )} /> {protectionForm.watch('SECURITY_CAPTCHA_ENABLED') && ( <> { const selectedProvider = CAPTCHA_PROVIDERS.find((x) => x.key === field.value) return ( {CAPTCHA_PROVIDERS.map((x) => ( {x.label} ))} How to set up {selectedProvider?.label}? ) }} /> (
setHidden(!hidden)} icon={hidden ? : } /> } >
)} />
)} {protectionForm.formState.isDirty && ( )}
) } export default ProtectionAuthSettingsForm