auto-claude: subtask-1-2 - Add attempt tracking and lockout UI feedback

Added visual feedback for failed login attempts and account lockout:
- Show remaining attempts warning after failed logins
- Display lockout message with time-to-reset when rate limited
- Added AlertCircle icons following ContactForm pattern
- Tracks lockout state and remaining attempts in component state

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-25 06:35:12 +01:00
co-authored by Claude Sonnet 4.5
parent 8752942af0
commit 1b89129b52
+37 -5
View File
@@ -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<string | null>(null);
const [remainingAttempts, setRemainingAttempts] = useState<number | null>(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() {
</div>
<form className="mt-8 space-y-6" onSubmit={handleLogin}>
{error && (
<div className="bg-red-500/10 border border-red-500/20 text-red-500 p-4 rounded-lg text-sm">
{error}
{isLockedOut && (
<div className="bg-red-500/10 border border-red-500/20 text-red-500 p-4 rounded-lg flex items-center">
<AlertCircle className="h-5 w-5 mr-2 flex-shrink-0" />
<div>
<p className="font-medium">Account temporarily locked</p>
<p className="text-sm mt-1">
Too many failed login attempts. Please try again in {lockoutMinutes} {lockoutMinutes === 1 ? 'minute' : 'minutes'}.
</p>
</div>
</div>
)}
{error && !isLockedOut && (
<div className="bg-red-500/10 border border-red-500/20 text-red-500 p-4 rounded-lg">
<div className="flex items-center">
<AlertCircle className="h-5 w-5 mr-2 flex-shrink-0" />
<span>{error}</span>
</div>
{remainingAttempts !== null && remainingAttempts > 0 && (
<p className="text-sm mt-2 ml-7">
{remainingAttempts} {remainingAttempts === 1 ? 'attempt' : 'attempts'} remaining before temporary lockout.
</p>
)}
</div>
)}