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 {
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,

View File

@ -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;

View File

@ -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;