diff --git a/instruction.md b/instruction.md index 1d35ae7..bb98704 100644 --- a/instruction.md +++ b/instruction.md @@ -137,4 +137,44 @@ scp -P 9022 Smsservice/smsserviceapplication.jar @localhost:/home/ { const token = localStorage.getItem("access_token"); diff --git a/src/app/(main)/settings/user_id/page.tsx b/src/app/(main)/settings/user_id/page.tsx index df8f301..eed5737 100644 --- a/src/app/(main)/settings/user_id/page.tsx +++ b/src/app/(main)/settings/user_id/page.tsx @@ -1,18 +1,403 @@ "use client"; import React, { useEffect, useState } from "react"; -import { Group, Loader, Paper, Text } from "@mantine/core"; +import { + TextInput, + Button, + Title, + Paper, + Group, + Text, + List, +} from "@mantine/core"; +import { notifications } from "@mantine/notifications"; +import { IconUser, IconRefresh } from "@tabler/icons-react"; +import { generateCaptcha } from "@/app/captcha"; +import { sendOtp, verifyOtp } from "@/app/_util/otp"; +export default function SetPreferredNameSimple() { + const [preferredName, setPreferredName] = useState(""); + const [confirmName, setConfirmName] = useState(""); + const [preferredNameError, setPreferredNameError] = useState(null); + const [confirmNameError, setConfirmNameError] = useState(null); + 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 [existingName, setExistingName] = useState(null); + const [loading, setLoading] = useState(true); -export default function ChangePassword() { - return ( - - - - The feature will be available soon... + const icon = ; + + // 🟢 Fetch name + generate captcha on mount + useEffect(() => { + checkPreferredName(); + regenerateCaptcha(); + }, []); + + // 🟢 OTP timer + 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]); + + // 🟢 API: Fetch preferred name + async function checkPreferredName() { + try { + const token = localStorage.getItem("access_token"); + const response = await fetch("/api/auth/user_name", { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }); + const data = await response.json(); + if (!response.ok) throw new Error(data.error || "Failed to fetch preferred name"); + setExistingName(data.user_name || null); + } catch (err) { + console.error(err); + setExistingName(null); + } finally { + setLoading(false); + } + } + + // 🟢 Captcha + const regenerateCaptcha = async () => { + const newCaptcha = await generateCaptcha(); + setCaptcha(newCaptcha); + setCaptchaInput(""); + }; + + // 🟢 Live Validation (kept from your previous version) + function onPreferredNameChange(value: string) { + const sanitized = value.replace(/[^A-Za-z0-9@_]/g, "").slice(0, 11); + setPreferredName(sanitized); + + if (sanitized.length > 0 && (sanitized.length < 5 || sanitized.length > 11)) { + setPreferredNameError("Preferred Name must be 5–11 characters."); + } else { + setPreferredNameError(null); + } + } + + function onConfirmNameChange(value: string) { + const sanitized = value.replace(/[^A-Za-z0-9@_]/g, "").slice(0, 11); + setConfirmName(sanitized); + + if (sanitized && sanitized !== preferredName) { + setConfirmNameError("Confirm name does not match Preferred Name."); + } else { + setConfirmNameError(null); + } + } + + // 🟢 OTP + async function handleSendOtp() { + try { + await sendOtp({ type: "USERNAME_UPDATED" }); + notifications.show({ + title: "OTP Sent", + message: "An OTP has been sent to your registered mobile.", + color: "blue", + }); + setStep("otp"); + setCountdown(180); + setTimerActive(true); + } catch (err: any) { + notifications.show({ + title: "Error", + message: err.message || "Failed to send OTP.", + color: "red", + }); + } + } + + async function handleVerifyOtp() { + try { + await verifyOtp(otp); + notifications.show({ + title: "OTP Verified", + message: "OTP has been successfully verified.", + color: "green", + }); + setOtpValidated(true); + setStep("final"); + } catch { + notifications.show({ + title: "Invalid OTP", + message: "The OTP entered is incorrect.", + color: "red", + }); + } + } + + // 🟢 Update Preferred Name API + async function handleUpdatePreferredName() { + const token = localStorage.getItem("access_token"); + if (!token) { + notifications.show({ + title: "Session Expired", + message: "Please log in again.", + color: "red", + }); + return; + } + + try { + const response = await fetch("/api/auth/user_name", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ user_name: preferredName }), + }); + + const result = await response.json(); + if (!response.ok) throw new Error(result.message || "Failed to update preferred name"); + + notifications.show({ + title: "Success", + message: "Preferred name updated successfully!", + color: "green", + }); + + resetForm(); + await checkPreferredName(); // refresh + } catch (err: any) { + notifications.show({ + title: "Error", + message: err.message || "Server error, please try again later.", + color: "red", + }); + } + } + + const resetForm = () => { + setPreferredName(""); + setConfirmName(""); + setCaptchaInput(""); + setOtp(""); + setOtpValidated(false); + setStep("form"); + regenerateCaptcha(); + }; + + // 🟢 Main Submit + const handleSubmit = async () => { + if (step === "form") { + if (!preferredName || !confirmName || !captchaInput) { + notifications.show({ + title: "Missing Fields", + message: "Please fill all mandatory fields.", + color: "red", + }); + return; + } + + if (preferredNameError || confirmNameError) { + notifications.show({ + title: "Invalid Input", + message: "Please correct highlighted fields before continuing.", + color: "red", + }); + return; + } + + if (captchaInput !== captcha) { + notifications.show({ + title: "Invalid Captcha", + message: "Please enter correct captcha.", + color: "red", + }); + regenerateCaptcha(); + return; + } + + await handleSendOtp(); + return; + } + + if (step === "otp") { + await handleVerifyOtp(); + return; + } + + if (step === "final") { + await handleUpdatePreferredName(); + return; + } + }; + + if (loading) return Loading...; + + return ( + + + Set Preferred Name + + +
+ {existingName && ( + + Current Preferred Name: + {existingName} + + )} + {!existingName && ( + + You have not set the user ID yet. Please set it first. + + )} + + onPreferredNameChange(e.currentTarget.value)} + withAsterisk + mb="xs" + maxLength={11} + readOnly={step !== "form"} + error={preferredNameError || undefined} + /> + onConfirmNameChange(e.currentTarget.value)} + withAsterisk + mb="xs" + rightSection={icon} + readOnly={step !== "form"} + error={confirmNameError || undefined} + /> + + + {/* CAPTCHA */} +
+ +
+
+ {captcha} +
+ + + + setCaptchaInput(e.currentTarget.value)} + withAsterisk + style={{ flexGrow: 1 }} + readOnly={step !== "form"} + /> +
+
+ + {/* OTP */} + {step !== "form" && ( + + setOtp(e.currentTarget.value)} + maxLength={6} + withAsterisk + disabled={otpValidated} + /> + {!otpValidated && ( + timerActive ? ( + + Resend OTP in{" "} + {String(Math.floor(countdown / 60)).padStart(2, "0")}: + {String(countdown % 60).padStart(2, "0")} - - - - ); + ) : ( + + ) + )} + + )} +
+ + {/* BUTTONS */} + + + + + + + + + Your Preferred Name must be 5–11 characters long and contain only + letters, numbers, and @ or _ symbols. + + + You can change your username a maximum of 5 times. + + + +
+ ); } diff --git a/src/app/SetPassword/page.tsx b/src/app/SetPassword/page.tsx index 9d40db4..c009044 100644 --- a/src/app/SetPassword/page.tsx +++ b/src/app/SetPassword/page.tsx @@ -310,7 +310,17 @@ export default function SetLoginPwd() { readOnly={captchaValidate} /> - {captcha} + e.preventDefault()} + onContextMenu={(e) => e.preventDefault()}> + {captcha} + + {/* CAPTCHA */} - {captcha} + e.preventDefault()} + onContextMenu={(e) => e.preventDefault()}> + + {captcha} */} - {captcha} + e.preventDefault()} + onContextMenu={(e) => e.preventDefault()} + > + {captcha} + +