Files
IB/src/app/(main)/settings/set_txn_password/page.tsx

344 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useEffect, useState } from "react";
import {
TextInput,
PasswordInput,
Button,
Title,
Paper,
Group,
Text
} from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { IconLock, IconRefresh } from "@tabler/icons-react";
import { generateCaptcha } from "@/app/captcha";
import { useRouter } from "next/navigation";
export default function ChangePassword() {
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [captcha, setCaptcha] = useState("");
const [captchaInput, setCaptchaInput] = useState("");
const [otp, setOtp] = 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 [generatedOtp, setGeneratedOtp] = useState('');
const router = useRouter();
const icon = <IconLock size={18} stroke={1.5} />;
const handleGenerateOtp = async () => {
const value = "123456"; // Or generate a random OTP
setGeneratedOtp(value);
setCountdown(180);
setTimerActive(true);
return value;
};
const regenerateCaptcha = async () => {
const newCaptcha = await generateCaptcha();
setCaptcha(newCaptcha);
setCaptchaInput("");
};
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]);
useEffect(() => {
regenerateCaptcha();
}, []);
const validatePasswordPolicy = (password: string) => {
return /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,}$/.test(
password
);
};
const handleSubmit = async () => {
// Step 1 → validate form
if (step === "form") {
if (!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 be at least 8 characters and contain alphanumeric and special characters.",
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;
}
// Passed → move to OTP step
await handleGenerateOtp();
setStep("otp");
notifications.show({
title: "OTP Sent",
message: "An OTP has been sent to your registered mobile.",
color: "blue",
});
return;
}
// Step 2 → validate OTP
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;
}
// Step 3 → Final API call to set transaction password
if (step === "final") {
const token = localStorage.getItem("access_token");
if (!token) {
router.push("/login");
return;
}
try {
const response = await fetch("/api/auth/transaction_password", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
transaction_password: newPassword,
}),
});
if (response.status === 401) {
localStorage.removeItem("access_token");
localStorage.clear();
sessionStorage.clear();
router.push("/login");
return;
}
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || "Failed to set transaction password");
}
notifications.show({
title: "Success",
message: "Transaction password set successfully.",
color: "green",
});
resetForm();
} catch (err: any) {
notifications.show({
title: "Error",
message: err.message || "Server error, please try again later",
color: "red",
});
}
}
};
const resetForm = () => {
setNewPassword("");
setConfirmPassword("");
setCaptchaInput("");
setOtp("");
setOtpValidated(false);
setStep("form");
regenerateCaptcha();
};
return (
<Paper shadow="sm" radius="md" p="md" withBorder h={400} >
<Title order={3} mb="sm">
Set Transaction Password
</Title>
<div style={{ overflowY: "auto", maxHeight: "280px", paddingRight: 8 }}>
<Group grow>
<PasswordInput
label="New Transaction Password"
placeholder="Enter new Transaction password"
value={newPassword}
onChange={(e) => setNewPassword(e.currentTarget.value)}
withAsterisk
mb="xs"
/>
<PasswordInput
label="Confirm Transaction Password"
placeholder="Re-enter new Transaction password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.currentTarget.value)}
withAsterisk
rightSection={icon}
mb="sm"
readOnly={step !== "form"}
onCopy={(e) => e.preventDefault()}
onPaste={(e) => e.preventDefault()}
onCut={(e) => e.preventDefault()}
/>
</Group>
{/* CAPTCHA */}
<div style={{ marginTop: 5 }}>
<label
style={{ display: "block", marginBottom: 4, fontSize: "14px" }}
>
Enter CAPTCHA <span style={{ color: "red" }}>*</span>
</label>
<div
style={{
display: "flex",
alignItems: "center",
gap: 10,
marginBottom: 5,
}}
>
<div
style={{
fontSize: "18px",
letterSpacing: "3px",
background: "#f3f4f6",
padding: "6px 12px",
borderRadius: "6px",
border: "1px solid #d1d5db",
userSelect: "none",
textDecoration: "line-through",
fontFamily: "cursive",
}}
>
{captcha}
</div>
<Button
size="xs"
variant="outline"
onClick={regenerateCaptcha}
style={{ height: 30, padding: "0 10px", lineHeight: "1" }}
disabled={step !== "form"}
>
Refresh
</Button>
<TextInput
placeholder="Enter above text"
value={captchaInput}
onChange={(e) => setCaptchaInput(e.currentTarget.value)}
withAsterisk
style={{ flexGrow: 1 }}
readOnly={step !== "form"}
/>
</div>
</div>
<Group grow>
{step !== "form" && (
<>
<Group gap="xs" align="flex-end">
<PasswordInput
label="OTP"
placeholder="Enter OTP"
type="otp"
value={otp}
maxLength={6}
onChange={(e) => setOtp(e.currentTarget.value)}
withAsterisk
disabled={otpValidated}
style={{ flex: 1 }}
/>
{!otpValidated && (
timerActive ? (
<Text size="xs" c="dimmed" style={{ minWidth: "120px" }}>
Resend OTP will be enabled in{" "}
{String(Math.floor(countdown / 60)).padStart(2, "0")}:
{String(countdown % 60).padStart(2, "0")}
</Text>
) : (
<IconRefresh
size={22}
style={{ cursor: "pointer", color: "blue", marginBottom: "6px" }}
onClick={handleGenerateOtp}
/>
)
)}
</Group>
</>
)}
</Group>
</div>
<Group mt="md" gap="sm">
<Button onClick={handleSubmit}>
{step === "form" && "Submit"}
{step === "otp" && "Validate OTP"}
{step === "final" && "Set Password"}
</Button>
<Button variant="outline" color="gray" onClick={resetForm}>
Reset
</Button>
</Group>
<Text size="sm" style={{ marginTop: "40px" }}>
<Text span c='red' fw={600}>Note : </Text>{""}
<Text span >Your new Login password must be 815 characters long and contain at least one number and one special character.</Text>
</Text>
</Paper>
);
}