diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index c2094755a9..c0ed0cd6d4 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -24,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') @@ -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,16 @@ 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')) { + errorHandled = true 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') @@ -271,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) }, @@ -278,9 +267,22 @@ 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) + } 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 +402,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..d50fbf9868 100644 --- a/apps/sim/app/(auth)/reset-password/reset-password-form.tsx +++ b/apps/sim/app/(auth)/reset-password/reset-password-form.tsx @@ -1,12 +1,13 @@ 'use client' -import { useEffect, useState } from 'react' +import { useState } from 'react' import { ArrowRight, ChevronRight, Eye, EyeOff } from 'lucide-react' 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 @@ -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..840765de8c 100644 --- a/apps/sim/app/(auth)/signup/signup-form.tsx +++ b/apps/sim/app/(auth)/signup/signup-form.tsx @@ -16,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') @@ -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..12901c51c2 100644 --- a/apps/sim/app/(auth)/sso/sso-form.tsx +++ b/apps/sim/app/(auth)/sso/sso-form.tsx @@ -13,6 +13,7 @@ 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') @@ -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..0eb41b8ba0 100644 --- a/apps/sim/app/(auth)/verify/verify-content.tsx +++ b/apps/sim/app/(auth)/verify/verify-content.tsx @@ -8,6 +8,7 @@ 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 @@ -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) {