From ef148b6504e350307ca85dbdcd173846c148d961 Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Thu, 25 Jun 2026 02:05:20 -0600 Subject: [PATCH] fix(studio): org MFA enforcement toggle visibility and UX (DEPR-606) (#47285) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? Bug fix. Resolves DEPR-606. ## What is the current behavior? On org Security settings, the MFA enforcement switch could appear on without a green track. Users without personal MFA saw a disabled toggle with a tooltip. ## What is the new behavior? - Switch checked state renders correctly (removed tooltip trigger from the switch). - Users who need personal MFA first see an admonition with a link to account security instead of a disabled toggle. I felt this was a better user experience and more straightforward than the alternative: fighting the TooltipTrigger’s `data-state` conflict with the Switch’s checked state. | Before | After | | --- | --- | | Security Organization Settings
Toolshed Supabase-4413F7B1-C7DC-4958-8C6F-ADEFDE4F310C | Security Organization Settings
Toolshed Supabase-ADD5BC82-B433-4EA0-A6BB-874703150663 | | Security Organization Settings
Toolshed Supabase-F57ED4AA-5A56-4F6A-8F35-569CAC26AFA2 | Security Organization Settings
Toolshed Supabase-AA795D85-1C17-4C08-9ED1-BBF08C28F2B4 | | Security Organization Settings
Toolshed Supabase-8D8A196F-FA27-4FD3-BC52-DB933E61D59A | Security Organization Settings
Toolshed Supabase-5BD4F063-B402-4E03-ACE4-254BB28C232C | ## Additional context Test on `/org//security` in light mode with and without personal MFA enabled. Or just hardcode the ternaries to see the various states on local. ## Summary by CodeRabbit * **New Features** * Added clearer guidance for organization security settings when MFA must first be enabled on a personal account. * Improved loading behavior while member data is fetched. * **Bug Fixes** * Prevented the MFA enforcement form from showing until personal MFA requirements are met. * Refined the MFA toggle disabled logic to apply only when appropriate. * **UI Improvements** * Replaced the MFA tooltip with an in-page notice. * Updated the primary action button label from “Save changes” to “Save.” --------- Co-authored-by: Cursor Co-authored-by: Joshen Lim --- .../Organization/SecuritySettings.tsx | 107 +++++++++--------- 1 file changed, 53 insertions(+), 54 deletions(-) diff --git a/apps/studio/components/interfaces/Organization/SecuritySettings.tsx b/apps/studio/components/interfaces/Organization/SecuritySettings.tsx index a8ea11d9644..cba6de2cbbc 100644 --- a/apps/studio/components/interfaces/Organization/SecuritySettings.tsx +++ b/apps/studio/components/interfaces/Organization/SecuritySettings.tsx @@ -1,29 +1,18 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' +import Link from 'next/link' import { useEffect } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' -import { - Button, - Card, - CardContent, - CardFooter, - Form, - FormControl, - FormField, - Switch, - Tooltip, - TooltipContent, - TooltipTrigger, -} from 'ui' +import { Button, Card, CardContent, CardFooter, Form, FormControl, FormField, Switch } from 'ui' +import { Admonition } from 'ui-patterns' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { z } from 'zod' import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold' import AlertError from '@/components/ui/AlertError' -import { InlineLink } from '@/components/ui/InlineLink' import NoPermission from '@/components/ui/NoPermission' import { UpgradeToPro } from '@/components/ui/UpgradeToPro' import { useOrganizationMembersQuery } from '@/data/organizations/organization-members-query' @@ -41,7 +30,13 @@ const schema = z.object({ export const SecuritySettings = () => { const { slug } = useParams() const { profile } = useProfile() - const { data: members } = useOrganizationMembersQuery({ slug }) + const { + data: members, + error: membersError, + isPending: isLoadingMembers, + isError: isMembersError, + isSuccess: isSuccessMembers, + } = useOrganizationMembersQuery({ slug }) const { can: canReadMfaConfig, isLoading: isLoadingPermissions } = useAsyncCheckPermissions( PermissionAction.READ, @@ -89,7 +84,19 @@ export const SecuritySettings = () => { }, [mfaConfig, form]) const hasMFAEnabled = - members?.find((member) => member.primary_email == profile?.primary_email)?.mfa_enabled || false + members?.find((member) => member.primary_email == profile?.primary_email)?.mfa_enabled ?? false + + const requiresPersonalMfa = isSuccessMembers && canUpdateMfaConfig && !hasMFAEnabled + + const isLoadingMfaEnforcementSettings = + isLoadingMfa || isLoadingPermissions || isLoadingEntitlement || isLoadingMembers + const hasMfaConfigError = (isErrorMfa || Boolean(mfaError)) && hasAccessToEnforceMfa + const canShowMfaEnforcementForm = + isSuccessMfa && hasAccessToEnforceMfa && isSuccessMembers && !requiresPersonalMfa + const isMfaEnforcementSwitchDisabled = + !hasAccessToEnforceMfa || !canUpdateMfaConfig || isUpdatingMfa + const isSaveMfaEnforcementDisabled = + isMfaEnforcementSwitchDisabled || isLoadingMfa || !form.formState.isDirty const onSubmit = (values: { enforceMfa: boolean }) => { if (!slug || !hasAccessToEnforceMfa) return @@ -108,7 +115,7 @@ export const SecuritySettings = () => { /> ) : ( <> - {isLoadingMfa || isLoadingPermissions || isLoadingEntitlement ? ( + {isLoadingMfaEnforcementSettings ? ( @@ -116,13 +123,31 @@ export const SecuritySettings = () => { ) : !canReadMfaConfig ? ( - ) : null} + ) : ( + requiresPersonalMfa && ( + + Set up MFA + + } + /> + ) + )} - {(isErrorMfa || mfaError) && hasAccessToEnforceMfa && ( + {isMembersError && ( + + )} + + {hasMfaConfigError && ( )} - {isSuccessMfa && hasAccessToEnforceMfa && ( + {canShowMfaEnforcementForm && (
@@ -133,36 +158,16 @@ export const SecuritySettings = () => { render={({ field }) => ( - - - - - {(!canUpdateMfaConfig || !hasMFAEnabled) && ( - - {!canUpdateMfaConfig ? ( - "You don't have permission to update MFA settings" - ) : ( - <> - Enable MFA{' '} - on your own account first - - )} - - )} - + )} @@ -183,16 +188,10 @@ export const SecuritySettings = () => {