| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- import { zodResolver } from '@hookform/resolvers/zod'
- import { SupportCategories } from '@supabase/shared-types/out/constants'
- import type { Factor } from '@supabase/supabase-js'
- import { useQueryClient } from '@tanstack/react-query'
- import { useAuthError } from 'common'
- import { Lock } from 'lucide-react'
- import Link from 'next/link'
- import { useRouter } from 'next/router'
- import { useEffect, useState } from 'react'
- import { SubmitHandler, useForm } from 'react-hook-form'
- import { Button, Form, FormControl, FormField, Input } from 'ui'
- import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
- import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
- import z from 'zod'
- import { SupportLink } from '../Support/SupportLink'
- import AlertError from '@/components/ui/AlertError'
- import { useMfaChallengeAndVerifyMutation } from '@/data/profile/mfa-challenge-and-verify-mutation'
- import { useMfaListFactorsQuery } from '@/data/profile/mfa-list-factors-query'
- import { useSignOut } from '@/lib/auth'
- import { getReturnToPath } from '@/lib/gotrue'
- const schema = z.object({
- code: z.string().min(1, 'MFA Code is required'),
- })
- const formId = 'sign-in-mfa-form'
- interface SignInMfaFormProps {
- context?: 'forgot-password' | 'sign-in'
- }
- export const SignInMfaForm = ({ context = 'sign-in' }: SignInMfaFormProps) => {
- const router = useRouter()
- const signOut = useSignOut()
- const queryClient = useQueryClient()
- const [selectedFactor, setSelectedFactor] = useState<Factor | null>(null)
- const form = useForm<z.infer<typeof schema>>({
- resolver: zodResolver(schema as any),
- defaultValues: { code: '' },
- })
- const { code } = form.watch()
- const {
- data: factors,
- error: factorsError,
- isError: isErrorFactors,
- isSuccess: isSuccessFactors,
- isPending: isLoadingFactors,
- } = useMfaListFactorsQuery()
- const {
- mutate: mfaChallengeAndVerify,
- isPending: isVerifying,
- isSuccess,
- } = useMfaChallengeAndVerifyMutation({
- onSuccess: async () => {
- await queryClient.resetQueries()
- if (context === 'forgot-password') {
- router.push({
- pathname: '/reset-password',
- query: router.query,
- })
- } else {
- router.push(getReturnToPath())
- }
- },
- })
- const onClickLogout = async () => {
- await signOut()
- await router.replace('/sign-in')
- }
- const onSubmit: SubmitHandler<z.infer<typeof schema>> = async ({ code }) => {
- if (selectedFactor) {
- mfaChallengeAndVerify({ factorId: selectedFactor.id, code, refreshFactors: false })
- }
- }
- useEffect(() => {
- if (isSuccessFactors) {
- // if the user wanders into this page and he has no MFA setup, send the user to the next screen
- if (factors.totp.length === 0) {
- queryClient.resetQueries().then(() => router.push(getReturnToPath()))
- }
- if (factors.totp.length > 0) {
- setSelectedFactor(factors.totp[0])
- }
- }
- }, [factors?.totp, isSuccessFactors, router, queryClient])
- useEffect(() => {
- if (code.length === 6) form.handleSubmit(onSubmit)()
- }, [code])
- const error = useAuthError()
- if (error) {
- return (
- <AlertError
- error={error}
- subject="Error while signing in"
- additionalActions={
- <Button asChild type="default">
- <Link href="/sign-in">Back to sign in</Link>
- </Button>
- }
- />
- )
- }
- return (
- <>
- {isLoadingFactors && <GenericSkeletonLoader />}
- {isErrorFactors && <AlertError error={factorsError} subject="Failed to retrieve factors" />}
- {isSuccessFactors && (
- <Form {...form}>
- <form id={formId} className="flex flex-col gap-4" onSubmit={form.handleSubmit(onSubmit)}>
- <FormField
- key="code"
- name="code"
- control={form.control}
- render={({ field }) => (
- <FormItemLayout
- name="code"
- label={
- selectedFactor && factors?.totp.length === 2
- ? `Code generated by ${selectedFactor.friendly_name}`
- : null
- }
- >
- <FormControl>
- <div className="relative">
- <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-foreground-light [&_svg]:stroke-[1.5] [&_svg]:h-[20px] [&_svg]:w-[20px]">
- <Lock />
- </div>
- <Input
- id="code"
- className="pl-10 font-mono"
- {...field}
- autoFocus
- autoComplete="off"
- autoCorrect="off"
- autoCapitalize="none"
- spellCheck="false"
- placeholder="XXXXXX"
- disabled={isVerifying}
- />
- </div>
- </FormControl>
- </FormItemLayout>
- )}
- />
- <div className="flex items-center justify-between gap-x-2">
- <Button
- block
- type="outline"
- size="large"
- disabled={isVerifying || isSuccess}
- onClick={onClickLogout}
- className="opacity-80 hover:opacity-100 transition"
- >
- Cancel
- </Button>
- <Button
- block
- form={formId}
- htmlType="submit"
- size="large"
- disabled={isVerifying || isSuccess}
- loading={isVerifying || isSuccess}
- >
- {isVerifying ? 'Verifying' : isSuccess ? 'Signing in' : 'Verify'}
- </Button>
- </div>
- </form>
- </Form>
- )}
- <div className="my-8">
- <div className="text-sm">
- <span className="text-foreground-light">Unable to sign in?</span>{' '}
- </div>
- <ul className="list-disc pl-6">
- {factors?.totp.length === 2 && (
- <li>
- <a
- className="text-sm text-foreground-light hover:text-foreground cursor-pointer"
- onClick={() =>
- setSelectedFactor(factors.totp.find((f) => f.id !== selectedFactor?.id)!)
- }
- >{`Authenticate using ${
- factors.totp.find((f) => f.id !== selectedFactor?.id)?.friendly_name
- }?`}</a>
- </li>
- )}
- <li>
- <Link
- href="/logout"
- className="text-sm transition text-foreground-light hover:text-foreground"
- >
- Force sign out and clear cookies
- </Link>
- </li>
- <li>
- <SupportLink
- className="text-sm transition text-foreground-light hover:text-foreground"
- queryParams={{
- subject: 'Unable to sign in via MFA',
- category: SupportCategories.LOGIN_ISSUES,
- }}
- >
- Reach out to us via support
- </SupportLink>
- </li>
- </ul>
- </div>
- </>
- )
- }
|