diff --git a/src/app/ChangePassword/page.tsx b/src/app/ChangePassword/page.tsx new file mode 100644 index 0000000..84f6b94 --- /dev/null +++ b/src/app/ChangePassword/page.tsx @@ -0,0 +1,408 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { + Text, + Button, + TextInput, + PasswordInput, + Title, + Card, + Box, + Image, + Group, +} from "@mantine/core"; +import { notifications } from "@mantine/notifications"; +import { useRouter } from "next/navigation"; +import NextImage from "next/image"; +import logo from "@/app/image/logo1.jpg"; +import changePwdImage from "@/app/image/change_password.jpg"; // reuse background illustration +import { generateCaptcha } from "@/app/captcha"; +import { IconLock, IconRefresh } from "@tabler/icons-react"; + +export default function ChangePassword() { + const router = useRouter(); + + // states + const [oldPassword, setOldPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [captcha, setCaptcha] = useState(""); + const [captchaInput, setCaptchaInput] = useState(""); + const [otp, setOtp] = useState(""); + const [generatedOtp, setGeneratedOtp] = useState(""); + const [otpValidated, setOtpValidated] = useState(false); + const [countdown, setCountdown] = useState(180); + const [timerActive, setTimerActive] = useState(false); + const [step, setStep] = useState<"form" | "otp" | "final">("form"); + const icon = ; + + // OTP generation + const handleGenerateOtp = async () => { + const value = "123456"; // replace with backend OTP if available + setGeneratedOtp(value); + setCountdown(180); + setTimerActive(true); + return value; + }; + + useEffect(() => { + regenerateCaptcha(); + }, []); + + useEffect(() => { + let interval: number | undefined; + if (timerActive && countdown > 0) { + interval = window.setInterval(() => { + setCountdown((prev) => prev - 1); + }, 1000); + } + if (countdown === 0) { + if (interval) clearInterval(interval); + setTimerActive(false); + } + return () => { + if (interval) clearInterval(interval); + }; + }, [timerActive, countdown]); + + // captcha + const regenerateCaptcha = async () => { + const newCaptcha = await generateCaptcha(); + setCaptcha(newCaptcha); + setCaptchaInput(""); + }; + + // password policy + const validatePasswordPolicy = (password: string) => { + return /^(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,15}$/.test( + password + ); + }; + + // handle submit + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (step === "form") { + if (!oldPassword || !newPassword || !confirmPassword || !captchaInput) { + notifications.show({ + title: "Missing Field", + message: "Please fill all mandatory fields.", + color: "red", + }); + return; + } + if (!validatePasswordPolicy(newPassword)) { + notifications.show({ + title: "Invalid Password", + message: + "Password must contain at least one capital letter(A-Z), one digit(0-9), one special symbol(e.g.,@,#,$), and be 8-15 characters long.", + color: "red", + }); + return; + } + if (newPassword !== confirmPassword) { + notifications.show({ + title: "Password Mismatch", + message: "Confirm password does not match new password.", + color: "red", + }); + return; + } + if (captchaInput !== captcha) { + notifications.show({ + title: "Invalid Captcha", + message: "Please enter correct Captcha", + color: "red", + }); + regenerateCaptcha(); + return; + } + + await handleGenerateOtp(); + setStep("otp"); + notifications.show({ + title: "OTP Sent", + message: "An OTP has been sent to your registered mobile.", + color: "blue", + }); + return; + } + + if (step === "otp") { + if (otp !== generatedOtp) { + notifications.show({ + title: "Invalid OTP", + message: "Please enter the correct OTP.", + color: "red", + }); + return; + } + setOtpValidated(true); + setStep("final"); + notifications.show({ + title: "OTP Verified", + message: "OTP has been successfully verified.", + color: "green", + }); + return; + } + + if (step === "final") { + const token = localStorage.getItem("access_token"); + if (!token) { + router.push("/login"); + return; + } + try { + const response = await fetch("/api/auth/change/login_password", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + OldLPsw: oldPassword, + newLPsw: newPassword, + confirmLPsw: confirmPassword, + }), + }); + + if (response.status === 401) { + localStorage.clear(); + sessionStorage.clear(); + router.push("/login"); + return; + } + + const result = await response.json(); + if (!response.ok) { + notifications.show({ + title: "Failed", + message: result.error || "Failed to change login password", + color: "red", + autoClose: false, + }); + } else { + notifications.show({ + title: "Success", + message: "Login password changed successfully.", + color: "green", + }); + resetForm(); + router.push("/login"); + } + } catch (err: any) { + notifications.show({ + title: "Error", + message: err.message || "Server error, please try again later", + color: "red", + }); + } + } + }; + + const resetForm = () => { + setOldPassword(""); + setNewPassword(""); + setConfirmPassword(""); + setCaptchaInput(""); + setOtp(""); + setOtpValidated(false); + setStep("form"); + regenerateCaptcha(); + }; + + return ( +
+ {/* Header */} + + ebanking + + THE KANGRA CENTRAL CO-OPERATIVE BANK LTD. + + + + {/* Body */} + + Change Password Image + + + + Change Password + +
+ setOldPassword(e.currentTarget.value)} + withAsterisk + readOnly={step !== "form"} + /> + setNewPassword(e.currentTarget.value)} + withAsterisk + readOnly={step !== "form"} + /> + setConfirmPassword(e.currentTarget.value)} + withAsterisk + rightSection={icon} + readOnly={step !== "form"} + /> + + {/* Captcha */} + {step === "form" && ( + + + {captcha} + + + setCaptchaInput(e.currentTarget.value)} + withAsterisk + /> + + )} + + {/* OTP */} + {step !== "form" && ( + + + setOtp(e.currentTarget.value)} + withAsterisk + disabled={otpValidated} + style={{ flex: 1 }} + /> + {!otpValidated && + (timerActive ? ( + + Resend OTP in{" "} + {String(Math.floor(countdown / 60)).padStart(2, "0")}: + {String(countdown % 60).padStart(2, "0")} + + ) : ( + + ))} + + + )} + + + + + + + Note: Login password must contain at least one + capital letter(A-Z), one digit(0-9), one special symbol + (e.g.,@,#,$), and be 8-15 characters long. + + +
+
+
+ + {/* Footer */} + + + © 2025 Kangra Central Co-Operative Bank + + +
+ ); +} diff --git a/src/app/image/change_password.jpg b/src/app/image/change_password.jpg new file mode 100644 index 0000000..4494c8f Binary files /dev/null and b/src/app/image/change_password.jpg differ diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 7eee408..ed630a3 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -10,6 +10,7 @@ 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 dayjs from "dayjs"; export default function Login() { const router = useRouter(); @@ -141,6 +142,24 @@ export default function Login() { const token = data.token; localStorage.setItem("access_token", token); localStorage.setItem("pswExpiryDate", data.loginPswExpiry); + + // Password Expiry Logic todo + 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") }