diff --git a/apps/sim/app/(auth)/components/oauth-provider-checker.tsx b/apps/sim/app/(auth)/components/oauth-provider-checker.tsx index 73a95f98b02..218377f07de 100644 --- a/apps/sim/app/(auth)/components/oauth-provider-checker.tsx +++ b/apps/sim/app/(auth)/components/oauth-provider-checker.tsx @@ -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 = @@ -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 } } diff --git a/apps/sim/app/(auth)/components/social-login-buttons.tsx b/apps/sim/app/(auth)/components/social-login-buttons.tsx index 674ebe2eeb0..ed46b9413aa 100644 --- a/apps/sim/app/(auth)/components/social-login-buttons.tsx +++ b/apps/sim/app/(auth)/components/social-login-buttons.tsx @@ -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 @@ -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 @@ -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) + } + } + const githubButton = ( + ) + + const hasAnyOAuthProvider = githubAvailable || googleAvailable || microsoftAvailable if (!hasAnyOAuthProvider && !children) { return null @@ -102,6 +140,7 @@ export function SocialLoginButtons({ return (
{googleAvailable && googleButton} + {microsoftAvailable && microsoftButton} {githubAvailable && githubButton} {children}
diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index 67ac09b9461..de314167c7e 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -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() @@ -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) @@ -483,6 +485,7 @@ export default function LoginPage({
diff --git a/apps/sim/app/(auth)/signup/page.tsx b/apps/sim/app/(auth)/signup/page.tsx index 1f01e004643..b43f6ebad56 100644 --- a/apps/sim/app/(auth)/signup/page.tsx +++ b/apps/sim/app/(auth)/signup/page.tsx @@ -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' @@ -14,13 +14,16 @@ export default async function SignupPage() { return
Registration is disabled, please contact your admin.
} - const { githubAvailable, googleAvailable, isProduction } = await getOAuthProviderStatus() + const { githubAvailable, googleAvailable, microsoftAvailable, isProduction } = + await getOAuthProviderStatus() return ( ) } diff --git a/apps/sim/app/(auth)/signup/signup-form.tsx b/apps/sim/app/(auth)/signup/signup-form.tsx index 90490160dff..ae73e36cb5a 100644 --- a/apps/sim/app/(auth)/signup/signup-form.tsx +++ b/apps/sim/app/(auth)/signup/signup-form.tsx @@ -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() @@ -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 ( <>
@@ -357,21 +373,13 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S

- {/* 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 && (
)} - {/* Email/Password Form - show unless explicitly disabled */} - {!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && ( + {emailEnabled && (
@@ -540,16 +548,7 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S )} - {/* 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 && (
@@ -562,26 +561,16 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S
)} - {(() => { - 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 - })() && ( -
+ {showBottomSection && ( +
- {isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED')) && ( + {ssoEnabled && !hasOnlySSO && ( )} @@ -625,14 +614,18 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S export default function SignupPage({ githubAvailable, googleAvailable, + microsoftAvailable, isProduction, + emailSignupEnabled, }: SignupFormProps) { return ( Loading…
}> ) diff --git a/apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx b/apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx index f64ee69b34c..d0a1a985ac0 100644 --- a/apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx +++ b/apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx @@ -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' @@ -40,6 +40,7 @@ let fetchPromise: Promise | null = null const FALLBACK_STATUS: ProviderStatus = { githubAvailable: false, googleAvailable: false, + microsoftAvailable: false, registrationDisabled: false, } @@ -49,9 +50,10 @@ const SOCIAL_BTN = function fetchProviderStatus(): Promise { if (fetchPromise) return fetchPromise fetchPromise = requestJson(getAuthProvidersContract, {}) - .then(({ githubAvailable, googleAvailable, registrationDisabled }) => ({ + .then(({ githubAvailable, googleAvailable, microsoftAvailable, registrationDisabled }) => ({ githubAvailable, googleAvailable, + microsoftAvailable, registrationDisabled, })) .catch(() => { @@ -66,14 +68,17 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal const [open, setOpen] = useState(false) const [view, setView] = useState(defaultView) const [providerStatus, setProviderStatus] = useState(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 @@ -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' }) @@ -184,6 +189,19 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal )} + {providerStatus.microsoftAvailable && ( + + )} {providerStatus.githubAvailable && (