import { useState, FormEvent } from 'react'; import { Navigate } from 'react-router-dom'; import { toast } from 'sonner'; import { Lock, Loader2 } from 'lucide-react'; import { useAuth } from '@/hooks/useAuth'; import { getErrorMessage } from '@/lib/api'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { cn } from '@/lib/utils'; import AmbientBackground from './AmbientBackground'; /** Validates password against backend rules: 12-128 chars, at least one letter + one non-letter. */ function validatePassword(password: string): string | null { if (password.length < 12) return 'Password must be at least 12 characters'; if (password.length > 128) return 'Password must be at most 128 characters'; if (!/[a-zA-Z]/.test(password)) return 'Password must contain at least one letter'; if (!/[^a-zA-Z]/.test(password)) return 'Password must contain at least one non-letter character'; return null; } export default function LockScreen() { const { authStatus, isLoading, login, setup, verifyTotp, mfaRequired, isLoginPending, isSetupPending, isTotpPending } = useAuth(); // Credentials state (shared across login/setup states) const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); // TOTP challenge state const [totpCode, setTotpCode] = useState(''); const [useBackupCode, setUseBackupCode] = useState(false); // Lockout handling (HTTP 423) const [lockoutMessage, setLockoutMessage] = useState(null); // Redirect authenticated users immediately if (!isLoading && authStatus?.authenticated) { return ; } const isSetup = authStatus?.setup_required === true; const handleCredentialSubmit = async (e: FormEvent) => { e.preventDefault(); setLockoutMessage(null); if (isSetup) { // Setup mode: validate password then create account const validationError = validatePassword(password); if (validationError) { toast.error(validationError); return; } if (password !== confirmPassword) { toast.error('Passwords do not match'); return; } try { await setup({ username, password }); // useAuth invalidates auth query → Navigate above handles redirect } catch (error) { toast.error(getErrorMessage(error, 'Failed to create account')); } } else { // Login mode try { await login({ username, password }); // If mfaRequired becomes true, the TOTP state renders automatically // If not required, useAuth invalidates auth query → Navigate above handles redirect } catch (error: any) { if (error?.response?.status === 423) { const msg = error.response.data?.detail || 'Account locked. Try again later.'; setLockoutMessage(msg); } else { toast.error(getErrorMessage(error, 'Invalid username or password')); } } } }; const handleTotpSubmit = async (e: FormEvent) => { e.preventDefault(); try { await verifyTotp(totpCode); // useAuth invalidates auth query → Navigate above handles redirect } catch (error) { toast.error(getErrorMessage(error, 'Invalid verification code')); setTotpCode(''); } }; return (
{/* Wordmark — in flex flow above card */} UMBRA {/* Auth card */} {mfaRequired ? ( // State C: TOTP challenge <>
Two-Factor Authentication {useBackupCode ? 'Enter one of your backup codes' : 'Enter the code from your authenticator app'}
setTotpCode( useBackupCode ? e.target.value.replace(/[^0-9-]/g, '') : e.target.value.replace(/\D/g, '') ) } placeholder={useBackupCode ? 'XXXX-XXXX' : '000000'} autoFocus autoComplete="one-time-code" className="text-center text-lg tracking-widest" />
) : ( // State A (setup) or State B (login) <>
{isSetup ? 'Welcome to UMBRA' : 'Sign in'} {isSetup ? 'Create your account to get started' : 'Enter your credentials to continue'}
{/* Lockout warning banner */} {lockoutMessage && (
)}
{ setUsername(e.target.value); setLockoutMessage(null); }} placeholder="Enter username" required autoFocus autoComplete="username" />
{ setPassword(e.target.value); setLockoutMessage(null); }} placeholder={isSetup ? 'Create a password' : 'Enter password'} required autoComplete={isSetup ? 'new-password' : 'current-password'} />
{isSetup && (
setConfirmPassword(e.target.value)} placeholder="Confirm your password" required autoComplete="new-password" />

Must be 12-128 characters with at least one letter and one non-letter.

)}
)}
); }