import { PermissionAction } from '@supabase/shared-types/out/constants' import { useEffect } from 'react' import { toast } from 'sonner' import { boolean, number, object, string } from 'yup' import { useParams } from 'common' import { FormActions } from 'components/ui/Forms/FormActions' import { FormHeader } from 'components/ui/Forms/FormHeader' import { FormPanel } from 'components/ui/Forms/FormPanel' import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection' import NoPermission from 'components/ui/NoPermission' 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 } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { IS_PLATFORM } from 'lib/constants' import FormField from '../AuthProvidersForm/FormField' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Form, InputNumber, Toggle, WarningIcon, } from 'ui' const schema = object({ JWT_EXP: number() .max(604800, 'Must be less than 604800') .required('Must have a JWT expiry value'), REFRESH_TOKEN_ROTATION_ENABLED: boolean().required(), SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: number() .min(0, 'Must be a value more than 0') .required('Must have a Reuse Interval value'), MFA_PHONE_OTP_LENGTH: number() .min(6, 'Must be a value 6 or larger') .max(30, 'must be a value no greater than 30'), MFA_PHONE_TEMPLATE: string().required('SMS template is required.'), MFA_MAX_ENROLLED_FACTORS: number() .min(0, 'Must be be a value more than 0') .max(30, 'Must be a value no greater than 30'), DB_MAX_POOL_SIZE: number() .min(1, 'Must be 1 or larger') .max(200, 'Must be a value no greater than 200'), API_MAX_REQUEST_DURATION: number() .min(5, 'Must be 5 or larger') .max(30, 'Must be a value no greater than 30'), MFA_TOTP: string().required(), MFA_PHONE: string().required(), }) function determineMFAStatus(verifyEnabled: boolean, enrollEnabled: boolean) { return verifyEnabled ? (enrollEnabled ? 'Enabled' : 'Verify Enabled') : 'Disabled' } const MFAFactorSelectionOptions = [ { label: 'Enabled', value: 'Enabled', }, { label: 'Verify Enabled', value: 'Verify Enabled', }, { label: 'Disabled', value: 'Disabled', }, ] const MfaStatusToState = (status: (typeof MFAFactorSelectionOptions)[number]['value']) => { return status === 'Enabled' ? { verifyEnabled: true, enrollEnabled: true } : status === 'Verify Enabled' ? { verifyEnabled: true, enrollEnabled: false } : { verifyEnabled: false, enrollEnabled: false } } const AdvancedAuthSettingsForm = () => { const { ref: projectRef } = useParams() const { data: authConfig, error: authConfigError, isLoading, isError, isSuccess, } = useAuthConfigQuery({ projectRef }) const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation() const formId = 'auth-config-advanced-form' const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') const organization = useSelectedOrganization() const { data: subscription, isSuccess: isSuccessSubscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug, }) const isTeamsEnterprisePlan = isSuccessSubscription && subscription.plan.id !== 'free' && subscription.plan.id !== 'pro' const isProPlanAndUp = isSuccessSubscription && subscription?.plan?.id !== 'free' const promptProPlanUpgrade = IS_PLATFORM && !isProPlanAndUp const projectAddons = subscription?.project_addons.find((addon) => addon.ref === projectRef) const hasPurchasedAuthMFAAddOn = projectAddons?.addons.some( (addon) => addon.type === 'auth_mfa_phone' ) const promptTeamsEnterpriseUpgrade = IS_PLATFORM && !isTeamsEnterprisePlan const INITIAL_VALUES = { SITE_URL: authConfig?.SITE_URL, JWT_EXP: authConfig?.JWT_EXP, REFRESH_TOKEN_ROTATION_ENABLED: authConfig?.REFRESH_TOKEN_ROTATION_ENABLED || false, MFA_PHONE_OTP_LENGTH: authConfig?.MFA_PHONE_OTP_LENGTH || 6, MFA_PHONE_TEMPLATE: authConfig?.MFA_PHONE_TEMPLATE || 'Your code is {{ .Code }}', SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: authConfig?.SECURITY_REFRESH_TOKEN_REUSE_INTERVAL, MFA_MAX_ENROLLED_FACTORS: authConfig?.MFA_MAX_ENROLLED_FACTORS || 10, DB_MAX_POOL_SIZE: authConfig?.DB_MAX_POOL_SIZE || 10, API_MAX_REQUEST_DURATION: authConfig?.API_MAX_REQUEST_DURATION || 10, // TOTP is enabled by default. Auth environment variables are distinct from UI state - we use MFA_TOTP and MFA_PHONE to hold the derivedUI state. // MFA_TOTP_VERIFY_ENABLED and MFA_TOTP_ENROLL_ENABLED -> Enabled // MFA_TOTP_VERIFY_ENABLED and !MFA_TOTP_ENROLL_ENABLED -> Verify Enabled // !MFA_TOTP_VERIFY_ENABLED and !MFA_TOTP_ENROLL_ENABLED -> Disabled MFA_TOTP: determineMFAStatus( authConfig?.MFA_TOTP_VERIFY_ENABLED ?? true, authConfig?.MFA_TOTP_ENROLL_ENABLED ?? true ) || 'Enabled', MFA_PHONE: determineMFAStatus( authConfig?.MFA_PHONE_VERIFY_ENABLED || false, authConfig?.MFA_PHONE_ENROLL_ENABLED || false ) || 'Disabled', } // For now, we support Twilio and Vonage. Twilio Verify is not supported and the remaining providers are community maintained. const sendSMSHookIsEnabled = authConfig?.HOOK_SEND_SMS_URI !== null && authConfig?.HOOK_SEND_SMS_ENABLED === true const hasValidMFAPhoneProvider = authConfig?.EXTERNAL_PHONE_ENABLED === true && (authConfig?.SMS_PROVIDER === 'twilio' || authConfig?.SMS_PROVIDER === 'vonage') const hasValidMFAProvider = hasValidMFAPhoneProvider || sendSMSHookIsEnabled const phoneMFAIsEnabled = INITIAL_VALUES.MFA_PHONE === 'Enabled' || INITIAL_VALUES.MFA_PHONE === 'Verify Enabled' const onSubmit = (values: any, { resetForm }: any) => { let payload = { ...values } const { verifyEnabled: MFA_TOTP_VERIFY_ENABLED, enrollEnabled: MFA_TOTP_ENROLL_ENABLED } = MfaStatusToState(values.MFA_TOTP) // MFA (Phone) is only available on Pro Plans and up. We translate the UI state, MFA_PHONE and MFA_TOTP into the underlying // Auth config state - MFA_PHONE_*_ENABLED and MFA_TOTP_*_ENABLED. if (isProPlanAndUp) { const { verifyEnabled: MFA_PHONE_VERIFY_ENABLED, enrollEnabled: MFA_PHONE_ENROLL_ENABLED } = MfaStatusToState(values.MFA_PHONE) payload = { ...payload, MFA_PHONE_ENROLL_ENABLED, MFA_PHONE_VERIFY_ENABLED, } } payload = { ...payload, MFA_TOTP_ENROLL_ENABLED, MFA_TOTP_VERIFY_ENABLED, } delete payload.MFA_TOTP delete payload.MFA_PHONE if (!isTeamsEnterprisePlan) { delete payload.DB_MAX_POOL_SIZE delete payload.API_MAX_REQUEST_DURATION } updateAuthConfig( { projectRef: projectRef!, config: payload }, { onError: (error) => { toast.error(`Failed to update settings: ${error?.message}`) }, onSuccess: () => { toast.success('Successfully updated settings') resetForm({ values: values, initialValues: values }) }, } ) } if (isError) { return ( Failed to retrieve auth configuration {authConfigError.message} ) } if (!canReadConfig) { return } return (
{({ handleReset, resetForm, values, initialValues, setFieldValue }: any) => { const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) // Form is reset once remote data is loaded in store // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { if (isSuccess) resetForm({ values: INITIAL_VALUES, initialValues: INITIAL_VALUES }) }, [isSuccess]) const hasUpgradedPhoneMFA = INITIAL_VALUES.MFA_PHONE === 'Disabled' && values.MFA_PHONE !== INITIAL_VALUES.MFA_PHONE return ( <> } > Access Tokens (JWT)}> seconds} disabled={!canUpdateConfig} /> Refresh Tokens}> {values.REFRESH_TOKEN_ROTATION_ENABLED && ( seconds} disabled={!canUpdateConfig} /> )} Multi-Factor Authentication (MFA)} > factors} disabled={!canUpdateConfig} /> Advanced MFA}> {promptProPlanUpgrade && ( )} {!hasValidMFAProvider && phoneMFAIsEnabled && ( Please configure a valid phone provider. Only Twilio, Vonage, and Send SMS Hooks are supported at this time. )} {hasUpgradedPhoneMFA && ( Enabling advanced MFA with phone will result in an additional charge of $75 per month for the first project in the organization and an additional $10 per month for additional projects. )} Max Direct Database Connections} > {promptTeamsEnterpriseUpgrade && ( )} connections} disabled={!canUpdateConfig || !isTeamsEnterprisePlan} /> Max Request Duration}> {promptTeamsEnterpriseUpgrade && ( )} seconds} disabled={!canUpdateConfig || !isTeamsEnterprisePlan} /> ) }} ) } export default AdvancedAuthSettingsForm