import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import { observer } from 'mobx-react-lite' import { useEffect, useState } from 'react' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, Form, IconAlertCircle, IconEye, IconEyeOff, Input, InputNumber, Radio, Toggle, } from 'ui' import { boolean, number, object, string } from 'yup' import { FormActions, FormHeader, FormPanel, FormSection, FormSectionContent, FormSectionLabel, } from 'components/ui/Forms' import UpgradeToPro from 'components/ui/UpgradeToPro' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useCheckPermissions, useFlag, useSelectedOrganization, useStore } from 'hooks' // Use a const string to represent no chars option. Represented as empty string on the backend side. const NO_REQUIRED_CHARACTERS = 'NO_REQUIRED_CHARS' const LETTERS_AND_DIGITS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:0123456789' const LOWER_UPPER_DIGITS = 'abcdefghijklmnopqrstuvwxyz:ABCDEFGHIJKLMNOPQRSTUVWXYZ:0123456789' const LOWER_UPPER_DIGITS_SYMBOLS = LOWER_UPPER_DIGITS + ':!@#$%^&*()_+-=[]{};\'\\\\:"|<>?,./`~' const schema = object({ DISABLE_SIGNUP: 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: string().required('Must have a Captcha secret'), }), SECURITY_CAPTCHA_PROVIDER: string().when('SECURITY_CAPTCHA_ENABLED', { is: true, then: string() .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(), }) function HoursOrNeverText({ value }: { value: number }) { if (value === 0) { return 'never' } else if (value === 1) { return 'hour' } else { return 'hours' } } const BasicAuthSettingsForm = observer(() => { const { ui } = useStore() const { ref: projectRef } = useParams() const { data: authConfig, error: authConfigError, isLoading, isError, isSuccess, } = useAuthConfigQuery({ projectRef }) const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation() const formId = 'auth-config-basic-settings' const [hidden, setHidden] = useState(true) const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') const organization = useSelectedOrganization() const { data: subscription, isSuccess: isSuccessSubscription } = useOrgSubscriptionQuery({ orgSlug: organization!.slug, }) const isProPlanAndUp = isSuccessSubscription && subscription?.plan?.id !== 'free' const singlePerUserReleased = useFlag('authSingleSessionPerUserReleased') const passwordStrengthReleased = useFlag('authPasswordStrengthReleased') const INITIAL_VALUES = { DISABLE_SIGNUP: !authConfig?.DISABLE_SIGNUP, 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, ...(singlePerUserReleased ? { SESSIONS_SINGLE_PER_USER: authConfig?.SESSIONS_SINGLE_PER_USER || false, } : null), ...(passwordStrengthReleased ? { PASSWORD_MIN_LENGTH: authConfig?.PASSWORD_MIN_LENGTH || 6, PASSWORD_REQUIRED_CHARACTERS: authConfig?.PASSWORD_REQUIRED_CHARACTERS || NO_REQUIRED_CHARACTERS, } : null), } const onSubmit = (values: any, { resetForm }: any) => { 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) => { ui.setNotification({ category: 'error', message: `Failed to update settings: ${error?.message}`, }) }, onSuccess: () => { ui.setNotification({ category: 'success', message: `Successfully updated settings`, }) resetForm({ values: values, initialValues: values }) }, } ) } if (isError) { return ( Failed to retrieve auth configuration {authConfigError.message} ) } return (
{({ handleReset, resetForm, values, initialValues }: any) => { const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) // Form is reset once remote data is loaded in store useEffect(() => { if (isSuccess) resetForm({ values: INITIAL_VALUES, initialValues: INITIAL_VALUES }) }, [isSuccess]) return ( <> } > User Signups}>
{passwordStrengthReleased && ( <> Passwords}> characters} disabled={!canUpdateConfig} /> <>
)} User Sessions}> {isProPlanAndUp ? ( <> ) : ( )} {singlePerUserReleased && ( )} } disabled={!canUpdateConfig || !isProPlanAndUp} /> } disabled={!canUpdateConfig || !isProPlanAndUp} />
Bot and Abuse Protection}> {values.SECURITY_CAPTCHA_ENABLED && ( <> : } type="default" onClick={() => setHidden(!hidden)} /> } /> )}
) }} ) }) export default BasicAuthSettingsForm