Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 5 additions & 2 deletions apps/sim/app/(auth)/components/social-login-buttons.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useEffect, useState } from 'react'
import { type ReactNode, useEffect, useState } from 'react'
import { GithubIcon, GoogleIcon } from '@/components/icons'
import { Button } from '@/components/ui/button'
import { client } from '@/lib/auth-client'
Expand All @@ -11,13 +11,15 @@ interface SocialLoginButtonsProps {
googleAvailable: boolean
callbackURL?: string
isProduction: boolean
children?: ReactNode
}

export function SocialLoginButtons({
githubAvailable,
googleAvailable,
callbackURL = '/workspace',
isProduction,
children,
}: SocialLoginButtonsProps) {
const [isGithubLoading, setIsGithubLoading] = useState(false)
const [isGoogleLoading, setIsGoogleLoading] = useState(false)
Expand Down Expand Up @@ -103,14 +105,15 @@ export function SocialLoginButtons({

const hasAnyOAuthProvider = githubAvailable || googleAvailable

if (!hasAnyOAuthProvider) {
if (!hasAnyOAuthProvider && !children) {
return null
}

return (
<div className={`${inter.className} grid gap-3 font-light`}>
{googleAvailable && googleButton}
{githubAvailable && githubButton}
{children}
</div>
)
}
53 changes: 53 additions & 0 deletions apps/sim/app/(auth)/components/sso-login-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use client'

import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { env, isTruthy } from '@/lib/env'
import { cn } from '@/lib/utils'

interface SSOLoginButtonProps {
callbackURL?: string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider using callbackUrl for consistency with Next.js conventions and other auth components

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/sim/app/(auth)/components/sso-login-button.tsx
Line: 9:9

Comment:
style: Consider using `callbackUrl` for consistency with Next.js conventions and other auth components

How can I resolve this? If you propose a fix, please make it concise.

className?: string
// Visual variant for button styling and placement contexts
// - 'primary' matches the main auth action button style
// - 'outline' matches social provider buttons
variant?: 'primary' | 'outline'
// Optional class used when variant is primary to match brand/gradient
primaryClassName?: string
}

export function SSOLoginButton({
callbackURL,
className,
variant = 'outline',
primaryClassName,
}: SSOLoginButtonProps) {
const router = useRouter()

if (!isTruthy(env.NEXT_PUBLIC_SSO_ENABLED)) {
return null
}

const handleSSOClick = () => {
const ssoUrl = `/sso${callbackURL ? `?callbackUrl=${encodeURIComponent(callbackURL)}` : ''}`
router.push(ssoUrl)
}

const primaryBtnClasses = cn(
primaryClassName || 'auth-button-gradient',
'flex w-full items-center justify-center gap-2 rounded-[10px] border font-medium text-[15px] text-white transition-all duration-200'
)

const outlineBtnClasses = cn('w-full rounded-[10px] shadow-sm hover:bg-gray-50')

return (
<Button
type='button'
onClick={handleSSOClick}
variant={variant === 'outline' ? 'outline' : undefined}
className={cn(variant === 'outline' ? outlineBtnClasses : primaryBtnClasses, className)}
>
Sign in with SSO
</Button>
)
}
222 changes: 131 additions & 91 deletions apps/sim/app/(auth)/login/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { client } from '@/lib/auth-client'
import { quickValidateEmail } from '@/lib/email/validation'
import { env, isFalsy, isTruthy } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils'
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
import { inter } from '@/app/fonts/inter'
import { soehne } from '@/app/fonts/soehne/soehne'

Expand Down Expand Up @@ -365,6 +367,14 @@ export default function LoginPage({
}
}

const ssoEnabled = isTruthy(env.NEXT_PUBLIC_SSO_ENABLED)
const emailEnabled = !isFalsy(env.NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED)
const hasSocial = githubAvailable || googleAvailable
const hasOnlySSO = ssoEnabled && !emailEnabled && !hasSocial
const showTopSSO = hasOnlySSO
const showBottomSection = hasSocial || (ssoEnabled && !hasOnlySSO)
const showDivider = (emailEnabled || showTopSSO) && showBottomSection

return (
<>
<div className='space-y-1 text-center'>
Expand All @@ -376,96 +386,111 @@ export default function LoginPage({
</p>
</div>

<form onSubmit={onSubmit} className={`${inter.className} mt-8 space-y-8`}>
<div className='space-y-6'>
<div className='space-y-2'>
<div className='flex items-center justify-between'>
<Label htmlFor='email'>Email</Label>
</div>
<Input
id='email'
name='email'
placeholder='Enter your email'
required
autoCapitalize='none'
autoComplete='email'
autoCorrect='off'
value={email}
onChange={handleEmailChange}
className={cn(
'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
showEmailValidationError &&
emailErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showEmailValidationError && emailErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{emailErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
{/* SSO Login Button (primary top-only when it is the only method) */}
{showTopSSO && (
<div className={`${inter.className} mt-8`}>
<SSOLoginButton
callbackURL={callbackUrl}
variant='primary'
primaryClassName={buttonClass}
/>
</div>
)}

{/* Email/Password Form - show unless explicitly disabled */}
{!isFalsy(env.NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED) && (
<form onSubmit={onSubmit} className={`${inter.className} mt-8 space-y-8`}>
<div className='space-y-6'>
<div className='space-y-2'>
<div className='flex items-center justify-between'>
<Label htmlFor='email'>Email</Label>
</div>
)}
</div>
<div className='space-y-2'>
<div className='flex items-center justify-between'>
<Label htmlFor='password'>Password</Label>
<button
type='button'
onClick={() => setForgotPasswordOpen(true)}
className='font-medium text-muted-foreground text-xs transition hover:text-foreground'
>
Forgot password?
</button>
</div>
<div className='relative'>
<Input
id='password'
name='password'
id='email'
name='email'
placeholder='Enter your email'
required
type={showPassword ? 'text' : 'password'}
autoCapitalize='none'
autoComplete='current-password'
autoComplete='email'
autoCorrect='off'
placeholder='Enter your password'
value={password}
onChange={handlePasswordChange}
value={email}
onChange={handleEmailChange}
className={cn(
'rounded-[10px] pr-10 shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
showValidationError &&
passwordErrors.length > 0 &&
'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
showEmailValidationError &&
emailErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
<button
type='button'
onClick={() => setShowPassword(!showPassword)}
className='-translate-y-1/2 absolute top-1/2 right-3 text-gray-500 transition hover:text-gray-700'
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
{showEmailValidationError && emailErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{emailErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
{showValidationError && passwordErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{passwordErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
<div className='space-y-2'>
<div className='flex items-center justify-between'>
<Label htmlFor='password'>Password</Label>
<button
type='button'
onClick={() => setForgotPasswordOpen(true)}
className='font-medium text-muted-foreground text-xs transition hover:text-foreground'
>
Forgot password?
</button>
</div>
)}
<div className='relative'>
<Input
id='password'
name='password'
required
type={showPassword ? 'text' : 'password'}
autoCapitalize='none'
autoComplete='current-password'
autoCorrect='off'
placeholder='Enter your password'
value={password}
onChange={handlePasswordChange}
className={cn(
'rounded-[10px] pr-10 shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
showValidationError &&
passwordErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
<button
type='button'
onClick={() => setShowPassword(!showPassword)}
className='-translate-y-1/2 absolute top-1/2 right-3 text-gray-500 transition hover:text-gray-700'
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
{showValidationError && passwordErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{passwordErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
</div>
</div>

<Button
type='submit'
className={`${buttonClass} flex w-full items-center justify-center gap-2 rounded-[10px] border font-medium text-[15px] text-white transition-all duration-200`}
disabled={isLoading}
>
{isLoading ? 'Signing in...' : 'Sign in'}
</Button>
</form>
<Button
type='submit'
className={`${buttonClass} flex w-full items-center justify-center gap-2 rounded-[10px] border font-medium text-[15px] text-white transition-all duration-200`}
disabled={isLoading}
>
{isLoading ? 'Signing in...' : 'Sign in'}
</Button>
</form>
)}

{(githubAvailable || googleAvailable) && (
{/* Divider - show when we have multiple auth methods */}
{showDivider && (
<div className={`${inter.className} relative my-6 font-light`}>
<div className='absolute inset-0 flex items-center'>
<div className='auth-divider w-full border-t' />
Expand All @@ -476,22 +501,37 @@ export default function LoginPage({
</div>
)}

<SocialLoginButtons
googleAvailable={googleAvailable}
githubAvailable={githubAvailable}
isProduction={isProduction}
callbackURL={callbackUrl}
/>
{showBottomSection && (
<div className={cn(inter.className, !emailEnabled ? 'mt-8' : undefined)}>
<SocialLoginButtons
googleAvailable={googleAvailable}
githubAvailable={githubAvailable}
isProduction={isProduction}
callbackURL={callbackUrl}
>
{ssoEnabled && !hasOnlySSO && (
<SSOLoginButton
callbackURL={callbackUrl}
variant='outline'
primaryClassName={buttonClass}
/>
)}
</SocialLoginButtons>
</div>
)}

<div className={`${inter.className} pt-6 text-center font-light text-[14px]`}>
<span className='font-normal'>Don't have an account? </span>
<Link
href={isInviteFlow ? `/signup?invite_flow=true&callbackUrl=${callbackUrl}` : '/signup'}
className='font-medium text-[var(--brand-accent-hex)] underline-offset-4 transition hover:text-[var(--brand-accent-hover-hex)] hover:underline'
>
Sign up
</Link>
</div>
{/* Only show signup link if email/password signup is enabled */}
{!isFalsy(env.NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED) && (
<div className={`${inter.className} pt-6 text-center font-light text-[14px]`}>
<span className='font-normal'>Don't have an account? </span>
<Link
href={isInviteFlow ? `/signup?invite_flow=true&callbackUrl=${callbackUrl}` : '/signup'}
className='font-medium text-[var(--brand-accent-hex)] underline-offset-4 transition hover:text-[var(--brand-accent-hover-hex)] hover:underline'
>
Sign up
</Link>
</div>
)}

<div
className={`${inter.className} auth-text-muted absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[13px] leading-relaxed sm:px-8 md:px-[44px]`}
Expand Down
Loading