679 lines
29 KiB
TypeScript
679 lines
29 KiB
TypeScript
"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'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>
|
||
);
|
||
}
|