mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
MFA Recovery codes: enforce recovery codes generation after setting up an MFA (#50343)
## What kind of change does this PR introduce? Afters users set up an MFA, automatically generate recovery codes ## How to test - On an account that doesn't have recovery codes generated yet, add a new MFA - When you finished verifying the MFA, it should automatically open the recovery codes modal introduced in previous PRs <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Improved the two-factor authentication setup flow by checking the latest recovery-code enrollment status before generating codes. - Recovery codes are now generated and displayed after verification when they are enabled but not yet enrolled. - Loading indicators now reflect recovery-code status checks, providing clearer feedback during setup. - **Improvements** - Updated the recovery-code confirmation message to explain how codes can restore access after losing access to an MFA app and remind users to store them securely. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
ee3fbc4e61
commit
c52fca1340
2 files changed
+36
-4
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useFlag } from 'common'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useForm, type SubmitHandler } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
@@ -10,11 +11,14 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { RecoveryCodesModal } from './RecoveryCodesModal'
|
||||
import InformationBox from '@/components/ui/InformationBox'
|
||||
import { organizationKeys } from '@/data/organizations/keys'
|
||||
import { useMfaChallengeAndVerifyMutation } from '@/data/profile/mfa-challenge-and-verify-mutation'
|
||||
import { useMfaEnrollMutation } from '@/data/profile/mfa-enroll-mutation'
|
||||
import { useMfaUnenrollMutation } from '@/data/profile/mfa-unenroll-mutation'
|
||||
import { useRecoveryCodesGenerateMutation } from '@/data/recovery-codes/recovery-codes-generate-mutation'
|
||||
import { useRecoveryCodesStatusQuery } from '@/data/recovery-codes/recovery-codes-status-query'
|
||||
import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization'
|
||||
|
||||
type TOTP = { qr_code: string; secret: string; uri: string }
|
||||
@@ -26,6 +30,15 @@ interface AddNewFactorModalProps {
|
||||
|
||||
export const AddNewFactorModal = ({ visible, onClose }: AddNewFactorModalProps) => {
|
||||
const { data, mutate: enroll, isPending: isEnrolling, reset } = useMfaEnrollMutation()
|
||||
const enableAuthRecoveryCodes = useFlag('enableAuthRecoveryCodes')
|
||||
const { isPending: isRecoveryCodesStatusPending, refetch: refetchRecoveryCodesStatus } =
|
||||
useRecoveryCodesStatusQuery({
|
||||
enabled: enableAuthRecoveryCodes,
|
||||
})
|
||||
|
||||
const recoveryCodesGenerateMutation = useRecoveryCodesGenerateMutation()
|
||||
|
||||
const [isRecoveryCodesModalOpen, setIsRecoveryCodesModalOpen] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) reset()
|
||||
@@ -35,7 +48,7 @@ export const AddNewFactorModal = ({ visible, onClose }: AddNewFactorModalProps)
|
||||
<>
|
||||
<FirstStep
|
||||
visible={visible && !Boolean(data)}
|
||||
isEnrolling={isEnrolling}
|
||||
isEnrolling={isEnrolling || (enableAuthRecoveryCodes && isRecoveryCodesStatusPending)}
|
||||
enroll={enroll}
|
||||
reset={reset}
|
||||
onClose={onClose}
|
||||
@@ -44,8 +57,24 @@ export const AddNewFactorModal = ({ visible, onClose }: AddNewFactorModalProps)
|
||||
visible={visible && Boolean(data)}
|
||||
factorName={data?.friendly_name ?? ''}
|
||||
factor={data as Extract<typeof data, { type: 'totp' }>}
|
||||
isLoading={isEnrolling}
|
||||
onClose={onClose}
|
||||
isLoading={isEnrolling || (enableAuthRecoveryCodes && isRecoveryCodesStatusPending)}
|
||||
onClose={async () => {
|
||||
if (enableAuthRecoveryCodes) {
|
||||
const { data: currentRecoveryCodesStatus } = await refetchRecoveryCodesStatus()
|
||||
const shouldGenerateRecoveryCodes = currentRecoveryCodesStatus?.status === 'unenrolled'
|
||||
|
||||
if (shouldGenerateRecoveryCodes) {
|
||||
recoveryCodesGenerateMutation.mutate({})
|
||||
setIsRecoveryCodesModalOpen(true)
|
||||
}
|
||||
}
|
||||
onClose()
|
||||
}}
|
||||
/>
|
||||
<RecoveryCodesModal
|
||||
open={isRecoveryCodesModalOpen}
|
||||
onOpenChange={(open) => setIsRecoveryCodesModalOpen(open)}
|
||||
mutation={recoveryCodesGenerateMutation}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -132,7 +132,10 @@ const GenerateRecoveryCodesModalContent = ({
|
||||
if (status === 'success') {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<p>Save your recovery codes somewhere safe.</p>
|
||||
<p>
|
||||
Recovery codes allow you to recover your account in case you lost access to your MFA apps.
|
||||
Save them somewhere safe.
|
||||
</p>
|
||||
<pre className="relative bg-muted rounded-md py-2 px-4">
|
||||
<code className="flex gap-2 flex-wrap justify-between">
|
||||
{codes?.map((code) => (
|
||||
|
||||
Reference in new issue
Block a user