From 004fd73cea36d5a60be636c71ed8c22c44543a77 Mon Sep 17 00:00:00 2001 From: Senthamilselvi Date: Wed, 5 Nov 2025 10:19:28 +0530 Subject: [PATCH] login page bugs solved --- ipi-survey-platform/src/App.jsx | 371 +++++++++++------- ipi-survey-platform/src/pages/Login/Login.jsx | 320 +++++++++++---- 2 files changed, 467 insertions(+), 224 deletions(-) diff --git a/ipi-survey-platform/src/App.jsx b/ipi-survey-platform/src/App.jsx index 4c77988..dba9215 100644 --- a/ipi-survey-platform/src/App.jsx +++ b/ipi-survey-platform/src/App.jsx @@ -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 -} + return ; +}; + +const SessionWarningModal = ({ show, remainingTime, onExtend, onLogout }) => { + if (!show) return null; + + const minutes = Math.floor(remainingTime / 60); + const seconds = remainingTime % 60; + + return ( +
+
+
+
+ + + +
+
+

+ Session Expiring Soon +

+

+ Your session will expire in {minutes}:{seconds.toString().padStart(2, '0')} due to inactivity. +

+

+ Would you like to stay signed in? +

+
+ + +
+
+
+
+
+ ); +}; 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 ( - - } /> - - - - )} + <> + window.handleExtendSession?.()} + onLogout={() => window.handleLogoutNow?.()} /> - - - - )} - /> - - - - )} - /> - - - - )} - /> - - - - )} - /> - - + } /> + - + + - } + } /> - + + + } + /> + - + - } + } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - + - } + } /> - - - - - )} - /> - } /> - - - - )} - /> - - - - )} - /> - - + + + } + /> + + + + + } + /> + + + + + } + /> + } /> + - + - } + } /> - - + - } + } /> - - } /> - } /> - } /> - } /> - - ) + + + + + } + /> + + } /> + } /> + } /> + } /> + + + ); } -export default App +export default App; \ No newline at end of file diff --git a/ipi-survey-platform/src/pages/Login/Login.jsx b/ipi-survey-platform/src/pages/Login/Login.jsx index 32624ab..13e82bd 100644 --- a/ipi-survey-platform/src/pages/Login/Login.jsx +++ b/ipi-survey-platform/src/pages/Login/Login.jsx @@ -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 (
{loading && ( @@ -221,13 +304,14 @@ const Login = () => { {toast && (
+ {toast.type === 'error' && } {toast.message}
-
- + {failedAttempts >= SHOW_CAPTCHA_AFTER && ( +
+ -
-
- {captcha} -
+
+
+ {captcha} +
- + +
+ +

+ Enter the characters shown above. Can't read it? Click refresh for a new code. +

+ + + {captchaError &&

{captchaError}

}
- -

- Enter the characters in the image. Can't read it? Refresh for a new code. -

- - - {captchaError &&

{captchaError}

} -
+ )}
@@ -385,4 +535,4 @@ const Login = () => { ); }; -export default Login; +export default Login; \ No newline at end of file