mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(auth): OAuth-only signup with Microsoft provider (#5073)
* feat(auth): OAuth-only signup with Microsoft provider - Remove email/password form from /signup — Google, Microsoft, GitHub OAuth only - Add Microsoft as a social provider (MICROSOFT_CLIENT_ID / MICROSOFT_CLIENT_SECRET / DISABLE_MICROSOFT_AUTH) - Wire microsoftAvailable through provider checker, API contract, providers route, and all auth UI - Hide "Continue with email" in auth modal signup view; login view unchanged - Fix MicrosoftIcon SVG to use official brand colors and proportions * fix(auth): remove unused useSession, guard invalid-callback warn with ref * feat(auth): gate email signup via DISABLE_EMAIL_SIGNUP flag * feat(auth): restore signup email form gated by NEXT_PUBLIC_DISABLE_EMAIL_SIGNUP * refactor(auth): single DISABLE_EMAIL_SIGNUP env var controls both ui and backend * fix(config): restore isHosted hostname check
This commit is contained in:
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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 = (
|
||||
<Button
|
||||
variant='outline'
|
||||
@@ -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
|
||||
@@ -102,6 +140,7 @@ export function SocialLoginButtons({
|
||||
return (
|
||||
<div className='grid gap-3 font-light'>
|
||||
{googleAvailable && googleButton}
|
||||
{microsoftAvailable && microsoftButton}
|
||||
{githubAvailable && githubButton}
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -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({
|
||||
<div className={cn(!emailEnabled ? 'mt-8' : undefined)}>
|
||||
<SocialLoginButtons
|
||||
googleAvailable={googleAvailable}
|
||||
microsoftAvailable={microsoftAvailable}
|
||||
githubAvailable={githubAvailable}
|
||||
isProduction={isProduction}
|
||||
callbackURL={callbackUrl}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 <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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<div className='space-y-1 text-center'>
|
||||
@@ -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'>
|
||||
@@ -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' />
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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<AuthProviderStatusResponse> | null = null
|
||||
const FALLBACK_STATUS: ProviderStatus = {
|
||||
githubAvailable: false,
|
||||
googleAvailable: false,
|
||||
microsoftAvailable: false,
|
||||
registrationDisabled: false,
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
@@ -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
|
||||
@@ -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
|
||||
</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'
|
||||
@@ -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'>
|
||||
|
||||
@@ -12,10 +12,11 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
|
||||
const parsed = await parseRequest(getAuthProvidersContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
|
||||
const { githubAvailable, googleAvailable } = await getOAuthProviderStatus()
|
||||
const { githubAvailable, googleAvailable, microsoftAvailable } = await getOAuthProviderStatus()
|
||||
return NextResponse.json({
|
||||
githubAvailable,
|
||||
googleAvailable,
|
||||
microsoftAvailable,
|
||||
registrationDisabled: isRegistrationDisabled,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2936,12 +2936,11 @@ export function ClickHouseIcon(props: SVGProps<SVGSVGElement>) {
|
||||
|
||||
export function MicrosoftIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 23 23' {...props}>
|
||||
<path fill='#f3f3f3' d='M0 0h23v23H0z' />
|
||||
<path fill='#f35325' d='M1 1h10v10H1z' />
|
||||
<path fill='#81bc06' d='M12 1h10v10H12z' />
|
||||
<path fill='#05a6f0' d='M1 12h10v10H1z' />
|
||||
<path fill='#ffba08' d='M12 12h10v10H12z' />
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 109 109' {...props}>
|
||||
<polygon fill='#F1511B' points='51.9,51.9 0,51.9 0,0 51.9,0' />
|
||||
<polygon fill='#80CC28' points='109.3,51.9 57.3,51.9 57.3,0 109.3,0' />
|
||||
<polygon fill='#00ADEF' points='51.9,109.3 0,109.3 0,57.4 51.9,57.4' />
|
||||
<polygon fill='#FBBC09' points='109.3,109.3 57.3,109.3 57.3,57.4 109.3,57.4' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export const ssoProvidersQuerySchema = z.object({
|
||||
export const authProviderStatusResponseSchema = z.object({
|
||||
githubAvailable: z.boolean(),
|
||||
googleAvailable: z.boolean(),
|
||||
microsoftAvailable: z.boolean(),
|
||||
registrationDisabled: z.boolean(),
|
||||
})
|
||||
|
||||
|
||||
@@ -65,10 +65,12 @@ import {
|
||||
isAuthDisabled,
|
||||
isBillingEnabled,
|
||||
isEmailPasswordEnabled,
|
||||
isEmailSignupDisabled,
|
||||
isEmailVerificationEnabled,
|
||||
isGithubAuthDisabled,
|
||||
isGoogleAuthDisabled,
|
||||
isHosted,
|
||||
isMicrosoftAuthDisabled,
|
||||
isOrganizationsEnabled,
|
||||
isRegistrationDisabled,
|
||||
isSignupEmailValidationEnabled,
|
||||
@@ -724,6 +726,15 @@ export const auth = betterAuth({
|
||||
],
|
||||
},
|
||||
}),
|
||||
...(!isMicrosoftAuthDisabled &&
|
||||
env.MICROSOFT_CLIENT_ID &&
|
||||
env.MICROSOFT_CLIENT_SECRET && {
|
||||
microsoft: {
|
||||
clientId: env.MICROSOFT_CLIENT_ID,
|
||||
clientSecret: env.MICROSOFT_CLIENT_SECRET,
|
||||
scope: ['openid', 'profile', 'email'],
|
||||
},
|
||||
}),
|
||||
},
|
||||
emailVerification: {
|
||||
autoSignInAfterVerification: true,
|
||||
@@ -874,6 +885,11 @@ export const auth = betterAuth({
|
||||
})
|
||||
}
|
||||
|
||||
if (isEmailSignupDisabled && ctx.path.startsWith('/sign-up/email'))
|
||||
throw new APIError('FORBIDDEN', {
|
||||
message: 'Email sign-up is disabled. Please use Google, Microsoft, or GitHub.',
|
||||
})
|
||||
|
||||
const isSignIn = ctx.path.startsWith('/sign-in')
|
||||
const isSignUp = ctx.path.startsWith('/sign-up')
|
||||
|
||||
|
||||
@@ -317,6 +317,8 @@ export const env = createEnv({
|
||||
GITHUB_CLIENT_SECRET: z.string().optional(), // GitHub OAuth client secret
|
||||
DISABLE_GOOGLE_AUTH: z.boolean().optional(), // Disable Google OAuth login even when credentials are configured
|
||||
DISABLE_GITHUB_AUTH: z.boolean().optional(), // Disable GitHub OAuth login even when credentials are configured
|
||||
DISABLE_MICROSOFT_AUTH: z.boolean().optional(), // Disable Microsoft OAuth login even when credentials are configured
|
||||
DISABLE_EMAIL_SIGNUP: z.boolean().optional(), // Block new email/password registrations while keeping email login working
|
||||
|
||||
X_CLIENT_ID: z.string().optional(), // X (Twitter) OAuth client ID
|
||||
X_CLIENT_SECRET: z.string().optional(), // X (Twitter) OAuth client secret
|
||||
|
||||
@@ -254,6 +254,19 @@ export const isGoogleAuthDisabled = isTruthy(env.DISABLE_GOOGLE_AUTH)
|
||||
*/
|
||||
export const isGithubAuthDisabled = isTruthy(env.DISABLE_GITHUB_AUTH)
|
||||
|
||||
/**
|
||||
* Is Microsoft OAuth login disabled
|
||||
* When true, the Microsoft OAuth login button is hidden even when credentials are configured
|
||||
*/
|
||||
export const isMicrosoftAuthDisabled = isTruthy(env.DISABLE_MICROSOFT_AUTH)
|
||||
|
||||
/**
|
||||
* Is email/password signup disabled
|
||||
* When true, new registrations via email/password are blocked at the server level.
|
||||
* Existing users can still sign in with email/password.
|
||||
*/
|
||||
export const isEmailSignupDisabled = isTruthy(env.DISABLE_EMAIL_SIGNUP)
|
||||
|
||||
/**
|
||||
* Is React Grab enabled for UI element debugging
|
||||
* When true and in development mode, enables React Grab for copying UI element context to clipboard
|
||||
|
||||
Reference in New Issue
Block a user