mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
fix(studio): org MFA enforcement toggle visibility and UX (DEPR-606) (#47285)
## 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 | | --- | --- | | <img width="1024" height="563" alt="Security Organization Settings Toolshed Supabase-4413F7B1-C7DC-4958-8C6F-ADEFDE4F310C" src="https://github.com/user-attachments/assets/8c71b0d8-db49-4af5-874b-5372df03379d" /> | <img width="1024" height="563" alt="Security Organization Settings Toolshed Supabase-ADD5BC82-B433-4EA0-A6BB-874703150663" src="https://github.com/user-attachments/assets/3c6d3545-fd58-426b-afaf-edd8f7ac4789" /> | | <img width="1024" height="563" alt="Security Organization Settings Toolshed Supabase-F57ED4AA-5A56-4F6A-8F35-569CAC26AFA2" src="https://github.com/user-attachments/assets/2bc49f34-7819-49fa-ac32-7e59678041fd" /> | <img width="1024" height="563" alt="Security Organization Settings Toolshed Supabase-AA795D85-1C17-4C08-9ED1-BBF08C28F2B4" src="https://github.com/user-attachments/assets/db1822b0-17fc-42df-bdec-0935e46ab5ff" /> | | <img width="1024" height="563" alt="Security Organization Settings Toolshed Supabase-8D8A196F-FA27-4FD3-BC52-DB933E61D59A" src="https://github.com/user-attachments/assets/bae30a9e-eda9-4a97-845c-0c4751f03a05" /> | <img width="1024" height="563" alt="Security Organization Settings Toolshed Supabase-5BD4F063-B402-4E03-ACE4-254BB28C232C" src="https://github.com/user-attachments/assets/0a3c4d6b-2981-47e9-9679-56bfcd7faf5d" /> | ## Additional context Test on `/org/<slug>/security` in light mode with and without personal MFA enabled. Or just hardcode the ternaries to see the various states on local. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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.” <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
1 file changed
+53
-54
@@ -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 ? (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<GenericSkeletonLoader />
|
||||
@@ -116,13 +123,31 @@ export const SecuritySettings = () => {
|
||||
</Card>
|
||||
) : !canReadMfaConfig ? (
|
||||
<NoPermission resourceText="view organization security settings" />
|
||||
) : null}
|
||||
) : (
|
||||
requiresPersonalMfa && (
|
||||
<Admonition
|
||||
type="note"
|
||||
layout="horizontal"
|
||||
title="Enable MFA on your account first"
|
||||
description="You need to set up multi-factor authentication (MFA) on your own account before you can enforce it on your organization."
|
||||
actions={
|
||||
<Button asChild variant="default">
|
||||
<Link href="/account/security">Set up MFA</Link>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
{(isErrorMfa || mfaError) && hasAccessToEnforceMfa && (
|
||||
{isMembersError && (
|
||||
<AlertError error={membersError} subject="Failed to retrieve organization members" />
|
||||
)}
|
||||
|
||||
{hasMfaConfigError && (
|
||||
<AlertError error={mfaError} subject="Failed to retrieve MFA enforcement status" />
|
||||
)}
|
||||
|
||||
{isSuccessMfa && hasAccessToEnforceMfa && (
|
||||
{canShowMfaEnforcementForm && (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<Card>
|
||||
@@ -133,36 +158,16 @@ export const SecuritySettings = () => {
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
className="justify-between"
|
||||
label="Require MFA to access organization"
|
||||
description="Team members must have MFA enabled and a valid MFA session to access the organization and any projects."
|
||||
>
|
||||
<FormControl>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled={
|
||||
!hasAccessToEnforceMfa ||
|
||||
!canUpdateMfaConfig ||
|
||||
!hasMFAEnabled ||
|
||||
isUpdatingMfa
|
||||
}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
{(!canUpdateMfaConfig || !hasMFAEnabled) && (
|
||||
<TooltipContent side="bottom">
|
||||
{!canUpdateMfaConfig ? (
|
||||
"You don't have permission to update MFA settings"
|
||||
) : (
|
||||
<>
|
||||
<InlineLink href="/account/security">Enable MFA</InlineLink>{' '}
|
||||
on your own account first
|
||||
</>
|
||||
)}
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled={isMfaEnforcementSwitchDisabled}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
@@ -183,16 +188,10 @@ export const SecuritySettings = () => {
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
disabled={
|
||||
!hasAccessToEnforceMfa ||
|
||||
!canUpdateMfaConfig ||
|
||||
isUpdatingMfa ||
|
||||
isLoadingMfa ||
|
||||
!form.formState.isDirty
|
||||
}
|
||||
disabled={isSaveMfaEnforcementDisabled}
|
||||
loading={isUpdatingMfa}
|
||||
>
|
||||
Save changes
|
||||
Save
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
Reference in new issue
Block a user