import { useState, useEffect } from 'react'; import { useAuth } from '../components/AuthProvider'; import { getAuthInfo, 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 + the break-glass path. // ============================================================================= export default function LoginPage() { const { login, error: authError } = useAuth(); const [key, setKey] = useState(''); const [submitting, setSubmitting] = useState(false); const [localError, setLocalError] = useState(null); const [providers, setProviders] = useState([]); 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); } } 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.

); }