'use client' import { useState, useEffect, useRef } from 'react' import { useAuth, useAuthContext, useFeatures } from '../contexts/AuthContext' import { useAllauthAPI } from '../contexts/APIContext' import { useStyles } from '../contexts/StylesContext' import { getAuthDetails } from '../api' import { AuthenticatorType } from '../defines' import { AuthSettings } from './settings/AuthSettings' import { AuthCard } from './AuthCard' import { AuthDjangoForm } from './AuthDjangoForm' import { Button } from './settings/SettingsComponents' import { LoginView } from './views/LoginView' import { SignupView } from './views/SignupView' import { MFAChooserView } from './views/MFAChooserView' import { MFAWebAuthnView } from './views/MFAWebAuthnView' import { MFATOTPView } from './views/MFATOTPView' import { MFARecoveryCodesView } from './views/MFARecoveryCodesView' /** * All possible views in the AllauthUI component. * Views are rendered based on state, not URLs. */ export type AllauthUIView = // Auth views (for unauthenticated users) | 'login' | 'signup' | 'resetPassword' | 'resetPasswordSent' | 'requestCode' | 'confirmCode' // MFA views (during auth flow) | 'mfaChooser' | 'mfaTotp' | 'mfaWebauthn' | 'mfaRecoveryCodes' // Authenticated views | 'settings' | 'logout' /** * Controls how AllauthUI behaves regarding auth/settings transitions. * * - `'auto'` (default): Full SPA - shows auth views when not authenticated, * automatically transitions to settings after login, and back to login after logout. * * - `'auth'`: Auth-only mode - only shows auth views (login, signup, MFA, etc.). * Never shows settings. Use `onAuthenticated` to handle post-login navigation. * Ideal for a dedicated login page. * * - `'settings'`: Settings-only mode - only shows settings views. * If not authenticated, calls `onUnauthenticated` or shows nothing. * Ideal for a dedicated settings page. */ export type AllauthUIMode = 'auto' | 'auth' | 'settings' interface AllauthUIProps { /** * Controls auth/settings transition behavior. * @default 'auto' */ mode?: AllauthUIMode /** * Initial view when component mounts (for 'auto' and 'auth' modes). * Defaults to 'login' for unauthenticated, 'settings' for authenticated (in auto mode). */ initialView?: AllauthUIView /** * Called when authentication completes successfully. * Required for 'auth' mode to handle post-login navigation. */ onAuthenticated?: () => void /** * Called when user is not authenticated (for 'settings' mode). * Use this to redirect to login page. */ onUnauthenticated?: () => void /** * Called when user logs out. * In 'auto' mode, defaults to showing login view. */ onLogout?: () => void /** * Which settings sections to show. * Defaults to all sections. */ settingsSections?: Array<'profile' | 'emails' | 'password' | 'passkeys' | 'connections' | 'mfa' | 'sessions'> /** * OAuth callback URL for social login providers. */ oauthCallbackUrl?: string } /** * AllauthUI is the main component for rendering auth UI. * * It can operate in three modes: * - `'auto'` (default): Full SPA handling login, MFA, settings, and logout * - `'auth'`: Auth-only for dedicated login pages * - `'settings'`: Settings-only for dedicated settings pages * * @example * ```tsx * // Full SPA mode (default) - handles everything * * * // Auth-only mode - for a dedicated login page * router.push('/dashboard')} /> * * // Settings-only mode - for a dedicated settings page * router.push('/login')} /> * ``` */ export function AllauthUI({ mode = 'auto', initialView, onAuthenticated, onUnauthenticated, onLogout, settingsSections, oauthCallbackUrl, }: AllauthUIProps) { const { isAuthenticated, pendingFlow } = useAuth() const { refresh } = useAuthContext() const api = useAllauthAPI() const styles = useStyles() const features = useFeatures() // Get available MFA types from pending flow const mfaTypes = pendingFlow?.types || [] // Internal view state const [view, setView] = useState(() => { if (initialView) return initialView // Settings mode always starts at settings if (mode === 'settings') return 'settings' // Auth mode always starts at login (or MFA if pending) if (mode === 'auth') { if (pendingFlow) { return mfaTypes.length === 1 ? getMFAView(mfaTypes[0]) : 'mfaChooser' } return 'login' } // Auto mode: settings if authenticated, login otherwise if (isAuthenticated) return 'settings' if (pendingFlow) { return mfaTypes.length === 1 ? getMFAView(mfaTypes[0]) : 'mfaChooser' } return 'login' }) // Track auth state changes const wasAuthenticated = useRef(isAuthenticated) const hadPendingFlow = useRef(!!pendingFlow) // Handle auth state transitions useEffect(() => { // User just became authenticated if (!wasAuthenticated.current && isAuthenticated) { if (onAuthenticated) { onAuthenticated() } else if (mode === 'auto') { setView('settings') } // In 'auth' mode without onAuthenticated, do nothing (stay on current view) } // User just logged out if (wasAuthenticated.current && !isAuthenticated) { if (onLogout) { onLogout() } else if (mode === 'auto') { setView('login') } else if (mode === 'settings' && onUnauthenticated) { onUnauthenticated() } } wasAuthenticated.current = isAuthenticated }, [isAuthenticated, onAuthenticated, onUnauthenticated, onLogout, mode]) // Handle MFA flow transitions useEffect(() => { if (pendingFlow && !hadPendingFlow.current) { // New MFA flow started if (mfaTypes.length === 1) { setView(getMFAView(mfaTypes[0])) } else if (mfaTypes.length > 1) { setView('mfaChooser') } } if (!pendingFlow && hadPendingFlow.current && isAuthenticated) { // MFA completed successfully if (onAuthenticated) { onAuthenticated() } else if (mode === 'auto') { setView('settings') } } hadPendingFlow.current = !!pendingFlow }, [pendingFlow, mfaTypes, isAuthenticated, onAuthenticated, mode]) // Settings mode: handle unauthenticated state useEffect(() => { if (mode === 'settings' && !isAuthenticated && onUnauthenticated) { onUnauthenticated() } }, [mode, isAuthenticated, onUnauthenticated]) // Handle logout const handleLogout = async () => { await api.session.logout() await refresh() if (onLogout) { onLogout() } else if (mode === 'auto') { setView('login') } // In settings mode, the useEffect will call onUnauthenticated } // Called after successful login/signup - check for MFA or complete auth const handleAuthSuccess = async () => { const newAuth = await refresh() const details = getAuthDetails(newAuth) // If fully authenticated, handle completion if (details.isAuthenticated) { if (onAuthenticated) { onAuthenticated() } else if (mode === 'auto') { setView('settings') } // In 'auth' mode without onAuthenticated, stay on current view } // If MFA pending, the useEffect will handle the view transition } // Render based on current view switch (view) { // ============================================ // Authenticated views // ============================================ case 'settings': // In auth mode, never show settings if (mode === 'auth') { return null } // Not authenticated - handle based on mode if (!isAuthenticated) { if (mode === 'settings' && onUnauthenticated) { // Will be handled by useEffect return null } // Auto mode: switch to login setView('login') return null } return ( setView('logout')} /> ) case 'logout': if (!isAuthenticated) { if (mode === 'auto') { setView('login') } return null } return ( setView('settings') }, ]} >
) // ============================================ // MFA views // ============================================ case 'mfaChooser': return ( setView('login')} /> ) case 'mfaTotp': return ( setView('login')} onBack={mfaTypes.length > 1 ? () => setView('mfaChooser') : undefined} /> ) case 'mfaWebauthn': return ( setView('login')} onBack={mfaTypes.length > 1 ? () => setView('mfaChooser') : undefined} /> ) case 'mfaRecoveryCodes': return ( setView('login')} onBack={mfaTypes.length > 1 ? () => setView('mfaChooser') : undefined} /> ) // ============================================ // Password reset views // ============================================ case 'resetPassword': return ( setView('resetPasswordSent')} footerLinks={[ { label: 'Back to Sign In', onClick: () => setView('login') }, ]} /> ) case 'resetPasswordSent': return ( setView('login') }, ]} /> ) // ============================================ // Login by code views // ============================================ case 'requestCode': // If login by code is disabled, redirect to login if (!features.loginByCodeEnabled) { setView('login') return null } return ( setView('confirmCode')} footerLinks={[ { label: 'Sign in with password instead', onClick: () => setView('login') }, ]} /> ) case 'confirmCode': // If login by code is disabled, redirect to login if (!features.loginByCodeEnabled) { setView('login') return null } return ( setView('requestCode') }, { label: 'Sign in with password instead', onClick: () => setView('login') }, ]} /> ) // ============================================ // Signup view // ============================================ case 'signup': // If signup is disabled, redirect to login if (!features.signupEnabled) { setView('login') return null } return ( setView('login')} /> ) // ============================================ // Login view (default) // ============================================ case 'login': default: return ( setView('signup') : undefined} onForgotPasswordClick={() => setView('resetPassword')} // Only provide login-by-code callback if feature is enabled onLoginByCodeClick={features.loginByCodeEnabled ? () => setView('requestCode') : undefined} oauthCallbackUrl={oauthCallbackUrl} /> ) } } /** * Get the view name for a given MFA authenticator type. */ function getMFAView(type: string): AllauthUIView { switch (type) { case AuthenticatorType.TOTP: return 'mfaTotp' case AuthenticatorType.WEBAUTHN: return 'mfaWebauthn' case AuthenticatorType.RECOVERY_CODES: return 'mfaRecoveryCodes' default: return 'mfaChooser' } }