diff --git a/apps/studio/components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx b/apps/studio/components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx index 674753ee1514d..34d3c2eb17d99 100644 --- a/apps/studio/components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx @@ -3,7 +3,7 @@ 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 { number, object, string, boolean } from 'yup' import { useParams } from 'common' import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold' @@ -32,6 +32,7 @@ import { SelectTrigger_Shadcn_, SelectValue_Shadcn_, Select_Shadcn_, + Switch, WarningIcon, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' @@ -79,6 +80,10 @@ const phoneSchema = object({ MFA_PHONE_TEMPLATE: string().required('SMS template is required.'), }) +const securitySchema = object({ + MFA_ALLOW_LOW_AAL: boolean().required(), +}) + const MfaAuthSettingsForm = () => { const { ref: projectRef } = useParams() const { data: authConfig, error: authConfigError, isError } = useAuthConfigQuery({ projectRef }) @@ -87,6 +92,7 @@ const MfaAuthSettingsForm = () => { // Separate loading states for each form const [isUpdatingTotpForm, setIsUpdatingTotpForm] = useState(false) const [isUpdatingPhoneForm, setIsUpdatingPhoneForm] = useState(false) + const [isUpdatingSecurityForm, setIsUpdatingSecurityForm] = useState(false) const [isConfirmationModalVisible, setIsConfirmationModalVisible] = useState(false) @@ -126,6 +132,13 @@ const MfaAuthSettingsForm = () => { }, }) + const securityForm = useForm({ + resolver: yupResolver(securitySchema), + defaultValues: { + MFA_ALLOW_LOW_AAL: false, + }, + }) + useEffect(() => { if (authConfig) { if (!isUpdatingTotpForm) { @@ -150,8 +163,14 @@ const MfaAuthSettingsForm = () => { MFA_PHONE_TEMPLATE: authConfig?.MFA_PHONE_TEMPLATE || 'Your code is {{ .Code }}', }) } + + if (!isUpdatingSecurityForm) { + securityForm.reset({ + MFA_ALLOW_LOW_AAL: authConfig?.MFA_ALLOW_LOW_AAL ?? true, + }) + } } - }, [authConfig, isUpdatingTotpForm, isUpdatingPhoneForm]) + }, [authConfig, isUpdatingTotpForm, isUpdatingPhoneForm, isUpdatingSecurityForm]) const onSubmitTotpForm = (values: any) => { const { verifyEnabled: MFA_TOTP_VERIFY_ENABLED, enrollEnabled: MFA_TOTP_ENROLL_ENABLED } = @@ -181,6 +200,26 @@ const MfaAuthSettingsForm = () => { ) } + const onSubmitSecurityForm = (values: any) => { + const payload = { ...values } + + setIsUpdatingSecurityForm(true) + + updateAuthConfig( + { projectRef: projectRef!, config: payload }, + { + onError: (error) => { + toast.error(`Failed to update phone MFA settings: ${error?.message}`) + setIsUpdatingSecurityForm(false) + }, + onSuccess: () => { + toast.success('Successfully updated phone MFA settings') + setIsUpdatingSecurityForm(false) + }, + } + ) + } + const onSubmitPhoneForm = (values: any) => { let payload = { ...values } @@ -461,6 +500,7 @@ const MfaAuthSettingsForm = () => { + { customers are using SMS MFA.

+ + + Enhanced MFA Security + + +
+ + + ( + + + field.onChange(!value)} + disabled={!canUpdateConfig} + /> + + + )} + /> + + + {securityForm.formState.isDirty && ( + + )} + + + +
+
+
) } diff --git a/apps/studio/data/projects/projects-query.ts b/apps/studio/data/projects/projects-query.ts index 6b37ad3ab94be..b25c705bd3fab 100644 --- a/apps/studio/data/projects/projects-query.ts +++ b/apps/studio/data/projects/projects-query.ts @@ -22,14 +22,13 @@ export async function getProjects({ signal?: AbortSignal headers?: Record }) { - const { data, error } = await get('/platform/projects', { - signal, - headers: { ...headers, Version: '2' }, - }) + const { data, error } = await get('/platform/projects', { signal, headers }) if (error) handleError(error) - // [Joshen] API TS issue - return data as unknown as PaginatedProjectsResponse + // The /platform/projects endpoint has a v2 which is activated by passing a {version: '2'} header. The v1 API returns + // all projects while the v2 returns paginated list of projects. Wrapping the v1 API response into a + // { projects: ProjectInfo[] } is intentional to be forward compatible with the structure of v2 for easier migration. + return { projects: data } } export type ProjectsData = Awaited>