Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
268e2f1
fix(zustand): updated to useShallow from deprecated createWithEqualit…
waleedlatif1 Jan 21, 2026
ea49640
fix(logger): use direct env access for webpack inlining (#2920)
waleedlatif1 Jan 21, 2026
fb8868c
fix(notifications): text overflow with line-clamp (#2921)
waleedlatif1 Jan 21, 2026
0ea0256
chore(helm): add env vars for Vertex AI, orgs, and telemetry (#2922)
waleedlatif1 Jan 21, 2026
9e81431
fix(auth): improve reset password flow and consolidate brand detectio…
waleedlatif1 Jan 21, 2026
8bbcf31
fix(action-bar): duplicate subflows with children (#2923)
waleedlatif1 Jan 21, 2026
5157f0b
fix(resolver): agent response format, input formats, root level (#2925)
icecrasher321 Jan 21, 2026
004e058
fix(messages-input): fix cursor alignment and auto-resize with overla…
waleedlatif1 Jan 21, 2026
103b31a
fix(stores): remove dead code causing log spam on startup (#2927)
waleedlatif1 Jan 22, 2026
e2ccefb
improvement(ui): use BrandedButton and BrandedLink components (#2930)
waleedlatif1 Jan 22, 2026
5987a6d
fix(custom-tools): remove unsafe title fallback in getCustomTool (#2929)
waleedlatif1 Jan 22, 2026
d681451
fix(null-bodies): empty bodies handling (#2931)
icecrasher321 Jan 22, 2026
7cfdf46
fix(token-refresh): microsoft, notion, x, linear (#2933)
icecrasher321 Jan 22, 2026
f3fcc28
fix(auth): handle EMAIL_NOT_VERIFIED in onError callback (#2932)
waleedlatif1 Jan 22, 2026
900d3ef
fix(workflow-selector): use dedicated selector for workflow dropdown …
waleedlatif1 Jan 22, 2026
2f0f246
feat(workflow-block): preview (#2935)
emir-karabeg Jan 22, 2026
42159c2
improvement(copilot): tool configs to show nested props (#2936)
icecrasher321 Jan 22, 2026
376f7cb
fix(auth): add genericOAuth providers to trustedProviders (#2937)
waleedlatif1 Jan 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 54 additions & 63 deletions apps/sim/app/(auth)/login/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

import { useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
import { ArrowRight, ChevronRight, Eye, EyeOff } from 'lucide-react'
import { Eye, EyeOff } from 'lucide-react'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
Expand All @@ -22,8 +21,10 @@ import { getBaseUrl } from '@/lib/core/utils/urls'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
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')

Expand Down Expand Up @@ -105,16 +106,14 @@ export default function LoginPage({
const [password, setPassword] = useState('')
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
const [showValidationError, setShowValidationError] = useState(false)
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
const [isButtonHovered, setIsButtonHovered] = useState(false)
const buttonClass = useBrandedButtonClass()

const [callbackUrl, setCallbackUrl] = useState('/workspace')
const [isInviteFlow, setIsInviteFlow] = useState(false)

const [forgotPasswordOpen, setForgotPasswordOpen] = useState(false)
const [forgotPasswordEmail, setForgotPasswordEmail] = useState('')
const [isSubmittingReset, setIsSubmittingReset] = useState(false)
const [isResetButtonHovered, setIsResetButtonHovered] = useState(false)
const [resetStatus, setResetStatus] = useState<{
type: 'success' | 'error' | null
message: string
Expand All @@ -123,6 +122,7 @@ export default function LoginPage({
const [email, setEmail] = useState('')
const [emailErrors, setEmailErrors] = useState<string[]>([])
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
const [resetSuccessMessage, setResetSuccessMessage] = useState<string | null>(null)

useEffect(() => {
setMounted(true)
Expand All @@ -139,32 +139,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(() => {
Expand Down Expand Up @@ -202,6 +182,13 @@ export default function LoginPage({
e.preventDefault()
setIsLoading(true)

const redirectToVerify = (emailToVerify: string) => {
if (typeof window !== 'undefined') {
sessionStorage.setItem('verificationEmail', emailToVerify)
}
router.push('/verify')
}

const formData = new FormData(e.currentTarget)
const emailRaw = formData.get('email') as string
const email = emailRaw.trim().toLowerCase()
Expand All @@ -221,6 +208,7 @@ export default function LoginPage({

try {
const safeCallbackUrl = validateCallbackUrl(callbackUrl) ? callbackUrl : '/workspace'
let errorHandled = false

const result = await client.signIn.email(
{
Expand All @@ -231,11 +219,16 @@ export default function LoginPage({
{
onError: (ctx) => {
logger.error('Login error:', ctx.error)
const errorMessage: string[] = ['Invalid email or password']

if (ctx.error.code?.includes('EMAIL_NOT_VERIFIED')) {
errorHandled = true
redirectToVerify(email)
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')
Expand Down Expand Up @@ -271,22 +264,33 @@ export default function LoginPage({
errorMessage.push('Too many requests. Please wait a moment before trying again.')
}

setResetSuccessMessage(null)
setPasswordErrors(errorMessage)
setShowValidationError(true)
},
}
)

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)
}
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') {
sessionStorage.setItem('verificationEmail', email)
}
router.push('/verify')
redirectToVerify(email)
return
}

Expand Down Expand Up @@ -400,6 +404,13 @@ export default function LoginPage({
</div>
)}

{/* Password reset success message */}
{resetSuccessMessage && (
<div className={`${inter.className} mt-1 space-y-1 text-[#4CAF50] text-xs`}>
<p>{resetSuccessMessage}</p>
</div>
)}

{/* Email/Password Form - show unless explicitly disabled */}
{!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && (
<form onSubmit={onSubmit} className={`${inter.className} mt-8 space-y-8`}>
Expand Down Expand Up @@ -482,24 +493,14 @@ export default function LoginPage({
</div>
</div>

<Button
<BrandedButton
type='submit'
onMouseEnter={() => setIsButtonHovered(true)}
onMouseLeave={() => setIsButtonHovered(false)}
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all'
disabled={isLoading}
loading={isLoading}
loadingText='Signing in'
>
<span className='flex items-center gap-1'>
{isLoading ? 'Signing in...' : 'Sign in'}
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
{isButtonHovered ? (
<ArrowRight className='h-4 w-4' aria-hidden='true' />
) : (
<ChevronRight className='h-4 w-4' aria-hidden='true' />
)}
</span>
</span>
</Button>
Sign in
</BrandedButton>
</form>
)}

Expand Down Expand Up @@ -610,25 +611,15 @@ export default function LoginPage({
<p>{resetStatus.message}</p>
</div>
)}
<Button
<BrandedButton
type='button'
onClick={handleForgotPassword}
onMouseEnter={() => setIsResetButtonHovered(true)}
onMouseLeave={() => setIsResetButtonHovered(false)}
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all'
disabled={isSubmittingReset}
loading={isSubmittingReset}
loadingText='Sending'
>
<span className='flex items-center gap-1'>
{isSubmittingReset ? 'Sending...' : 'Send Reset Link'}
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
{isResetButtonHovered ? (
<ArrowRight className='h-4 w-4' aria-hidden='true' />
) : (
<ChevronRight className='h-4 w-4' aria-hidden='true' />
)}
</span>
</span>
</Button>
Send Reset Link
</BrandedButton>
</div>
</DialogContent>
</Dialog>
Expand Down
107 changes: 14 additions & 93 deletions apps/sim/app/(auth)/reset-password/reset-password-form.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
'use client'

import { useEffect, useState } from 'react'
import { ArrowRight, ChevronRight, Eye, EyeOff } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { useState } from 'react'
import { Eye, EyeOff } from 'lucide-react'
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 { BrandedButton } from '@/app/(auth)/components/branded-button'

interface RequestResetFormProps {
email: string
Expand All @@ -27,36 +27,6 @@ export function RequestResetForm({
statusMessage,
className,
}: RequestResetFormProps) {
const [buttonClass, setButtonClass] = useState('branded-button-gradient')
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)
Expand Down Expand Up @@ -94,24 +64,14 @@ export function RequestResetForm({
)}
</div>

<Button
<BrandedButton
type='submit'
disabled={isSubmitting}
onMouseEnter={() => setIsButtonHovered(true)}
onMouseLeave={() => setIsButtonHovered(false)}
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all'
loading={isSubmitting}
loadingText='Sending'
>
<span className='flex items-center gap-1'>
{isSubmitting ? 'Sending...' : 'Send Reset Link'}
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
{isButtonHovered ? (
<ArrowRight className='h-4 w-4' aria-hidden='true' />
) : (
<ChevronRight className='h-4 w-4' aria-hidden='true' />
)}
</span>
</span>
</Button>
Send Reset Link
</BrandedButton>
</form>
)
}
Expand All @@ -138,35 +98,6 @@ 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 [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()
Expand Down Expand Up @@ -296,24 +227,14 @@ export function SetNewPasswordForm({
)}
</div>

<Button
disabled={isSubmitting || !token}
<BrandedButton
type='submit'
onMouseEnter={() => setIsButtonHovered(true)}
onMouseLeave={() => setIsButtonHovered(false)}
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all'
disabled={isSubmitting || !token}
loading={isSubmitting}
loadingText='Resetting'
>
<span className='flex items-center gap-1'>
{isSubmitting ? 'Resetting...' : 'Reset Password'}
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
{isButtonHovered ? (
<ArrowRight className='h-4 w-4' aria-hidden='true' />
) : (
<ChevronRight className='h-4 w-4' aria-hidden='true' />
)}
</span>
</span>
</Button>
Reset Password
</BrandedButton>
</form>
)
}
Loading
Loading