diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx index 6372229..af18291 100644 --- a/src/components/auth/LoginForm.tsx +++ b/src/components/auth/LoginForm.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { useRouter } from 'next/navigation'; -import { Lock, Loader2 } from 'lucide-react'; +import { Lock, Loader2, AlertCircle } from 'lucide-react'; import { useTranslations, useLocale } from 'next-intl'; import { createClient } from '@/lib/supabase/client'; import { rateLimiter } from '../../utils/rateLimiting'; @@ -15,17 +15,24 @@ export default function LoginForm() { const [password, setPassword] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const [remainingAttempts, setRemainingAttempts] = useState(null); + const [isLockedOut, setIsLockedOut] = useState(false); + const [lockoutMinutes, setLockoutMinutes] = useState(0); const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); setError(null); + setRemainingAttempts(null); + setIsLockedOut(false); try { // Check rate limiting const clientIp = '127.0.0.1'; // In production, get this from the request if (rateLimiter.isRateLimited(clientIp)) { const timeToReset = Math.ceil(rateLimiter.getTimeToReset(clientIp) / 1000 / 60); + setIsLockedOut(true); + setLockoutMinutes(timeToReset); throw new Error(`Too many attempts. Please try again in ${timeToReset} minutes.`); } @@ -35,7 +42,12 @@ export default function LoginForm() { password, }); - if (error) throw error; + if (error) { + // Get remaining attempts after failed login + const remaining = rateLimiter.getRemainingAttempts(clientIp); + setRemainingAttempts(remaining); + throw error; + } router.push(`/${locale}/dashboard`); } catch (err) { setError(err instanceof Error ? err.message : t('form.errors.default')); @@ -53,9 +65,29 @@ export default function LoginForm() {
- {error && ( -
- {error} + {isLockedOut && ( +
+ +
+

Account temporarily locked

+

+ Too many failed login attempts. Please try again in {lockoutMinutes} {lockoutMinutes === 1 ? 'minute' : 'minutes'}. +

+
+
+ )} + + {error && !isLockedOut && ( +
+
+ + {error} +
+ {remainingAttempts !== null && remainingAttempts > 0 && ( +

+ {remainingAttempts} {remainingAttempts === 1 ? 'attempt' : 'attempts'} remaining before temporary lockout. +

+ )}
)}