// @ts-nocheck import HCaptcha from '@hcaptcha/react-hcaptcha' import { zodResolver } from '@hookform/resolvers/zod' import type { AuthError } from '@supabase/supabase-js' import { useQueryClient } from '@tanstack/react-query' import { Eye, EyeOff } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect, useRef, useState } from 'react' import { useForm, type SubmitHandler } from 'react-hook-form' import { toast } from 'sonner' import { Button, Form, FormControl, FormField, Input } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import z from 'zod' import { LastSignInWrapper } from './LastSignInWrapper' import { useAddLoginEvent } from '@/data/misc/audit-login-mutation' import { getMfaAuthenticatorAssuranceLevel } from '@/data/profile/mfa-authenticator-assurance-level-query' import { useSendEventMutation } from '@/data/telemetry/send-event-mutation' import { useLastSignIn } from '@/hooks/misc/useLastSignIn' import { captureCriticalError } from '@/lib/error-reporting' import { auth, buildPathWithParams, getReturnToPath } from '@/lib/gotrue' const schema = z.object({ email: z.string().min(1, 'Email is required').email('Must be a valid email'), password: z.string().min(1, 'Password is required'), }) const formId = 'sign-in-form' export const SignInForm = () => { const router = useRouter() const queryClient = useQueryClient() const [_, setLastSignIn] = useLastSignIn() const [passwordHidden, setPasswordHidden] = useState(true) const [captchaToken, setCaptchaToken] = useState(null) const captchaRef = useRef(null) const [returnTo, setReturnTo] = useState(null) const form = useForm>({ resolver: zodResolver(schema as any), defaultValues: { email: '', password: '' }, }) const isSubmitting = form.formState.isSubmitting useEffect(() => { // Only call getReturnToPath after component mounts client-side setReturnTo(getReturnToPath()) }, []) const { mutate: sendEvent } = useSendEventMutation() const { mutate: addLoginEvent } = useAddLoginEvent() let forgotPasswordUrl = `/forgot-password` if (returnTo && !returnTo.includes('/forgot-password')) { forgotPasswordUrl = `${forgotPasswordUrl}?returnTo=${encodeURIComponent(returnTo)}` } const onSubmit: SubmitHandler> = async ({ email, password }) => { const toastId = toast.loading('Signing in...') let token = captchaToken if (!token) { const captchaResponse = await captchaRef.current?.execute({ async: true }) token = captchaResponse?.response ?? null } const { error } = await auth.signInWithPassword({ email, password, options: { captchaToken: token ?? undefined }, }) if (!error) { setLastSignIn('email') try { const data = await getMfaAuthenticatorAssuranceLevel() if (data) { if (data.currentLevel !== data.nextLevel) { toast.success(`You need to provide your second factor authentication`, { id: toastId }) const url = buildPathWithParams('/sign-in-mfa') router.replace(url) return } } toast.success(`Signed in successfully!`, { id: toastId }) sendEvent({ action: 'sign_in', properties: { category: 'account', method: 'email' }, }) addLoginEvent({}) await queryClient.resetQueries() // since we're already on the /sign-in page, prevent redirect loops let redirectPath = '/organizations' if (returnTo && returnTo !== '/sign-in') { redirectPath = returnTo } router.push(redirectPath) } catch (error: any) { toast.error(`Failed to sign in: ${(error as AuthError).message}`, { id: toastId }) captureCriticalError(error, 'sign in via EP') } } else { setCaptchaToken(null) captchaRef.current?.resetCaptcha() if (error.message.toLowerCase() === 'email not confirmed') { return toast.error( 'Account has not been verified, please check the link sent to your email', { id: toastId } ) } toast.error(error.message, { id: toastId }) } } return (
( )} />
(
)} /> {/* positioned using absolute instead of labelOptional prop so tabbing between inputs works smoothly */} Forgot password?
{ setCaptchaToken(token) }} onExpire={() => { setCaptchaToken(null) }} />
) }