import { yupResolver } from '@hookform/resolvers/yup'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { number, object, string } from 'yup'
import { useParams } from 'common'
import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold'
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 { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { IS_PLATFORM } from 'lib/constants'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
Button,
Card,
CardContent,
CardFooter,
FormControl_Shadcn_,
FormField_Shadcn_,
Form_Shadcn_,
Input_Shadcn_,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
WarningIcon,
PrePostTab,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
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 totpSchema = object({
MFA_TOTP: string().required(),
MFA_MAX_ENROLLED_FACTORS: number()
.min(0, 'Must be a value 0 or larger')
.max(30, 'Must be a value no greater than 30'),
})
const phoneSchema = object({
MFA_PHONE: string().required(),
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.'),
})
const MfaAuthSettingsForm = () => {
const { ref: projectRef } = useParams()
const { data: authConfig, error: authConfigError, isError } = useAuthConfigQuery({ projectRef })
const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation()
// Separate loading states for each form
const [isUpdatingTotpForm, setIsUpdatingTotpForm] = useState(false)
const [isUpdatingPhoneForm, setIsUpdatingPhoneForm] = useState(false)
const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue')
const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
const organization = useSelectedOrganization()
const isProPlanAndUp = organization?.plan?.id !== 'free'
const promptProPlanUpgrade = IS_PLATFORM && !isProPlanAndUp
// 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
const hasValidMFAProvider = hasValidMFAPhoneProvider || sendSMSHookIsEnabled
const totpForm = useForm({
resolver: yupResolver(totpSchema),
defaultValues: {
MFA_TOTP: 'Enabled',
MFA_MAX_ENROLLED_FACTORS: 10,
},
})
const phoneForm = useForm({
resolver: yupResolver(phoneSchema),
defaultValues: {
MFA_PHONE: 'Disabled',
MFA_PHONE_OTP_LENGTH: 6,
MFA_PHONE_TEMPLATE: 'Your code is {{ .Code }}',
},
})
useEffect(() => {
if (authConfig) {
if (!isUpdatingTotpForm) {
totpForm.reset({
MFA_TOTP:
determineMFAStatus(
authConfig?.MFA_TOTP_VERIFY_ENABLED ?? true,
authConfig?.MFA_TOTP_ENROLL_ENABLED ?? true
) || 'Enabled',
MFA_MAX_ENROLLED_FACTORS: authConfig?.MFA_MAX_ENROLLED_FACTORS ?? 10,
})
}
if (!isUpdatingPhoneForm) {
phoneForm.reset({
MFA_PHONE:
determineMFAStatus(
authConfig?.MFA_PHONE_VERIFY_ENABLED || false,
authConfig?.MFA_PHONE_ENROLL_ENABLED || false
) || 'Disabled',
MFA_PHONE_OTP_LENGTH: authConfig?.MFA_PHONE_OTP_LENGTH || 6,
MFA_PHONE_TEMPLATE: authConfig?.MFA_PHONE_TEMPLATE || 'Your code is {{ .Code }}',
})
}
}
}, [authConfig, isUpdatingTotpForm, isUpdatingPhoneForm])
const onSubmitTotpForm = (values: any) => {
const { verifyEnabled: MFA_TOTP_VERIFY_ENABLED, enrollEnabled: MFA_TOTP_ENROLL_ENABLED } =
MfaStatusToState(values.MFA_TOTP)
const payload = {
...values,
MFA_TOTP_ENROLL_ENABLED,
MFA_TOTP_VERIFY_ENABLED,
}
delete payload.MFA_TOTP
setIsUpdatingTotpForm(true)
updateAuthConfig(
{ projectRef: projectRef!, config: payload },
{
onError: (error) => {
toast.error(`Failed to update TOTP settings: ${error?.message}`)
setIsUpdatingTotpForm(false)
},
onSuccess: () => {
toast.success('Successfully updated TOTP settings')
setIsUpdatingTotpForm(false)
},
}
)
}
const onSubmitPhoneForm = (values: any) => {
let payload = { ...values }
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,
}
}
delete payload.MFA_PHONE
setIsUpdatingPhoneForm(true)
updateAuthConfig(
{ projectRef: projectRef!, config: payload },
{
onError: (error) => {
toast.error(`Failed to update phone MFA settings: ${error?.message}`)
setIsUpdatingPhoneForm(false)
},
onSuccess: () => {
toast.success('Successfully updated phone MFA settings')
setIsUpdatingPhoneForm(false)
},
}
)
}
if (isError) {
return (
Failed to retrieve auth configuration
{authConfigError.message}
)
}
if (!canReadConfig) {
return
}
const phoneMFAIsEnabled =
phoneForm.watch('MFA_PHONE') === 'Enabled' || phoneForm.watch('MFA_PHONE') === 'Verify Enabled'
const hasUpgradedPhoneMFA =
authConfig && !authConfig.MFA_PHONE_VERIFY_ENABLED && phoneMFAIsEnabled
return (
<>
Multi-Factor Authentication (MFA)
SMS MFA
>
)
}
export default MfaAuthSettingsForm