diff --git a/apps/sim/app/(auth)/components/oauth-provider-checker.tsx b/apps/sim/app/(auth)/components/oauth-provider-checker.tsx index 8aff925c3..43766112b 100644 --- a/apps/sim/app/(auth)/components/oauth-provider-checker.tsx +++ b/apps/sim/app/(auth)/components/oauth-provider-checker.tsx @@ -4,19 +4,9 @@ import { env } from '@/lib/env' import { isProd } from '@/lib/environment' export async function getOAuthProviderStatus() { - const githubAvailable = !!( - env.GITHUB_CLIENT_ID && - env.GITHUB_CLIENT_SECRET && - env.GITHUB_CLIENT_ID !== 'placeholder' && - env.GITHUB_CLIENT_SECRET !== 'placeholder' - ) + const githubAvailable = !!(env.GITHUB_CLIENT_ID && env.GITHUB_CLIENT_SECRET) - const googleAvailable = !!( - env.GOOGLE_CLIENT_ID && - env.GOOGLE_CLIENT_SECRET && - env.GOOGLE_CLIENT_ID !== 'placeholder' && - env.GOOGLE_CLIENT_SECRET !== 'placeholder' - ) + const googleAvailable = !!(env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) return { githubAvailable, googleAvailable, isProduction: isProd } } diff --git a/apps/sim/app/(auth)/components/social-login-buttons.tsx b/apps/sim/app/(auth)/components/social-login-buttons.tsx index 4cb19691f..7c76325b6 100644 --- a/apps/sim/app/(auth)/components/social-login-buttons.tsx +++ b/apps/sim/app/(auth)/components/social-login-buttons.tsx @@ -37,12 +37,6 @@ export function SocialLoginButtons({ setIsGithubLoading(true) try { await client.signIn.social({ provider: 'github', callbackURL }) - - // Mark that the user has previously logged in - if (typeof window !== 'undefined') { - localStorage.setItem('has_logged_in_before', 'true') - document.cookie = 'has_logged_in_before=true; path=/; max-age=31536000; SameSite=Lax' // 1 year expiry - } } catch (err: any) { let errorMessage = 'Failed to sign in with GitHub' @@ -66,13 +60,6 @@ export function SocialLoginButtons({ setIsGoogleLoading(true) try { await client.signIn.social({ provider: 'google', callbackURL }) - - // Mark that the user has previously logged in - if (typeof window !== 'undefined') { - localStorage.setItem('has_logged_in_before', 'true') - // Also set a cookie to enable middleware to check login status - document.cookie = 'has_logged_in_before=true; path=/; max-age=31536000; SameSite=Lax' // 1 year expiry - } } catch (err: any) { let errorMessage = 'Failed to sign in with Google' diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index f48b599ba..d30b03b63 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -74,12 +74,12 @@ const validatePassword = (passwordValue: string): string[] => { if (!PASSWORD_VALIDATIONS.required.test(passwordValue)) { errors.push(PASSWORD_VALIDATIONS.required.message) - return errors // Return early for required field + return errors } if (!PASSWORD_VALIDATIONS.notEmpty.test(passwordValue)) { errors.push(PASSWORD_VALIDATIONS.notEmpty.message) - return errors // Return early for empty field + return errors } return errors @@ -104,11 +104,9 @@ export default function LoginPage({ const [showValidationError, setShowValidationError] = useState(false) const [buttonClass, setButtonClass] = useState('auth-button-gradient') - // Initialize state for URL parameters const [callbackUrl, setCallbackUrl] = useState('/workspace') const [isInviteFlow, setIsInviteFlow] = useState(false) - // Forgot password states const [forgotPasswordOpen, setForgotPasswordOpen] = useState(false) const [forgotPasswordEmail, setForgotPasswordEmail] = useState('') const [isSubmittingReset, setIsSubmittingReset] = useState(false) @@ -117,25 +115,20 @@ export default function LoginPage({ message: string }>({ type: null, message: '' }) - // Email validation state const [email, setEmail] = useState('') const [emailErrors, setEmailErrors] = useState([]) const [showEmailValidationError, setShowEmailValidationError] = useState(false) - // Extract URL parameters after component mounts to avoid SSR issues useEffect(() => { setMounted(true) - // Only access search params on the client side if (searchParams) { const callback = searchParams.get('callbackUrl') if (callback) { - // Validate the callbackUrl before setting it if (validateCallbackUrl(callback)) { setCallbackUrl(callback) } else { logger.warn('Invalid callback URL detected and blocked:', { url: callback }) - // Keep the default safe value ('/workspace') } } @@ -143,12 +136,10 @@ export default function LoginPage({ setIsInviteFlow(inviteFlow) } - // Check if CSS variable has been customized const checkCustomBrand = () => { const computedStyle = getComputedStyle(document.documentElement) const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim() - // Check if the CSS variable exists and is different from the default if (brandAccent && brandAccent !== '#6f3dfa') { setButtonClass('auth-button-custom') } else { @@ -158,7 +149,6 @@ export default function LoginPage({ checkCustomBrand() - // Also check on window resize or theme changes window.addEventListener('resize', checkCustomBrand) const observer = new MutationObserver(checkCustomBrand) observer.observe(document.documentElement, { @@ -189,7 +179,6 @@ export default function LoginPage({ const newEmail = e.target.value setEmail(newEmail) - // Silently validate but don't show errors until submit const errors = validateEmailField(newEmail) setEmailErrors(errors) setShowEmailValidationError(false) @@ -199,7 +188,6 @@ export default function LoginPage({ const newPassword = e.target.value setPassword(newPassword) - // Silently validate but don't show errors until submit const errors = validatePassword(newPassword) setPasswordErrors(errors) setShowValidationError(false) @@ -210,26 +198,23 @@ export default function LoginPage({ setIsLoading(true) const formData = new FormData(e.currentTarget) - const email = formData.get('email') as string + const emailRaw = formData.get('email') as string + const email = emailRaw.trim().toLowerCase() - // Validate email on submit const emailValidationErrors = validateEmailField(email) setEmailErrors(emailValidationErrors) setShowEmailValidationError(emailValidationErrors.length > 0) - // Validate password on submit const passwordValidationErrors = validatePassword(password) setPasswordErrors(passwordValidationErrors) setShowValidationError(passwordValidationErrors.length > 0) - // If there are validation errors, stop submission if (emailValidationErrors.length > 0 || passwordValidationErrors.length > 0) { setIsLoading(false) return } try { - // Final validation before submission const safeCallbackUrl = validateCallbackUrl(callbackUrl) ? callbackUrl : '/workspace' const result = await client.signIn.email( @@ -291,33 +276,13 @@ export default function LoginPage({ setIsLoading(false) return } - - // Mark that the user has previously logged in - if (typeof window !== 'undefined') { - localStorage.setItem('has_logged_in_before', 'true') - document.cookie = 'has_logged_in_before=true; path=/; max-age=31536000; SameSite=Lax' // 1 year expiry - } } catch (err: any) { - // Handle only the special verification case that requires a redirect if (err.message?.includes('not verified') || err.code?.includes('EMAIL_NOT_VERIFIED')) { - try { - await client.emailOtp.sendVerificationOtp({ - email, - type: 'email-verification', - }) - - if (typeof window !== 'undefined') { - sessionStorage.setItem('verificationEmail', email) - } - - router.push('/verify') - return - } catch (_verifyErr) { - setPasswordErrors(['Failed to send verification code. Please try again later.']) - setShowValidationError(true) - setIsLoading(false) - return + if (typeof window !== 'undefined') { + sessionStorage.setItem('verificationEmail', email) } + router.push('/verify') + return } console.error('Uncaught login error:', err) diff --git a/apps/sim/app/(auth)/reset-password/page.tsx b/apps/sim/app/(auth)/reset-password/page.tsx index 478ea777a..8474d054e 100644 --- a/apps/sim/app/(auth)/reset-password/page.tsx +++ b/apps/sim/app/(auth)/reset-password/page.tsx @@ -24,7 +24,6 @@ function ResetPasswordContent() { text: '', }) - // Validate token presence useEffect(() => { if (!token) { setStatusMessage({ @@ -60,7 +59,6 @@ function ResetPasswordContent() { text: 'Password reset successful! Redirecting to login...', }) - // Redirect to login page after 1.5 seconds setTimeout(() => { router.push('/login?resetSuccess=true') }, 1500) 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 ff6e39f45..fe1377e1b 100644 --- a/apps/sim/app/(auth)/reset-password/reset-password-form.tsx +++ b/apps/sim/app/(auth)/reset-password/reset-password-form.tsx @@ -30,12 +30,10 @@ export function RequestResetForm({ const [buttonClass, setButtonClass] = useState('auth-button-gradient') useEffect(() => { - // Check if CSS variable has been customized const checkCustomBrand = () => { const computedStyle = getComputedStyle(document.documentElement) const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim() - // Check if the CSS variable exists and is different from the default if (brandAccent && brandAccent !== '#6f3dfa') { setButtonClass('auth-button-custom') } else { @@ -45,7 +43,6 @@ export function RequestResetForm({ checkCustomBrand() - // Also check on window resize or theme changes window.addEventListener('resize', checkCustomBrand) const observer = new MutationObserver(checkCustomBrand) observer.observe(document.documentElement, { @@ -132,12 +129,10 @@ export function SetNewPasswordForm({ const [buttonClass, setButtonClass] = useState('auth-button-gradient') useEffect(() => { - // Check if CSS variable has been customized const checkCustomBrand = () => { const computedStyle = getComputedStyle(document.documentElement) const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim() - // Check if the CSS variable exists and is different from the default if (brandAccent && brandAccent !== '#6f3dfa') { setButtonClass('auth-button-custom') } else { @@ -147,7 +142,6 @@ export function SetNewPasswordForm({ checkCustomBrand() - // Also check on window resize or theme changes window.addEventListener('resize', checkCustomBrand) const observer = new MutationObserver(checkCustomBrand) observer.observe(document.documentElement, { @@ -164,7 +158,6 @@ export function SetNewPasswordForm({ const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() - // Simple validation if (password.length < 8) { setValidationMessage('Password must be at least 8 characters long') return diff --git a/apps/sim/app/(auth)/signup/page.tsx b/apps/sim/app/(auth)/signup/page.tsx index 3fededdd1..6e0734a1f 100644 --- a/apps/sim/app/(auth)/signup/page.tsx +++ b/apps/sim/app/(auth)/signup/page.tsx @@ -2,7 +2,6 @@ import { env, isTruthy } from '@/lib/env' import { getOAuthProviderStatus } from '@/app/(auth)/components/oauth-provider-checker' import SignupForm from '@/app/(auth)/signup/signup-form' -// Force dynamic rendering to avoid prerender errors with search params export const dynamic = 'force-dynamic' export default async function SignupPage() { diff --git a/apps/sim/app/(auth)/signup/signup-form.tsx b/apps/sim/app/(auth)/signup/signup-form.tsx index f72a40558..0dbba3025 100644 --- a/apps/sim/app/(auth)/signup/signup-form.tsx +++ b/apps/sim/app/(auth)/signup/signup-form.tsx @@ -95,7 +95,6 @@ function SignupFormContent({ const [isInviteFlow, setIsInviteFlow] = useState(false) const [buttonClass, setButtonClass] = useState('auth-button-gradient') - // Name validation state const [name, setName] = useState('') const [nameErrors, setNameErrors] = useState([]) const [showNameValidationError, setShowNameValidationError] = useState(false) @@ -107,29 +106,24 @@ function SignupFormContent({ setEmail(emailParam) } - // Handle redirection for invitation flow const redirectParam = searchParams.get('redirect') if (redirectParam) { setRedirectUrl(redirectParam) - // Check if this is part of an invitation flow if (redirectParam.startsWith('/invite/')) { setIsInviteFlow(true) } } - // Explicitly check for invite_flow parameter const inviteFlowParam = searchParams.get('invite_flow') if (inviteFlowParam === 'true') { setIsInviteFlow(true) } - // Check if CSS variable has been customized const checkCustomBrand = () => { const computedStyle = getComputedStyle(document.documentElement) const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim() - // Check if the CSS variable exists and is different from the default if (brandAccent && brandAccent !== '#6f3dfa') { setButtonClass('auth-button-custom') } else { @@ -139,7 +133,6 @@ function SignupFormContent({ checkCustomBrand() - // Also check on window resize or theme changes window.addEventListener('resize', checkCustomBrand) const observer = new MutationObserver(checkCustomBrand) observer.observe(document.documentElement, { @@ -153,7 +146,6 @@ function SignupFormContent({ } }, [searchParams]) - // Validate password and return array of error messages const validatePassword = (passwordValue: string): string[] => { const errors: string[] = [] @@ -180,18 +172,17 @@ function SignupFormContent({ return errors } - // Validate name and return array of error messages const validateName = (nameValue: string): string[] => { const errors: string[] = [] if (!NAME_VALIDATIONS.required.test(nameValue)) { errors.push(NAME_VALIDATIONS.required.message) - return errors // Return early for required field + return errors } if (!NAME_VALIDATIONS.notEmpty.test(nameValue)) { errors.push(NAME_VALIDATIONS.notEmpty.message) - return errors // Return early for empty field + return errors } if (!NAME_VALIDATIONS.validCharacters.regex.test(nameValue.trim())) { @@ -209,7 +200,6 @@ function SignupFormContent({ const newPassword = e.target.value setPassword(newPassword) - // Silently validate but don't show errors const errors = validatePassword(newPassword) setPasswordErrors(errors) setShowValidationError(false) @@ -228,12 +218,10 @@ function SignupFormContent({ const newEmail = e.target.value setEmail(newEmail) - // Silently validate but don't show errors until submit const errors = validateEmailField(newEmail) setEmailErrors(errors) setShowEmailValidationError(false) - // Clear any previous server-side email errors when the user starts typing if (emailError) { setEmailError('') } @@ -244,7 +232,8 @@ function SignupFormContent({ setIsLoading(true) const formData = new FormData(e.currentTarget) - const emailValue = formData.get('email') as string + const emailValueRaw = formData.get('email') as string + const emailValue = emailValueRaw.trim().toLowerCase() const passwordValue = formData.get('password') as string const nameValue = formData.get('name') as string @@ -348,7 +337,6 @@ function SignupFormContent({ return } - // Refresh session to get the new user data immediately after signup try { await refetchSession() logger.info('Session refreshed after successful signup') @@ -356,34 +344,23 @@ function SignupFormContent({ logger.error('Failed to refresh session after signup:', sessionError) } - // For new signups, always require verification if (typeof window !== 'undefined') { sessionStorage.setItem('verificationEmail', emailValue) - localStorage.setItem('has_logged_in_before', 'true') - - // Set cookie flag for middleware check - document.cookie = 'requiresEmailVerification=true; path=/; max-age=900; SameSite=Lax' // 15 min expiry - document.cookie = 'has_logged_in_before=true; path=/; max-age=31536000; SameSite=Lax' - - // Store invitation flow state if applicable if (isInviteFlow && redirectUrl) { sessionStorage.setItem('inviteRedirectUrl', redirectUrl) sessionStorage.setItem('isInviteFlow', 'true') } } - // Send verification OTP manually try { await client.emailOtp.sendVerificationOtp({ email: emailValue, - type: 'email-verification', + type: 'sign-in', }) - } catch (otpError) { - logger.error('Failed to send OTP:', otpError) - // Continue anyway - user can use resend button + } catch (otpErr) { + logger.warn('Failed to send sign-in OTP after signup; user can press Resend', otpErr) } - // Always redirect to verification for new signups router.push('/verify?fromSignup=true') } catch (error) { logger.error('Signup error:', error) diff --git a/apps/sim/app/(auth)/verify/page.tsx b/apps/sim/app/(auth)/verify/page.tsx index 55c185675..7e169b876 100644 --- a/apps/sim/app/(auth)/verify/page.tsx +++ b/apps/sim/app/(auth)/verify/page.tsx @@ -1,14 +1,11 @@ import { env } from '@/lib/env' import { isProd } from '@/lib/environment' -import { getBaseUrl } from '@/lib/urls/utils' import { VerifyContent } from '@/app/(auth)/verify/verify-content' -// Force dynamic rendering to avoid prerender errors with search params export const dynamic = 'force-dynamic' export default function VerifyPage() { - const baseUrl = getBaseUrl() - const hasResendKey = Boolean(env.RESEND_API_KEY && env.RESEND_API_KEY !== 'placeholder') + const hasResendKey = Boolean(env.RESEND_API_KEY) - return + return } diff --git a/apps/sim/app/(auth)/verify/use-verification.ts b/apps/sim/app/(auth)/verify/use-verification.ts index ecaf68036..3d4b1d32a 100644 --- a/apps/sim/app/(auth)/verify/use-verification.ts +++ b/apps/sim/app/(auth)/verify/use-verification.ts @@ -3,7 +3,6 @@ import { useEffect, useState } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import { client, useSession } from '@/lib/auth-client' -import { env, isTruthy } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' const logger = createLogger('useVerification') @@ -47,61 +46,39 @@ export function useVerification({ useEffect(() => { if (typeof window !== 'undefined') { - // Get stored email const storedEmail = sessionStorage.getItem('verificationEmail') if (storedEmail) { setEmail(storedEmail) } - // Check for redirect information const storedRedirectUrl = sessionStorage.getItem('inviteRedirectUrl') if (storedRedirectUrl) { setRedirectUrl(storedRedirectUrl) } - // Check if this is an invite flow const storedIsInviteFlow = sessionStorage.getItem('isInviteFlow') if (storedIsInviteFlow === 'true') { setIsInviteFlow(true) } } - // Also check URL parameters for redirect information const redirectParam = searchParams.get('redirectAfter') if (redirectParam) { setRedirectUrl(redirectParam) } - // Check for invite_flow parameter const inviteFlowParam = searchParams.get('invite_flow') if (inviteFlowParam === 'true') { setIsInviteFlow(true) } }, [searchParams]) - // Send initial OTP code if this is the first load useEffect(() => { if (email && !isSendingInitialOtp && hasResendKey) { setIsSendingInitialOtp(true) - - // Only send verification OTP if we're coming from login page - // Skip this if coming from signup since the OTP is already sent - if (!searchParams.get('fromSignup')) { - client.emailOtp - .sendVerificationOtp({ - email, - type: 'email-verification', - }) - .then(() => {}) - .catch((error) => { - logger.error('Failed to send initial verification code:', error) - setErrorMessage('Failed to send verification code. Please use the resend button.') - }) - } } - }, [email, isSendingInitialOtp, searchParams, hasResendKey]) + }, [email, isSendingInitialOtp, hasResendKey]) - // Enable the verify button when all 6 digits are entered const isOtpComplete = otp.length === 6 async function verifyCode() { @@ -112,25 +89,24 @@ export function useVerification({ setErrorMessage('') try { - // Call the verification API with the OTP code - const response = await client.emailOtp.verifyEmail({ - email, + const normalizedEmail = email.trim().toLowerCase() + const response = await client.signIn.emailOtp({ + email: normalizedEmail, otp, }) - // Check if verification was successful if (response && !response.error) { setIsVerified(true) - // Clear verification requirements and session storage + try { + await refetchSession() + } catch (e) { + logger.warn('Failed to refetch session after verification', e) + } + if (typeof window !== 'undefined') { sessionStorage.removeItem('verificationEmail') - // Clear the verification requirement flag - document.cookie = - 'requiresEmailVerification=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT' - - // Also clear invite-related items if (isInviteFlow) { sessionStorage.removeItem('inviteRedirectUrl') sessionStorage.removeItem('isInviteFlow') @@ -139,24 +115,20 @@ export function useVerification({ setTimeout(() => { if (isInviteFlow && redirectUrl) { - // For invitation flow, redirect to the invitation page window.location.href = redirectUrl } else { - // Default redirect to dashboard window.location.href = '/workspace' } }, 1000) } else { logger.info('Setting invalid OTP state - API error response') const message = 'Invalid verification code. Please check and try again.' - // Set both state variables to ensure the error shows setIsInvalidOtp(true) setErrorMessage(message) logger.info('Error state after API error:', { isInvalidOtp: true, errorMessage: message, }) - // Clear the OTP input on invalid code setOtp('') } } catch (error: any) { @@ -171,7 +143,6 @@ export function useVerification({ message = 'Too many failed attempts. Please request a new code.' } - // Set both state variables to ensure the error shows setIsInvalidOtp(true) setErrorMessage(message) logger.info('Error state after caught error:', { @@ -179,7 +150,6 @@ export function useVerification({ errorMessage: message, }) - // Clear the OTP input on error setOtp('') } finally { setIsLoading(false) @@ -192,10 +162,11 @@ export function useVerification({ setIsLoading(true) setErrorMessage('') + const normalizedEmail = email.trim().toLowerCase() client.emailOtp .sendVerificationOtp({ - email, - type: 'email-verification', + email: normalizedEmail, + type: 'sign-in', }) .then(() => {}) .catch(() => { @@ -207,7 +178,6 @@ export function useVerification({ } function handleOtpChange(value: string) { - // Only clear error when user is actively typing a new code if (value.length === 6) { setIsInvalidOtp(false) setErrorMessage('') @@ -215,12 +185,11 @@ export function useVerification({ setOtp(value) } - // Auto-submit when OTP is complete useEffect(() => { if (otp.length === 6 && email && !isLoading && !isVerified) { const timeoutId = setTimeout(() => { verifyCode() - }, 300) // Small delay to ensure UI is ready + }, 300) return () => clearTimeout(timeoutId) } @@ -229,17 +198,8 @@ export function useVerification({ useEffect(() => { if (typeof window !== 'undefined') { if (!isProduction || !hasResendKey) { - const storedEmail = sessionStorage.getItem('verificationEmail') - } - - const isDevOrDocker = !isProduction || isTruthy(env.DOCKER_BUILD) - - if (isDevOrDocker || !hasResendKey) { setIsVerified(true) - document.cookie = - 'requiresEmailVerification=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT' - const timeoutId = setTimeout(() => { window.location.href = '/workspace' }, 1000) diff --git a/apps/sim/app/(auth)/verify/verify-content.tsx b/apps/sim/app/(auth)/verify/verify-content.tsx index b6dd4e6c5..c402bd901 100644 --- a/apps/sim/app/(auth)/verify/verify-content.tsx +++ b/apps/sim/app/(auth)/verify/verify-content.tsx @@ -11,7 +11,6 @@ import { soehne } from '@/app/fonts/soehne/soehne' interface VerifyContentProps { hasResendKey: boolean - baseUrl: string isProduction: boolean } @@ -56,30 +55,13 @@ function VerificationForm({ setCountdown(30) } - const handleCancelVerification = () => { - // Clear verification data - if (typeof window !== 'undefined') { - sessionStorage.removeItem('verificationEmail') - sessionStorage.removeItem('inviteRedirectUrl') - sessionStorage.removeItem('isInviteFlow') - - // Clear the verification requirement cookie - document.cookie = 'requiresEmailVerification=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT' - } - - // Redirect to login - router.push('/login') - } - const [buttonClass, setButtonClass] = useState('auth-button-gradient') useEffect(() => { - // Check if CSS variable has been customized const checkCustomBrand = () => { const computedStyle = getComputedStyle(document.documentElement) const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim() - // Check if the CSS variable exists and is different from the default if (brandAccent && brandAccent !== '#6f3dfa') { setButtonClass('auth-button-custom') } else { @@ -89,7 +71,6 @@ function VerificationForm({ checkCustomBrand() - // Also check on window resize or theme changes window.addEventListener('resize', checkCustomBrand) const observer = new MutationObserver(checkCustomBrand) observer.observe(document.documentElement, { @@ -232,21 +213,27 @@ function VerificationForm({ )} - {/*
+
-
*/} +
)} ) } -// Fallback component while the verification form is loading function VerificationFormFallback() { return (
@@ -258,7 +245,7 @@ function VerificationFormFallback() { ) } -export function VerifyContent({ hasResendKey, baseUrl, isProduction }: VerifyContentProps) { +export function VerifyContent({ hasResendKey, isProduction }: VerifyContentProps) { return ( }> diff --git a/apps/sim/lib/auth.ts b/apps/sim/lib/auth.ts index 6140a50aa..8ffdd3de4 100644 --- a/apps/sim/lib/auth.ts +++ b/apps/sim/lib/auth.ts @@ -147,7 +147,7 @@ export const auth = betterAuth({ }, emailAndPassword: { enabled: true, - requireEmailVerification: false, + requireEmailVerification: isProd, sendVerificationOnSignUp: false, throwOnMissingCredentials: true, throwOnInvalidCredentials: true, @@ -174,7 +174,6 @@ export const auth = betterAuth({ if (ctx.path.startsWith('/sign-up') && isTruthy(env.DISABLE_REGISTRATION)) throw new Error('Registration is disabled, please contact your admin.') - // Check email and domain whitelist for sign-in and sign-up if ( (ctx.path.startsWith('/sign-in') || ctx.path.startsWith('/sign-up')) && (env.ALLOWED_LOGIN_EMAILS || env.ALLOWED_LOGIN_DOMAINS) @@ -184,7 +183,6 @@ export const auth = betterAuth({ if (requestEmail) { let isAllowed = false - // Check specific email whitelist if (env.ALLOWED_LOGIN_EMAILS) { const allowedEmails = env.ALLOWED_LOGIN_EMAILS.split(',').map((email) => email.trim().toLowerCase() @@ -192,7 +190,6 @@ export const auth = betterAuth({ isAllowed = allowedEmails.includes(requestEmail) } - // Check domain whitelist if not already allowed if (!isAllowed && env.ALLOWED_LOGIN_DOMAINS) { const allowedDomains = env.ALLOWED_LOGIN_DOMAINS.split(',').map((domain) => domain.trim().toLowerCase() @@ -234,7 +231,6 @@ export const auth = betterAuth({ throw new Error('Email is required') } - // Validate email before sending OTP const validation = quickValidateEmail(data.email) if (!validation.isValid) { logger.warn('Email validation failed', { @@ -250,7 +246,6 @@ export const auth = betterAuth({ const html = await renderOTPEmail(data.otp, data.email, data.type) - // Send email via consolidated mailer (supports Resend, Azure, or logging fallback) const result = await sendEmail({ to: data.email, subject: getEmailSubject(data.type), @@ -259,7 +254,6 @@ export const auth = betterAuth({ emailType: 'transactional', }) - // If no email service is configured, log verification code for development if (!result.success && result.message.includes('no email service configured')) { logger.info('🔑 VERIFICATION CODE FOR LOGIN/SIGNUP', { email: data.email, @@ -300,7 +294,6 @@ export const auth = betterAuth({ redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/github-repo`, getUserInfo: async (tokens) => { try { - // Fetch user profile const profileResponse = await fetch('https://api.github.com/user', { headers: { Authorization: `Bearer ${tokens.accessToken}`, @@ -318,7 +311,6 @@ export const auth = betterAuth({ const profile = await profileResponse.json() - // If email is null, fetch emails separately if (!profile.email) { const emailsResponse = await fetch('https://api.github.com/user/emails', { headers: { @@ -330,7 +322,6 @@ export const auth = betterAuth({ if (emailsResponse.ok) { const emails = await emailsResponse.json() - // Find primary email or use the first one const primaryEmail = emails.find( (email: { primary: boolean; email: string; verified: boolean }) => @@ -366,7 +357,7 @@ export const auth = betterAuth({ }, }, - // Google providers for different purposes + // Google providers { providerId: 'google-email', clientId: env.GOOGLE_CLIENT_ID as string, @@ -378,7 +369,6 @@ export const auth = betterAuth({ 'https://www.googleapis.com/auth/userinfo.profile', 'https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/gmail.modify', - // 'https://www.googleapis.com/auth/gmail.readonly', 'https://www.googleapis.com/auth/gmail.labels', ], prompt: 'consent', @@ -598,11 +588,9 @@ export const auth = betterAuth({ try { logger.info('Creating Wealthbox user profile from token data') - // Generate a unique identifier since we can't fetch user info const uniqueId = `wealthbox-${Date.now()}` const now = new Date() - // Create a synthetic user profile return { id: uniqueId, name: 'Wealthbox User', @@ -625,8 +613,6 @@ export const auth = betterAuth({ clientSecret: env.SUPABASE_CLIENT_SECRET as string, authorizationUrl: 'https://api.supabase.com/v1/oauth/authorize', tokenUrl: 'https://api.supabase.com/v1/oauth/token', - // Supabase doesn't have a standard userInfo endpoint that works with our flow, - // so we use a dummy URL and rely on our custom getUserInfo implementation userInfoUrl: 'https://dummy-not-used.supabase.co', scopes: ['database.read', 'database.write', 'projects.read'], responseType: 'code', @@ -636,11 +622,9 @@ export const auth = betterAuth({ try { logger.info('Creating Supabase user profile from token data') - // Extract user identifier from tokens if possible let userId = 'supabase-user' if (tokens.idToken) { try { - // Try to decode the JWT to get user information const decodedToken = JSON.parse( Buffer.from(tokens.idToken.split('.')[1], 'base64').toString() ) @@ -654,12 +638,9 @@ export const auth = betterAuth({ } } - // Generate a unique enough identifier const uniqueId = `${userId}-${Date.now()}` - const now = new Date() - // Create a synthetic user profile since we can't fetch one return { id: uniqueId, name: 'Supabase User', @@ -721,7 +702,7 @@ export const auth = betterAuth({ return { id: profile.data.id, name: profile.data.name || 'X User', - email: `${profile.data.username}@x.com`, // Create synthetic email with username + email: `${profile.data.username}@x.com`, image: profile.data.profile_image_url, emailVerified: profile.data.verified || false, createdAt: now, @@ -774,7 +755,7 @@ export const auth = betterAuth({ name: profile.name || profile.display_name || 'Confluence User', email: profile.email || `${profile.account_id}@atlassian.com`, image: profile.picture || undefined, - emailVerified: true, // Assume verified since it's an Atlassian account + emailVerified: true, createdAt: now, updatedAt: now, } @@ -895,7 +876,7 @@ export const auth = betterAuth({ name: profile.name || profile.display_name || 'Jira User', email: profile.email || `${profile.account_id}@atlassian.com`, image: profile.picture || undefined, - emailVerified: true, // Assume verified since it's an Atlassian account + emailVerified: true, createdAt: now, updatedAt: now, } @@ -933,7 +914,7 @@ export const auth = betterAuth({ userInfoUrl: 'https://api.notion.com/v1/users/me', scopes: ['workspace.content', 'workspace.name', 'page.read', 'page.write'], responseType: 'code', - pkce: false, // Notion doesn't support PKCE + pkce: false, accessType: 'offline', authentication: 'basic', prompt: 'consent', @@ -943,7 +924,7 @@ export const auth = betterAuth({ const response = await fetch('https://api.notion.com/v1/users/me', { headers: { Authorization: `Bearer ${tokens.accessToken}`, - 'Notion-Version': '2022-06-28', // Specify the Notion API version + 'Notion-Version': '2022-06-28', }, }) @@ -1011,7 +992,7 @@ export const auth = betterAuth({ return { id: data.id, name: data.name || 'Reddit User', - email: `${data.name}@reddit.user`, // Reddit doesn't provide email in identity scope + email: `${data.name}@reddit.user`, image: data.icon_img || undefined, emailVerified: false, createdAt: now, @@ -1128,7 +1109,6 @@ export const auth = betterAuth({ let userId = 'slack-bot' if (tokens.idToken) { try { - // Try to decode the JWT to get user information const decodedToken = JSON.parse( Buffer.from(tokens.idToken.split('.')[1], 'base64').toString() ) @@ -1140,12 +1120,9 @@ export const auth = betterAuth({ } } - // Generate a unique enough identifier const uniqueId = `${userId}-${Date.now()}` - const now = new Date() - // Create a synthetic user profile since we can't fetch one return { id: uniqueId, name: 'Slack Bot', @@ -1230,7 +1207,6 @@ export const auth = betterAuth({ status: subscription.status, }) - // Sync usage limits for the new subscription try { await syncSubscriptionUsageLimits(subscription) } catch (error) { @@ -1241,7 +1217,6 @@ export const auth = betterAuth({ }) } - // Send welcome email for Pro and Team plans try { const { sendPlanWelcomeEmail } = await import('@/lib/billing') await sendPlanWelcomeEmail(subscription) @@ -1286,9 +1261,7 @@ export const auth = betterAuth({ referenceId: subscription.referenceId, }) - // Reset usage limits back to free tier defaults try { - // This will sync limits based on the now-inactive subscription (defaulting to free tier) await syncSubscriptionUsageLimits(subscription) logger.info('[onSubscriptionDeleted] Reset usage limits to free tier', { @@ -1311,7 +1284,6 @@ export const auth = betterAuth({ }) try { - // Handle invoice events switch (event.type) { case 'invoice.payment_succeeded': { await handleInvoicePaymentSucceeded(event) @@ -1347,13 +1319,11 @@ export const auth = betterAuth({ eventType: event.type, error, }) - throw error // Re-throw to signal webhook failure to Stripe + throw error } }, }), - // Add organization plugin as a separate entry in the plugins array organization({ - // Allow team plan subscribers to create organizations allowUserToCreateOrganization: async (user) => { const dbSubscriptions = await db .select() @@ -1457,11 +1427,9 @@ export const auth = betterAuth({ signUp: '/signup', error: '/error', verify: '/verify', - verifyRequest: '/verify-request', }, }) -// Server-side auth helpers export async function getSession() { const hdrs = await headers() return await auth.api.getSession({ diff --git a/apps/sim/middleware.ts b/apps/sim/middleware.ts index a4b330b12..de4a80004 100644 --- a/apps/sim/middleware.ts +++ b/apps/sim/middleware.ts @@ -138,12 +138,7 @@ export async function middleware(request: NextRequest) { return NextResponse.redirect(new URL('/login', request.url)) } - // Check if user needs email verification - const requiresVerification = request.cookies.get('requiresEmailVerification') - if (requiresVerification?.value === 'true') { - return NextResponse.redirect(new URL('/verify', request.url)) - } - + // Email verification is enforced by Better Auth (server-side). No cookie gating here. return NextResponse.next() } diff --git a/docker-compose.local.yml b/docker-compose.local.yml index f9fe6d599..8e55a4392 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -15,11 +15,6 @@ services: - NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000} - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET:-your_auth_secret_here} - ENCRYPTION_KEY=${ENCRYPTION_KEY:-your_encryption_key_here} - - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-placeholder} - - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-placeholder} - - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-placeholder} - - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-placeholder} - - RESEND_API_KEY=${RESEND_API_KEY:-placeholder} - OLLAMA_URL=${OLLAMA_URL:-http://localhost:11434} - NEXT_PUBLIC_SOCKET_URL=${NEXT_PUBLIC_SOCKET_URL:-http://localhost:3002} depends_on: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 5bb40e252..c41563da1 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -14,11 +14,6 @@ services: - NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000} - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET:-your_auth_secret_here} - ENCRYPTION_KEY=${ENCRYPTION_KEY:-your_encryption_key_here} - - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-placeholder} - - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-placeholder} - - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-placeholder} - - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-placeholder} - - RESEND_API_KEY=${RESEND_API_KEY:-placeholder} - OLLAMA_URL=${OLLAMA_URL:-http://localhost:11434} - SOCKET_SERVER_URL=${SOCKET_SERVER_URL:-http://localhost:3002} - NEXT_PUBLIC_SOCKET_URL=${NEXT_PUBLIC_SOCKET_URL:-http://localhost:3002}