feat : From home page user can get statement.

feat :Session timeout for 5 min
feat : login time two step verification
This commit is contained in:
2025-10-13 11:59:58 +05:30
parent 922e4356ce
commit b5cc4ac714
4 changed files with 460 additions and 112 deletions

View File

@@ -56,7 +56,6 @@ export default function SendToBeneficiaryOwn() {
setCountdown(180);
setTimerActive(true);
} catch (err: any) {
console.error('Send OTP failed', err);
notifications.show({
title: 'Error',
message: err.message || 'Send OTP failed.Please try again later.',

View File

@@ -71,7 +71,6 @@ export default function SendToBeneficiaryOthers() {
setCountdown(180);
setTimerActive(true);
} catch (err: any) {
console.error('Send OTP failed', err);
notifications.show({
title: 'Error',
message: err.message || 'Send OTP failed.Please try again later.',

View File

@@ -12,6 +12,7 @@ interface SendOtpPayload {
ref?: string;
date?: string;
userOtp?: string;
username?:string
}
function getStoredMobileNumber(): string | null {
@@ -43,7 +44,6 @@ export async function sendOtp(payload: SendOtpPayload) {
}
}
export async function verifyOtp(otp: string) {
try {
const mobileNumber = getStoredMobileNumber();
@@ -58,3 +58,18 @@ export async function verifyOtp(otp: string) {
throw error.response?.data || error;
}
}
export async function verifyLoginOtp(otp: string,mobileNumber:string) {
try {
// const mobileNumber = getStoredMobileNumber();
const response = await axios.post(
`http://localhost:8080/api/otp/verify?mobileNumber=${mobileNumber}`,
{ otp },
{ headers: { 'Content-Type': 'application/json' } }
);
return response.data;
} catch (error: any) {
console.error('Error verifying OTP:', error.response?.data || error.message);
throw error.response?.data || error;
}
}

View File

@@ -4,13 +4,14 @@ import { Text, Button, TextInput, PasswordInput, Title, Card, Group, Flex, Box,
import { notifications } from "@mantine/notifications";
import { Providers } from "@/app/providers";
import { useRouter } from "next/navigation";
import { sendOtp, verifyLoginOtp } from '@/app/_util/otp';
import NextImage from "next/image";
import styles from './page.module.css';
import logo from '@/app/image/logo1.jpg';
import frontPage from '@/app/image/ib_front_1.jpg';
import dynamic from 'next/dynamic';
import { generateCaptcha } from '@/app/captcha';
import { IconShieldLockFilled } from "@tabler/icons-react";
import { IconRefresh, IconShieldLockFilled } from "@tabler/icons-react";
import dayjs from "dayjs";
@@ -27,6 +28,46 @@ export default function Login() {
const [otpStep, setOtpStep] = useState(false);
const [otp, setOtp] = useState("");
const [loading, setLoading] = useState(false);
const [otpRequired, setOtpRequired] = useState(false);
const [otpVerified, setOtpVerified] = useState(false);
const [buttonLabel, setButtonLabel] = useState("Submit");
const [mobile, setMobile] = useState("");
async function handleSendOtp(mobile?: string) {
console.log("hi mobile", mobile);
if (!mobile) {
notifications.show({
title: 'Error',
message: 'Mobile number not found.Contact to administrator',
color: 'red',
});
return false;
}
try {
console.log(CIF);
await sendOtp({ type: 'LOGIN_OTP', username: CIF, mobileNumber: mobile });
return true;
}
catch (err: any) {
notifications.show({
title: 'Error',
message: `${err.error}.SMS vendor facing some issue.Please try again later.` || 'Send OTP failed.Please try again later.',
color: 'red',
});
return false;
}
}
async function handleVerifyOtp(mobile?: string) {
try {
if (mobile) {
await verifyLoginOtp(otp, mobile);
return true;
}
} catch {
return false;
}
}
useEffect(() => {
const loadCaptcha = async () => {
@@ -61,94 +102,329 @@ export default function Login() {
setInputCaptcha("");
};
// async function handleLogin(e: React.FormEvent) {
// e.preventDefault();
// if (!otpRequired && !otpVerified) {
// const onlyDigit = /^\d{11}$/;
// if (!onlyDigit.test(CIF)) {
// // setError('Input value must be 11 digit');
// notifications.show({
// withBorder: true,
// color: "red",
// title: "Invalid UserId",
// message: "UserID must be 11 digit",
// autoClose: 5000,
// });
// return;
// }
// if (!inputCaptcha) {
// notifications.show({
// withBorder: true,
// color: "red",
// title: "Invalid Captcha",
// message: "Please fill the Captcha filed",
// autoClose: 5000,
// });
// return;
// }
// if (inputCaptcha !== captcha) {
// notifications.show({
// withBorder: true,
// color: "red",
// title: "Captcha Error",
// message: "Please enter the correct captcha",
// autoClose: 5000,
// });
// regenerateCaptcha();
// return;
// }
// if (!CIF || !psw) {
// notifications.show({
// withBorder: true,
// color: "red",
// title: "Invalid Input",
// message: "Please fill UserId and Password",
// autoClose: 5000,
// });
// return;
// }
// }
// try {
// if (!otpRequired || otpVerified) {
// const response = await fetch('api/auth/login', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// 'X-Login-Type': 'IB',
// },
// body: JSON.stringify({
// customerNo: CIF,
// password: psw,
// otp: otp
// }),
// });
// const data = await response.json();
// console.log(data);
// setIsLogging(true);
// if (data.status === "OTP_REQUIRED" && response.status === 202) {
// console.log(data.mobile);
// setMobile(data.mobile);
// setOtpRequired(true);
// setButtonLabel("Verify OTP");
// const otpSent = await handleSendOtp(data.mobile); // auto-send
// if (otpSent) {
// notifications.show({
// color: "orange",
// title: "OTP Required",
// message: "OTP sent to your registered mobile number",
// });
// }
// return;
// }
// if (data.error === "MIGRATED_USER_HAS_NO_PASSWORD") {
// //console.log("Migration issue detected → opening modal");
// setOpened(true);
// return;
// }
// if (!response.ok) {
// notifications.show({
// withBorder: true,
// color: "red",
// title: "Error",
// message: data?.error || "Internal Server Error",
// autoClose: 5000,
// });
// regenerateCaptcha();
// localStorage.removeItem("access_token");
// localStorage.clear();
// sessionStorage.clear();
// return;
// }
// // setIsLogging(true);
// if (response.ok) {
// // console.log(data);
// setOtp("");
// const token = data.token;
// localStorage.setItem("access_token", token);
// localStorage.setItem("pswExpiryDate", data.loginPswExpiry);
// // console.log("Expiry Date:",(dayjs(data.loginPswExpiry)).diff(dayjs(), "day"));
// // Password Expiry Logic
// if (data.loginPswExpiry && (dayjs(data.loginPswExpiry)).diff(dayjs(), "day") < 0) {
// notifications.show({
// withBorder: true,
// color: "orange",
// title: "Password Expired",
// message: "Your password has expired, please set a new one.",
// autoClose: 4000,
// });
// router.push("/ChangePassword");
// return;
// }
// if (data.FirstTimeLogin === true) {
// router.push("/SetPassword")
// }
// else {
// router.push("/home");
// }
// }
// else {
// regenerateCaptcha();
// setIsLogging(false);
// notifications.show({
// withBorder: true,
// color: "red",
// title: "Wrong User Id or Password",
// message: "Wrong User Id or Password",
// autoClose: 5000,
// });
// }
// }
// if (otpRequired && !otpVerified) {
// if (!otp) {
// notifications.show({
// color: "red",
// title: "Invalid OTP",
// message: "Please enter OTP before verifying",
// });
// setIsLogging(false);
// return;
// }
// console.log("hi verify mob", mobile);
// const verified = await handleVerifyOtp(mobile);
// if (!verified) {
// notifications.show({
// title: "Invalid OTP",
// message: "The OTP entered does not match",
// color: "red",
// });
// return;
// }
// notifications.show({
// color: "green",
// title: "OTP Verified",
// message: "Please click Login to continue",
// });
// setOtpVerified(true);
// setButtonLabel("Login");
// setIsLogging(false);
// return;
// }
// }
// catch (error: any) {
// notifications.show({
// withBorder: true,
// color: "red",
// title: "Error",
// message: "Internal Server Error,Please try again Later",
// autoClose: 5000,
// });
// return;
// }
// finally {
// setIsLogging(false);
// }
// }
// For migration User
async function handleLogin(e: React.FormEvent) {
e.preventDefault();
const onlyDigit = /^\d{11}$/;
if (!onlyDigit.test(CIF)) {
// setError('Input value must be 11 digit');
notifications.show({
withBorder: true,
color: "red",
title: "Invalid UserId",
message: "UserID must be 11 digit",
autoClose: 5000,
});
return;
}
if (!inputCaptcha) {
notifications.show({
withBorder: true,
color: "red",
title: "Invalid Captcha",
message: "Please fill the Captcha filed",
autoClose: 5000,
});
return;
}
if (inputCaptcha !== captcha) {
notifications.show({
withBorder: true,
color: "red",
title: "Captcha Error",
message: "Please enter the correct captcha",
autoClose: 5000,
});
regenerateCaptcha();
return;
}
if (!CIF || !psw) {
notifications.show({
withBorder: true,
color: "red",
title: "Invalid Input",
message: "Please fill UserId and Password",
autoClose: 5000,
});
return;
}
try {
const response = await fetch('api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
customerNo: CIF,
password: psw,
}),
});
const data = await response.json();
// console.log(data);
if (data.error === "MIGRATED_USER_HAS_NO_PASSWORD") {
//console.log("Migration issue detected → opening modal");
setOpened(true);
return;
}
if (!response.ok) {
notifications.show({
withBorder: true,
color: "red",
title: "Error",
message: data?.error || "Internal Server Error",
autoClose: 5000,
});
regenerateCaptcha();
localStorage.removeItem("access_token");
localStorage.clear();
sessionStorage.clear();
return;
setIsLogging(true); // show loading & disable inputs
try {
// --- Validation (before any API call) ---
if (!otpRequired && !otpVerified) {
const onlyDigit = /^\d{11}$/;
if (!onlyDigit.test(CIF)) {
notifications.show({
withBorder: true,
color: "red",
title: "Invalid UserId",
message: "UserID must be 11 digit",
autoClose: 5000,
});
setIsLogging(false);
return;
}
if (!inputCaptcha) {
notifications.show({
withBorder: true,
color: "red",
title: "Invalid Captcha",
message: "Please fill the Captcha field",
autoClose: 5000,
});
setIsLogging(false);
return;
}
if (inputCaptcha !== captcha) {
notifications.show({
withBorder: true,
color: "red",
title: "Captcha Error",
message: "Please enter the correct captcha",
autoClose: 5000,
});
regenerateCaptcha();
setIsLogging(false);
return;
}
if (!CIF || !psw) {
notifications.show({
withBorder: true,
color: "red",
title: "Invalid Input",
message: "Please fill UserId and Password",
autoClose: 5000,
});
setIsLogging(false);
return;
}
}
setIsLogging(true);
if (response.ok) {
// console.log(data);
// --- LOGIN API FLOW ---
if (!otpRequired || otpVerified) {
const response = await fetch("api/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Login-Type": "IB",
},
body: JSON.stringify({
customerNo: CIF,
password: psw,
otp: otp,
}),
});
const data = await response.json();
console.log(data);
// 1⃣ OTP Required
if (data.status === "OTP_REQUIRED" && response.status === 202) {
setMobile(data.mobile);
setOtpRequired(true);
setButtonLabel("Verify OTP");
const otpSent = await handleSendOtp(data.mobile);
if (otpSent) {
notifications.show({
color: "orange",
title: "OTP Required",
message: "OTP sent to your registered mobile number",
});
}
setIsLogging(false);
return;
}
// 2⃣ Migrated user (no password)
if (data.error === "MIGRATED_USER_HAS_NO_PASSWORD") {
setOpened(true);
setIsLogging(false);
return;
}
// 3⃣ Invalid login
if (!response.ok) {
notifications.show({
withBorder: true,
color: "red",
title: "Error",
message: data?.error || "Internal Server Error",
autoClose: 5000,
});
regenerateCaptcha();
localStorage.removeItem("access_token");
localStorage.clear();
sessionStorage.clear();
setIsLogging(false);
return;
}
// 4⃣ Successful login
setOtp("");
const token = data.token;
localStorage.setItem("access_token", token);
localStorage.setItem("pswExpiryDate", data.loginPswExpiry);
// console.log("Expiry Date:",(dayjs(data.loginPswExpiry)).diff(dayjs(), "day"));
// Password Expiry Logic todo
if (data.loginPswExpiry && (dayjs(data.loginPswExpiry)).diff(dayjs(), "day") < 0) {
if (
data.loginPswExpiry &&
dayjs(data.loginPswExpiry).diff(dayjs(), "day") < 0
) {
notifications.show({
withBorder: true,
color: "orange",
@@ -161,39 +437,62 @@ export default function Login() {
}
if (data.FirstTimeLogin === true) {
router.push("/SetPassword")
}
else {
router.push("/SetPassword");
} else {
router.push("/home");
}
}
// --- OTP Verification Flow ---
if (otpRequired && !otpVerified) {
if (!otp) {
notifications.show({
color: "red",
title: "Invalid OTP",
message: "Please enter OTP before verifying",
});
setIsLogging(false);
return;
}
const verified = await handleVerifyOtp(mobile);
if (!verified) {
notifications.show({
title: "Invalid OTP",
message: "The OTP entered does not match",
color: "red",
});
setIsLogging(false);
return;
}
}
else {
regenerateCaptcha();
setIsLogging(false);
notifications.show({
withBorder: true,
color: "red",
title: "Wrong User Id or Password",
message: "Wrong User Id or Password",
autoClose: 5000,
color: "green",
title: "OTP Verified",
message: "Please click Login to continue",
});
setOtpVerified(true);
setButtonLabel("Login");
setIsLogging(false);
return;
}
}
catch (error: any) {
} catch (error: any) {
notifications.show({
withBorder: true,
color: "red",
title: "Error",
message: "Internal Server Error,Please try again Later",
message: "Internal Server Error, Please try again later",
autoClose: 5000,
});
return;
} finally {
// Ensure we always stop loader if still active
setIsLogging(false);
}
}
// For migration User
const sendOtp = async () => {
const sendMigrationOtp = async () => {
try {
setLoading(true);
const res = await fetch(`/api/otp/send/set-password?customerNo=${CIF}`, {
@@ -220,7 +519,7 @@ export default function Login() {
};
// For migration User
const verifyOtp = async () => {
const verifyMigrationOtp = async () => {
if (!otp) {
notifications.show({
color: "red",
@@ -294,7 +593,7 @@ export default function Login() {
Cancel
</Button>
{/* Call the API for send otp */}
<Button onClick={sendOtp} loading={loading}>
<Button onClick={sendMigrationOtp} loading={loading}>
OK
</Button>
</Group>
@@ -321,7 +620,7 @@ export default function Login() {
>
Back
</Button>
<Button onClick={verifyOtp} loading={loading}>
<Button onClick={verifyMigrationOtp} loading={loading}>
Verify OTP
</Button>
</Group>
@@ -415,7 +714,7 @@ export default function Login() {
/>
</div>
<Box w={{ base: "100%", md: "45%" }} p="lg">
<Card shadow="md" padding="xl" radius="md" style={{ maxWidth: 550, margin: "0 auto", height: '68vh' }}>
<Card shadow="md" padding="xl" radius="md" style={{ maxWidth: 550, height: '70vh' }}>
<form onSubmit={handleLogin}>
<TextInput
label="User ID"
@@ -426,6 +725,9 @@ export default function Login() {
if (input.length <= 11) SetCIF(input);
}}
withAsterisk
// disabled={otpRequired}
disabled={isLogging || otpRequired}
readOnly={isLogging}
/>
<PasswordInput
label="Password"
@@ -433,7 +735,10 @@ export default function Login() {
value={psw}
onChange={(e) => SetPsw(e.currentTarget.value)}
withAsterisk
mt="sm"
// disabled={otpRequired}
disabled={isLogging || otpRequired}
readOnly={isLogging}
// mt="sm"
/>
<Box style={{ textAlign: "right" }}>
{/* <Anchor
@@ -443,9 +748,9 @@ export default function Login() {
Forgot Password?
</Anchor> */}
</Box>
<Group mt="sm" align="center">
<Group align="center">
<Box style={{ backgroundColor: "#fff", fontSize: "18px", textDecoration: "line-through", padding: "4px 8px", fontFamily: "cursive" }}>{captcha}</Box>
<Button size="xs" variant="light" onClick={regenerateCaptcha}>Refresh</Button>
<Button size="xs" variant="light" onClick={regenerateCaptcha} disabled={otpRequired}>Refresh</Button>
</Group>
<TextInput
label="Enter CAPTCHA"
@@ -453,11 +758,41 @@ export default function Login() {
value={inputCaptcha}
onChange={(e) => setInputCaptcha(e.currentTarget.value)}
withAsterisk
mt="sm"
// disabled={otpRequired}
disabled={isLogging || otpRequired}
readOnly={isLogging}
// mt="sm"
/>
<Button type="submit" fullWidth mt="md" disabled={isLogging}>
{isLogging ? "Logging..." : "Login"}
{otpRequired && (
<Group align="end" gap="xs">
<PasswordInput
label="Enter OTP"
placeholder="Enter OTP"
value={otp}
onChange={(e) => setOtp(e.currentTarget.value)}
withAsterisk
style={{ flex: 1 }}
readOnly={otpVerified}
/>
<Tooltip label="Resend OTP">
<Button
variant="subtle"
px={10}
disabled={isLogging || otpVerified}
onClick={() => handleSendOtp(mobile)}
style={{ alignSelf: "flex-end", marginBottom: 4 }}
>
<IconRefresh size={20} /> Resend
</Button>
</Tooltip>
</Group>
)}
<Button type="submit" fullWidth mt="md" loading={isLogging} disabled={isLogging}>
{isLogging ? "Processing..." : buttonLabel}
</Button>
</form>
</Card>
</Box>