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:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
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 { useTranslations, useLocale } from 'next-intl';
|
||||||
import { createClient } from '@/lib/supabase/client';
|
import { createClient } from '@/lib/supabase/client';
|
||||||
import { rateLimiter } from '../../utils/rateLimiting';
|
import { rateLimiter } from '../../utils/rateLimiting';
|
||||||
@@ -15,17 +15,24 @@ export default function LoginForm() {
|
|||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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) => {
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setRemainingAttempts(null);
|
||||||
|
setIsLockedOut(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check rate limiting
|
// Check rate limiting
|
||||||
const clientIp = '127.0.0.1'; // In production, get this from the request
|
const clientIp = '127.0.0.1'; // In production, get this from the request
|
||||||
if (rateLimiter.isRateLimited(clientIp)) {
|
if (rateLimiter.isRateLimited(clientIp)) {
|
||||||
const timeToReset = Math.ceil(rateLimiter.getTimeToReset(clientIp) / 1000 / 60);
|
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.`);
|
throw new Error(`Too many attempts. Please try again in ${timeToReset} minutes.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +42,12 @@ export default function LoginForm() {
|
|||||||
password,
|
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`);
|
router.push(`/${locale}/dashboard`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : t('form.errors.default'));
|
setError(err instanceof Error ? err.message : t('form.errors.default'));
|
||||||
@@ -53,9 +65,29 @@ export default function LoginForm() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form className="mt-8 space-y-6" onSubmit={handleLogin}>
|
<form className="mt-8 space-y-6" onSubmit={handleLogin}>
|
||||||
{error && (
|
{isLockedOut && (
|
||||||
<div className="bg-red-500/10 border border-red-500/20 text-red-500 p-4 rounded-lg text-sm">
|
<div className="bg-red-500/10 border border-red-500/20 text-red-500 p-4 rounded-lg flex items-center">
|
||||||
{error}
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user