Skip to content

Commit 9e81431

Browse files
authored
fix(auth): improve reset password flow and consolidate brand detection (#2924)
* fix(auth): improve reset password flow and consolidate brand detection * fix(auth): set errorHandled for EMAIL_NOT_VERIFIED to prevent duplicate error * fix(auth): clear success message on login errors * chore(auth): fix import order per lint
1 parent 0ea0256 commit 9e81431

File tree

6 files changed

+46
-163
lines changed

6 files changed

+46
-163
lines changed

apps/sim/app/(auth)/login/login-form.tsx

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { inter } from '@/app/_styles/fonts/inter/inter'
2424
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
2525
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
2626
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
27+
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
2728

2829
const logger = createLogger('LoginForm')
2930

@@ -105,7 +106,7 @@ export default function LoginPage({
105106
const [password, setPassword] = useState('')
106107
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
107108
const [showValidationError, setShowValidationError] = useState(false)
108-
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
109+
const buttonClass = useBrandedButtonClass()
109110
const [isButtonHovered, setIsButtonHovered] = useState(false)
110111

111112
const [callbackUrl, setCallbackUrl] = useState('/workspace')
@@ -123,6 +124,7 @@ export default function LoginPage({
123124
const [email, setEmail] = useState('')
124125
const [emailErrors, setEmailErrors] = useState<string[]>([])
125126
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
127+
const [resetSuccessMessage, setResetSuccessMessage] = useState<string | null>(null)
126128

127129
useEffect(() => {
128130
setMounted(true)
@@ -139,32 +141,12 @@ export default function LoginPage({
139141

140142
const inviteFlow = searchParams.get('invite_flow') === 'true'
141143
setIsInviteFlow(inviteFlow)
142-
}
143-
144-
const checkCustomBrand = () => {
145-
const computedStyle = getComputedStyle(document.documentElement)
146-
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
147144

148-
if (brandAccent && brandAccent !== '#6f3dfa') {
149-
setButtonClass('branded-button-custom')
150-
} else {
151-
setButtonClass('branded-button-gradient')
145+
const resetSuccess = searchParams.get('resetSuccess') === 'true'
146+
if (resetSuccess) {
147+
setResetSuccessMessage('Password reset successful. Please sign in with your new password.')
152148
}
153149
}
154-
155-
checkCustomBrand()
156-
157-
window.addEventListener('resize', checkCustomBrand)
158-
const observer = new MutationObserver(checkCustomBrand)
159-
observer.observe(document.documentElement, {
160-
attributes: true,
161-
attributeFilter: ['style', 'class'],
162-
})
163-
164-
return () => {
165-
window.removeEventListener('resize', checkCustomBrand)
166-
observer.disconnect()
167-
}
168150
}, [searchParams])
169151

170152
useEffect(() => {
@@ -221,6 +203,7 @@ export default function LoginPage({
221203

222204
try {
223205
const safeCallbackUrl = validateCallbackUrl(callbackUrl) ? callbackUrl : '/workspace'
206+
let errorHandled = false
224207

225208
const result = await client.signIn.email(
226209
{
@@ -231,11 +214,16 @@ export default function LoginPage({
231214
{
232215
onError: (ctx) => {
233216
logger.error('Login error:', ctx.error)
234-
const errorMessage: string[] = ['Invalid email or password']
235217

218+
// EMAIL_NOT_VERIFIED is handled by the catch block which redirects to /verify
236219
if (ctx.error.code?.includes('EMAIL_NOT_VERIFIED')) {
220+
errorHandled = true
237221
return
238222
}
223+
224+
errorHandled = true
225+
const errorMessage: string[] = ['Invalid email or password']
226+
239227
if (
240228
ctx.error.code?.includes('BAD_REQUEST') ||
241229
ctx.error.message?.includes('Email and password sign in is not enabled')
@@ -271,16 +259,30 @@ export default function LoginPage({
271259
errorMessage.push('Too many requests. Please wait a moment before trying again.')
272260
}
273261

262+
setResetSuccessMessage(null)
274263
setPasswordErrors(errorMessage)
275264
setShowValidationError(true)
276265
},
277266
}
278267
)
279268

280269
if (!result || result.error) {
270+
// Show error if not already handled by onError callback
271+
if (!errorHandled) {
272+
setResetSuccessMessage(null)
273+
const errorMessage = result?.error?.message || 'Login failed. Please try again.'
274+
setPasswordErrors([errorMessage])
275+
setShowValidationError(true)
276+
}
281277
setIsLoading(false)
282278
return
283279
}
280+
281+
// Clear reset success message on successful login
282+
setResetSuccessMessage(null)
283+
284+
// Explicit redirect fallback if better-auth doesn't redirect
285+
router.push(safeCallbackUrl)
284286
} catch (err: any) {
285287
if (err.message?.includes('not verified') || err.code?.includes('EMAIL_NOT_VERIFIED')) {
286288
if (typeof window !== 'undefined') {
@@ -400,6 +402,13 @@ export default function LoginPage({
400402
</div>
401403
)}
402404

405+
{/* Password reset success message */}
406+
{resetSuccessMessage && (
407+
<div className={`${inter.className} mt-1 space-y-1 text-[#4CAF50] text-xs`}>
408+
<p>{resetSuccessMessage}</p>
409+
</div>
410+
)}
411+
403412
{/* Email/Password Form - show unless explicitly disabled */}
404413
{!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && (
405414
<form onSubmit={onSubmit} className={`${inter.className} mt-8 space-y-8`}>

apps/sim/app/(auth)/reset-password/reset-password-form.tsx

Lines changed: 4 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
'use client'
22

3-
import { useEffect, useState } from 'react'
3+
import { useState } from 'react'
44
import { ArrowRight, ChevronRight, Eye, EyeOff } from 'lucide-react'
55
import { Button } from '@/components/ui/button'
66
import { Input } from '@/components/ui/input'
77
import { Label } from '@/components/ui/label'
88
import { cn } from '@/lib/core/utils/cn'
99
import { inter } from '@/app/_styles/fonts/inter/inter'
10+
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
1011

1112
interface RequestResetFormProps {
1213
email: string
@@ -27,36 +28,9 @@ export function RequestResetForm({
2728
statusMessage,
2829
className,
2930
}: RequestResetFormProps) {
30-
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
31+
const buttonClass = useBrandedButtonClass()
3132
const [isButtonHovered, setIsButtonHovered] = useState(false)
3233

33-
useEffect(() => {
34-
const checkCustomBrand = () => {
35-
const computedStyle = getComputedStyle(document.documentElement)
36-
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
37-
38-
if (brandAccent && brandAccent !== '#6f3dfa') {
39-
setButtonClass('branded-button-custom')
40-
} else {
41-
setButtonClass('branded-button-gradient')
42-
}
43-
}
44-
45-
checkCustomBrand()
46-
47-
window.addEventListener('resize', checkCustomBrand)
48-
const observer = new MutationObserver(checkCustomBrand)
49-
observer.observe(document.documentElement, {
50-
attributes: true,
51-
attributeFilter: ['style', 'class'],
52-
})
53-
54-
return () => {
55-
window.removeEventListener('resize', checkCustomBrand)
56-
observer.disconnect()
57-
}
58-
}, [])
59-
6034
const handleSubmit = async (e: React.FormEvent) => {
6135
e.preventDefault()
6236
onSubmit(email)
@@ -138,36 +112,9 @@ export function SetNewPasswordForm({
138112
const [validationMessage, setValidationMessage] = useState('')
139113
const [showPassword, setShowPassword] = useState(false)
140114
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
141-
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
115+
const buttonClass = useBrandedButtonClass()
142116
const [isButtonHovered, setIsButtonHovered] = useState(false)
143117

144-
useEffect(() => {
145-
const checkCustomBrand = () => {
146-
const computedStyle = getComputedStyle(document.documentElement)
147-
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
148-
149-
if (brandAccent && brandAccent !== '#6f3dfa') {
150-
setButtonClass('branded-button-custom')
151-
} else {
152-
setButtonClass('branded-button-gradient')
153-
}
154-
}
155-
156-
checkCustomBrand()
157-
158-
window.addEventListener('resize', checkCustomBrand)
159-
const observer = new MutationObserver(checkCustomBrand)
160-
observer.observe(document.documentElement, {
161-
attributes: true,
162-
attributeFilter: ['style', 'class'],
163-
})
164-
165-
return () => {
166-
window.removeEventListener('resize', checkCustomBrand)
167-
observer.disconnect()
168-
}
169-
}, [])
170-
171118
const handleSubmit = async (e: React.FormEvent) => {
172119
e.preventDefault()
173120

apps/sim/app/(auth)/signup/signup-form.tsx

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { inter } from '@/app/_styles/fonts/inter/inter'
1616
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
1717
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
1818
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
19+
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
1920

2021
const logger = createLogger('SignupForm')
2122

@@ -95,7 +96,7 @@ function SignupFormContent({
9596
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
9697
const [redirectUrl, setRedirectUrl] = useState('')
9798
const [isInviteFlow, setIsInviteFlow] = useState(false)
98-
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
99+
const buttonClass = useBrandedButtonClass()
99100
const [isButtonHovered, setIsButtonHovered] = useState(false)
100101

101102
const [name, setName] = useState('')
@@ -126,31 +127,6 @@ function SignupFormContent({
126127
if (inviteFlowParam === 'true') {
127128
setIsInviteFlow(true)
128129
}
129-
130-
const checkCustomBrand = () => {
131-
const computedStyle = getComputedStyle(document.documentElement)
132-
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
133-
134-
if (brandAccent && brandAccent !== '#6f3dfa') {
135-
setButtonClass('branded-button-custom')
136-
} else {
137-
setButtonClass('branded-button-gradient')
138-
}
139-
}
140-
141-
checkCustomBrand()
142-
143-
window.addEventListener('resize', checkCustomBrand)
144-
const observer = new MutationObserver(checkCustomBrand)
145-
observer.observe(document.documentElement, {
146-
attributes: true,
147-
attributeFilter: ['style', 'class'],
148-
})
149-
150-
return () => {
151-
window.removeEventListener('resize', checkCustomBrand)
152-
observer.disconnect()
153-
}
154130
}, [searchParams])
155131

156132
const validatePassword = (passwordValue: string): string[] => {

apps/sim/app/(auth)/sso/sso-form.tsx

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { cn } from '@/lib/core/utils/cn'
1313
import { quickValidateEmail } from '@/lib/messaging/email/validation'
1414
import { inter } from '@/app/_styles/fonts/inter/inter'
1515
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
16+
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
1617

1718
const logger = createLogger('SSOForm')
1819

@@ -57,7 +58,7 @@ export default function SSOForm() {
5758
const [email, setEmail] = useState('')
5859
const [emailErrors, setEmailErrors] = useState<string[]>([])
5960
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
60-
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
61+
const buttonClass = useBrandedButtonClass()
6162
const [callbackUrl, setCallbackUrl] = useState('/workspace')
6263

6364
useEffect(() => {
@@ -90,31 +91,6 @@ export default function SSOForm() {
9091
setShowEmailValidationError(true)
9192
}
9293
}
93-
94-
const checkCustomBrand = () => {
95-
const computedStyle = getComputedStyle(document.documentElement)
96-
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
97-
98-
if (brandAccent && brandAccent !== '#6f3dfa') {
99-
setButtonClass('branded-button-custom')
100-
} else {
101-
setButtonClass('branded-button-gradient')
102-
}
103-
}
104-
105-
checkCustomBrand()
106-
107-
window.addEventListener('resize', checkCustomBrand)
108-
const observer = new MutationObserver(checkCustomBrand)
109-
observer.observe(document.documentElement, {
110-
attributes: true,
111-
attributeFilter: ['style', 'class'],
112-
})
113-
114-
return () => {
115-
window.removeEventListener('resize', checkCustomBrand)
116-
observer.disconnect()
117-
}
11894
}, [searchParams])
11995

12096
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {

apps/sim/app/(auth)/verify/verify-content.tsx

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { cn } from '@/lib/core/utils/cn'
88
import { inter } from '@/app/_styles/fonts/inter/inter'
99
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
1010
import { useVerification } from '@/app/(auth)/verify/use-verification'
11+
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
1112

1213
interface VerifyContentProps {
1314
hasEmailService: boolean
@@ -58,34 +59,7 @@ function VerificationForm({
5859
setCountdown(30)
5960
}
6061

61-
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
62-
63-
useEffect(() => {
64-
const checkCustomBrand = () => {
65-
const computedStyle = getComputedStyle(document.documentElement)
66-
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
67-
68-
if (brandAccent && brandAccent !== '#6f3dfa') {
69-
setButtonClass('branded-button-custom')
70-
} else {
71-
setButtonClass('branded-button-gradient')
72-
}
73-
}
74-
75-
checkCustomBrand()
76-
77-
window.addEventListener('resize', checkCustomBrand)
78-
const observer = new MutationObserver(checkCustomBrand)
79-
observer.observe(document.documentElement, {
80-
attributes: true,
81-
attributeFilter: ['style', 'class'],
82-
})
83-
84-
return () => {
85-
window.removeEventListener('resize', checkCustomBrand)
86-
observer.disconnect()
87-
}
88-
}, [])
62+
const buttonClass = useBrandedButtonClass()
8963

9064
return (
9165
<>

apps/sim/app/api/auth/reset-password/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ const resetPasswordSchema = z.object({
1515
.max(100, 'Password must not exceed 100 characters')
1616
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
1717
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
18-
.regex(/[0-9]/, 'Password must contain at least one number'),
18+
.regex(/[0-9]/, 'Password must contain at least one number')
19+
.regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character'),
1920
})
2021

2122
export async function POST(request: NextRequest) {

0 commit comments

Comments
 (0)