import { useEffect, useRef, useState } from 'react'; import { useAuth } from '@/context/AuthContext'; import { cn } from '@/lib/utils'; import { oidcAutoRedirectUrl } from '@/lib/oidcAutoRedirect'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Checkbox } from '@/components/ui/checkbox'; import { ArrowRight, KeyRound, Loader2 } from 'lucide-react'; import { AuthCanvas } from '@/components/auth/AuthCanvas'; import { AuthStepHeader } from '@/components/auth/AuthStepHeader'; import { ErrorRail } from '@/components/auth/ErrorRail'; interface SSOProvider { provider: string; displayName: string; type: 'ldap' | 'oidc'; } const INPUT_CLASS = 'h-11 bg-background/60 border-card-border font-sans text-base shadow-[inset_0_2px_4px_0_oklch(0_0_0/0.25)] placeholder:text-stat-subtitle/60 focus-visible:border-brand/60 focus-visible:ring-2 focus-visible:ring-brand/40 focus-visible:ring-offset-0'; function getProviderIcon(provider: string) { switch (provider) { case 'oidc_google': return ( ); case 'oidc_github': return ( ); case 'oidc_okta': return ( ); case 'oidc_custom': default: return ; } } export function Login({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { const { login, ssoLdapLogin } = useAuth(); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(() => { const params = new URLSearchParams(window.location.search); const ssoError = params.get('sso_error'); if (ssoError) { window.history.replaceState({}, '', window.location.pathname); return ssoError; } return ''; }); // Capture once: returning from a failed OIDC attempt must stay on Login, not bounce again. const hadSsoErrorRef = useRef(error.length > 0); const [isLoading, setIsLoading] = useState(false); const [loginMode, setLoginMode] = useState<'local' | 'ldap'>('local'); const [ssoProviders, setSsoProviders] = useState([]); const [localLoginEnabled, setLocalLoginEnabled] = useState(false); const [discoveryError, setDiscoveryError] = useState(''); const [discoveryReady, setDiscoveryReady] = useState(false); const [oidcRedirecting, setOidcRedirecting] = useState(false); const [capsLock, setCapsLock] = useState(false); const [rememberMe, setRememberMe] = useState(false); useEffect(() => { let cancelled = false; (async () => { try { const [statusRes, providersRes] = await Promise.all([ fetch('/api/auth/status', { credentials: 'include' }), fetch('/api/auth/sso/providers', { credentials: 'include' }), ]); if (cancelled) return; if (!statusRes.ok) { // Fail closed: keep localLoginEnabled false so a status outage never // reveals the password form under SSO-only. setDiscoveryError('Could not load authentication status. Refresh the page and try again.'); setDiscoveryReady(true); return; } const status = await statusRes.json() as { localLoginEnabled?: boolean }; const enabled = status.localLoginEnabled !== false; setLocalLoginEnabled(enabled); if (!providersRes.ok) { if (!enabled) { setDiscoveryError('Could not load identity providers. Refresh the page and try again.'); } setSsoProviders([]); setDiscoveryReady(true); return; } const providers = await providersRes.json() as SSOProvider[]; const list = Array.isArray(providers) ? providers : []; const autoUrl = oidcAutoRedirectUrl({ localLoginEnabled: enabled, providers: list, hadSsoError: hadSsoErrorRef.current, }); if (autoUrl) { if (cancelled) return; setOidcRedirecting(true); setDiscoveryReady(true); window.location.replace(autoUrl); return; } setSsoProviders(list); if (!enabled && list.some((p) => p.type === 'ldap')) { setLoginMode('ldap'); } if (!enabled && list.length === 0) { setDiscoveryError('No identity providers are available. Contact your administrator.'); } setDiscoveryReady(true); } catch (e) { console.warn('[Login] Auth discovery failed:', e); if (!cancelled) { setDiscoveryError('Could not load authentication options. Refresh the page and try again.'); setDiscoveryReady(true); } } })(); return () => { cancelled = true; }; }, []); const hasLdap = ssoProviders.some((p) => p.type === 'ldap'); const oidcProviders = ssoProviders.filter((p) => p.type === 'oidc'); const showPasswordForm = localLoginEnabled || (hasLdap && loginMode === 'ldap'); const showLocalLdapToggle = localLoginEnabled && hasLdap; // Fail closed: under SSO-only, never show the local form after a discovery error. const blockLocalFallback = !localLoginEnabled && !!discoveryError; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!localLoginEnabled && loginMode !== 'ldap') return; setError(''); setIsLoading(true); const result = loginMode === 'ldap' && ssoLdapLogin ? await ssoLdapLogin(username, password, rememberMe) : await login(username, password, rememberMe); if (!result.success) setError(result.error || 'Login failed'); setIsLoading(false); }; const handlePasswordKey = (e: React.KeyboardEvent) => { if (typeof e.getModifierState === 'function') { setCapsLock(e.getModifierState('CapsLock')); } }; const footerLabel = !discoveryReady ? 'Console' : !localLoginEnabled ? 'Console · SSO' : loginMode === 'ldap' ? 'Console · LDAP' : 'Console · Local'; return (
{footerLabel} Secure by default
} >
{showLocalLdapToggle && (
setLoginMode('local')} /> setLoginMode('ldap')} />
)}
{discoveryReady && blockLocalFallback && ( {discoveryError} )} {discoveryReady && oidcRedirecting && (
Redirecting to your identity provider...
)} {discoveryReady && !blockLocalFallback && !oidcRedirecting && error && !showPasswordForm && ( {error} )} {discoveryReady && !blockLocalFallback && !oidcRedirecting && showPasswordForm && (
setUsername(e.target.value)} className={INPUT_CLASS} />
{capsLock && ( Caps Lock On )}
setPassword(e.target.value)} onKeyDown={handlePasswordKey} onKeyUp={handlePasswordKey} className={INPUT_CLASS} />
setRememberMe(c === true)} />
{error && {error}}
)} {discoveryReady && !blockLocalFallback && !oidcRedirecting && oidcProviders.length > 0 && (
{showPasswordForm && (
Or continue with
)}
{oidcProviders.map((p) => ( ))}
)} {discoveryReady && !blockLocalFallback && !oidcRedirecting && !showPasswordForm && oidcProviders.length === 0 && !discoveryError && ( No identity providers are available. Contact your administrator. )}
); } function ModePill({ active, label, onClick }: { active: boolean; label: string; onClick: () => void }) { return ( ); }