removed captacha
This commit is contained in:
parent
9745092a33
commit
de3a72ca35
@ -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,
|
||||
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-[#F7F7F7] flex items-center justify-center px-4">
|
||||
{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>
|
||||
)}
|
||||
|
||||
|
||||
{toast && (
|
||||
<div className="fixed top-6 inset-x-0 z-50 flex justify-center px-4">
|
||||
<div
|
||||
@ -345,7 +310,7 @@ const Login = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<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">
|
||||
@ -356,7 +321,7 @@ const Login = () => {
|
||||
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">
|
||||
@ -376,7 +341,7 @@ const Login = () => {
|
||||
</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">
|
||||
@ -387,7 +352,7 @@ const Login = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email address
|
||||
@ -402,16 +367,16 @@ const Login = () => {
|
||||
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'
|
||||
: isLocked
|
||||
? 'border-gray-300 bg-gray-100 cursor-not-allowed'
|
||||
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
|
||||
@ -426,10 +391,10 @@ const Login = () => {
|
||||
}}
|
||||
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'
|
||||
: isLocked
|
||||
? 'border-gray-300 bg-gray-100 cursor-not-allowed'
|
||||
passwordError
|
||||
? 'border-red-500 bg-red-50'
|
||||
: isLocked
|
||||
? 'border-gray-300 bg-gray-100 cursor-not-allowed'
|
||||
: 'border-[#92722A] bg-white'
|
||||
}`}
|
||||
/>
|
||||
@ -441,7 +406,7 @@ const Login = () => {
|
||||
aria-label="Toggle password visibility"
|
||||
>
|
||||
{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">
|
||||
<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" />
|
||||
@ -456,56 +421,7 @@ const Login = () => {
|
||||
</div>
|
||||
{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>
|
||||
|
||||
<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">
|
||||
<label className="inline-flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
@ -527,13 +443,12 @@ const Login = () => {
|
||||
Forgot password?
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={
|
||||
loading ||
|
||||
isLocked ||
|
||||
!userCaptcha.trim()
|
||||
isLocked
|
||||
}
|
||||
className={`inline-flex items-center justify-center w-full h-12 rounded-lg text-white font-medium ${
|
||||
isLocked
|
||||
@ -543,10 +458,10 @@ const Login = () => {
|
||||
: 'bg-[#92722A] hover:bg-[#7b5c1f]'
|
||||
} transition-colors`}
|
||||
>
|
||||
{isLocked
|
||||
? `Locked (${formatTime(remainingTime)})`
|
||||
: loading
|
||||
? 'Signing in…'
|
||||
{isLocked
|
||||
? `Locked (${formatTime(remainingTime)})`
|
||||
: loading
|
||||
? 'Signing in…'
|
||||
: 'Sign in'}
|
||||
</button>
|
||||
</form>
|
||||
@ -555,5 +470,6 @@ const Login = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
|
||||
export default Login;
|
||||
|
||||
@ -1,48 +1,32 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import apiClient from '@/services/api/apiClient';
|
||||
import { RefreshCw } from "lucide-react";
|
||||
|
||||
const logoSrc = '/assets/images/FCSCLogo.svg';
|
||||
|
||||
|
||||
const PublicContactForm = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
userType: "establishment_user",
|
||||
establishmentName: "",
|
||||
establishmentId: "",
|
||||
email: "",
|
||||
});
|
||||
|
||||
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
const [captcha, setCaptcha] = useState("");
|
||||
const [userCaptcha, setUserCaptcha] = useState("");
|
||||
const [captchaError, setCaptchaError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState("");
|
||||
const [errorMsg, setErrorMsg] = useState("");
|
||||
const [otpStep, setOtpStep] = useState(false);
|
||||
const [otp, setOtp] = useState("");
|
||||
const [otpMsg, setOtpMsg] = useState("");
|
||||
const [otpStep, setOtpStep] = useState(false);
|
||||
const [otp, setOtp] = useState("");
|
||||
const [otpMsg, setOtpMsg] = useState("");
|
||||
const [otpError, setOtpError] = useState("");
|
||||
const [toast, setToast] = useState(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(() => {
|
||||
return () => {
|
||||
if (toastTimeoutRef.current) {
|
||||
@ -50,7 +34,7 @@ const PublicContactForm = () => {
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
const showToast = (type, message) => {
|
||||
if (!message) return;
|
||||
if (toastTimeoutRef.current) {
|
||||
@ -63,7 +47,7 @@ const PublicContactForm = () => {
|
||||
toastTimeoutRef.current = null;
|
||||
}, 4000);
|
||||
};
|
||||
|
||||
|
||||
const closeToast = () => {
|
||||
if (toastTimeoutRef.current) {
|
||||
window.clearTimeout(toastTimeoutRef.current);
|
||||
@ -71,52 +55,41 @@ const PublicContactForm = () => {
|
||||
}
|
||||
setToast(null);
|
||||
};
|
||||
|
||||
|
||||
const validate = (data = formData) => {
|
||||
const newErrors = {};
|
||||
|
||||
|
||||
if (!data.userType) {
|
||||
newErrors.userType = "Please select user type";
|
||||
} else if (data.userType === 'establishment_user') {
|
||||
if (!data.establishmentName.trim()) newErrors.establishmentName = "Required.";
|
||||
if (!data.establishmentId.trim()) newErrors.establishmentId = "Required.";
|
||||
}
|
||||
|
||||
|
||||
if (!data.email.trim()) newErrors.email = "Required.";
|
||||
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email))
|
||||
newErrors.email = "Enter a valid email address.";
|
||||
|
||||
|
||||
setErrors(newErrors);
|
||||
setIsValid(Object.keys(newErrors).length === 0);
|
||||
return newErrors;
|
||||
};
|
||||
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
const updated = { ...formData, [name]: value };
|
||||
setFormData(updated);
|
||||
validate(updated);
|
||||
};
|
||||
|
||||
const handleCaptchaChange = (e) => {
|
||||
setUserCaptcha(e.target.value.toUpperCase());
|
||||
setCaptchaError("");
|
||||
};
|
||||
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setErrorMsg("");
|
||||
setSuccess("");
|
||||
|
||||
|
||||
const validationErrors = validate();
|
||||
if (Object.keys(validationErrors).length > 0) return;
|
||||
|
||||
if (userCaptcha !== captcha) {
|
||||
setCaptchaError("Incorrect CAPTCHA. Please try again.");
|
||||
generateCaptcha();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const payload = {
|
||||
@ -125,14 +98,14 @@ const PublicContactForm = () => {
|
||||
establishment_code: formData.establishmentId.trim(),
|
||||
registered_email: formData.email.trim(),
|
||||
};
|
||||
|
||||
|
||||
const response = await apiClient.post("/forgot-password/request-otp", payload);
|
||||
|
||||
|
||||
if (response?.data) {
|
||||
showToast('success', 'Verification Code sent to your registered email successfully!');
|
||||
setTimeout(() => {
|
||||
navigate("/change-password", {
|
||||
state: {
|
||||
state: {
|
||||
email: formData.email.trim(),
|
||||
user_type: formData.userType
|
||||
},
|
||||
@ -151,38 +124,38 @@ const PublicContactForm = () => {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleConfirmOtp = async () => {
|
||||
if (!otp.trim()) {
|
||||
setOtpError("Verification code is required");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setOtpError("");
|
||||
setLoading(true);
|
||||
setErrorMsg("");
|
||||
setSuccess("");
|
||||
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
registered_email: formData.email.trim(),
|
||||
otp: otp.trim(),
|
||||
user_type: formData.userType // Add user_type to the verification request
|
||||
};
|
||||
|
||||
|
||||
const response = await apiClient.post("/forgot-password/verify-otp", payload);
|
||||
|
||||
|
||||
if (response?.data?.status === "success") {
|
||||
setSuccess("Verification Code verified successfully. Redirecting to change password...");
|
||||
setTimeout(() => navigate("/change-password", {
|
||||
state: {
|
||||
setTimeout(() => navigate("/change-password", {
|
||||
state: {
|
||||
email: formData.email.trim(),
|
||||
user_type: formData.userType // Ensure user_type is passed
|
||||
}
|
||||
}
|
||||
}), 1500);
|
||||
} else {
|
||||
const serverMsg = response?.data?.message?.toLowerCase() || "";
|
||||
|
||||
|
||||
if (serverMsg.includes("expired")) {
|
||||
setOtpError("Verification code has expired. Please request a new one.");
|
||||
} else if (serverMsg.includes("invalid")) {
|
||||
@ -193,7 +166,7 @@ const PublicContactForm = () => {
|
||||
}
|
||||
} catch (err) {
|
||||
const serverMsg = err.response?.data?.message?.toLowerCase() || "";
|
||||
|
||||
|
||||
if (serverMsg.includes("expired")) {
|
||||
setOtpError("Verification code has expired. Please request a new one.");
|
||||
} else if (serverMsg.includes("invalid")) {
|
||||
@ -205,12 +178,12 @@ const PublicContactForm = () => {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleResendOtp = () => {
|
||||
setOtpMsg("A new Verification Code has been sent to your registered email.");
|
||||
setOtp("");
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F7F7F7] flex items-center justify-center px-4">
|
||||
{toast && (
|
||||
@ -235,7 +208,7 @@ const PublicContactForm = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<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-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."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
{!otpStep ? (
|
||||
<form onSubmit={handleSubmit} className="px-7 py-8 space-y-4">
|
||||
{errorMsg && (
|
||||
@ -257,7 +230,7 @@ const PublicContactForm = () => {
|
||||
{errorMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
User Type <span className="text-red-500">*</span>
|
||||
@ -282,7 +255,7 @@ const PublicContactForm = () => {
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{formData.userType === 'establishment_user' && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
@ -303,7 +276,7 @@ const PublicContactForm = () => {
|
||||
<p className="text-xs text-red-600 mt-1">{errors.establishmentName}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Establishment ID <span className="text-red-500">*</span>
|
||||
@ -324,7 +297,7 @@ const PublicContactForm = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<InputField
|
||||
label="Registered Email"
|
||||
name="email"
|
||||
@ -334,56 +307,19 @@ const PublicContactForm = () => {
|
||||
onChange={handleChange}
|
||||
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
|
||||
type="submit"
|
||||
disabled={!isValid || loading || !userCaptcha}
|
||||
disabled={!isValid || loading}
|
||||
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-[#92722A] hover:bg-[#7b5c1f]"
|
||||
} transition-colors`}
|
||||
>
|
||||
{loading ? "Submitting…" : "Submit"}
|
||||
</button>
|
||||
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(-1)}
|
||||
@ -399,7 +335,7 @@ const PublicContactForm = () => {
|
||||
{otpMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<InputField
|
||||
label="Verification Code"
|
||||
name="otp"
|
||||
@ -408,7 +344,7 @@ const PublicContactForm = () => {
|
||||
placeholder="Enter the 6-digit code"
|
||||
error={otpError}
|
||||
/>
|
||||
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleConfirmOtp}
|
||||
@ -430,7 +366,7 @@ const PublicContactForm = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const InputField = ({
|
||||
label,
|
||||
name,
|
||||
@ -458,5 +394,5 @@ const InputField = ({
|
||||
{error && <p className="text-xs text-red-600 mt-1">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
export default PublicContactForm;
|
||||
Loading…
Reference in New Issue
Block a user