From abb39e0d57d40d64b6c1c041c1acc232bdacdeb4 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 21 Jan 2026 13:52:11 -0800 Subject: [PATCH 1/4] fix(auth): improve reset password flow and consolidate brand detection --- apps/sim/app/(auth)/login/login-form.tsx | 56 +++++++++-------- .../reset-password/reset-password-form.tsx | 61 ++----------------- apps/sim/app/(auth)/signup/signup-form.tsx | 28 +-------- apps/sim/app/(auth)/sso/sso-form.tsx | 28 +-------- apps/sim/app/(auth)/verify/verify-content.tsx | 30 +-------- apps/sim/app/api/auth/reset-password/route.ts | 3 +- 6 files changed, 43 insertions(+), 163 deletions(-) diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index c2094755a9..cde6bd4dd7 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -15,6 +15,7 @@ import { } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' +import { useBrandedButtonClass } from '@/hooks/use-branded-button-class' import { client } from '@/lib/auth/auth-client' import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env' import { cn } from '@/lib/core/utils/cn' @@ -105,7 +106,7 @@ export default function LoginPage({ const [password, setPassword] = useState('') const [passwordErrors, setPasswordErrors] = useState([]) const [showValidationError, setShowValidationError] = useState(false) - const [buttonClass, setButtonClass] = useState('branded-button-gradient') + const buttonClass = useBrandedButtonClass() const [isButtonHovered, setIsButtonHovered] = useState(false) const [callbackUrl, setCallbackUrl] = useState('/workspace') @@ -123,6 +124,7 @@ export default function LoginPage({ const [email, setEmail] = useState('') const [emailErrors, setEmailErrors] = useState([]) const [showEmailValidationError, setShowEmailValidationError] = useState(false) + const [resetSuccessMessage, setResetSuccessMessage] = useState(null) useEffect(() => { setMounted(true) @@ -139,32 +141,12 @@ export default function LoginPage({ const inviteFlow = searchParams.get('invite_flow') === 'true' setIsInviteFlow(inviteFlow) - } - - const checkCustomBrand = () => { - const computedStyle = getComputedStyle(document.documentElement) - const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim() - if (brandAccent && brandAccent !== '#6f3dfa') { - setButtonClass('branded-button-custom') - } else { - setButtonClass('branded-button-gradient') + const resetSuccess = searchParams.get('resetSuccess') === 'true' + if (resetSuccess) { + setResetSuccessMessage('Password reset successful. Please sign in with your new password.') } } - - checkCustomBrand() - - window.addEventListener('resize', checkCustomBrand) - const observer = new MutationObserver(checkCustomBrand) - observer.observe(document.documentElement, { - attributes: true, - attributeFilter: ['style', 'class'], - }) - - return () => { - window.removeEventListener('resize', checkCustomBrand) - observer.disconnect() - } }, [searchParams]) useEffect(() => { @@ -221,6 +203,7 @@ export default function LoginPage({ try { const safeCallbackUrl = validateCallbackUrl(callbackUrl) ? callbackUrl : '/workspace' + let errorHandled = false const result = await client.signIn.email( { @@ -231,11 +214,15 @@ export default function LoginPage({ { onError: (ctx) => { logger.error('Login error:', ctx.error) - const errorMessage: string[] = ['Invalid email or password'] + // EMAIL_NOT_VERIFIED is handled by the catch block which redirects to /verify if (ctx.error.code?.includes('EMAIL_NOT_VERIFIED')) { return } + + errorHandled = true + const errorMessage: string[] = ['Invalid email or password'] + if ( ctx.error.code?.includes('BAD_REQUEST') || ctx.error.message?.includes('Email and password sign in is not enabled') @@ -278,9 +265,21 @@ export default function LoginPage({ ) if (!result || result.error) { + // Show error if not already handled by onError callback + if (!errorHandled) { + const errorMessage = result?.error?.message || 'Login failed. Please try again.' + setPasswordErrors([errorMessage]) + setShowValidationError(true) + } setIsLoading(false) return } + + // Clear reset success message on successful login + setResetSuccessMessage(null) + + // Explicit redirect fallback if better-auth doesn't redirect + router.push(safeCallbackUrl) } catch (err: any) { if (err.message?.includes('not verified') || err.code?.includes('EMAIL_NOT_VERIFIED')) { if (typeof window !== 'undefined') { @@ -400,6 +399,13 @@ export default function LoginPage({ )} + {/* Password reset success message */} + {resetSuccessMessage && ( +
+

{resetSuccessMessage}

+
+ )} + {/* Email/Password Form - show unless explicitly disabled */} {!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && (
diff --git a/apps/sim/app/(auth)/reset-password/reset-password-form.tsx b/apps/sim/app/(auth)/reset-password/reset-password-form.tsx index 7212b52d53..4998fea814 100644 --- a/apps/sim/app/(auth)/reset-password/reset-password-form.tsx +++ b/apps/sim/app/(auth)/reset-password/reset-password-form.tsx @@ -1,7 +1,8 @@ 'use client' -import { useEffect, useState } from 'react' +import { useState } from 'react' import { ArrowRight, ChevronRight, Eye, EyeOff } from 'lucide-react' +import { useBrandedButtonClass } from '@/hooks/use-branded-button-class' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' @@ -27,36 +28,9 @@ export function RequestResetForm({ statusMessage, className, }: RequestResetFormProps) { - const [buttonClass, setButtonClass] = useState('branded-button-gradient') + const buttonClass = useBrandedButtonClass() const [isButtonHovered, setIsButtonHovered] = useState(false) - useEffect(() => { - const checkCustomBrand = () => { - const computedStyle = getComputedStyle(document.documentElement) - const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim() - - if (brandAccent && brandAccent !== '#6f3dfa') { - setButtonClass('branded-button-custom') - } else { - setButtonClass('branded-button-gradient') - } - } - - checkCustomBrand() - - window.addEventListener('resize', checkCustomBrand) - const observer = new MutationObserver(checkCustomBrand) - observer.observe(document.documentElement, { - attributes: true, - attributeFilter: ['style', 'class'], - }) - - return () => { - window.removeEventListener('resize', checkCustomBrand) - observer.disconnect() - } - }, []) - const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() onSubmit(email) @@ -138,36 +112,9 @@ export function SetNewPasswordForm({ const [validationMessage, setValidationMessage] = useState('') const [showPassword, setShowPassword] = useState(false) const [showConfirmPassword, setShowConfirmPassword] = useState(false) - const [buttonClass, setButtonClass] = useState('branded-button-gradient') + const buttonClass = useBrandedButtonClass() const [isButtonHovered, setIsButtonHovered] = useState(false) - useEffect(() => { - const checkCustomBrand = () => { - const computedStyle = getComputedStyle(document.documentElement) - const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim() - - if (brandAccent && brandAccent !== '#6f3dfa') { - setButtonClass('branded-button-custom') - } else { - setButtonClass('branded-button-gradient') - } - } - - checkCustomBrand() - - window.addEventListener('resize', checkCustomBrand) - const observer = new MutationObserver(checkCustomBrand) - observer.observe(document.documentElement, { - attributes: true, - attributeFilter: ['style', 'class'], - }) - - return () => { - window.removeEventListener('resize', checkCustomBrand) - observer.disconnect() - } - }, []) - const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() diff --git a/apps/sim/app/(auth)/signup/signup-form.tsx b/apps/sim/app/(auth)/signup/signup-form.tsx index 5aeb59fa67..07f70fa9be 100644 --- a/apps/sim/app/(auth)/signup/signup-form.tsx +++ b/apps/sim/app/(auth)/signup/signup-form.tsx @@ -8,6 +8,7 @@ import { useRouter, useSearchParams } from 'next/navigation' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' +import { useBrandedButtonClass } from '@/hooks/use-branded-button-class' import { client, useSession } from '@/lib/auth/auth-client' import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env' import { cn } from '@/lib/core/utils/cn' @@ -95,7 +96,7 @@ function SignupFormContent({ const [showEmailValidationError, setShowEmailValidationError] = useState(false) const [redirectUrl, setRedirectUrl] = useState('') const [isInviteFlow, setIsInviteFlow] = useState(false) - const [buttonClass, setButtonClass] = useState('branded-button-gradient') + const buttonClass = useBrandedButtonClass() const [isButtonHovered, setIsButtonHovered] = useState(false) const [name, setName] = useState('') @@ -126,31 +127,6 @@ function SignupFormContent({ if (inviteFlowParam === 'true') { setIsInviteFlow(true) } - - const checkCustomBrand = () => { - const computedStyle = getComputedStyle(document.documentElement) - const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim() - - if (brandAccent && brandAccent !== '#6f3dfa') { - setButtonClass('branded-button-custom') - } else { - setButtonClass('branded-button-gradient') - } - } - - checkCustomBrand() - - window.addEventListener('resize', checkCustomBrand) - const observer = new MutationObserver(checkCustomBrand) - observer.observe(document.documentElement, { - attributes: true, - attributeFilter: ['style', 'class'], - }) - - return () => { - window.removeEventListener('resize', checkCustomBrand) - observer.disconnect() - } }, [searchParams]) const validatePassword = (passwordValue: string): string[] => { diff --git a/apps/sim/app/(auth)/sso/sso-form.tsx b/apps/sim/app/(auth)/sso/sso-form.tsx index 0d371bbaff..51db7b5de2 100644 --- a/apps/sim/app/(auth)/sso/sso-form.tsx +++ b/apps/sim/app/(auth)/sso/sso-form.tsx @@ -7,6 +7,7 @@ import { useRouter, useSearchParams } from 'next/navigation' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' +import { useBrandedButtonClass } from '@/hooks/use-branded-button-class' import { client } from '@/lib/auth/auth-client' import { env, isFalsy } from '@/lib/core/config/env' import { cn } from '@/lib/core/utils/cn' @@ -57,7 +58,7 @@ export default function SSOForm() { const [email, setEmail] = useState('') const [emailErrors, setEmailErrors] = useState([]) const [showEmailValidationError, setShowEmailValidationError] = useState(false) - const [buttonClass, setButtonClass] = useState('branded-button-gradient') + const buttonClass = useBrandedButtonClass() const [callbackUrl, setCallbackUrl] = useState('/workspace') useEffect(() => { @@ -90,31 +91,6 @@ export default function SSOForm() { setShowEmailValidationError(true) } } - - const checkCustomBrand = () => { - const computedStyle = getComputedStyle(document.documentElement) - const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim() - - if (brandAccent && brandAccent !== '#6f3dfa') { - setButtonClass('branded-button-custom') - } else { - setButtonClass('branded-button-gradient') - } - } - - checkCustomBrand() - - window.addEventListener('resize', checkCustomBrand) - const observer = new MutationObserver(checkCustomBrand) - observer.observe(document.documentElement, { - attributes: true, - attributeFilter: ['style', 'class'], - }) - - return () => { - window.removeEventListener('resize', checkCustomBrand) - observer.disconnect() - } }, [searchParams]) const handleEmailChange = (e: React.ChangeEvent) => { diff --git a/apps/sim/app/(auth)/verify/verify-content.tsx b/apps/sim/app/(auth)/verify/verify-content.tsx index ed05354b94..a1f2c77f7c 100644 --- a/apps/sim/app/(auth)/verify/verify-content.tsx +++ b/apps/sim/app/(auth)/verify/verify-content.tsx @@ -2,6 +2,7 @@ import { Suspense, useEffect, useState } from 'react' import { useRouter } from 'next/navigation' +import { useBrandedButtonClass } from '@/hooks/use-branded-button-class' import { Button } from '@/components/ui/button' import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp' import { cn } from '@/lib/core/utils/cn' @@ -58,34 +59,7 @@ function VerificationForm({ setCountdown(30) } - const [buttonClass, setButtonClass] = useState('branded-button-gradient') - - useEffect(() => { - const checkCustomBrand = () => { - const computedStyle = getComputedStyle(document.documentElement) - const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim() - - if (brandAccent && brandAccent !== '#6f3dfa') { - setButtonClass('branded-button-custom') - } else { - setButtonClass('branded-button-gradient') - } - } - - checkCustomBrand() - - window.addEventListener('resize', checkCustomBrand) - const observer = new MutationObserver(checkCustomBrand) - observer.observe(document.documentElement, { - attributes: true, - attributeFilter: ['style', 'class'], - }) - - return () => { - window.removeEventListener('resize', checkCustomBrand) - observer.disconnect() - } - }, []) + const buttonClass = useBrandedButtonClass() return ( <> diff --git a/apps/sim/app/api/auth/reset-password/route.ts b/apps/sim/app/api/auth/reset-password/route.ts index 0caa1494f2..1d47be1035 100644 --- a/apps/sim/app/api/auth/reset-password/route.ts +++ b/apps/sim/app/api/auth/reset-password/route.ts @@ -15,7 +15,8 @@ const resetPasswordSchema = z.object({ .max(100, 'Password must not exceed 100 characters') .regex(/[A-Z]/, 'Password must contain at least one uppercase letter') .regex(/[a-z]/, 'Password must contain at least one lowercase letter') - .regex(/[0-9]/, 'Password must contain at least one number'), + .regex(/[0-9]/, 'Password must contain at least one number') + .regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character'), }) export async function POST(request: NextRequest) { From 31f188b96b539bbe9542ca87f77ce7d1828eeacf Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 21 Jan 2026 14:09:10 -0800 Subject: [PATCH 2/4] fix(auth): set errorHandled for EMAIL_NOT_VERIFIED to prevent duplicate error --- apps/sim/app/(auth)/login/login-form.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index cde6bd4dd7..931e4923d2 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -217,6 +217,7 @@ export default function LoginPage({ // EMAIL_NOT_VERIFIED is handled by the catch block which redirects to /verify if (ctx.error.code?.includes('EMAIL_NOT_VERIFIED')) { + errorHandled = true return } From ee7cae40e4af0f391a25b47c2806de6f173522ad Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 21 Jan 2026 14:14:00 -0800 Subject: [PATCH 3/4] fix(auth): clear success message on login errors --- apps/sim/app/(auth)/login/login-form.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index 931e4923d2..feeb4f3372 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -259,6 +259,7 @@ export default function LoginPage({ errorMessage.push('Too many requests. Please wait a moment before trying again.') } + setResetSuccessMessage(null) setPasswordErrors(errorMessage) setShowValidationError(true) }, @@ -268,6 +269,7 @@ export default function LoginPage({ if (!result || result.error) { // Show error if not already handled by onError callback if (!errorHandled) { + setResetSuccessMessage(null) const errorMessage = result?.error?.message || 'Login failed. Please try again.' setPasswordErrors([errorMessage]) setShowValidationError(true) From b52bda5d2360600e6f70641af0846d5707163493 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 21 Jan 2026 14:17:19 -0800 Subject: [PATCH 4/4] chore(auth): fix import order per lint --- apps/sim/app/(auth)/login/login-form.tsx | 2 +- apps/sim/app/(auth)/reset-password/reset-password-form.tsx | 2 +- apps/sim/app/(auth)/signup/signup-form.tsx | 2 +- apps/sim/app/(auth)/sso/sso-form.tsx | 2 +- apps/sim/app/(auth)/verify/verify-content.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index feeb4f3372..c0ed0cd6d4 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -15,7 +15,6 @@ import { } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { useBrandedButtonClass } from '@/hooks/use-branded-button-class' import { client } from '@/lib/auth/auth-client' import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env' import { cn } from '@/lib/core/utils/cn' @@ -25,6 +24,7 @@ import { inter } from '@/app/_styles/fonts/inter/inter' import { soehne } from '@/app/_styles/fonts/soehne/soehne' import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons' import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button' +import { useBrandedButtonClass } from '@/hooks/use-branded-button-class' const logger = createLogger('LoginForm') diff --git a/apps/sim/app/(auth)/reset-password/reset-password-form.tsx b/apps/sim/app/(auth)/reset-password/reset-password-form.tsx index 4998fea814..d50fbf9868 100644 --- a/apps/sim/app/(auth)/reset-password/reset-password-form.tsx +++ b/apps/sim/app/(auth)/reset-password/reset-password-form.tsx @@ -2,12 +2,12 @@ import { useState } from 'react' import { ArrowRight, ChevronRight, Eye, EyeOff } from 'lucide-react' -import { useBrandedButtonClass } from '@/hooks/use-branded-button-class' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { cn } from '@/lib/core/utils/cn' import { inter } from '@/app/_styles/fonts/inter/inter' +import { useBrandedButtonClass } from '@/hooks/use-branded-button-class' interface RequestResetFormProps { email: string diff --git a/apps/sim/app/(auth)/signup/signup-form.tsx b/apps/sim/app/(auth)/signup/signup-form.tsx index 07f70fa9be..840765de8c 100644 --- a/apps/sim/app/(auth)/signup/signup-form.tsx +++ b/apps/sim/app/(auth)/signup/signup-form.tsx @@ -8,7 +8,6 @@ import { useRouter, useSearchParams } from 'next/navigation' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { useBrandedButtonClass } from '@/hooks/use-branded-button-class' import { client, useSession } from '@/lib/auth/auth-client' import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env' import { cn } from '@/lib/core/utils/cn' @@ -17,6 +16,7 @@ import { inter } from '@/app/_styles/fonts/inter/inter' import { soehne } from '@/app/_styles/fonts/soehne/soehne' import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons' import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button' +import { useBrandedButtonClass } from '@/hooks/use-branded-button-class' const logger = createLogger('SignupForm') diff --git a/apps/sim/app/(auth)/sso/sso-form.tsx b/apps/sim/app/(auth)/sso/sso-form.tsx index 51db7b5de2..12901c51c2 100644 --- a/apps/sim/app/(auth)/sso/sso-form.tsx +++ b/apps/sim/app/(auth)/sso/sso-form.tsx @@ -7,13 +7,13 @@ import { useRouter, useSearchParams } from 'next/navigation' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { useBrandedButtonClass } from '@/hooks/use-branded-button-class' import { client } from '@/lib/auth/auth-client' import { env, isFalsy } from '@/lib/core/config/env' import { cn } from '@/lib/core/utils/cn' import { quickValidateEmail } from '@/lib/messaging/email/validation' import { inter } from '@/app/_styles/fonts/inter/inter' import { soehne } from '@/app/_styles/fonts/soehne/soehne' +import { useBrandedButtonClass } from '@/hooks/use-branded-button-class' const logger = createLogger('SSOForm') diff --git a/apps/sim/app/(auth)/verify/verify-content.tsx b/apps/sim/app/(auth)/verify/verify-content.tsx index a1f2c77f7c..0eb41b8ba0 100644 --- a/apps/sim/app/(auth)/verify/verify-content.tsx +++ b/apps/sim/app/(auth)/verify/verify-content.tsx @@ -2,13 +2,13 @@ import { Suspense, useEffect, useState } from 'react' import { useRouter } from 'next/navigation' -import { useBrandedButtonClass } from '@/hooks/use-branded-button-class' import { Button } from '@/components/ui/button' import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp' import { cn } from '@/lib/core/utils/cn' import { inter } from '@/app/_styles/fonts/inter/inter' import { soehne } from '@/app/_styles/fonts/soehne/soehne' import { useVerification } from '@/app/(auth)/verify/use-verification' +import { useBrandedButtonClass } from '@/hooks/use-branded-button-class' interface VerifyContentProps { hasEmailService: boolean