import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../components/AuthProvider'; import { getAuthInfo, breakglassLogin, type AuthInfoOIDCProvider } from '../api/client'; // ============================================================================= // LoginPage — Bundle 2 Phase 8 / multi-mode entry surface. // // Pre-Bundle-2: API-key-only sign-in form. // Post-Bundle-2: when `/auth/info` reports `oidc_providers[]`, the // page renders one "Sign in with X" button per provider; clicking // navigates to the provider's `login_url` (which 302s through the // IdP and back to /auth/oidc/callback). The API-key form remains as // a fallback for Bearer-mode deployments. // // Audit 2026-05-10 CRIT-4 closure: an inline break-glass form below // the API-key form lets admins recover during SSO incidents without // crafting curl commands. The link is intentionally low-key // (text-amber-600 small text) — break-glass is the deliberate-bypass // path, not the everyday-login path. // ============================================================================= export default function LoginPage() { const { login, error: authError } = useAuth(); const navigate = useNavigate(); const [key, setKey] = useState(''); const [submitting, setSubmitting] = useState(false); const [localError, setLocalError] = useState(null); const [providers, setProviders] = useState([]); // Break-glass inline form state. const [showBreakglass, setShowBreakglass] = useState(false); const [bgActorID, setBgActorID] = useState(''); const [bgPassword, setBgPassword] = useState(''); const [bgError, setBgError] = useState(null); const [bgSubmitting, setBgSubmitting] = useState(false); const error = localError || authError; // On mount, fetch /auth/info and extract any configured OIDC // providers so we can render the "Sign in with X" buttons. Errors // are non-fatal — fall back to the API-key form. useEffect(() => { getAuthInfo() .then(info => { if (info.oidc_providers && info.oidc_providers.length > 0) { setProviders(info.oidc_providers); } }) .catch(() => { // Server may be pre-Phase-6; ignore. }); }, []); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!key.trim()) return; setSubmitting(true); setLocalError(null); try { await login(key.trim()); } catch { setLocalError('Invalid API key. Check your key and try again.'); } finally { setSubmitting(false); } } async function handleBreakglassSubmit(e: React.FormEvent) { e.preventDefault(); if (!bgActorID.trim() || !bgPassword) return; setBgSubmitting(true); setBgError(null); try { await breakglassLogin(bgActorID.trim(), bgPassword); // breakglassLogin sets the session cookie via Set-Cookie; navigate // to the dashboard, which the AuthProvider will re-validate via // its session-cookie path on next render. navigate('/'); } catch (err) { setBgError(err instanceof Error ? err.message : 'Break-glass login failed.'); } finally { setBgSubmitting(false); } } return (

certctl

Certificate Control Plane

{providers.length > 0 && (

Sign in with your identity provider

{providers.map(p => ( Sign in with {p.display_name} ))}
)}
{providers.length > 0 && (

— or sign in with API key —

)}
setKey(e.target.value)} placeholder="Enter your API key" autoFocus={providers.length === 0} className="w-full bg-white border border-surface-border rounded px-3 py-2.5 text-sm text-ink placeholder-ink-faint focus:outline-none focus:border-brand-400 focus:ring-1 focus:ring-brand-400/20" data-testid="login-api-key-input" />
{error && (
{error}
)}

The API key is set via CERTCTL_AUTH_SECRET on the server.

{/* Break-glass entry — low-visibility on purpose. CRIT-4 closure. */}
{!showBreakglass ? ( ) : (

Break-glass admin login — every action is audited. Use only during SSO incidents.

setBgActorID(e.target.value)} autoComplete="off" spellCheck={false} placeholder="actor-..." className="w-full bg-white border border-amber-300 rounded px-3 py-2 text-sm text-ink placeholder-ink-faint focus:outline-none focus:border-amber-500 focus:ring-1 focus:ring-amber-500/20" data-testid="login-breakglass-actor-id" />
setBgPassword(e.target.value)} autoComplete="off" className="w-full bg-white border border-amber-300 rounded px-3 py-2 text-sm text-ink placeholder-ink-faint focus:outline-none focus:border-amber-500 focus:ring-1 focus:ring-amber-500/20" data-testid="login-breakglass-password" />
{bgError && (
{bgError}
)}
)}
); }