removed captacha

This commit is contained in:
Malini 2025-11-21 13:14:45 +05:30
parent 9745092a33
commit de3a72ca35
3 changed files with 129 additions and 277 deletions

View File

@ -1782,7 +1782,7 @@ const CompanyProfile = () => {
try { try {
const countResponse = await fetchEstablishments({ params: countParams, signal: controller.signal }); const countResponse = await fetchEstablishments({ params: countParams, signal: controller.signal });
const countPayload = countResponse?.data ?? countResponse; const countPayload = countResponse?.data ?? countResponse;
const totalRecords = countPayload?.pagination?.total_records || 0; const totalRecords = countResponse?.pagination?.total_records || 0;
const params = { const params = {
search: debouncedSearch || undefined, search: debouncedSearch || undefined,

View File

@ -1,50 +1,36 @@
import React from 'react'; import React from 'react';
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import { login } from '@/services/auth/authService.js'; 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 logoSrc = '/assets/images/FCSCLogo.svg';
const MAX_FAILED_ATTEMPTS = 3; const MAX_FAILED_ATTEMPTS = 3;
const SHOW_CAPTCHA_AFTER = 0; const LOCK_DURATION_MINUTES = 2;
const LOCK_DURATION_MINUTES = 2;
const Login = () => { const Login = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const [email, setEmail] = React.useState(''); const [email, setEmail] = React.useState('');
const [password, setPassword] = React.useState(''); const [password, setPassword] = React.useState('');
const [showPassword, setShowPassword] = React.useState(false); const [showPassword, setShowPassword] = React.useState(false);
const [rememberMe, setRememberMe] = React.useState(true); const [rememberMe, setRememberMe] = React.useState(true);
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const [toast, setToast] = React.useState(null); const [toast, setToast] = React.useState(null);
const [emailError, setEmailError] = React.useState(''); const [emailError, setEmailError] = React.useState('');
const [passwordError, setPasswordError] = 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 [isLocked, setIsLocked] = React.useState(false);
const [lockExpiry, setLockExpiry] = React.useState(null); const [lockExpiry, setLockExpiry] = React.useState(null);
const [failedAttempts, setFailedAttempts] = React.useState(0); const [failedAttempts, setFailedAttempts] = React.useState(0);
const [remainingTime, setRemainingTime] = React.useState(0); const [remainingTime, setRemainingTime] = React.useState(0);
const toastTimeoutRef = React.useRef(null); const toastTimeoutRef = React.useRef(null);
const navigateTimeoutRef = React.useRef(null); const navigateTimeoutRef = React.useRef(null);
const countdownIntervalRef = 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(() => { const closeToast = React.useCallback(() => {
if (toastTimeoutRef.current) { if (toastTimeoutRef.current) {
clearTimeout(toastTimeoutRef.current); clearTimeout(toastTimeoutRef.current);
@ -52,7 +38,7 @@ const Login = () => {
} }
setToast(null); setToast(null);
}, []); }, []);
const showToast = React.useCallback((type, message) => { const showToast = React.useCallback((type, message) => {
if (!message) return; if (!message) return;
if (toastTimeoutRef.current) { if (toastTimeoutRef.current) {
@ -65,7 +51,7 @@ const Login = () => {
toastTimeoutRef.current = null; toastTimeoutRef.current = null;
}, 4000); }, 4000);
}, []); }, []);
const scheduleNavigate = React.useCallback( const scheduleNavigate = React.useCallback(
(path) => { (path) => {
if (!path) return; if (!path) return;
@ -80,18 +66,18 @@ const Login = () => {
}, },
[navigate] [navigate]
); );
const startCountdown = React.useCallback((lockUntil) => { const startCountdown = React.useCallback((lockUntil) => {
if (countdownIntervalRef.current) { if (countdownIntervalRef.current) {
clearInterval(countdownIntervalRef.current); clearInterval(countdownIntervalRef.current);
} }
const updateTimer = () => { const updateTimer = () => {
const now = Date.now(); const now = Date.now();
const remaining = Math.max(0, Math.ceil((parseInt(lockUntil, 10) - now) / 1000)); const remaining = Math.max(0, Math.ceil((parseInt(lockUntil, 10) - now) / 1000));
setRemainingTime(remaining); setRemainingTime(remaining);
if (remaining <= 0) { if (remaining <= 0) {
clearInterval(countdownIntervalRef.current); clearInterval(countdownIntervalRef.current);
countdownIntervalRef.current = null; countdownIntervalRef.current = null;
@ -101,20 +87,21 @@ const Login = () => {
showToast('success', 'Account unlocked. You can try logging in again.'); showToast('success', 'Account unlocked. You can try logging in again.');
} }
}; };
updateTimer(); updateTimer();
countdownIntervalRef.current = setInterval(updateTimer, 1000); countdownIntervalRef.current = setInterval(updateTimer, 1000);
}, [showToast]); }, [showToast]);
// Check lock status when email changes
React.useEffect(() => { React.useEffect(() => {
generateCaptcha(); if (!email) return;
// Get the stored attempts and lock status for the current email // Get the stored attempts and lock status for the current email
const storedAttempts = JSON.parse(localStorage.getItem('login_attempts') || '{}'); const storedAttempts = JSON.parse(localStorage.getItem('login_attempts') || '{}');
const emailAttempts = storedAttempts[email] || { attempts: 0, lockUntil: null }; const emailAttempts = storedAttempts[email] || { attempts: 0, lockUntil: null };
setFailedAttempts(emailAttempts.attempts); setFailedAttempts(emailAttempts.attempts);
if (emailAttempts.lockUntil && Date.now() < emailAttempts.lockUntil) { if (emailAttempts.lockUntil && Date.now() < emailAttempts.lockUntil) {
setIsLocked(true); setIsLocked(true);
setLockExpiry(emailAttempts.lockUntil); setLockExpiry(emailAttempts.lockUntil);
@ -128,8 +115,8 @@ const Login = () => {
} else { } else {
setIsLocked(false); setIsLocked(false);
} }
}, [generateCaptcha, startCountdown, email]); }, [email, startCountdown]);
React.useEffect( React.useEffect(
() => () => { () => () => {
if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current); 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 emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const validateForm = () => { const validateForm = () => {
let valid = true; let valid = true;
setEmailError(''); setEmailError('');
setPasswordError(''); setPasswordError('');
setCaptchaError('');
const trimmedEmail = email.trim(); const trimmedEmail = email.trim();
const trimmedPassword = password.trim(); const trimmedPassword = password.trim();
if (!trimmedEmail) { if (!trimmedEmail) {
setEmailError('Email is required'); setEmailError('Email is required');
valid = false; valid = false;
@ -168,47 +143,39 @@ const Login = () => {
setEmailError('Enter a valid email address.'); setEmailError('Enter a valid email address.');
valid = false; valid = false;
} }
if (!trimmedPassword) { if (!trimmedPassword) {
setPasswordError('Password is required'); setPasswordError('Password is required');
valid = false; 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; return valid;
}; };
const submit = async (e) => { const submit = async (e) => {
e.preventDefault(); e.preventDefault();
if (isLocked) { if (isLocked) {
const minutes = Math.floor(remainingTime / 60); const minutes = Math.floor(remainingTime / 60);
const seconds = remainingTime % 60; const seconds = remainingTime % 60;
showToast('error', `Account locked. Please wait ${minutes}m ${seconds}s or contact support.`); showToast('error', `Account locked. Please wait ${minutes}m ${seconds}s or contact support.`);
return; return;
} }
if (!validateForm()) return; if (!validateForm()) return;
setLoading(true); setLoading(true);
try { try {
const response = await login({ email, password }); const response = await login({ email, password });
if (response?.status !== 'success' || !response?.data) { if (response?.status !== 'success' || !response?.data) {
throw new Error('Invalid credentials'); throw new Error('Invalid credentials');
} }
const token = response.data; const token = response.data;
sessionStorage.setItem('auth_token', token); sessionStorage.setItem('auth_token', token);
let payload; let payload;
try { try {
const [, base64Payload] = token.split('.'); const [, base64Payload] = token.split('.');
@ -216,29 +183,29 @@ const Login = () => {
} catch { } catch {
throw new Error('Invalid token format.'); throw new Error('Invalid token format.');
} }
const role = payload?.role ?? (payload?.is_admin ? 'Admin' : undefined); const role = payload?.role ?? (payload?.is_admin ? 'Admin' : undefined);
if (role) sessionStorage.setItem('user_role', role); if (role) sessionStorage.setItem('user_role', role);
const profile = { const profile = {
id: payload?.id || payload?.user_id || '', id: payload?.id || payload?.user_id || '',
name: payload?.name || payload?.username || '', name: payload?.name || payload?.username || '',
email: payload?.email || '', email: payload?.email || '',
}; };
sessionStorage.setItem('user_profile', JSON.stringify(profile)); sessionStorage.setItem('user_profile', JSON.stringify(profile));
const establishmentId = payload?.establishment_id ?? payload?.establishmentId; const establishmentId = payload?.establishment_id ?? payload?.establishmentId;
const isAdmin = const isAdmin =
String(role || '').toLowerCase() === 'admin' || payload?.is_admin === true; String(role || '').toLowerCase() === 'admin' || payload?.is_admin === true;
const successMessage = response?.message || 'Login successful.'; const successMessage = response?.message || 'Login successful.';
// Clear login attempts for this email // Clear login attempts for this email
const storedAttempts = JSON.parse(localStorage.getItem('login_attempts') || '{}'); const storedAttempts = JSON.parse(localStorage.getItem('login_attempts') || '{}');
if (storedAttempts[email]) { if (storedAttempts[email]) {
delete storedAttempts[email]; delete storedAttempts[email];
localStorage.setItem('login_attempts', JSON.stringify(storedAttempts)); localStorage.setItem('login_attempts', JSON.stringify(storedAttempts));
} }
setFailedAttempts(0); setFailedAttempts(0);
setIsLocked(false); setIsLocked(false);
setRemainingTime(0); setRemainingTime(0);
@ -246,14 +213,14 @@ const Login = () => {
clearInterval(countdownIntervalRef.current); clearInterval(countdownIntervalRef.current);
countdownIntervalRef.current = null; countdownIntervalRef.current = null;
} }
if (isAdmin) { if (isAdmin) {
sessionStorage.removeItem('establishment_id'); sessionStorage.removeItem('establishment_id');
showToast('success', successMessage); showToast('success', successMessage);
scheduleNavigate('/admin/dashboard'); scheduleNavigate('/admin/dashboard');
return; return;
} }
if (rememberMe) { if (rememberMe) {
sessionStorage.setItem('remember_me', 'true'); sessionStorage.setItem('remember_me', 'true');
sessionStorage.setItem('remembered_email', email); sessionStorage.setItem('remembered_email', email);
@ -263,7 +230,7 @@ const Login = () => {
sessionStorage.removeItem('remembered_email'); sessionStorage.removeItem('remembered_email');
sessionStorage.removeItem('remembered_password'); sessionStorage.removeItem('remembered_password');
} }
if (establishmentId) { if (establishmentId) {
sessionStorage.setItem('establishment_id', String(establishmentId)); sessionStorage.setItem('establishment_id', String(establishmentId));
showToast('success', successMessage); showToast('success', successMessage);
@ -273,26 +240,24 @@ const Login = () => {
} }
} catch (err) { } catch (err) {
console.error('Login failed', err); console.error('Login failed', err);
const storedAttempts = JSON.parse(localStorage.getItem('login_attempts') || '{}'); const storedAttempts = JSON.parse(localStorage.getItem('login_attempts') || '{}');
const attempts = (storedAttempts[email]?.attempts || 0) + 1; const attempts = (storedAttempts[email]?.attempts || 0) + 1;
// Update attempts for this email // Update attempts for this email
const updatedAttempts = { const updatedAttempts = {
...storedAttempts, ...storedAttempts,
[email]: { [email]: {
attempts, attempts,
lockUntil: attempts >= MAX_FAILED_ATTEMPTS lockUntil: attempts >= MAX_FAILED_ATTEMPTS
? Date.now() + LOCK_DURATION_MINUTES * 60 * 1000 ? Date.now() + LOCK_DURATION_MINUTES * 60 * 1000
: null : null
} }
}; };
localStorage.setItem('login_attempts', JSON.stringify(updatedAttempts)); localStorage.setItem('login_attempts', JSON.stringify(updatedAttempts));
setFailedAttempts(attempts); setFailedAttempts(attempts);
refreshCaptcha();
if (attempts >= MAX_FAILED_ATTEMPTS) { if (attempts >= MAX_FAILED_ATTEMPTS) {
const lockUntil = Date.now() + LOCK_DURATION_MINUTES * 60 * 1000; const lockUntil = Date.now() + LOCK_DURATION_MINUTES * 60 * 1000;
setIsLocked(true); setIsLocked(true);
@ -307,13 +272,13 @@ const Login = () => {
setLoading(false); setLoading(false);
} }
}; };
const formatTime = (seconds) => { const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60); const mins = Math.floor(seconds / 60);
const secs = seconds % 60; const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`; return `${mins}:${secs.toString().padStart(2, '0')}`;
}; };
return ( return (
<div className="min-h-screen bg-[#F7F7F7] flex items-center justify-center px-4"> <div className="min-h-screen bg-[#F7F7F7] flex items-center justify-center px-4">
{loading && ( {loading && (
@ -321,7 +286,7 @@ const Login = () => {
<div className="h-12 w-12 animate-spin rounded-full border-[3px] border-white border-t-transparent" /> <div className="h-12 w-12 animate-spin rounded-full border-[3px] border-white border-t-transparent" />
</div> </div>
)} )}
{toast && ( {toast && (
<div className="fixed top-6 inset-x-0 z-50 flex justify-center px-4"> <div className="fixed top-6 inset-x-0 z-50 flex justify-center px-4">
<div <div
@ -345,7 +310,7 @@ const Login = () => {
</div> </div>
</div> </div>
)} )}
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<div className="bg-white rounded-lg shadow-sm ring-1 ring-gray-200 overflow-hidden"> <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="px-7 py-6 border-b border-gray-200 bg-[#F2ECCF] text-center">
@ -356,7 +321,7 @@ const Login = () => {
Industrial Production Index (IPI) Survey Portal Industrial Production Index (IPI) Survey Portal
</h1> </h1>
</div> </div>
<form onSubmit={submit} className="px-7 py-8 space-y-4"> <form onSubmit={submit} className="px-7 py-8 space-y-4">
{isLocked && remainingTime > 0 && ( {isLocked && remainingTime > 0 && (
<div className="bg-red-50 border-l-4 border-red-500 p-4 rounded"> <div className="bg-red-50 border-l-4 border-red-500 p-4 rounded">
@ -376,7 +341,7 @@ const Login = () => {
</div> </div>
</div> </div>
)} )}
{!isLocked && failedAttempts > 0 && failedAttempts < MAX_FAILED_ATTEMPTS && ( {!isLocked && failedAttempts > 0 && failedAttempts < MAX_FAILED_ATTEMPTS && (
<div className="bg-yellow-50 border-l-4 border-yellow-500 p-3 rounded"> <div className="bg-yellow-50 border-l-4 border-yellow-500 p-3 rounded">
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
@ -387,7 +352,7 @@ const Login = () => {
</div> </div>
</div> </div>
)} )}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Email address Email address
@ -402,16 +367,16 @@ const Login = () => {
placeholder="name@company.com" placeholder="name@company.com"
disabled={isLocked} disabled={isLocked}
className={`block w-full h-10 rounded-md border-2 px-4 text-sm focus:ring-0 ${ className={`block w-full h-10 rounded-md border-2 px-4 text-sm focus:ring-0 ${
emailError emailError
? 'border-red-500 bg-red-50' ? 'border-red-500 bg-red-50'
: isLocked : isLocked
? 'border-gray-300 bg-gray-100 cursor-not-allowed' ? 'border-gray-300 bg-gray-100 cursor-not-allowed'
: 'border-[#92722A] bg-white' : 'border-[#92722A] bg-white'
}`} }`}
/> />
{emailError && <p className="text-xs text-red-600 mt-1">{emailError}</p>} {emailError && <p className="text-xs text-red-600 mt-1">{emailError}</p>}
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Password Password
@ -426,10 +391,10 @@ const Login = () => {
}} }}
disabled={isLocked} disabled={isLocked}
className={`block w-full h-10 rounded-md border-2 px-4 pr-11 text-sm focus:ring-0 ${ className={`block w-full h-10 rounded-md border-2 px-4 pr-11 text-sm focus:ring-0 ${
passwordError passwordError
? 'border-red-500 bg-red-50' ? 'border-red-500 bg-red-50'
: isLocked : isLocked
? 'border-gray-300 bg-gray-100 cursor-not-allowed' ? 'border-gray-300 bg-gray-100 cursor-not-allowed'
: 'border-[#92722A] bg-white' : 'border-[#92722A] bg-white'
}`} }`}
/> />
@ -441,7 +406,7 @@ const Login = () => {
aria-label="Toggle password visibility" aria-label="Toggle password visibility"
> >
{showPassword ? ( {showPassword ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-[#92722A]" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-[#92722A]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8S1 12 1 12z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8S1 12 1 12z" />
<circle cx="12" cy="12" r="3" strokeWidth="2" /> <circle cx="12" cy="12" r="3" strokeWidth="2" />
@ -456,56 +421,7 @@ const Login = () => {
</div> </div>
{passwordError && <p className="text-xs text-red-600 mt-1">{passwordError}</p>} {passwordError && <p className="text-xs text-red-600 mt-1">{passwordError}</p>}
</div> </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>
<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}
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>
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-sm">
<label className="inline-flex items-center gap-2 cursor-pointer"> <label className="inline-flex items-center gap-2 cursor-pointer">
<input <input
@ -527,13 +443,12 @@ const Login = () => {
Forgot password? Forgot password?
</button> </button>
</div> </div>
<button <button
type="submit" type="submit"
disabled={ disabled={
loading || loading ||
isLocked || isLocked
!userCaptcha.trim()
} }
className={`inline-flex items-center justify-center w-full h-12 rounded-lg text-white font-medium ${ className={`inline-flex items-center justify-center w-full h-12 rounded-lg text-white font-medium ${
isLocked isLocked
@ -543,10 +458,10 @@ const Login = () => {
: 'bg-[#92722A] hover:bg-[#7b5c1f]' : 'bg-[#92722A] hover:bg-[#7b5c1f]'
} transition-colors`} } transition-colors`}
> >
{isLocked {isLocked
? `Locked (${formatTime(remainingTime)})` ? `Locked (${formatTime(remainingTime)})`
: loading : loading
? 'Signing in…' ? 'Signing in…'
: 'Sign in'} : 'Sign in'}
</button> </button>
</form> </form>
@ -555,5 +470,6 @@ const Login = () => {
</div> </div>
); );
}; };
export default Login; export default Login;

View File

@ -1,48 +1,32 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import apiClient from '@/services/api/apiClient'; import apiClient from '@/services/api/apiClient';
import { RefreshCw } from "lucide-react";
const logoSrc = '/assets/images/FCSCLogo.svg'; const logoSrc = '/assets/images/FCSCLogo.svg';
const PublicContactForm = () => { const PublicContactForm = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
userType: "establishment_user", userType: "establishment_user",
establishmentName: "", establishmentName: "",
establishmentId: "", establishmentId: "",
email: "", email: "",
}); });
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
const [isValid, setIsValid] = useState(false); const [isValid, setIsValid] = useState(false);
const [captcha, setCaptcha] = useState("");
const [userCaptcha, setUserCaptcha] = useState("");
const [captchaError, setCaptchaError] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(""); const [success, setSuccess] = useState("");
const [errorMsg, setErrorMsg] = useState(""); const [errorMsg, setErrorMsg] = useState("");
const [otpStep, setOtpStep] = useState(false); const [otpStep, setOtpStep] = useState(false);
const [otp, setOtp] = useState(""); const [otp, setOtp] = useState("");
const [otpMsg, setOtpMsg] = useState(""); const [otpMsg, setOtpMsg] = useState("");
const [otpError, setOtpError] = useState(""); const [otpError, setOtpError] = useState("");
const [toast, setToast] = useState(null); const [toast, setToast] = useState(null);
const toastTimeoutRef = React.useRef(null); const toastTimeoutRef = React.useRef(null);
const generateCaptcha = () => {
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
let code = "";
for (let i = 0; i < 6; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length));
}
setCaptcha(code);
};
useEffect(() => {
generateCaptcha();
}, []);
useEffect(() => { useEffect(() => {
return () => { return () => {
if (toastTimeoutRef.current) { if (toastTimeoutRef.current) {
@ -50,7 +34,7 @@ const PublicContactForm = () => {
} }
}; };
}, []); }, []);
const showToast = (type, message) => { const showToast = (type, message) => {
if (!message) return; if (!message) return;
if (toastTimeoutRef.current) { if (toastTimeoutRef.current) {
@ -63,7 +47,7 @@ const PublicContactForm = () => {
toastTimeoutRef.current = null; toastTimeoutRef.current = null;
}, 4000); }, 4000);
}; };
const closeToast = () => { const closeToast = () => {
if (toastTimeoutRef.current) { if (toastTimeoutRef.current) {
window.clearTimeout(toastTimeoutRef.current); window.clearTimeout(toastTimeoutRef.current);
@ -71,52 +55,41 @@ const PublicContactForm = () => {
} }
setToast(null); setToast(null);
}; };
const validate = (data = formData) => { const validate = (data = formData) => {
const newErrors = {}; const newErrors = {};
if (!data.userType) { if (!data.userType) {
newErrors.userType = "Please select user type"; newErrors.userType = "Please select user type";
} else if (data.userType === 'establishment_user') { } else if (data.userType === 'establishment_user') {
if (!data.establishmentName.trim()) newErrors.establishmentName = "Required."; if (!data.establishmentName.trim()) newErrors.establishmentName = "Required.";
if (!data.establishmentId.trim()) newErrors.establishmentId = "Required."; if (!data.establishmentId.trim()) newErrors.establishmentId = "Required.";
} }
if (!data.email.trim()) newErrors.email = "Required."; if (!data.email.trim()) newErrors.email = "Required.";
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email))
newErrors.email = "Enter a valid email address."; newErrors.email = "Enter a valid email address.";
setErrors(newErrors); setErrors(newErrors);
setIsValid(Object.keys(newErrors).length === 0); setIsValid(Object.keys(newErrors).length === 0);
return newErrors; return newErrors;
}; };
const handleChange = (e) => { const handleChange = (e) => {
const { name, value } = e.target; const { name, value } = e.target;
const updated = { ...formData, [name]: value }; const updated = { ...formData, [name]: value };
setFormData(updated); setFormData(updated);
validate(updated); validate(updated);
}; };
const handleCaptchaChange = (e) => {
setUserCaptcha(e.target.value.toUpperCase());
setCaptchaError("");
};
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
setErrorMsg(""); setErrorMsg("");
setSuccess(""); setSuccess("");
const validationErrors = validate(); const validationErrors = validate();
if (Object.keys(validationErrors).length > 0) return; if (Object.keys(validationErrors).length > 0) return;
if (userCaptcha !== captcha) {
setCaptchaError("Incorrect CAPTCHA. Please try again.");
generateCaptcha();
return;
}
setLoading(true); setLoading(true);
try { try {
const payload = { const payload = {
@ -125,14 +98,14 @@ const PublicContactForm = () => {
establishment_code: formData.establishmentId.trim(), establishment_code: formData.establishmentId.trim(),
registered_email: formData.email.trim(), registered_email: formData.email.trim(),
}; };
const response = await apiClient.post("/forgot-password/request-otp", payload); const response = await apiClient.post("/forgot-password/request-otp", payload);
if (response?.data) { if (response?.data) {
showToast('success', 'Verification Code sent to your registered email successfully!'); showToast('success', 'Verification Code sent to your registered email successfully!');
setTimeout(() => { setTimeout(() => {
navigate("/change-password", { navigate("/change-password", {
state: { state: {
email: formData.email.trim(), email: formData.email.trim(),
user_type: formData.userType user_type: formData.userType
}, },
@ -151,38 +124,38 @@ const PublicContactForm = () => {
setLoading(false); setLoading(false);
} }
}; };
const handleConfirmOtp = async () => { const handleConfirmOtp = async () => {
if (!otp.trim()) { if (!otp.trim()) {
setOtpError("Verification code is required"); setOtpError("Verification code is required");
return; return;
} }
setOtpError(""); setOtpError("");
setLoading(true); setLoading(true);
setErrorMsg(""); setErrorMsg("");
setSuccess(""); setSuccess("");
try { try {
const payload = { const payload = {
registered_email: formData.email.trim(), registered_email: formData.email.trim(),
otp: otp.trim(), otp: otp.trim(),
user_type: formData.userType // Add user_type to the verification request user_type: formData.userType // Add user_type to the verification request
}; };
const response = await apiClient.post("/forgot-password/verify-otp", payload); const response = await apiClient.post("/forgot-password/verify-otp", payload);
if (response?.data?.status === "success") { if (response?.data?.status === "success") {
setSuccess("Verification Code verified successfully. Redirecting to change password..."); setSuccess("Verification Code verified successfully. Redirecting to change password...");
setTimeout(() => navigate("/change-password", { setTimeout(() => navigate("/change-password", {
state: { state: {
email: formData.email.trim(), email: formData.email.trim(),
user_type: formData.userType // Ensure user_type is passed user_type: formData.userType // Ensure user_type is passed
} }
}), 1500); }), 1500);
} else { } else {
const serverMsg = response?.data?.message?.toLowerCase() || ""; const serverMsg = response?.data?.message?.toLowerCase() || "";
if (serverMsg.includes("expired")) { if (serverMsg.includes("expired")) {
setOtpError("Verification code has expired. Please request a new one."); setOtpError("Verification code has expired. Please request a new one.");
} else if (serverMsg.includes("invalid")) { } else if (serverMsg.includes("invalid")) {
@ -193,7 +166,7 @@ const PublicContactForm = () => {
} }
} catch (err) { } catch (err) {
const serverMsg = err.response?.data?.message?.toLowerCase() || ""; const serverMsg = err.response?.data?.message?.toLowerCase() || "";
if (serverMsg.includes("expired")) { if (serverMsg.includes("expired")) {
setOtpError("Verification code has expired. Please request a new one."); setOtpError("Verification code has expired. Please request a new one.");
} else if (serverMsg.includes("invalid")) { } else if (serverMsg.includes("invalid")) {
@ -205,12 +178,12 @@ const PublicContactForm = () => {
setLoading(false); setLoading(false);
} }
}; };
const handleResendOtp = () => { const handleResendOtp = () => {
setOtpMsg("A new Verification Code has been sent to your registered email."); setOtpMsg("A new Verification Code has been sent to your registered email.");
setOtp(""); setOtp("");
}; };
return ( return (
<div className="min-h-screen bg-[#F7F7F7] flex items-center justify-center px-4"> <div className="min-h-screen bg-[#F7F7F7] flex items-center justify-center px-4">
{toast && ( {toast && (
@ -235,7 +208,7 @@ const PublicContactForm = () => {
</div> </div>
</div> </div>
)} )}
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<div className="bg-white rounded-lg shadow-sm ring-1 ring-gray-200 overflow-hidden"> <div className="bg-white rounded-lg shadow-sm ring-1 ring-gray-200 overflow-hidden">
<div className="px-7 py-8 border-b border-gray-200 bg-[#F2ECCF] text-center"> <div className="px-7 py-8 border-b border-gray-200 bg-[#F2ECCF] text-center">
@ -249,7 +222,7 @@ const PublicContactForm = () => {
: "Enter the verification code sent to your registered email."} : "Enter the verification code sent to your registered email."}
</p> </p>
</div> </div>
{!otpStep ? ( {!otpStep ? (
<form onSubmit={handleSubmit} className="px-7 py-8 space-y-4"> <form onSubmit={handleSubmit} className="px-7 py-8 space-y-4">
{errorMsg && ( {errorMsg && (
@ -257,7 +230,7 @@ const PublicContactForm = () => {
{errorMsg} {errorMsg}
</div> </div>
)} )}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
User Type <span className="text-red-500">*</span> User Type <span className="text-red-500">*</span>
@ -282,7 +255,7 @@ const PublicContactForm = () => {
{success} {success}
</div> </div>
)} )}
{formData.userType === 'establishment_user' && ( {formData.userType === 'establishment_user' && (
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
@ -303,7 +276,7 @@ const PublicContactForm = () => {
<p className="text-xs text-red-600 mt-1">{errors.establishmentName}</p> <p className="text-xs text-red-600 mt-1">{errors.establishmentName}</p>
)} )}
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Establishment ID <span className="text-red-500">*</span> Establishment ID <span className="text-red-500">*</span>
@ -324,7 +297,7 @@ const PublicContactForm = () => {
</div> </div>
</div> </div>
)} )}
<InputField <InputField
label="Registered Email" label="Registered Email"
name="email" name="email"
@ -334,56 +307,19 @@ const PublicContactForm = () => {
onChange={handleChange} onChange={handleChange}
error={errors.email} error={errors.email}
/> />
<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-3">
<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"
aria-hidden="true"
>
{captcha}
</div>
<button
type="button"
onClick={generateCaptcha}
className="flex items-center gap-1 text-sm text-[#92722A] hover:underline"
>
<RefreshCw size={14} className="text-[#92722A]" />
Refresh
</button>
</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.
<span className="font-mono"></span>
</p>
<input
type="text"
name="captcha"
value={userCaptcha}
onChange={handleCaptchaChange}
placeholder="Type the characters"
className={`mt-2 block w-full h-10 rounded-md border-2 ${
captchaError ? "border-red-500" : "border-[#92722A]"
} focus:border-[#92722A] focus:ring-0 px-4 text-sm bg-white`}
/>
{captchaError && <p className="text-xs text-red-600 mt-1">{captchaError}</p>}
</div>
<button <button
type="submit" type="submit"
disabled={!isValid || loading || !userCaptcha} disabled={!isValid || loading}
className={`inline-flex items-center justify-center w-full h-12 rounded-lg text-white ${ className={`inline-flex items-center justify-center w-full h-12 rounded-lg text-white ${
!isValid || loading || !userCaptcha !isValid || loading
? "bg-[#bfa06a] cursor-not-allowed" ? "bg-[#bfa06a] cursor-not-allowed"
: "bg-[#92722A] hover:bg-[#7b5c1f]" : "bg-[#92722A] hover:bg-[#7b5c1f]"
} transition-colors`} } transition-colors`}
> >
{loading ? "Submitting…" : "Submit"} {loading ? "Submitting…" : "Submit"}
</button> </button>
<button <button
type="button" type="button"
onClick={() => navigate(-1)} onClick={() => navigate(-1)}
@ -399,7 +335,7 @@ const PublicContactForm = () => {
{otpMsg} {otpMsg}
</div> </div>
)} )}
<InputField <InputField
label="Verification Code" label="Verification Code"
name="otp" name="otp"
@ -408,7 +344,7 @@ const PublicContactForm = () => {
placeholder="Enter the 6-digit code" placeholder="Enter the 6-digit code"
error={otpError} error={otpError}
/> />
<div className="flex gap-3"> <div className="flex gap-3">
<button <button
onClick={handleConfirmOtp} onClick={handleConfirmOtp}
@ -430,7 +366,7 @@ const PublicContactForm = () => {
</div> </div>
); );
}; };
const InputField = ({ const InputField = ({
label, label,
name, name,
@ -458,5 +394,5 @@ const InputField = ({
{error && <p className="text-xs text-red-600 mt-1">{error}</p>} {error && <p className="text-xs text-red-600 mt-1">{error}</p>}
</div> </div>
); );
export default PublicContactForm; export default PublicContactForm;