diff --git a/ipi-survey-platform/src/pages/Admin/configuration/CompanyProfile.jsx b/ipi-survey-platform/src/pages/Admin/configuration/CompanyProfile.jsx index 3fad0c0..6a3918d 100644 --- a/ipi-survey-platform/src/pages/Admin/configuration/CompanyProfile.jsx +++ b/ipi-survey-platform/src/pages/Admin/configuration/CompanyProfile.jsx @@ -1782,7 +1782,7 @@ const CompanyProfile = () => { try { const countResponse = await fetchEstablishments({ params: countParams, signal: controller.signal }); const countPayload = countResponse?.data ?? countResponse; - const totalRecords = countPayload?.pagination?.total_records || 0; + const totalRecords = countResponse?.pagination?.total_records || 0; const params = { search: debouncedSearch || undefined, diff --git a/ipi-survey-platform/src/pages/Login/Login.jsx b/ipi-survey-platform/src/pages/Login/Login.jsx index 1189f28..5ae7946 100644 --- a/ipi-survey-platform/src/pages/Login/Login.jsx +++ b/ipi-survey-platform/src/pages/Login/Login.jsx @@ -1,50 +1,36 @@ import React from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { login } from '@/services/auth/authService.js'; -import { RefreshCw, AlertCircle, Clock } from "lucide-react"; - +import { AlertCircle, Clock } from "lucide-react"; + const logoSrc = '/assets/images/FCSCLogo.svg'; - + const MAX_FAILED_ATTEMPTS = 3; -const SHOW_CAPTCHA_AFTER = 0; -const LOCK_DURATION_MINUTES = 2; - +const LOCK_DURATION_MINUTES = 2; + const Login = () => { const navigate = useNavigate(); const location = useLocation(); - + const [email, setEmail] = React.useState(''); const [password, setPassword] = React.useState(''); const [showPassword, setShowPassword] = React.useState(false); const [rememberMe, setRememberMe] = React.useState(true); const [loading, setLoading] = React.useState(false); const [toast, setToast] = React.useState(null); - + const [emailError, setEmailError] = React.useState(''); const [passwordError, setPasswordError] = React.useState(''); - - const [captcha, setCaptcha] = React.useState(''); - 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'; - let code = ''; - for (let i = 0; i < 6; i++) { - code += chars.charAt(Math.floor(Math.random() * chars.length)); - } - setCaptcha(code); - }, []); - + const closeToast = React.useCallback(() => { if (toastTimeoutRef.current) { clearTimeout(toastTimeoutRef.current); @@ -52,7 +38,7 @@ const Login = () => { } setToast(null); }, []); - + const showToast = React.useCallback((type, message) => { if (!message) return; if (toastTimeoutRef.current) { @@ -65,7 +51,7 @@ const Login = () => { toastTimeoutRef.current = null; }, 4000); }, []); - + const scheduleNavigate = React.useCallback( (path) => { if (!path) return; @@ -80,18 +66,18 @@ const Login = () => { }, [navigate] ); - + 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; @@ -101,20 +87,21 @@ const Login = () => { showToast('success', 'Account unlocked. You can try logging in again.'); } }; - + updateTimer(); countdownIntervalRef.current = setInterval(updateTimer, 1000); }, [showToast]); - + + // Check lock status when email changes React.useEffect(() => { - generateCaptcha(); - + if (!email) return; + // Get the stored attempts and lock status for the current email const storedAttempts = JSON.parse(localStorage.getItem('login_attempts') || '{}'); const emailAttempts = storedAttempts[email] || { attempts: 0, lockUntil: null }; - + setFailedAttempts(emailAttempts.attempts); - + if (emailAttempts.lockUntil && Date.now() < emailAttempts.lockUntil) { setIsLocked(true); setLockExpiry(emailAttempts.lockUntil); @@ -128,8 +115,8 @@ const Login = () => { } else { setIsLocked(false); } - }, [generateCaptcha, startCountdown, email]); - + }, [email, startCountdown]); + React.useEffect( () => () => { if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current); @@ -138,29 +125,17 @@ const Login = () => { }, [] ); - - const handleCaptchaChange = (e) => { - setUserCaptcha(e.target.value.toUpperCase()); - if (captchaError) setCaptchaError(''); - }; - - const refreshCaptcha = () => { - generateCaptcha(); - setUserCaptcha(''); - setCaptchaError(''); - }; - + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - + const validateForm = () => { let valid = true; setEmailError(''); setPasswordError(''); - setCaptchaError(''); - + const trimmedEmail = email.trim(); const trimmedPassword = password.trim(); - + if (!trimmedEmail) { setEmailError('Email is required'); valid = false; @@ -168,47 +143,39 @@ 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 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); - + try { const response = await login({ email, password }); - + if (response?.status !== 'success' || !response?.data) { throw new Error('Invalid credentials'); } - + const token = response.data; sessionStorage.setItem('auth_token', token); - + let payload; try { const [, base64Payload] = token.split('.'); @@ -216,29 +183,29 @@ const Login = () => { } catch { throw new Error('Invalid token format.'); } - + const role = payload?.role ?? (payload?.is_admin ? 'Admin' : undefined); if (role) sessionStorage.setItem('user_role', role); - + const profile = { id: payload?.id || payload?.user_id || '', name: payload?.name || payload?.username || '', email: payload?.email || '', }; sessionStorage.setItem('user_profile', JSON.stringify(profile)); - + const establishmentId = payload?.establishment_id ?? payload?.establishmentId; const isAdmin = String(role || '').toLowerCase() === 'admin' || payload?.is_admin === true; const successMessage = response?.message || 'Login successful.'; - + // Clear login attempts for this email const storedAttempts = JSON.parse(localStorage.getItem('login_attempts') || '{}'); if (storedAttempts[email]) { delete storedAttempts[email]; localStorage.setItem('login_attempts', JSON.stringify(storedAttempts)); } - + setFailedAttempts(0); setIsLocked(false); setRemainingTime(0); @@ -246,14 +213,14 @@ const Login = () => { clearInterval(countdownIntervalRef.current); countdownIntervalRef.current = null; } - + if (isAdmin) { sessionStorage.removeItem('establishment_id'); showToast('success', successMessage); scheduleNavigate('/admin/dashboard'); return; } - + if (rememberMe) { sessionStorage.setItem('remember_me', 'true'); sessionStorage.setItem('remembered_email', email); @@ -263,7 +230,7 @@ const Login = () => { sessionStorage.removeItem('remembered_email'); sessionStorage.removeItem('remembered_password'); } - + if (establishmentId) { sessionStorage.setItem('establishment_id', String(establishmentId)); showToast('success', successMessage); @@ -273,26 +240,24 @@ const Login = () => { } } catch (err) { console.error('Login failed', err); - + const storedAttempts = JSON.parse(localStorage.getItem('login_attempts') || '{}'); const attempts = (storedAttempts[email]?.attempts || 0) + 1; - + // Update attempts for this email const updatedAttempts = { ...storedAttempts, [email]: { attempts, - lockUntil: attempts >= MAX_FAILED_ATTEMPTS - ? Date.now() + LOCK_DURATION_MINUTES * 60 * 1000 + lockUntil: attempts >= MAX_FAILED_ATTEMPTS + ? Date.now() + LOCK_DURATION_MINUTES * 60 * 1000 : null } }; - + localStorage.setItem('login_attempts', JSON.stringify(updatedAttempts)); setFailedAttempts(attempts); - - refreshCaptcha(); - + if (attempts >= MAX_FAILED_ATTEMPTS) { const lockUntil = Date.now() + LOCK_DURATION_MINUTES * 60 * 1000; setIsLocked(true); @@ -307,13 +272,13 @@ const Login = () => { setLoading(false); } }; - + const formatTime = (seconds) => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins}:${secs.toString().padStart(2, '0')}`; }; - + return (
{passwordError}
}- Enter the characters shown above. Can't read it? Click refresh for a new code. -
- - - {captchaError &&{captchaError}
} -{errors.establishmentName}
)}- Enter the characters in the image. Can't read it? Refresh for a new code. - -
- - {captchaError &&{captchaError}
} -{error}
} ); - + export default PublicContactForm; \ No newline at end of file