diff --git a/frontend/src/components/auth/LockScreen.tsx b/frontend/src/components/auth/LockScreen.tsx index 2d0dd39..65f347d 100644 --- a/frontend/src/components/auth/LockScreen.tsx +++ b/frontend/src/components/auth/LockScreen.tsx @@ -1,115 +1,282 @@ import { useState, FormEvent } from 'react'; -import { useNavigate, Navigate } from 'react-router-dom'; +import { Navigate } from 'react-router-dom'; import { toast } from 'sonner'; -import { Lock } from 'lucide-react'; +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'; + +/** 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 navigate = useNavigate(); - const { authStatus, login, setup, isLoginPending, isSetupPending } = useAuth(); - const [pin, setPin] = useState(''); - const [confirmPin, setConfirmPin] = useState(''); + const { authStatus, isLoading, login, setup, verifyTotp, mfaRequired, isLoginPending, isSetupPending, isTotpPending } = useAuth(); - // Redirect authenticated users to dashboard - if (authStatus?.authenticated) { + // 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 handleSubmit = async (e: FormEvent) => { - e.preventDefault(); + const isSetup = authStatus?.setup_required === true; - if (authStatus?.setup_required) { - if (pin !== confirmPin) { - toast.error('PINs do not match'); + 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 (pin.length < 4) { - toast.error('PIN must be at least 4 characters'); + if (password !== confirmPassword) { + toast.error('Passwords do not match'); return; } try { - await setup(pin); - toast.success('PIN created successfully'); - navigate('/dashboard'); + await setup({ username, password }); + // useAuth invalidates auth query → Navigate above handles redirect } catch (error) { - toast.error(getErrorMessage(error, 'Failed to create PIN')); + toast.error(getErrorMessage(error, 'Failed to create account')); } } else { + // Login mode try { - await login(pin); - navigate('/dashboard'); - } catch (error) { - toast.error(getErrorMessage(error, 'Invalid PIN')); - setPin(''); + 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 isSetup = authStatus?.setup_required; + 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 ( -
- - -
- -
- - {isSetup ? 'Welcome to UMBRA' : 'Enter PIN'} - - - {isSetup - ? 'Create a PIN to secure your account' - : 'Enter your PIN to access your dashboard'} - -
- -
-
- - setPin(e.target.value)} - placeholder="Enter PIN" - required - autoFocus - className="text-center text-lg tracking-widest" - /> -
- {isSetup && ( -
- - setConfirmPin(e.target.value)} - placeholder="Confirm PIN" - required - className="text-center text-lg tracking-widest" - /> +
+ {/* Ambient glow blobs */} +