fix(errors): fix error handling for signup/signin

This commit is contained in:
Waleed Latif
2025-05-19 00:32:40 -07:00
parent c8b2c9ea63
commit 1ec9770df3
4 changed files with 121 additions and 90 deletions

View File

@@ -10,7 +10,6 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { client } from '@/lib/auth-client'
import { createLogger } from '@/lib/logs/console-logger'
import { useNotificationStore } from '@/stores/notifications/store'
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
const logger = createLogger('LoginForm')
@@ -49,9 +48,10 @@ export default function LoginPage({
const searchParams = useSearchParams()
const [isLoading, setIsLoading] = useState(false)
const [mounted, setMounted] = useState(false)
const { addNotification } = useNotificationStore()
const [showPassword, setShowPassword] = useState(false)
const [password, setPassword] = useState('')
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
const [showValidationError, setShowValidationError] = useState(false)
// Initialize state for URL parameters
const [callbackUrl, setCallbackUrl] = useState('/w')
@@ -121,45 +121,47 @@ export default function LoginPage({
{
onError: (ctx) => {
console.error('Login error:', ctx.error)
let errorMessage = 'Invalid email or password'
let errorMessage: string[] = ['Invalid email or password']
// Handle all possible error cases from Better Auth
if (ctx.error.message?.includes('EMAIL_NOT_VERIFIED')) {
if (ctx.error.code?.includes('EMAIL_NOT_VERIFIED')) {
return
} else if (
ctx.error.message?.includes('BAD_REQUEST') ||
ctx.error.code?.includes('BAD_REQUEST') ||
ctx.error.message?.includes('Email and password sign in is not enabled')
) {
errorMessage = 'Email sign in is currently disabled.'
errorMessage.push('Email sign in is currently disabled.')
} else if (
ctx.error.message?.includes('INVALID_CREDENTIALS') ||
ctx.error.code?.includes('INVALID_CREDENTIALS') ||
ctx.error.message?.includes('invalid password')
) {
errorMessage = 'Invalid email or password. Please try again.'
errorMessage.push('Invalid email or password. Please try again.')
} else if (
ctx.error.message?.includes('USER_NOT_FOUND') ||
ctx.error.code?.includes('USER_NOT_FOUND') ||
ctx.error.message?.includes('not found')
) {
errorMessage = 'No account found with this email. Please sign up first.'
} else if (ctx.error.message?.includes('MISSING_CREDENTIALS')) {
errorMessage = 'Please enter both email and password.'
} else if (ctx.error.message?.includes('EMAIL_PASSWORD_DISABLED')) {
errorMessage = 'Email and password login is disabled.'
} else if (ctx.error.message?.includes('FAILED_TO_CREATE_SESSION')) {
errorMessage = 'Failed to create session. Please try again later.'
} else if (ctx.error.message?.includes('too many attempts')) {
errorMessage =
errorMessage.push('No account found with this email. Please sign up first.')
} else if (ctx.error.code?.includes('MISSING_CREDENTIALS')) {
errorMessage.push('Please enter both email and password.')
} else if (ctx.error.code?.includes('EMAIL_PASSWORD_DISABLED')) {
errorMessage.push('Email and password login is disabled.')
} else if (ctx.error.code?.includes('FAILED_TO_CREATE_SESSION')) {
errorMessage.push('Failed to create session. Please try again later.')
} else if (ctx.error.code?.includes('too many attempts')) {
errorMessage.push(
'Too many login attempts. Please try again later or reset your password.'
} else if (ctx.error.message?.includes('account locked')) {
errorMessage =
)
} else if (ctx.error.code?.includes('account locked')) {
errorMessage.push(
'Your account has been locked for security. Please reset your password.'
} else if (ctx.error.message?.includes('network')) {
errorMessage = 'Network error. Please check your connection and try again.'
)
} else if (ctx.error.code?.includes('network')) {
errorMessage.push('Network error. Please check your connection and try again.')
} else if (ctx.error.message?.includes('rate limit')) {
errorMessage = 'Too many requests. Please wait a moment before trying again.'
errorMessage.push('Too many requests. Please wait a moment before trying again.')
}
addNotification('error', errorMessage, null)
setPasswordErrors(errorMessage)
setShowValidationError(true)
},
}
)
@@ -176,7 +178,7 @@ export default function LoginPage({
}
} catch (err: any) {
// Handle only the special verification case that requires a redirect
if (err.message?.includes('not verified') || err.message?.includes('EMAIL_NOT_VERIFIED')) {
if (err.message?.includes('not verified') || err.code?.includes('EMAIL_NOT_VERIFIED')) {
try {
await client.emailOtp.sendVerificationOtp({
email,
@@ -190,11 +192,8 @@ export default function LoginPage({
router.push(`/verify`)
return
} catch (verifyErr) {
addNotification(
'error',
'Failed to send verification code. Please try again later.',
null
)
setPasswordErrors(['Failed to send verification code. Please try again later.'])
setShowValidationError(true)
setIsLoading(false)
return
}
@@ -308,7 +307,13 @@ export default function LoginPage({
autoCorrect="off"
placeholder="Enter your password"
value={password}
onChange={(e) => setPassword(e.target.value)}
onChange={(e) => {
setPassword(e.target.value)
if (showValidationError) {
setShowValidationError(false)
setPasswordErrors([])
}
}}
className="bg-neutral-900 border-neutral-700 text-white pr-10"
/>
<button
@@ -320,6 +325,13 @@ export default function LoginPage({
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
{showValidationError && passwordErrors.length > 0 && (
<div className="text-xs text-red-400 mt-1 space-y-1">
{passwordErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
</div>

View File

@@ -3,12 +3,12 @@
import { Suspense, useEffect, useState } from 'react'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { Command, CornerDownLeft, Eye, EyeOff } from 'lucide-react'
import { Eye, EyeOff } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { client } from '@/lib/auth-client'
import { useNotificationStore } from '@/stores/notifications/store'
import { cn } from '@/lib/utils'
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
const PASSWORD_VALIDATIONS = {
@@ -41,12 +41,12 @@ function SignupFormContent({
const searchParams = useSearchParams()
const [isLoading, setIsLoading] = useState(false)
const [, setMounted] = useState(false)
const { addNotification } = useNotificationStore()
const [showPassword, setShowPassword] = useState(false)
const [password, setPassword] = useState('')
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
const [showValidationError, setShowValidationError] = useState(false)
const [email, setEmail] = useState('')
const [emailError, setEmailError] = useState('')
const [waitlistToken, setWaitlistToken] = useState('')
const [redirectUrl, setRedirectUrl] = useState('')
const [isInviteFlow, setIsInviteFlow] = useState(false)
@@ -144,6 +144,14 @@ function SignupFormContent({
setShowValidationError(false)
}
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value)
// Clear any previous email errors when the user starts typing
if (emailError) {
setEmailError('')
}
}
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setIsLoading(true)
@@ -163,7 +171,8 @@ function SignupFormContent({
try {
if (errors.length > 0) {
// Show first error as notification
addNotification('error', errors[0], null)
setPasswordErrors([errors[0]])
setShowValidationError(true)
setIsLoading(false)
return
}
@@ -177,33 +186,42 @@ function SignupFormContent({
{
onError: (ctx) => {
console.error('Signup error:', ctx.error)
let errorMessage = 'Failed to create account'
let errorMessage: string[] = ['Failed to create account']
// Handle all possible error cases from Better Auth
if (ctx.error.status === 422 || ctx.error.message?.includes('already exists')) {
errorMessage = 'An account with this email already exists. Please sign in instead.'
if (ctx.error.code?.includes('USER_ALREADY_EXISTS')) {
errorMessage.push(
'An account with this email already exists. Please sign in instead.'
)
setEmailError(errorMessage[0])
} else if (
ctx.error.message?.includes('BAD_REQUEST') ||
ctx.error.code?.includes('BAD_REQUEST') ||
ctx.error.message?.includes('Email and password sign up is not enabled')
) {
errorMessage = 'Email signup is currently disabled.'
} else if (ctx.error.message?.includes('INVALID_EMAIL')) {
errorMessage = 'Please enter a valid email address.'
} else if (ctx.error.message?.includes('PASSWORD_TOO_SHORT')) {
errorMessage = 'Password must be at least 8 characters long.'
} else if (ctx.error.message?.includes('MISSING_CREDENTIALS')) {
errorMessage = 'Please enter all required fields.'
} else if (ctx.error.message?.includes('EMAIL_PASSWORD_DISABLED')) {
errorMessage = 'Email and password signup is disabled.'
} else if (ctx.error.message?.includes('FAILED_TO_CREATE_USER')) {
errorMessage = 'Failed to create account. Please try again later.'
} else if (ctx.error.message?.includes('network')) {
errorMessage = 'Network error. Please check your connection and try again.'
} else if (ctx.error.message?.includes('rate limit')) {
errorMessage = 'Too many requests. Please wait a moment before trying again.'
errorMessage.push('Email signup is currently disabled.')
setEmailError(errorMessage[0])
} else if (ctx.error.code?.includes('INVALID_EMAIL')) {
errorMessage.push('Please enter a valid email address.')
setEmailError(errorMessage[0])
} else if (ctx.error.code?.includes('PASSWORD_TOO_SHORT')) {
errorMessage.push('Password must be at least 8 characters long.')
setPasswordErrors(errorMessage)
setShowValidationError(true)
} else if (ctx.error.code?.includes('PASSWORD_TOO_LONG')) {
errorMessage.push('Password must be less than 128 characters long.')
setPasswordErrors(errorMessage)
setShowValidationError(true)
} else if (ctx.error.code?.includes('network')) {
errorMessage.push('Network error. Please check your connection and try again.')
setPasswordErrors(errorMessage)
setShowValidationError(true)
} else if (ctx.error.code?.includes('rate limit')) {
errorMessage.push('Too many requests. Please wait a moment before trying again.')
setPasswordErrors(errorMessage)
setShowValidationError(true)
} else {
setPasswordErrors(errorMessage)
setShowValidationError(true)
}
addNotification('error', errorMessage, null)
},
}
)
@@ -255,7 +273,8 @@ function SignupFormContent({
router.push('/verify')
} catch (error) {
console.error('Failed to send verification code:', error)
addNotification('error', 'Account created but failed to send verification code.', null)
setPasswordErrors(['Account created but failed to send verification code.'])
setShowValidationError(true)
router.push('/login')
}
} catch (error) {
@@ -304,9 +323,17 @@ function SignupFormContent({
autoComplete="email"
autoCorrect="off"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="bg-neutral-900 border-neutral-700 text-white"
onChange={handleEmailChange}
className={cn(
'bg-neutral-900 border-neutral-700 text-white',
emailError && 'border-red-500 focus-visible:ring-red-500'
)}
/>
{emailError && (
<div className="text-xs text-red-400 mt-1">
<p>{emailError}</p>
</div>
)}
</div>
<div className="space-y-2">
<Label htmlFor="password" className="text-neutral-300">

View File

@@ -7,7 +7,6 @@ import { HelpCircle, ScrollText, Send, Settings } from 'lucide-react'
import { Skeleton } from '@/components/ui/skeleton'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useSession } from '@/lib/auth-client'
import { isProd } from '@/lib/environment'
import { useSidebarStore } from '@/stores/sidebar/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { WorkflowMetadata } from '@/stores/workflows/registry/types'
@@ -157,9 +156,6 @@ export function Sidebar() {
mode === 'hover' &&
((isHovered && !isAnyModalOpen && explicitMouseEnter) || workspaceDropdownOpen)
// Invite members is only shown in production
const shouldShowInviteMembers = isProd
return (
<aside
className={clsx(
@@ -276,19 +272,17 @@ export function Sidebar() {
<div className="flex-shrink-0 px-3 pb-3 pt-1">
<div className="flex flex-col space-y-[1px]">
{/* Invite members button */}
{shouldShowInviteMembers && (
<Tooltip>
<TooltipTrigger asChild>
<div
onClick={() => setShowInviteMembers(true)}
className="flex items-center justify-center rounded-md text-sm font-medium text-muted-foreground hover:bg-accent/50 cursor-pointer w-8 h-8 mx-auto"
>
<Send className="h-[18px] w-[18px]" />
</div>
</TooltipTrigger>
<TooltipContent side="right">Invite Members</TooltipContent>
</Tooltip>
)}
<Tooltip>
<TooltipTrigger asChild>
<div
onClick={() => setShowInviteMembers(true)}
className="flex items-center justify-center rounded-md text-sm font-medium text-muted-foreground hover:bg-accent/50 cursor-pointer w-8 h-8 mx-auto"
>
<Send className="h-[18px] w-[18px]" />
</div>
</TooltipTrigger>
<TooltipContent side="right">Invite Members</TooltipContent>
</Tooltip>
{/* Help button */}
<Tooltip>
@@ -315,17 +309,15 @@ export function Sidebar() {
) : (
<>
{/* Invite members bar */}
{shouldShowInviteMembers && (
<div className="flex-shrink-0 px-3 pt-1">
<div
onClick={() => setShowInviteMembers(true)}
className="flex items-center rounded-md px-2 py-1.5 text-sm font-medium text-muted-foreground hover:bg-accent/50 cursor-pointer"
>
<Send className="h-[18px] w-[18px]" />
<span className="ml-2">Invite members</span>
</div>
<div className="flex-shrink-0 px-3 pt-1">
<div
onClick={() => setShowInviteMembers(true)}
className="flex items-center rounded-md px-2 py-1.5 text-sm font-medium text-muted-foreground hover:bg-accent/50 cursor-pointer"
>
<Send className="h-[18px] w-[18px]" />
<span className="ml-2">Invite members</span>
</div>
)}
</div>
{/* Bottom buttons container */}
<div className="flex-shrink-0 px-3 pb-3 pt-1">

View File

@@ -165,7 +165,7 @@ export const auth = betterAuth({
otp: string
type: 'sign-in' | 'email-verification' | 'forget-password'
}) => {
if (isDevOrDocker) {
if (!isProd) {
logger.info('Skipping email verification in dev/docker')
return
}
@@ -205,7 +205,7 @@ export const auth = betterAuth({
throw error
}
},
sendVerificationOnSignUp: !isDevOrDocker,
sendVerificationOnSignUp: isProd,
otpLength: 6,
expiresIn: 15 * 60,
}),