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
12 changes: 10 additions & 2 deletions apps/sim/app/(auth)/components/oauth-provider-checker.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { env } from '@/lib/core/config/env'
import { isGithubAuthDisabled, isGoogleAuthDisabled, isProd } from '@/lib/core/config/feature-flags'
import {
isGithubAuthDisabled,
isGoogleAuthDisabled,
isMicrosoftAuthDisabled,
isProd,
} from '@/lib/core/config/feature-flags'

export async function getOAuthProviderStatus() {
const githubAvailable =
Expand All @@ -8,5 +13,8 @@ export async function getOAuthProviderStatus() {
const googleAvailable =
!!(env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) && !isGoogleAuthDisabled

return { githubAvailable, googleAvailable, isProduction: isProd }
const microsoftAvailable =
!!(env.MICROSOFT_CLIENT_ID && env.MICROSOFT_CLIENT_SECRET) && !isMicrosoftAuthDisabled

return { githubAvailable, googleAvailable, microsoftAvailable, isProduction: isProd }
}
43 changes: 41 additions & 2 deletions apps/sim/app/(auth)/components/social-login-buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import { type ReactNode, useState } from 'react'
import { Button } from '@/components/emcn'
import { GithubIcon, GoogleIcon } from '@/components/icons'
import { GithubIcon, GoogleIcon, MicrosoftIcon } from '@/components/icons'
import { client } from '@/lib/auth/auth-client'

interface SocialLoginButtonsProps {
githubAvailable: boolean
googleAvailable: boolean
microsoftAvailable: boolean
callbackURL?: string
isProduction: boolean
children?: ReactNode
Expand All @@ -16,12 +17,14 @@ interface SocialLoginButtonsProps {
export function SocialLoginButtons({
githubAvailable,
googleAvailable,
microsoftAvailable,
callbackURL = '/workspace',
isProduction,
children,
}: SocialLoginButtonsProps) {
const [isGithubLoading, setIsGithubLoading] = useState(false)
const [isGoogleLoading, setIsGoogleLoading] = useState(false)
const [isMicrosoftLoading, setIsMicrosoftLoading] = useState(false)

async function signInWithGithub() {
if (!githubAvailable) return
Expand Down Expand Up @@ -69,6 +72,29 @@ export function SocialLoginButtons({
}
}

async function signInWithMicrosoft() {
if (!microsoftAvailable) return

setIsMicrosoftLoading(true)
try {
await client.signIn.social({ provider: 'microsoft', callbackURL })
} catch (err: any) {
let errorMessage = 'Failed to sign in with Microsoft'

if (err.message?.includes('account exists')) {
errorMessage = 'An account with this email already exists. Please sign in instead.'
} else if (err.message?.includes('cancelled')) {
errorMessage = 'Microsoft sign in was cancelled. Please try again.'
} else if (err.message?.includes('network')) {
errorMessage = 'Network error. Please check your connection and try again.'
} else if (err.message?.includes('rate limit')) {
errorMessage = 'Too many attempts. Please try again later.'
}
} finally {
setIsMicrosoftLoading(false)
}
}
Comment thread
TheodoreSpeaks marked this conversation as resolved.

const githubButton = (
<Button
variant='outline'
Expand All @@ -93,7 +119,19 @@ export function SocialLoginButtons({
</Button>
)

const hasAnyOAuthProvider = githubAvailable || googleAvailable
const microsoftButton = (
<Button
variant='outline'
className='w-full rounded-sm border-[var(--landing-border-strong)] py-1.5 text-sm'
disabled={!microsoftAvailable || isMicrosoftLoading}
onClick={signInWithMicrosoft}
>
<MicrosoftIcon className='!h-[18px] !w-[18px] mr-1' />
{isMicrosoftLoading ? 'Connecting...' : 'Microsoft'}
</Button>
)

const hasAnyOAuthProvider = githubAvailable || googleAvailable || microsoftAvailable

if (!hasAnyOAuthProvider && !children) {
return null
Expand All @@ -102,6 +140,7 @@ export function SocialLoginButtons({
return (
<div className='grid gap-3 font-light'>
{googleAvailable && googleButton}
{microsoftAvailable && microsoftButton}
{githubAvailable && githubButton}
{children}
</div>
Expand Down
5 changes: 4 additions & 1 deletion apps/sim/app/(auth)/login/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,12 @@ const validatePassword = (passwordValue: string): string[] => {
export default function LoginPage({
githubAvailable,
googleAvailable,
microsoftAvailable,
isProduction,
}: {
githubAvailable: boolean
googleAvailable: boolean
microsoftAvailable: boolean
isProduction: boolean
}) {
const router = useRouter()
Expand Down Expand Up @@ -335,7 +337,7 @@ export default function LoginPage({

const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
const emailEnabled = !isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED'))
const hasSocial = githubAvailable || googleAvailable
const hasSocial = githubAvailable || googleAvailable || microsoftAvailable
const hasOnlySSO = ssoEnabled && !emailEnabled && !hasSocial
const showTopSSO = hasOnlySSO
const showBottomSection = hasSocial || (ssoEnabled && !hasOnlySSO)
Expand Down Expand Up @@ -483,6 +485,7 @@ export default function LoginPage({
<div className={cn(!emailEnabled ? 'mt-8' : undefined)}>
<SocialLoginButtons
googleAvailable={googleAvailable}
microsoftAvailable={microsoftAvailable}
githubAvailable={githubAvailable}
isProduction={isProduction}
callbackURL={callbackUrl}
Expand Down
4 changes: 3 additions & 1 deletion apps/sim/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ export const metadata: Metadata = {
export const dynamic = 'force-dynamic'

export default async function LoginPage() {
const { githubAvailable, googleAvailable, isProduction } = await getOAuthProviderStatus()
const { githubAvailable, googleAvailable, microsoftAvailable, isProduction } =
await getOAuthProviderStatus()

return (
<Suspense fallback={null}>
<LoginForm
githubAvailable={githubAvailable}
googleAvailable={googleAvailable}
microsoftAvailable={microsoftAvailable}
isProduction={isProduction}
/>
</Suspense>
Expand Down
7 changes: 5 additions & 2 deletions apps/sim/app/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Metadata } from 'next'
import { isRegistrationDisabled } from '@/lib/core/config/feature-flags'
import { isEmailSignupDisabled, isRegistrationDisabled } from '@/lib/core/config/feature-flags'
import { getOAuthProviderStatus } from '@/app/(auth)/components/oauth-provider-checker'
import SignupForm from '@/app/(auth)/signup/signup-form'

Expand All @@ -14,13 +14,16 @@ export default async function SignupPage() {
return <div>Registration is disabled, please contact your admin.</div>
}

const { githubAvailable, googleAvailable, isProduction } = await getOAuthProviderStatus()
const { githubAvailable, googleAvailable, microsoftAvailable, isProduction } =
await getOAuthProviderStatus()

return (
<SignupForm
githubAvailable={githubAvailable}
googleAvailable={googleAvailable}
microsoftAvailable={microsoftAvailable}
isProduction={isProduction}
emailSignupEnabled={!isEmailSignupDisabled}
/>
)
}
63 changes: 28 additions & 35 deletions apps/sim/app/(auth)/signup/signup-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,18 @@ const validateEmailField = (emailValue: string): string[] => {
interface SignupFormProps {
githubAvailable: boolean
googleAvailable: boolean
microsoftAvailable: boolean
isProduction: boolean
emailSignupEnabled: boolean
}

function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: SignupFormProps) {
function SignupFormContent({
githubAvailable,
googleAvailable,
microsoftAvailable,
isProduction,
emailSignupEnabled,
}: SignupFormProps) {
const router = useRouter()
const searchParams = useSearchParams()
const { refetch: refetchSession } = useSession()
Expand Down Expand Up @@ -346,6 +354,14 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S
}
}

const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
const emailEnabled =
!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && emailSignupEnabled
const hasSocial = githubAvailable || googleAvailable || microsoftAvailable
const hasOnlySSO = ssoEnabled && !emailEnabled && !hasSocial
const showBottomSection = hasSocial || (ssoEnabled && !hasOnlySSO)
const showDivider = (emailEnabled || hasOnlySSO) && showBottomSection

return (
<>
<div className='space-y-1 text-center'>
Expand All @@ -357,21 +373,13 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S
</p>
</div>

{/* SSO Login Button (primary top-only when it is the only method) */}
{(() => {
const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
const emailEnabled = !isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED'))
const hasSocial = githubAvailable || googleAvailable
const hasOnlySSO = ssoEnabled && !emailEnabled && !hasSocial
return hasOnlySSO
})() && (
{hasOnlySSO && (
<div className='mt-8'>
<SSOLoginButton callbackURL={redirectUrl || '/workspace'} variant='primary' />
</div>
)}

{/* Email/Password Form - show unless explicitly disabled */}
{!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && (
{emailEnabled && (
<form onSubmit={onSubmit} className='mt-8 space-y-10'>
<div className='space-y-6'>
<div className='space-y-2'>
Expand Down Expand Up @@ -540,16 +548,7 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S
</form>
)}

{/* Divider - show when we have multiple auth methods */}
{(() => {
const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
const emailEnabled = !isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED'))
const hasSocial = githubAvailable || googleAvailable
const hasOnlySSO = ssoEnabled && !emailEnabled && !hasSocial
const showBottomSection = hasSocial || (ssoEnabled && !hasOnlySSO)
const showDivider = (emailEnabled || hasOnlySSO) && showBottomSection
return showDivider
})() && (
{showDivider && (
<div className='relative my-6 font-light'>
<div className='absolute inset-0 flex items-center'>
<div className='w-full border-[var(--landing-bg-elevated)] border-t' />
Expand All @@ -562,26 +561,16 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S
</div>
)}

{(() => {
const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
const emailEnabled = !isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED'))
const hasSocial = githubAvailable || googleAvailable
const hasOnlySSO = ssoEnabled && !emailEnabled && !hasSocial
const showBottomSection = hasSocial || (ssoEnabled && !hasOnlySSO)
return showBottomSection
})() && (
<div
className={cn(
isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) ? 'mt-8' : undefined
)}
>
{showBottomSection && (
<div className={cn(!emailEnabled ? 'mt-8' : undefined)}>
<SocialLoginButtons
githubAvailable={githubAvailable}
googleAvailable={googleAvailable}
microsoftAvailable={microsoftAvailable}
callbackURL={redirectUrl || '/workspace'}
isProduction={isProduction}
>
{isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED')) && (
{ssoEnabled && !hasOnlySSO && (
<SSOLoginButton callbackURL={redirectUrl || '/workspace'} variant='outline' />
)}
</SocialLoginButtons>
Expand Down Expand Up @@ -625,14 +614,18 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S
export default function SignupPage({
githubAvailable,
googleAvailable,
microsoftAvailable,
isProduction,
emailSignupEnabled,
}: SignupFormProps) {
return (
<Suspense fallback={<div className='flex h-screen items-center justify-center'>Loading…</div>}>
<SignupFormContent
githubAvailable={githubAvailable}
googleAvailable={googleAvailable}
microsoftAvailable={microsoftAvailable}
isProduction={isProduction}
emailSignupEnabled={emailSignupEnabled}
/>
</Suspense>
)
Expand Down
31 changes: 25 additions & 6 deletions apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
ModalTitle,
ModalTrigger,
} from '@/components/emcn'
import { GithubIcon, GoogleIcon } from '@/components/icons'
import { GithubIcon, GoogleIcon, MicrosoftIcon } from '@/components/icons'
import { requestJson } from '@/lib/api/client/request'
import { type AuthProviderStatusResponse, getAuthProvidersContract } from '@/lib/api/contracts/auth'
import { client } from '@/lib/auth/auth-client'
Expand All @@ -40,6 +40,7 @@ let fetchPromise: Promise<AuthProviderStatusResponse> | null = null
const FALLBACK_STATUS: ProviderStatus = {
githubAvailable: false,
googleAvailable: false,
microsoftAvailable: false,
registrationDisabled: false,
}

Expand All @@ -49,9 +50,10 @@ const SOCIAL_BTN =
function fetchProviderStatus(): Promise<ProviderStatus> {
if (fetchPromise) return fetchPromise
fetchPromise = requestJson(getAuthProvidersContract, {})
.then(({ githubAvailable, googleAvailable, registrationDisabled }) => ({
.then(({ githubAvailable, googleAvailable, microsoftAvailable, registrationDisabled }) => ({
githubAvailable,
googleAvailable,
microsoftAvailable,
registrationDisabled,
}))
.catch(() => {
Expand All @@ -66,14 +68,17 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal
const [open, setOpen] = useState(false)
const [view, setView] = useState<AuthView>(defaultView)
const [providerStatus, setProviderStatus] = useState<ProviderStatus | null>(null)
const [socialLoading, setSocialLoading] = useState<'github' | 'google' | null>(null)
const [socialLoading, setSocialLoading] = useState<'github' | 'google' | 'microsoft' | null>(null)
const brand = useMemo(() => getBrandConfig(), [])

useEffect(() => {
fetchProviderStatus().then(setProviderStatus)
}, [])

const hasSocial = providerStatus?.githubAvailable || providerStatus?.googleAvailable
const hasSocial =
providerStatus?.githubAvailable ||
providerStatus?.googleAvailable ||
providerStatus?.microsoftAvailable
const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
const emailEnabled = !isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED'))
const hasModalContent = hasSocial || ssoEnabled
Expand Down Expand Up @@ -104,7 +109,7 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal
}
}

async function handleSocialLogin(provider: 'github' | 'google') {
async function handleSocialLogin(provider: 'github' | 'google' | 'microsoft') {
setSocialLoading(provider)
try {
await client.signIn.social({ provider, callbackURL: '/workspace' })
Expand Down Expand Up @@ -184,6 +189,19 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal
</span>
</button>
)}
{providerStatus.microsoftAvailable && (
<button
type='button'
onClick={() => handleSocialLogin('microsoft')}
disabled={!!socialLoading}
className={SOCIAL_BTN}
>
<MicrosoftIcon className='absolute left-4 size-[18px] shrink-0' />
<span>
{socialLoading === 'microsoft' ? 'Connecting...' : 'Continue with Microsoft'}
</span>
</button>
)}
{providerStatus.githubAvailable && (
<button
type='button'
Expand All @@ -204,7 +222,8 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal
)}
</div>

{emailEnabled && (
{/* Email option only available on login — signup is OAuth-only */}
{emailEnabled && view === 'login' && (
<>
<div className='relative my-4'>
<div className='absolute inset-0 flex items-center'>
Expand Down
Loading
Loading