login page bugs solved

This commit is contained in:
Senthamilselvi 2025-11-05 10:19:28 +05:30
parent 43c4d65a25
commit 004fd73cea
2 changed files with 467 additions and 224 deletions

View File

@ -16,175 +16,268 @@ import PublicContactForm from '@/pages/PublicContactForm';
import EditCompanyProfile from '@/pages/Admin/configuration/EditCompanyProfile';
const RequireRole = ({ allowedRoles = [], children }) => {
let role
let token
let role;
let token;
try {
role = sessionStorage.getItem('user_role')
token = sessionStorage.getItem('auth_token')
role = sessionStorage.getItem('user_role');
token = sessionStorage.getItem('auth_token');
} catch (error) {
role = null
token = null
role = null;
token = null;
}
const normalizedRole = String(role || '').toLowerCase()
const isAllowed = Boolean(token) && allowedRoles.some((allowed) => String(allowed || '').toLowerCase() === normalizedRole)
const normalizedRole = String(role || '').toLowerCase();
const isAllowed =
Boolean(token) &&
allowedRoles.some(
(allowed) => String(allowed || '').toLowerCase() === normalizedRole
);
if (isAllowed) {
return children
return children;
}
return <Navigate to="/login" replace />
}
return <Navigate to="/login" replace />;
};
const SessionWarningModal = ({ show, remainingTime, onExtend, onLogout }) => {
if (!show) return null;
const minutes = Math.floor(remainingTime / 60);
const seconds = remainingTime % 60;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full mx-4 p-6">
<div className="flex items-start gap-4">
<div className="flex-shrink-0 w-12 h-12 rounded-full bg-yellow-100 flex items-center justify-center">
<svg className="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Session Expiring Soon
</h3>
<p className="text-sm text-gray-600 mb-4">
Your session will expire in <strong className="text-yellow-600 font-mono text-base">{minutes}:{seconds.toString().padStart(2, '0')}</strong> due to inactivity.
</p>
<p className="text-sm text-gray-600 mb-6">
Would you like to stay signed in?
</p>
<div className="flex gap-3">
<button
onClick={onExtend}
className="flex-1 bg-[#92722A] hover:bg-[#7b5c1f] text-white px-4 py-2 rounded-lg font-medium transition-colors"
>
Stay Signed In
</button>
<button
onClick={onLogout}
className="flex-1 bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded-lg font-medium transition-colors"
>
Log Out Now
</button>
</div>
</div>
</div>
</div>
</div>
);
};
function App() {
const [showWarning, setShowWarning] = React.useState(false);
const [warningTime, setWarningTime] = React.useState(0);
React.useEffect(() => {
const TIMEOUT_MINUTES = 15;
const WARNING_MINUTES = 2;
let timer;
let warningTimer;
let warningInterval;
const clearAllTimers = () => {
clearTimeout(timer);
clearTimeout(warningTimer);
clearInterval(warningInterval);
timer = null;
warningTimer = null;
warningInterval = null;
};
const performLogout = () => {
sessionStorage.clear();
localStorage.removeItem('failed_attempts');
localStorage.removeItem('lock_until');
setShowWarning(false);
alert('Session expired. Please log in again.');
window.location.href = '/login';
};
const showWarningModal = () => {
setShowWarning(true);
let timeLeft = WARNING_MINUTES * 60;
setWarningTime(timeLeft);
warningInterval = setInterval(() => {
timeLeft--;
setWarningTime(timeLeft);
if (timeLeft <= 0) {
clearInterval(warningInterval);
performLogout();
}
}, 1000);
};
const resetTimer = () => {
clearAllTimers();
setShowWarning(false);
warningTimer = setTimeout(() => {
showWarningModal();
}, (TIMEOUT_MINUTES - WARNING_MINUTES) * 60 * 1000);
timer = setTimeout(() => {
performLogout();
}, TIMEOUT_MINUTES * 60 * 1000);
};
const handleExtendSession = () => {
setShowWarning(false);
resetTimer();
};
const handleLogoutNow = () => {
clearAllTimers();
performLogout();
};
window.handleExtendSession = handleExtendSession;
window.handleLogoutNow = handleLogoutNow;
const events = ['mousemove', 'keydown', 'click', 'scroll', 'touchstart'];
events.forEach(event => {
window.addEventListener(event, resetTimer);
});
resetTimer();
return () => {
clearAllTimers();
events.forEach(event => {
window.removeEventListener(event, resetTimer);
});
delete window.handleExtendSession;
delete window.handleLogoutNow;
};
}, []);
return (
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/history" element={(
<RequireRole allowedRoles={['EstablishmentUser']}>
<History />
</RequireRole>
)}
<>
<SessionWarningModal
show={showWarning}
remainingTime={warningTime}
onExtend={() => window.handleExtendSession?.()}
onLogout={() => window.handleLogoutNow?.()}
/>
<Route
path="/dashboard"
element={(
<RequireRole allowedRoles={['EstablishmentUser']}>
<Dashboard />
</RequireRole>
)}
/>
<Route
path="/admin/dashboard"
element={(
<RequireRole allowedRoles={['Admin']}>
<AdminDashboard />
</RequireRole>
)}
/>
<Route
path="/admin/validations"
element={(
<RequireRole allowedRoles={['Admin','EstablishmentUser']}>
<Validations />
</RequireRole>
)}
/>
<Route
path="/admin/validations/:id"
element={(
<RequireRole allowedRoles={['Admin','EstablishmentUser']}>
<ValidationReview />
</RequireRole>
)}
/>
<Route path="/admin/configuration">
<Route
index
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/history"
element={
<RequireRole allowedRoles={['Admin','EstablishmentUser']}>
<Configuration />
<RequireRole allowedRoles={['EstablishmentUser']}>
<History />
</RequireRole>
}
}
/>
<Route
path="quarterly"
<Route
path="/dashboard"
element={
<RequireRole allowedRoles={['EstablishmentUser']}>
<Dashboard />
</RequireRole>
}
/>
<Route
path="/admin/dashboard"
element={
<RequireRole allowedRoles={['Admin']}>
<Configuration />
<AdminDashboard />
</RequireRole>
}
}
/>
<Route
path="hscodes"
element={
<RequireRole allowedRoles={['Admin']}>
<Configuration />
</RequireRole>
}
/>
<Route
path="unitmaster"
element={
<RequireRole allowedRoles={['Admin']}>
<Configuration />
</RequireRole>
}
/>
<Route
path="admin-users"
element={
<RequireRole allowedRoles={['Admin']}>
<Configuration />
</RequireRole>
}
/>
<Route
path="admin-users/edit/:id"
element={
<RequireRole allowedRoles={['Admin']}>
<Configuration />
</RequireRole>
}
/>
<Route
path="profile"
<Route
path="/admin/validations"
element={
<RequireRole allowedRoles={['Admin', 'EstablishmentUser']}>
<Configuration />
<Validations />
</RequireRole>
}
}
/>
</Route>
<Route
path="/admin/users"
element={(
<RequireRole allowedRoles={['Admin']}>
<AdminUsers />
</RequireRole>
)}
/>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route
path="/survey"
element={(
<RequireRole allowedRoles={['EstablishmentUser']}>
<Survey />
</RequireRole>
)}
/>
<Route
path="/overview"
element={(
<RequireRole allowedRoles={['EstablishmentUser']}>
<Overview />
</RequireRole>
)}
/>
<Route path="profile">
<Route
index
<Route
path="/admin/validations/:id"
element={
<RequireRole allowedRoles={['Admin', 'EstablishmentUser']}>
<ValidationReview />
</RequireRole>
}
/>
<Route path="/admin/configuration">
<Route
index
element={
<RequireRole allowedRoles={['Admin', 'EstablishmentUser']}>
<Configuration />
</RequireRole>
}
/>
</Route>
<Route
path="/admin/users"
element={
<RequireRole allowedRoles={['Admin']}>
<AdminUsers />
</RequireRole>
}
/>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route
path="/survey"
element={
<RequireRole allowedRoles={['EstablishmentUser']}>
<EditCompanyProfile />
<Survey />
</RequireRole>
}
}
/>
<Route
path="edit/:id"
<Route
path="/overview"
element={
<RequireRole allowedRoles={['EstablishmentUser']}>
<EditCompanyProfile />
<Overview />
</RequireRole>
}
}
/>
</Route>
<Route path="/change-password" element={<ChangePassword />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/reset-password" element={<PublicContactForm />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
)
<Route path="profile">
<Route
index
element={
<RequireRole allowedRoles={['EstablishmentUser']}>
<EditCompanyProfile />
</RequireRole>
}
/>
</Route>
<Route path="/change-password" element={<ChangePassword />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/reset-password" element={<PublicContactForm />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</>
);
}
export default App
export default App;

View File

@ -1,10 +1,13 @@
import React from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { login } from '@/services/auth/authService.js';
import { RefreshCw } from "lucide-react";
import { RefreshCw, AlertCircle, Clock } from "lucide-react";
const logoSrc = '/assets/images/FCSCLogo.svg';
const MAX_FAILED_ATTEMPTS = 3;
const SHOW_CAPTCHA_AFTER = 2;
const LOCK_DURATION_MINUTES = 2;
const Login = () => {
const navigate = useNavigate();
const location = useLocation();
@ -23,8 +26,14 @@ const Login = () => {
const [userCaptcha, setUserCaptcha] = React.useState('');
const [captchaError, setCaptchaError] = React.useState('');
const [isLocked, setIsLocked] = React.useState(false);
const [lockExpiry, setLockExpiry] = React.useState(null);
const [failedAttempts, setFailedAttempts] = React.useState(0);
const [remainingTime, setRemainingTime] = React.useState(0);
const toastTimeoutRef = React.useRef(null);
const navigateTimeoutRef = React.useRef(null);
const countdownIntervalRef = React.useRef(null);
const generateCaptcha = React.useCallback(() => {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
@ -35,7 +44,6 @@ const Login = () => {
setCaptcha(code);
}, []);
const closeToast = React.useCallback(() => {
if (toastTimeoutRef.current) {
clearTimeout(toastTimeoutRef.current);
@ -72,21 +80,53 @@ const Login = () => {
[navigate]
);
React.useEffect(() => {
generateCaptcha();
}, [generateCaptcha]);
const startCountdown = React.useCallback((lockUntil) => {
if (countdownIntervalRef.current) {
clearInterval(countdownIntervalRef.current);
}
const updateTimer = () => {
const now = Date.now();
const remaining = Math.max(0, Math.ceil((parseInt(lockUntil, 10) - now) / 1000));
setRemainingTime(remaining);
if (remaining <= 0) {
clearInterval(countdownIntervalRef.current);
countdownIntervalRef.current = null;
setIsLocked(false);
setLockExpiry(null);
localStorage.removeItem('lock_until');
showToast('success', 'Account unlocked. You can try logging in again.');
}
};
updateTimer();
countdownIntervalRef.current = setInterval(updateTimer, 1000);
}, [showToast]);
React.useEffect(() => {
if (location.state?.showSuccessToast && location.state?.successMessage) {
showToast('success', location.state.successMessage);
window.history.replaceState({}, document.title);
generateCaptcha();
const lockUntil = localStorage.getItem('lock_until');
const attempts = parseInt(localStorage.getItem('failed_attempts') || '0', 10);
if (lockUntil && Date.now() < parseInt(lockUntil, 10)) {
setIsLocked(true);
setLockExpiry(lockUntil);
startCountdown(lockUntil);
} else {
localStorage.removeItem('lock_until');
setIsLocked(false);
}
}, [location.state, showToast]);
setFailedAttempts(attempts);
}, [generateCaptcha, startCountdown]);
React.useEffect(
() => () => {
if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current);
if (navigateTimeoutRef.current) clearTimeout(navigateTimeoutRef.current);
if (countdownIntervalRef.current) clearInterval(countdownIntervalRef.current);
},
[]
);
@ -109,10 +149,10 @@ const Login = () => {
setEmailError('');
setPasswordError('');
setCaptchaError('');
const trimmedEmail = email.trim();
const trimmedPassword = password.trim();
if (!trimmedEmail) {
setEmailError('Email is required');
valid = false;
@ -120,26 +160,35 @@ const Login = () => {
setEmailError('Enter a valid email address.');
valid = false;
}
if (!trimmedPassword) {
setPasswordError('Password is required');
valid = false;
}
if (!userCaptcha.trim()) {
setCaptchaError('Please enter the CAPTCHA.');
valid = false;
} else if (userCaptcha !== captcha) {
setCaptchaError('Invalid. Try again or refresh for a new code.');
valid = false;
if (failedAttempts >= SHOW_CAPTCHA_AFTER) {
if (!userCaptcha.trim()) {
setCaptchaError('Please enter the CAPTCHA.');
valid = false;
} else if (userCaptcha !== captcha) {
setCaptchaError('Invalid CAPTCHA. Try again or refresh for a new code.');
valid = false;
}
}
return valid;
};
};
const submit = async (e) => {
e.preventDefault();
if (isLocked) {
const minutes = Math.floor(remainingTime / 60);
const seconds = remainingTime % 60;
showToast('error', `Account locked. Please wait ${minutes}m ${seconds}s or contact support.`);
return;
}
if (!validateForm()) return;
setLoading(true);
@ -148,7 +197,7 @@ const Login = () => {
const response = await login({ email, password });
if (response?.status !== 'success' || !response?.data) {
throw new Error('Email or password is incorrect.');
throw new Error('Invalid credentials');
}
const token = response.data;
@ -157,10 +206,9 @@ const Login = () => {
let payload;
try {
const [, base64Payload] = token.split('.');
const json = atob(base64Payload);
payload = JSON.parse(json);
payload = JSON.parse(atob(base64Payload));
} catch {
throw new Error('Received invalid token.');
throw new Error('Invalid token format.');
}
const role = payload?.role ?? (payload?.is_admin ? 'Admin' : undefined);
@ -178,6 +226,16 @@ const Login = () => {
String(role || '').toLowerCase() === 'admin' || payload?.is_admin === true;
const successMessage = response?.message || 'Login successful.';
localStorage.removeItem('failed_attempts');
localStorage.removeItem('lock_until');
setFailedAttempts(0);
setIsLocked(false);
setRemainingTime(0);
if (countdownIntervalRef.current) {
clearInterval(countdownIntervalRef.current);
countdownIntervalRef.current = null;
}
if (isAdmin) {
sessionStorage.removeItem('establishment_id');
showToast('success', successMessage);
@ -204,12 +262,37 @@ const Login = () => {
}
} catch (err) {
console.error('Login failed', err);
showToast('error', 'Email or password is incorrect.');
const attempts = failedAttempts + 1;
localStorage.setItem('failed_attempts', attempts);
setFailedAttempts(attempts);
if (attempts >= SHOW_CAPTCHA_AFTER) {
refreshCaptcha();
}
if (attempts >= MAX_FAILED_ATTEMPTS) {
const lockUntil = Date.now() + LOCK_DURATION_MINUTES * 60 * 1000;
localStorage.setItem('lock_until', lockUntil);
setIsLocked(true);
setLockExpiry(lockUntil);
startCountdown(lockUntil);
showToast('error', `Too many failed attempts. Account locked for ${LOCK_DURATION_MINUTES} minutes.`);
} else {
const remainingAttempts = MAX_FAILED_ATTEMPTS - attempts;
showToast('error', `Email or password is incorrect. ${remainingAttempts} attempt${remainingAttempts !== 1 ? 's' : ''} remaining.`);
}
} finally {
setLoading(false);
}
};
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
return (
<div className="min-h-screen bg-[#F7F7F7] flex items-center justify-center px-4">
{loading && (
@ -221,13 +304,14 @@ const Login = () => {
{toast && (
<div className="fixed top-6 inset-x-0 z-50 flex justify-center px-4">
<div
className={`text-sm rounded px-3 py-2 border flex items-start justify-between gap-3 shadow ${
className={`text-sm rounded-lg px-4 py-3 border flex items-start justify-between gap-3 shadow-lg max-w-md ${
toast.type === 'success'
? 'bg-[#F3FAF4] border-[#C6E7D8] text-[#2F663C]'
: 'bg-[#FEF2F2] border-[#FECACA] text-[#B91C1C]'
}`}
>
<div className="flex items-start gap-2">
{toast.type === 'error' && <AlertCircle className="h-4 w-4 mt-0.5 flex-shrink-0" />}
<span>{toast.message}</span>
</div>
<button
@ -244,23 +328,49 @@ const Login = () => {
<div className="w-full max-w-md">
<div className="bg-white rounded-lg shadow-sm ring-1 ring-gray-200 overflow-hidden">
<div className="px-7 py-6 border-b border-gray-200 bg-[#F2ECCF] text-center">
<div className="mb-2">
<img
src={logoSrc}
alt="FCSC Logo"
className="mx-auto h-16 w-auto"
/>
</div>
<h1 className="text-[17px] font-semibold text-[#1F2937] mt-0">
Industrial Production Index (IPI) Survey Portal
</h1>
<div className="mb-2">
<img src={logoSrc} alt="FCSC Logo" className="mx-auto h-16 w-auto" />
</div>
<h1 className="text-[17px] font-semibold text-[#1F2937] mt-0">
Industrial Production Index (IPI) Survey Portal
</h1>
</div>
<form onSubmit={submit} className="px-7 py-8 space-y-4">
{isLocked && remainingTime > 0 && (
<div className="bg-red-50 border-l-4 border-red-500 p-4 rounded">
<div className="flex items-start gap-3">
<Clock className="h-5 w-5 text-red-500 mt-0.5 flex-shrink-0" />
<div className="flex-1">
<h3 className="text-sm font-semibold text-red-800 mb-1">
Account Temporarily Locked
</h3>
<p className="text-sm text-red-700 mb-2">
Too many failed login attempts. Please wait before trying again.
</p>
<div className="text-lg font-mono font-bold text-red-900">
{formatTime(remainingTime)}
</div>
</div>
</div>
</div>
)}
{!isLocked && failedAttempts > 0 && failedAttempts < MAX_FAILED_ATTEMPTS && (
<div className="bg-yellow-50 border-l-4 border-yellow-500 p-3 rounded">
<div className="flex items-start gap-2">
<AlertCircle className="h-4 w-4 text-yellow-600 mt-0.5 flex-shrink-0" />
<p className="text-sm text-yellow-800">
<strong>{MAX_FAILED_ATTEMPTS - failedAttempts}</strong> login attempt{MAX_FAILED_ATTEMPTS - failedAttempts !== 1 ? 's' : ''} remaining before account lock.
</p>
</div>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Email address</label>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email address
</label>
<input
type="text"
value={email}
@ -269,28 +379,44 @@ const Login = () => {
if (emailError) setEmailError('');
}}
placeholder="name@company.com"
disabled={isLocked}
className={`block w-full h-10 rounded-md border-2 px-4 text-sm focus:ring-0 ${
emailError ? 'border-red-500 bg-red-50' : 'border-[#92722A] bg-white'
emailError
? 'border-red-500 bg-red-50'
: isLocked
? 'border-gray-300 bg-gray-100 cursor-not-allowed'
: 'border-[#92722A] bg-white'
}`}
/>
{emailError && <p className="text-xs text-red-600 mt-1">{emailError}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Password</label>
<label className="block text-sm font-medium text-gray-700 mb-1">
Password
</label>
<div className="relative">
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
onChange={(e) => {
setPassword(e.target.value);
if (passwordError) setPasswordError('');
}}
disabled={isLocked}
className={`block w-full h-10 rounded-md border-2 px-4 pr-11 text-sm focus:ring-0 ${
passwordError ? 'border-red-500 bg-red-50' : 'border-[#92722A] bg-white'
passwordError
? 'border-red-500 bg-red-50'
: isLocked
? 'border-gray-300 bg-gray-100 cursor-not-allowed'
: 'border-[#92722A] bg-white'
}`}
/>
<button
type="button"
className="absolute inset-y-0 right-0 px-3 flex items-center cursor-pointer"
onClick={() => setShowPassword((v) => !v)}
disabled={isLocked}
aria-label="Toggle password visibility"
>
{showPassword ? (
@ -309,44 +435,55 @@ const Login = () => {
{passwordError && <p className="text-xs text-red-600 mt-1">{passwordError}</p>}
</div>
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 mb-1">
CAPTCHA Verification <span className="text-red-500">*</span>
</label>
{failedAttempts >= SHOW_CAPTCHA_AFTER && (
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 mb-1">
CAPTCHA Verification <span className="text-red-500">*</span>
</label>
<div className="flex items-center gap-2">
<div className="flex justify-center items-center font-mono text-lg tracking-widest bg-gray-100 border-2 border-[#92722A] rounded-md px-4 py-2 select-none h-10">
{captcha}
</div>
<div className="flex items-center gap-2">
<div className="flex justify-center items-center font-mono text-lg tracking-widest bg-gray-100 border-2 border-[#92722A] rounded-md px-4 py-2 select-none h-10">
{captcha}
</div>
<button
type="button"
onClick={refreshCaptcha}
className="flex items-center gap-1 text-[#92722A] hover:text-[#7b5c1f]"
title="Refresh CAPTCHA"
>
<RefreshCw className="h-4 w-4" />
<span className="text-sm leading-none">Refresh</span>
</button>
<button
type="button"
onClick={refreshCaptcha}
disabled={isLocked}
className={`flex items-center gap-1 ${
isLocked
? 'text-gray-400 cursor-not-allowed'
: 'text-[#92722A] hover:text-[#7b5c1f]'
}`}
title="Refresh CAPTCHA"
>
<RefreshCw className="h-4 w-4" />
<span className="text-sm leading-none">Refresh</span>
</button>
</div>
<p className="text-xs text-gray-500 mt-1">
Enter the characters shown above. Can't read it? Click refresh for a new code.
</p>
<input
type="text"
id="captcha"
value={userCaptcha}
onChange={handleCaptchaChange}
placeholder="Type the characters"
disabled={isLocked}
className={`mt-2 block w-full h-10 rounded-md border-2 px-4 text-sm focus:ring-0 ${
captchaError
? 'border-red-500 bg-red-50'
: isLocked
? 'border-gray-300 bg-gray-100 cursor-not-allowed'
: 'border-[#92722A] bg-white'
}`}
/>
{captchaError && <p className="text-xs text-red-600 mt-1">{captchaError}</p>}
</div>
<p className="text-xs text-gray-500 mt-1">
Enter the characters in the image. Can't read it? Refresh for a new code.
</p>
<input
type="text"
id="captcha"
value={userCaptcha}
onChange={handleCaptchaChange}
placeholder="Type the characters"
className={`mt-2 block w-full h-10 rounded-md border-2 px-4 text-sm focus:ring-0 ${
captchaError ? 'border-red-500 bg-red-50' : 'border-[#92722A] bg-white'
}`}
/>
{captchaError && <p className="text-xs text-red-600 mt-1">{captchaError}</p>}
</div>
)}
<div className="flex items-center justify-between text-sm">
<label className="inline-flex items-center gap-2 cursor-pointer">
@ -355,8 +492,11 @@ const Login = () => {
className="h-4 w-4 text-[#92722A] border-gray-300 rounded cursor-pointer"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
disabled={isLocked}
/>
<span className="text-gray-700">Keep me signed in on this device</span>
<span className={`${isLocked ? 'text-gray-400' : 'text-gray-700'}`}>
Keep me signed in on this device
</span>
</label>
<button
type="button"
@ -369,14 +509,24 @@ const Login = () => {
<button
type="submit"
disabled={loading || !userCaptcha.trim()}
className={`inline-flex items-center justify-center w-full h-12 rounded-lg text-white ${
loading || !userCaptcha.trim()
disabled={
loading ||
isLocked ||
(failedAttempts >= SHOW_CAPTCHA_AFTER && !userCaptcha.trim())
}
className={`inline-flex items-center justify-center w-full h-12 rounded-lg text-white font-medium ${
isLocked
? 'bg-gray-400 cursor-not-allowed'
: loading
? 'bg-[#bfa06a] cursor-not-allowed'
: 'bg-[#92722A] hover:bg-[#7b5c1f]'
} transition-colors`}
>
{loading ? 'Signing in…' : 'Sign in'}
{isLocked
? `Locked (${formatTime(remainingTime)})`
: loading
? 'Signing in…'
: 'Sign in'}
</button>
</form>
</div>
@ -385,4 +535,4 @@ const Login = () => {
);
};
export default Login;
export default Login;