Files
IB/src/app/login/page.tsx

679 lines
29 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import React, { useState, useEffect, memo, useRef } from "react";
import { Text, Button, TextInput, PasswordInput, Title, Card, Group, Flex, Box, Image, Anchor, Tooltip, Modal } from "@mantine/core";
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 { IconRefresh, IconShieldLockFilled } from "@tabler/icons-react";
import dayjs from "dayjs";
import { fetchAndStoreUserName } from "../_util/userdetails";
export default function Login() {
const router = useRouter();
const [CIF, SetCIF] = useState("");
const [psw, SetPsw] = useState("");
const [captcha, setCaptcha] = useState("");
const [inputCaptcha, setInputCaptcha] = useState("");
const [isLogging, setIsLogging] = useState(false);
const ClientCarousel = dynamic(() => import('./clientCarousel'), { ssr: false });
const headerRef = useRef<HTMLHeadingElement>(null);
const [opened, setOpened] = useState(false);
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) {
if (!mobile) {
notifications.show({
title: 'Error',
message: 'Mobile number not found. Contact administrator.',
color: 'red',
});
return false;
}
try {
const maskedCIF = CIF?.replace(/.(?=.{3})/g, '*');
await sendOtp({ type: 'LOGIN_OTP', username: CIF, mobileNumber: mobile });
// await sendOtp({ type: 'LOGIN_OTP', username: maskedCIF, mobileNumber: "7890544527" });
notifications.show({
color: 'orange',
title: 'OTP Required',
message: 'OTP sent to your registered mobile number.',
});
return true;
} catch (err: any) {
notifications.show({
title: 'Error',
message: err?.message || 'Send OTP failed. Please try again later.',
color: 'red',
});
return false;
}
}
async function handleVerifyOtp(mobile?: string) {
try {
if (mobile) {
await verifyLoginOtp(otp, mobile);
// await verifyLoginOtp(otp, '7890544527');
return true;
}
}
catch (err: any) {
notifications.show({
title: `${err.message}`,
message: 'OTP verification failed. Please try again later.',
color: 'red',
});
return false;
}
}
useEffect(() => {
const loadCaptcha = async () => {
const newCaptcha = await generateCaptcha();
setCaptcha(newCaptcha);
};
loadCaptcha();
}, []);
useEffect(() => {
const headerData = [
"THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.",
"कांगड़ा केन्द्रीय सहकारी बैंक सीमित",
];
let index = 0;
const interval = setInterval(() => {
index = (index + 1) % headerData.length;
if (headerRef.current) {
headerRef.current.textContent = headerData[index];
}
}, 2000);
return () => clearInterval(interval);
}, []);
const regenerateCaptcha = () => {
const loadCaptcha = async () => {
const newCaptcha = await generateCaptcha();
setCaptcha(newCaptcha);
};
loadCaptcha();
setInputCaptcha("");
};
async function handleLogin(e: React.FormEvent) {
e.preventDefault();
if (isLogging) return;
setIsLogging(true); // show loading & disable inputs
try {
// --- Validation (before any API call) ---
if (!otpRequired && !otpVerified) {
// const onlyDigit = /^\d{11}$/;
const onlyDigit = /^[A-Za-z0-9@_]{5,11}$/;
if (!onlyDigit.test(CIF)) {
notifications.show({
withBorder: true,
color: "red",
title: "Invalid UserId",
message: "The User ID or Username must contain 5 to 11 alphanumeric characters.",
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;
}
}
// --- LOGIN API FLOW ---
if (!otpRequired || otpVerified) {
const isNumeric = /^\d+$/.test(CIF);
const response = await fetch("api/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Login-Type": "IB",
},
body: JSON.stringify({
customerNo: isNumeric ? CIF : null,
userName: isNumeric ? null : 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);
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);
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;
}
// Fetching mobile no and user name
await fetchAndStoreUserName(token);
if (data.FirstTimeLogin === true) {
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) {
setIsLogging(false);
return;
}
notifications.show({
color: "Blue",
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,
});
}
// finally {
// // Ensure we always stop loader if still active
// setIsLogging(false);
// }
}
const sendMigrationOtp = async () => {
try {
setLoading(true);
const res = await fetch(`/api/otp/send/set-password?customerNo=${CIF}`, {
method: "GET",
headers: { "Content-Type": "application/json", 'X-Login-Type': 'IB', },
});
if (!res.ok) throw new Error("Failed to send OTP");
notifications.show({
color: "green",
title: "OTP Sent",
message: "Please check your registered mobile.",
});
setOtpStep(true);
} catch (err: any) {
notifications.show({
color: "red",
title: "Error",
message: err.message || "Could not send OTP",
});
} finally {
setLoading(false);
}
};
// For migration User
const verifyMigrationOtp = async () => {
if (!otp) {
notifications.show({
color: "red",
title: "Invalid Input",
message: "Please enter OTP before verifying",
});
return;
}
try {
setLoading(true);
const res = await fetch(`/api/otp/verify/set-password?customerNo=${CIF}&otp=${otp}`, {
method: "GET",
headers: { "Content-Type": "application/json", 'X-Login-Type': 'IB', },
});
const data = await res.json();
// console.log(data)
if (!res.ok) {
notifications.show({
color: "red",
title: "Error",
message: data.error || "OTP verification failed",
});
return;
}
localStorage.setItem("access_token", data.token);
notifications.show({
color: "green",
title: "OTP Verified",
message: "Redirecting to set password page...",
});
setOpened(false);
router.push("/SetPassword");
} catch (err: any) {
notifications.show({
color: "red",
title: "Error",
message: err.message || "OTP verification failed",
});
} finally {
setLoading(false);
}
};
return (
<Providers>
<Modal
opened={opened}
onClose={() => {
setOpened(false);
setOtpStep(false);
setOtp("");
}}
title="User Migration Notice"
centered
>
{!otpStep ? (
// STEP 1: Migration Notice
<>
<Text>
Your account is being migrated to the new system.
You need to set a new password to continue login.
</Text>
<Group mt="md" justify="flex-end">
<Button
variant="outline"
onClick={() => setOpened(false)}
>
Cancel
</Button>
{/* Call the API for send otp */}
<Button onClick={sendMigrationOtp} loading={loading}>
OK
</Button>
</Group>
</>
) : (
// STEP 2: OTP Verification (Frontend-only)
<>
<Text size="sm" c="green">Enter the OTP sent to your registered mobile</Text>
<TextInput
label="OTP"
placeholder="Enter OTP"
value={otp}
withAsterisk
onChange={(e) => setOtp(e.currentTarget.value)}
/>
<Group mt="md" justify="flex-end">
<Button
variant="outline"
onClick={() => {
setOtp("");
setOtpStep(false);
}}
disabled={loading}
>
Back
</Button>
<Button onClick={verifyMigrationOtp} loading={loading}>
Verify OTP
</Button>
</Group>
</>
)}
</Modal>
{/* Main Screen */}
<div style={{ backgroundColor: "#f8f9fa", width: "100%", height: "auto" }}>
{/* Header */}
<Box
component="header"
className={styles.header}
style={{
width: "100%",
padding: "0.8rem 2rem",
background: "linear-gradient(15deg, rgba(10, 114, 40, 1) 55%, rgba(101, 101, 184, 1) 100%)",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
color: "white",
boxShadow: "0 2px 6px rgba(0,0,0,0.15)",
position: "sticky",
top: 0,
zIndex: 100,
}}
>
<Group gap="md">
<Image
src={logo}
component={NextImage}
fit="contain"
alt="ebanking"
style={{ width: "60px", height: "auto" }}
/>
<div>
<Title order={3} style={{ fontFamily: "Roboto", color: "white", marginBottom: 2 }}>
THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.
</Title>
<Text size="xs" c="white" style={{ opacity: 0.85 }}>
Head Office: Dharmshala, District Kangra (H.P), Pin: 176215
</Text>
</div>
</Group>
</Box>
<div style={{ marginTop: '10px' }}>
{/* Movable text */}
<Box
style={{
width: "100%",
height: "0.5%",
overflow: "hidden",
whiteSpace: "nowrap",
padding: "8px 0",
}}
>
<Text
component="span"
style={{
display: "inline-block",
paddingLeft: "100%",
animation: "scroll-left 60s linear infinite",
fontWeight: "bold",
color: "#004d99",
}}
>
Always login to our Net Banking site directly or through Banks website.
Do not disclose your User Id and Password to any third party and keep Your User Id and Password strictly confidential.
KCC Bank never asks for User Id,Passwords and Pins through email or phone.
Be ware of Phishing mails with links to fake bank&apos;s websites asking for personal information are in circulation.
Please DO NOT Click on the links given in the emails asking for personal details like bank account number, user ID and password.
If you had shared your User Id and Password through such mails or links, please change your Password immediately.
Inform the Bank/branch in which your account is maintained for resetting your password.
</Text>
<style>
{`
@keyframes scroll-left {
0% { transform: translateX(0%); }
100% { transform: translateX(-100%); }
}
`}
</style>
</Box>
{/* Main */}
<div style={{
display: "flex", height: "75vh", overflow: "hidden", position: "relative",
// background: 'linear-gradient(to right, #02081eff, #0a3d91)'
background: 'linear-gradient(179deg,rgba(1, 13, 18, 1) 49%, rgba(77, 82, 79, 1) 87%)'
}}>
<div style={{ flex: 1, backgroundColor: "#c1e0f0", position: "relative" }}>
<Image
fit="cover"
src={frontPage}
component={NextImage}
alt="ebanking"
style={{ width: "100%", height: "100%" }}
/>
</div>
<Box w={{ base: "100%", md: "45%" }} p="lg">
<Card shadow="md" padding="xl" radius="md" style={{ maxWidth: 550, height: '70vh', justifyContent: 'space-between' }} >
<form onSubmit={handleLogin}>
<TextInput
label="User ID / User Name"
placeholder="Enter your CIF No / User Name"
value={CIF}
onInput={(e) => {
// const input = e.currentTarget.value.replace(/\D/g, "");
const input = e.currentTarget.value.replace(/[^A-Za-z0-9@_]/g, "");
if (input.length <= 11) SetCIF(input);
}}
withAsterisk
// disabled={otpRequired}
disabled={isLogging || otpRequired}
readOnly={isLogging}
/>
<PasswordInput
label="Password"
placeholder="Enter your password"
value={psw}
onChange={(e) => SetPsw(e.currentTarget.value)}
withAsterisk
// disabled={otpRequired}
disabled={isLogging || otpRequired}
readOnly={isLogging}
// mt="sm"
/>
<Box style={{ textAlign: "right" }}>
{/* <Anchor
style={{ fontSize: "14px", color: "#1c7ed6", cursor: "pointer" }}
onClick={() => router.push("/ValidateUser")}
>
Forgot Password?
</Anchor> */}
</Box>
<Group align="center">
<Box style={{
backgroundColor: "#fff", fontSize: "18px", textDecoration: "line-through", padding: "4px 8px", fontFamily: "Verdana",
userSelect: "none",
pointerEvents: "none",
}}
onCopy={(e) => e.preventDefault()}
onContextMenu={(e) => e.preventDefault()}
>
{captcha}
</Box>
<Button size="xs" variant="light" onClick={regenerateCaptcha} disabled={otpRequired}>Refresh</Button>
</Group>
<TextInput
label="Enter CAPTCHA"
placeholder="Enter above text"
value={inputCaptcha}
onChange={(e) => setInputCaptcha(e.currentTarget.value)}
withAsterisk
// disabled={otpRequired}
disabled={isLogging || otpRequired}
readOnly={isLogging}
// mt="sm"
/>
{otpRequired && (
<Group align="end" gap="xs">
<PasswordInput
label="Enter OTP"
placeholder="Enter OTP"
value={otp}
maxLength={6}
onChange={(e) => setOtp(e.currentTarget.value)}
withAsterisk
style={{ flex: 1 }}
disabled={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="sm" loading={isLogging} disabled={isLogging}>
{isLogging ? "Processing..." : buttonLabel}
</Button>
<Box mt="xs">
<Text size="sm">
<Text component="span" c="red" fw={600}>Note: </Text>
<Text component="span" c="black">
Existing users logging in to the new Internet Banking for the first time should use their CIF number to avoid login issues.
</Text>
</Text>
</Box>
</form>
</Card>
</Box>
</div>
{/* Carousel and Notes */}
<Flex direction={{ base: "column", md: "row" }} mt="md" px="md" py="sm" gap="sm">
<Box w={{ base: "100%", md: "85%" }}>
<ClientCarousel />
</Box>
<Box w={{ base: "100%", md: "25%" }} p="md" style={{ textAlign: "center", border: '1px solid #5c5c3d' }}>
<Title order={2}> <IconShieldLockFilled />Security Notes :</Title>
<Text mt="sm" size="md">When you Login, Your User Id and Password travels in an encrypted and highly secured mode.</Text>
<Text mt="sm" fs="italic">For more information on Products and Services, Please Visit</Text>
<Anchor href="http://www.kccb.in/"> http://www.kccb.in/</Anchor>
</Box>
</Flex>
{/* Footer */}
<Box
component="footer"
style={{
width: "100%",
textAlign: "center",
padding: "10px 0",
bottom: 0,
left: 0,
zIndex: 1000,
fontSize: "14px",
}}
>
<Text>
© 2025 KCC Bank. All rights reserved. |{" "}
<Anchor href="document/disclaimer.pdf" target="_blank" rel="noopener noreferrer">Disclaimer</Anchor> |{" "}
<Anchor href="document/privacy_policy.pdf" target="_blank" rel="noopener noreferrer">Privacy Policy</Anchor> |{" "}
<Anchor href="document/phishing.pdf" target="_blank" rel="noopener noreferrer">Phishing</Anchor> |{" "}
<Anchor href="document/security_tips.pdf" target="_blank" rel="noopener noreferrer">Security Tips</Anchor> |{" "}
<Anchor href="document/grievance.jpg" target="_blank" rel="noopener noreferrer">Grievances</Anchor>
</Text>
</Box>
</div>
</div>
</Providers>
);
}