From 16883875fedac41583b373365c5b62e118607f27 Mon Sep 17 00:00:00 2001 From: Malini Date: Wed, 31 Dec 2025 18:18:41 +0530 Subject: [PATCH] added oyp --- ipi-survey-platform/src/App.jsx | 9 + ipi-survey-platform/src/pages/Login/Login.jsx | 59 ++- .../pages/OTPVerification/OTPVerification.jsx | 402 ++++++++++++++++++ .../src/services/auth/authService.js | 18 + 4 files changed, 479 insertions(+), 9 deletions(-) create mode 100644 ipi-survey-platform/src/pages/OTPVerification/OTPVerification.jsx diff --git a/ipi-survey-platform/src/App.jsx b/ipi-survey-platform/src/App.jsx index 01bd53c..a95194b 100644 --- a/ipi-survey-platform/src/App.jsx +++ b/ipi-survey-platform/src/App.jsx @@ -9,6 +9,7 @@ import Configuration from '@/pages/Admin/Configuration'; import Survey from '@/pages/Survey/Survey'; import Overview from '@/pages/Overview/Overview'; import Login from '@/pages/Login/Login'; +import OTPVerification from '@/pages/OTPVerification/OTPVerification'; import History from '@/pages/History/History'; import ChangePassword from '@/pages/ChangePassword/ChangePassword'; import ForgotPassword from '@/pages/ForgotPassword/ForgotPassword'; @@ -186,6 +187,14 @@ function App() { } /> + + + + } + /> { countdownIntervalRef.current = null; } - // Show success message - showToast('success', response.message || 'Login successful'); - - // Redirect based on role - if (userData.role === 'Admin') { - navigate('/admin/dashboard', { replace: true }); - } else { - navigate('/dashboard'); + try { + // Request OTP after successful login + const otpResponse = await requestOtp(email); + + if (otpResponse.status === 'success' && otpResponse.message === 'OTP sent successfully to your registered email') { + // Store user data in localStorage + localStorage.setItem('user_role', userData.role); + localStorage.setItem('user_profile', JSON.stringify({ + id: userData.id, + name: userData.name, + email: userData.email + })); + + // Store establishment_id if user is an EstablishmentUser + if (userData.role === 'EstablishmentUser' && userData.establishment_id) { + localStorage.setItem('establishment_id', userData.establishment_id); + } + + // Show success message + showToast('success', 'OTP sent successfully to your registered email'); + + // Add a small delay before navigation to allow toast to be visible + setTimeout(() => { + // Calculate OTP expiration time (3 minutes from now) + const otpExpiryTime = Date.now() + (3 * 60 * 1000); + + // Navigate to OTP verification with email and expiry time + navigate('/verify-otp', { + state: { + email, + otpExpiryTime, + from: location.state?.from || (userData.role === 'Admin' ? '/admin/dashboard' : '/dashboard') + } + }); + }, 1000); // 1 second delay + } else { + throw new Error('Failed to send OTP'); + } + } catch (otpError) { + console.error('OTP request failed:', otpError); + showToast('error', 'Login successful but failed to send OTP. Please try again.'); + // Still navigate to OTP page but show error + navigate('/verify-otp', { + state: { + email, + from: location.state?.from || (userData.role === 'Admin' ? '/admin/dashboard' : '/dashboard'), + error: 'Failed to send OTP. Please request a new one.' + } + }); } } else { throw new Error(response?.message || 'Invalid credentials'); diff --git a/ipi-survey-platform/src/pages/OTPVerification/OTPVerification.jsx b/ipi-survey-platform/src/pages/OTPVerification/OTPVerification.jsx new file mode 100644 index 0000000..6eff3b8 --- /dev/null +++ b/ipi-survey-platform/src/pages/OTPVerification/OTPVerification.jsx @@ -0,0 +1,402 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { requestOtp, verifyOtp } from '@/services/auth/authService'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { AlertCircle, Clock, CheckCircle } from "lucide-react"; + +const logoSrc = '/assets/images/FCSCLogo.svg'; + +const OTPVerification = () => { + const navigate = useNavigate(); + const location = useLocation(); + + const [otp, setOtp] = useState(['', '', '', '', '', '']); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + const [toast, setToast] = useState(null); + const [resendDisabled, setResendDisabled] = useState(false); + const [countdown, setCountdown] = useState(600); + const [attemptsRemaining, setAttemptsRemaining] = useState(3); + + const inputRefs = useRef(Array(6).fill(null).map(() => React.createRef())); + const toastTimeoutRef = useRef(null); + const countdownRef = useRef(null); + + // Get email from location state or use a default + const maskedEmail = location.state?.email ? + `${location.state.email.split('@')[0].substring(0, 2)}***@${location.state.email.split('@')[1]}` : + 'na***@company.com'; + + // Toast handling + const closeToast = useCallback(() => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + toastTimeoutRef.current = null; + } + setToast(null); + }, []); + + const showToast = useCallback((type, message) => { + if (!message) return; + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + toastTimeoutRef.current = null; + } + setToast({ type, message }); + toastTimeoutRef.current = setTimeout(() => { + setToast(null); + toastTimeoutRef.current = null; + }, 4000); + }, []); + + // Handle OTP input change + const handleOtpChange = (e, index) => { + const value = e.target.value; + + // Only allow numbers and limit to 1 character + if (value && !/^\d*$/.test(value)) return; + + const newOtp = [...otp]; + newOtp[index] = value.slice(-1); + setOtp(newOtp); + + // Move to next input if there's a value and we're not on the last input + if (value && index < 5 && inputRefs.current[index + 1]?.current) { + inputRefs.current[index + 1].current.focus(); + } + }; + + // Handle backspace + const handleKeyDown = (e, index) => { + if (e.key === 'Backspace' && !otp[index] && index > 0 && inputRefs.current[index - 1]?.current) { + // Move to previous input on backspace + inputRefs.current[index - 1].current.focus(); + } + }; + + // Handle paste + const handlePaste = (e) => { + e.preventDefault(); + const pasteData = e.clipboardData.getData('text/plain').trim(); + if (/^\d{6}$/.test(pasteData)) { + const newOtp = pasteData.split('').slice(0, 6); + setOtp([...newOtp, ...Array(6 - newOtp.length).fill('')]); + } + }; + + // Handle verify OTP + const handleVerify = async (e) => { + e.preventDefault(); + const otpValue = otp.join(''); + + if (otpValue.length !== 6) { + setError('Please enter a valid 6-digit code'); + return; + } + + setLoading(true); + setError(''); + + try { + const response = await verifyOtp({ + registered_email: location.state?.email, + otp: otpValue + }); + + if (response?.status === 'success') { + // Show success message immediately + showToast('success', 'Verified! You are signed in. Redirecting to your dashboard.'); + + // Check for admin role in multiple possible locations + const userRole = localStorage.getItem('user_role') || + response?.data?.user?.role || + response?.data?.role; + + // Check if user is admin (case-insensitive check) + const isAdmin = userRole?.toLowerCase() === 'admin' || + response?.data?.isAdmin === true; + + // Use the redirect path from location state if available, otherwise determine based on role + const redirectPath = location.state?.from || + (isAdmin ? '/admin/dashboard' : '/dashboard'); + + // Wait for 5 seconds before redirecting + setLoading(true); + setTimeout(() => { + navigate(redirectPath, { replace: true }); + }, 1000); + } else { + throw new Error(response?.message || 'Invalid OTP'); + } + + } catch (err) { + console.error('OTP verification failed', err); + const errorMessage = err.response?.data?.message || + err.message || + 'An error occurred during OTP verification. Please try again.'; + + const newAttempts = attemptsRemaining - 1; + setAttemptsRemaining(newAttempts); + + // Set the error message for incorrect OTP + const displayError = errorMessage || 'Invalid verification code'; + setError(displayError); + + if (newAttempts <= 0) { + setOtp(Array(6).fill('')); + // Show error message and redirect to login after a short delay + setError('Maximum attempts reached. Redirecting to login...'); + setTimeout(() => { + navigate('/login', { replace: true }); + }, 2000); + } else { + setOtp(Array(6).fill('')); + // Focus the first input if it exists + if (inputRefs.current[0]) { + inputRefs.current[0].focus(); + } + } + } finally { + setLoading(false); + } + }; + + // Handle resend OTP + const handleResend = async () => { + if (resendDisabled) return; + + // Show loading state immediately + setResendDisabled(true); + setLoading(true); + + try { + // Call the API to resend OTP + const response = await requestOtp(location.state?.email); + + // Update state and show success toast immediately + const tenMinutesInSeconds = 10 * 60; + setCountdown(tenMinutesInSeconds); + setAttemptsRemaining(3); + setError(''); + setOtp(Array(6).fill('')); + + // Use setTimeout to ensure the toast shows up in the next tick + setTimeout(() => { + showToast('success', response.message || 'A new OTP has been sent to your email.'); + }, 0); + + } catch (error) { + console.error('Error resending OTP:', error); + showToast('error', 'An error occurred while sending OTP. Please try again.'); + } finally { + setLoading(false); + + // Re-enable resend after 60 seconds + const timer = setTimeout(() => { + setResendDisabled(false); + }, 60000); + + // Cleanup timer on component unmount + return () => clearTimeout(timer); + } + }; + + // Start countdown on component mount + useEffect(() => { + // Always use 10 minutes for the countdown + const tenMinutesInMs = 10 * 60 * 1000; + const expiryTime = Date.now() + tenMinutesInMs; + + const updateCountdown = () => { + const now = Date.now(); + const remainingSeconds = Math.max(0, Math.floor((expiryTime - now) / 1000)); + + setCountdown(remainingSeconds); + + if (remainingSeconds <= 0) { + clearInterval(countdownRef.current); + setResendDisabled(false); + } + }; + + // Initial update + updateCountdown(); + + // Set up interval for countdown updates + countdownRef.current = setInterval(updateCountdown, 1000); + + return () => { + if (countdownRef.current) clearInterval(countdownRef.current); + }; + }, []); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current); + if (countdownRef.current) clearInterval(countdownRef.current); + }; + }, []); + + const minutes = Math.floor(countdown / 60); + const seconds = countdown % 60; + + return ( +
+ {/* Toast Notification */} + {toast && ( +
+
+
+ {toast.type === 'error' ? ( + + ) : toast.type === 'success' ? ( + + ) : null} + {toast.message} +
+ +
+
+ )} + +
+
+ {/* Logo Section */} +
+
+ FCSC Logo +
+

+ IIP (Index of Industrial Production) +

+
+ +
+ {/*
+

Email OTP

+

Step 2 of 2

+
*/} + +
+
+
+

Verify your sign-in

+

+ We sent a 6-digit authentication code to {maskedEmail} +

+
+ + {/* OTP Inputs */} +
+
+ {otp.map((digit, index) => ( + { inputRefs.current[index] = { current: el } }} + type="text" + inputMode="numeric" + pattern="[0-9]*" + maxLength={1} + value={digit} + onChange={(e) => handleOtpChange(e, index)} + onKeyDown={(e) => handleKeyDown(e, index)} + onPaste={handlePaste} + className={`w-12 h-12 text-center text-xl bg-transparent border-2 rounded focus:outline-none focus:ring-0 ${ + error ? 'border-red-500' : + digit ? 'border-[#92722A]' : + 'border-gray-300 hover:border-gray-400 focus:border-[#92722A] focus:border-b-2 focus:border-b-[#92722A]' + }`} + autoFocus={index === 0} + disabled={loading || attemptsRemaining <= 0} + /> + ))} +
+ + {error && ( +
+ + {error} +
+ )} + +
+
+ + + Code expires in {minutes}:{seconds < 10 ? `0${seconds}` : seconds} + +
+ Attempts: {attemptsRemaining}/3 +
+
+ +
+ + + +
+ +
+
+
+
+ +
+ +

+ Having trouble?{' '} + +

+
+
+
+
+
+ ); +}; + +export default OTPVerification; diff --git a/ipi-survey-platform/src/services/auth/authService.js b/ipi-survey-platform/src/services/auth/authService.js index 0950c60..76a5b71 100644 --- a/ipi-survey-platform/src/services/auth/authService.js +++ b/ipi-survey-platform/src/services/auth/authService.js @@ -34,10 +34,28 @@ export const logout = async () => { return response.data; }; +export const requestOtp = async (email) => { + const response = await apiClient.post( + '/auth/request-otp', + { registered_email: email }, + { skipAuth: true, withCredentials: true } + ); + return response.data; +}; + +export const verifyOtp = async ({ registered_email, otp }) => { + const response = await apiClient.post( + '/auth/verify-otp', + { registered_email, otp }, + { skipAuth: true, withCredentials: true } + ); + return response.data; +}; export default { login, changePassword, requestPasswordReset, logout, + requestOtp, };