mirror of
https://github.com/supabase/supabase.git
synced 2026-10-16 06:39:09 +03:00
202 lines
5.5 KiB
TypeScript
202 lines
5.5 KiB
TypeScript
import ConfirmationModal from 'components/ui/ConfirmationModal'
|
|
import InformationBox from 'components/ui/InformationBox'
|
|
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
|
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 { useStore } from 'hooks'
|
|
import Image from 'next/image'
|
|
import { Dispatch, SetStateAction, useEffect, useState } from 'react'
|
|
import { Input, Modal } from 'ui'
|
|
|
|
const AddNewFactorModal = ({ onClose }: { onClose: () => void }) => {
|
|
const [verificationComplete, setVerificationComplete] = useState(false)
|
|
// Generate a name with a number between 0 and 1000
|
|
const [name, setName] = useState(`App ${Math.floor(Math.random() * 1000)}`)
|
|
|
|
const {
|
|
mutate: enroll,
|
|
data,
|
|
reset: resetEnrollment,
|
|
isLoading: isEnrolling,
|
|
} = useMfaEnrollMutation()
|
|
const { mutate: unenroll } = useMfaUnenrollMutation()
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
// when the modal is closed, if there's a factor id which hasn't been verified, unenroll it
|
|
if (data?.id && !verificationComplete) {
|
|
// delete the internal state of enroll mutation so that the next time enroll is called,
|
|
// it makes an API call
|
|
resetEnrollment()
|
|
unenroll({ factorId: data.id })
|
|
}
|
|
}
|
|
}, [])
|
|
|
|
return !data ? (
|
|
<FirstStep name={name} setName={setName} enroll={enroll} onClose={onClose} />
|
|
) : (
|
|
<SecondStep
|
|
factorName={name}
|
|
factor={data}
|
|
isLoading={isEnrolling}
|
|
onSuccess={() => {
|
|
setVerificationComplete(true)
|
|
onClose()
|
|
}}
|
|
onClose={onClose}
|
|
/>
|
|
)
|
|
}
|
|
|
|
const FirstStep = ({
|
|
name,
|
|
setName,
|
|
enroll,
|
|
onClose,
|
|
}: {
|
|
name: string
|
|
setName: Dispatch<SetStateAction<string>>
|
|
enroll: (params: { factorType: 'totp'; friendlyName?: string }) => void
|
|
onClose: () => void
|
|
}) => {
|
|
return (
|
|
<ConfirmationModal
|
|
size="medium"
|
|
visible
|
|
header="Add a new authenticator app as a factor"
|
|
buttonLabel="Generate QR"
|
|
buttonLoadingLabel="Generating QR"
|
|
buttonDisabled={name.length === 0}
|
|
onSelectCancel={onClose}
|
|
onSelectConfirm={() => {
|
|
enroll({
|
|
factorType: 'totp',
|
|
friendlyName: name,
|
|
})
|
|
}}
|
|
>
|
|
<Modal.Content>
|
|
<>
|
|
<div className="pt-6 pb-5">
|
|
<Input
|
|
label="Provide a name to identify this app"
|
|
descriptionText="A string will be randomly generated if a name is not provided"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
/>
|
|
</div>
|
|
</>
|
|
</Modal.Content>
|
|
</ConfirmationModal>
|
|
)
|
|
}
|
|
|
|
const SecondStep = ({
|
|
factorName,
|
|
factor,
|
|
isLoading,
|
|
onSuccess,
|
|
onClose,
|
|
}: {
|
|
factorName: string
|
|
factor: {
|
|
id: string
|
|
type: 'totp'
|
|
totp: {
|
|
qr_code: string
|
|
secret: string
|
|
uri: string
|
|
}
|
|
}
|
|
isLoading: boolean
|
|
onSuccess: () => void
|
|
onClose: () => void
|
|
}) => {
|
|
const [code, setCode] = useState('')
|
|
|
|
const { ui } = useStore()
|
|
|
|
const {
|
|
mutate: challengeAndVerify,
|
|
isLoading: isVerifying,
|
|
isSuccess: verificationComplete,
|
|
} = useMfaChallengeAndVerifyMutation({
|
|
onError: (error) => {
|
|
ui.setNotification({
|
|
category: 'error',
|
|
message: `Failed to add a second factor authentication: ${error?.message}`,
|
|
})
|
|
},
|
|
onSuccess: () => {
|
|
ui.setNotification({
|
|
category: 'success',
|
|
message: `Successfully added a second factor authentication`,
|
|
})
|
|
onSuccess()
|
|
},
|
|
})
|
|
|
|
return (
|
|
<ConfirmationModal
|
|
size="medium"
|
|
visible
|
|
header={`Verify new factor ${factorName}`}
|
|
buttonLabel="Confirm"
|
|
buttonLoadingLabel="Confirming"
|
|
loading={isVerifying}
|
|
onSelectCancel={onClose}
|
|
onSelectConfirm={() => challengeAndVerify({ factorId: factor.id, code })}
|
|
>
|
|
<Modal.Content>
|
|
<>
|
|
<div className="py-4 px-4 text-sm">
|
|
<span>
|
|
Use an authenticator app to scan the following QR code, and provide the code from the
|
|
app to complete the enrolment.
|
|
</span>
|
|
</div>
|
|
{isLoading ? (
|
|
<div className="pb-4 px-4">
|
|
<GenericSkeletonLoader />
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="flex justify-center py-3">
|
|
<div className="h-48 w-48 bg-white rounded">
|
|
<Image width={190} height={190} src={factor.totp.qr_code} alt={factor.totp.uri} />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<InformationBox
|
|
title="Unable to scan?"
|
|
description={
|
|
<Input
|
|
copy
|
|
disabled
|
|
id="ref"
|
|
size="small"
|
|
label="You can also enter this secret key into your authenticator app"
|
|
value={factor.totp.secret}
|
|
/>
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="pt-2 pb-4">
|
|
<Input
|
|
label="Authentication code"
|
|
value={code}
|
|
onChange={(e) => setCode(e.target.value)}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</>
|
|
</Modal.Content>
|
|
</ConfirmationModal>
|
|
)
|
|
}
|
|
|
|
export default AddNewFactorModal
|