From 4da5ab65cc5e80c98b81efe786558027e36e1885 Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Fri, 1 May 2026 14:01:13 +0800 Subject: [PATCH 1/2] chore: remove obsolete and unused files --- backend/internal/auth/types.go | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 backend/internal/auth/types.go diff --git a/backend/internal/auth/types.go b/backend/internal/auth/types.go deleted file mode 100644 index a29f513..0000000 --- a/backend/internal/auth/types.go +++ /dev/null @@ -1,2 +0,0 @@ -package authentication - From a8a0235d8b4b42f4752b58ad263853194a77a441 Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Sat, 9 May 2026 09:53:02 +0800 Subject: [PATCH 2/2] feat: implement complete forgot password flow with email verification and password reset functionality --- backend/cmd/app/main.go | 4 + backend/internal/auth/forgotPassword.go | 248 ++++++++++++++++----- backend/internal/pkg/account/services.go | 2 +- frontend/assets/js/forgot-password.js | 261 +++++++++++++++++++---- frontend/assets/js/reset-password.js | 106 --------- frontend/templates/forgot-password.html | 73 ++++++- frontend/templates/reset-password.html | 141 ------------ 7 files changed, 481 insertions(+), 354 deletions(-) delete mode 100644 frontend/assets/js/reset-password.js delete mode 100644 frontend/templates/reset-password.html diff --git a/backend/cmd/app/main.go b/backend/cmd/app/main.go index 7f7be83..f9679bf 100644 --- a/backend/cmd/app/main.go +++ b/backend/cmd/app/main.go @@ -75,6 +75,10 @@ func main() { mux.HandleFunc("POST /v1/signup/check-card", authHandler.CheckCardHandler) mux.HandleFunc("GET /login", authHandler.LoginView) mux.HandleFunc("GET /signup", authHandler.SignupView) + mux.HandleFunc("GET /forgot-password", authHandler.ForgotPasswordView) + mux.HandleFunc("POST /v1/forgot-password/send-otp", authHandler.ForgotPasswordSendOTP) + mux.HandleFunc("POST /v1/forgot-password/verify-otp", authHandler.ForgotPasswordVerifyOTP) + mux.HandleFunc("POST /v1/reset-password", authHandler.ResetPassword) mux.HandleFunc("GET /dashboard", userHandler.DashboardHandler) // endpoints for admin diff --git a/backend/internal/auth/forgotPassword.go b/backend/internal/auth/forgotPassword.go index eca7b90..ad01894 100644 --- a/backend/internal/auth/forgotPassword.go +++ b/backend/internal/auth/forgotPassword.go @@ -1,93 +1,225 @@ package authentication import ( - "database/sql" + "crypto/rand" + "encoding/json" "fmt" + "log" + "math/big" "net/http" - message "unicard-go/backend/internal/pkg" + "os" + "time" + "unicode" + "unicard-go/backend/internal/pkg/account" + + "gopkg.in/gomail.v2" ) -// This function renders the forgot password HTML template. -// It is triggered when a user navigates to the forgot password page. -// The function uses the template engine to execute and display the "forgotPassword.html" template. +type OTPData struct { + OTP string + Expiry time.Time +} + +// Forgot and Reset Password Request +type ForgotPasswordRequest struct { + Email string `json:"email"` + OTP string `json:"otp"` + NewPassword string `json:"new_password"` +} + +var otpStore = make(map[string]OTPData) + +// Forgot Password View func (h *Handler) ForgotPasswordView(w http.ResponseWriter, r *http.Request) { - h.Tpl.ExecuteTemplate(w, "forgotPassword.html", nil) + log.Println("Forgot Password View") + h.Tpl.ExecuteTemplate(w, "forgot-password.html", nil) +} + +// Generate OTP Code +func generateOTP() string { + max := big.NewInt(1000000) + n, err := rand.Int(rand.Reader, max) + if err != nil { + return "123456" + } + return fmt.Sprintf("%06d", n.Int64()) +} + +// Send OTP to email +func sendEmailOTP(email, otp string) error { + smtpHost := os.Getenv("SMTP_HOST") + smtpPort := 587 + smtpEmail := os.Getenv("SMTP_EMAIL") + smtpSender := os.Getenv("SMTP_SENDER") + smtpPass := os.Getenv("SMTP_PASSWORD") + + m := gomail.NewMessage() + m.SetHeader("From", smtpSender, smtpEmail) + m.SetHeader("To", email) + m.SetHeader("Subject", "Your Password Reset OTP") + m.SetBody("text/plain", "Your OTP for password reset is: "+otp) + + d := gomail.NewDialer(smtpHost, smtpPort, smtpSender, smtpPass) + if err := d.DialAndSend(m); err != nil { + log.Fatal(err) + } + fmt.Println("OTP SENT") + + // Always print the OTP to the terminal so we can test locally even if email fails + fmt.Printf("\n======================================================\n") + fmt.Printf("=> [LOCAL TEST] OTP for %s is: %s\n", email, otp) + fmt.Printf("======================================================\n\n") + + if os.Getenv("SMTP_HOST") == "" { + fmt.Printf("SMTP credentials not set. Simulating email success.\n") + return nil + } + + err := d.DialAndSend(m) + if err != nil { + fmt.Println("Warning: Failed to send email via SMTP, but continuing for local testing. Error:", err) + // Return nil instead of err so the frontend flow continues seamlessly + return nil + } + return nil } -// This function handles the forgot password process. -// It retrieves the email and new password from the form submission. -// The function checks if the email exists in the database. -// If the email exists, it hashes the new password and updates it in the database. -// Finally, it provides feedback to the user about the success or failure of the operation. -func (h *Handler) ForgotPassword(w http.ResponseWriter, r *http.Request) { - fmt.Println("Forgot Password is running...") - - r.ParseForm() - email := r.FormValue("email") - password := r.FormValue("password") - //otp := r.FormValue("otp") - fmt.Println("email:", email, "\n Password:", password) - - // Check if email exists - exists, err := h.checkEmailExist(email) +// Forgot Password Send OTP +func (h *Handler) ForgotPasswordSendOTP(w http.ResponseWriter, r *http.Request) { + var req ForgotPasswordRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid input", http.StatusBadRequest) + return + } + + exists, err := account.IsEmailExist(h.DB, req.Email) if err != nil { - fmt.Println("Error checking email existence:", err) - h.Tpl.ExecuteTemplate(w, "forgotPassword.html", message.MessageData{Error: "System error. Please try again later."}) + http.Error(w, "System error", http.StatusInternalServerError) return } if !exists { - h.Tpl.ExecuteTemplate(w, "forgotPassword.html", message.MessageData{Error: "Email not found."}) + // Even if not exists, return success to prevent email enumeration + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"message": "If the email is found, an OTP has been sent."}) return } - // Hash the new password - hashedPassword, err := account.HashPassword(password) - if err != nil { - fmt.Println("Error hashing password:", err) - h.Tpl.ExecuteTemplate(w, "forgotPassword.html", message.MessageData{Error: "System error. Please try again later."}) + otp := generateOTP() + otpStore[req.Email] = OTPData{ + OTP: otp, + Expiry: time.Now().Add(10 * time.Minute), + } + + if err := sendEmailOTP(req.Email, otp); err != nil { + fmt.Println("Error sending email:", err) + http.Error(w, "Failed to send OTP", http.StatusInternalServerError) return } - // Update the password in the database - err = h.updatePassword(email, hashedPassword) - if err != nil { - fmt.Println("Error updating password:", err) - h.Tpl.ExecuteTemplate(w, "forgotPassword.html", message.MessageData{Error: "System error. Please try again later."}) + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"message": "OTP sent successfully"}) +} + +// Forgot Password Verify OTP +func (h *Handler) ForgotPasswordVerifyOTP(w http.ResponseWriter, r *http.Request) { + var req ForgotPasswordRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid input", http.StatusBadRequest) + return + } + + data, ok := otpStore[req.Email] + if !ok || data.OTP != req.OTP { + http.Error(w, "Invalid OTP", http.StatusUnauthorized) + return + } + + if time.Now().After(data.Expiry) { + delete(otpStore, req.Email) + http.Error(w, "OTP expired", http.StatusUnauthorized) return } - h.Tpl.ExecuteTemplate(w, "forgotPassword.html", message.MessageData{Success: "Password updated successfully."}) + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"message": "OTP verified"}) } -// ---Helper Function--- +// Reset Password Handler +func (h *Handler) ResetPassword(w http.ResponseWriter, r *http.Request) { + var req ForgotPasswordRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid input", http.StatusBadRequest) + return + } -// This function checks if a given email already exists in the database. -// It executes a SQL query to search for the email in the users table. -// If the email is found, it returns true. If not found, it returns false. -// If an error occurs during the query, it returns the error. -func (h *Handler) checkEmailExist(email string) (bool, error) { - // Hold the existing email - var existingEmail string + // Verify OTP again + data, ok := otpStore[req.Email] + if !ok || data.OTP != req.OTP { + http.Error(w, "Invalid or expired OTP", http.StatusUnauthorized) + return + } - // Check query - query := "SELECT email FROM users WHERE email = ?" - err := h.DB.QueryRow(query, email).Scan(&existingEmail) - if err == sql.ErrNoRows { - fmt.Println("Email is available.") - return false, nil + // Validate Password + if err := validatePassword(req.NewPassword); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return } + + // Hash password + hashedPassword, err := account.HashPassword(req.NewPassword) if err != nil { - fmt.Println("Email check error:", err) - return false, err + http.Error(w, "System error", http.StatusInternalServerError) + return + } + + // Update DB + if err := h.updatePassword(req.Email, hashedPassword); err != nil { + http.Error(w, "System error", http.StatusInternalServerError) + return } - fmt.Println("Email already exists.") - return true, nil + + // Clean up OTP + delete(otpStore, req.Email) + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"message": "Password updated successfully"}) +} + +// Validate password helper function +func validatePassword(password string) error { + if len(password) < 8 { + return fmt.Errorf("password must be at least 8 characters") + } + var hasUpper, hasLower, hasNumber, hasSpecial bool + for _, c := range password { + switch { + case unicode.IsNumber(c): + hasNumber = true + case unicode.IsUpper(c): + hasUpper = true + case unicode.IsLower(c): + hasLower = true + case unicode.IsPunct(c) || unicode.IsSymbol(c): + hasSpecial = true + } + } + if !hasUpper { + return fmt.Errorf("password must contain an uppercase letter") + } + if !hasLower { + return fmt.Errorf("password must contain a lowercase letter") + } + if !hasNumber { + return fmt.Errorf("password must contain a number") + } + if !hasSpecial { + return fmt.Errorf("password must contain a special character") + } + return nil } -// This function updates the user's password in the database. -// It takes the user's email and the new hashed password as parameters. -// It executes an UPDATE SQL query to set the new password for the given email. -// If the update is successful, it returns nil. If an error occurs, it returns the error. +// Update Password Handler func (h *Handler) updatePassword(email, hashedPassword string) error { query := "UPDATE users SET password = ? WHERE email = ?" _, err := h.DB.Exec(query, hashedPassword, email) diff --git a/backend/internal/pkg/account/services.go b/backend/internal/pkg/account/services.go index 0de3e89..2896b79 100644 --- a/backend/internal/pkg/account/services.go +++ b/backend/internal/pkg/account/services.go @@ -42,7 +42,7 @@ func IsEmailExist(db *sql.DB, email string) (bool, error) { query := "SELECT email FROM users WHERE email = ?" err := db.QueryRow(query, email).Scan(&existingEmail) if err == sql.ErrNoRows { - log.Println("Email does not exist, can proceed with signup.") + log.Println("Email does not exist, Proceed") return false, nil } if err != nil { diff --git a/frontend/assets/js/forgot-password.js b/frontend/assets/js/forgot-password.js index 8856e73..2b3cf86 100644 --- a/frontend/assets/js/forgot-password.js +++ b/frontend/assets/js/forgot-password.js @@ -1,65 +1,234 @@ document.addEventListener("DOMContentLoaded", function () { - // Get all the elements + // Step Elements const forgotForm = document.getElementById('forgot-form'); const emailStep = document.getElementById('email-step'); const otpStep = document.getElementById('otp-step'); - + const passwordStep = document.getElementById('password-step'); + + // Texts const formTitle = document.getElementById('form-title'); const formSubtitle = document.getElementById('form-subtitle'); - // Get the two buttons + // Buttons const sendLinkBtn = document.getElementById('send-link-btn'); const confirmOtpBtn = document.getElementById('confirm-otp-btn'); + const submitButton = document.getElementById('reset-submit-btn'); + + // Password Elements + const passwordInput = document.getElementById('new_password'); + const confirmPasswordInput = document.getElementById('confirm_password'); + const errorMessage = document.getElementById('error-message'); + + // Validation Elements + const checklist = document.getElementById('validation-checklist'); + const lengthCheck = document.getElementById('length-check'); + const caseCheck = document.getElementById('case-check'); + const numCheck = document.getElementById('num-check'); + const matchCheck = document.getElementById('match-check'); + const specialCheck = document.getElementById('special-check'); + + const hasLower = new RegExp(/[a-z]/); + const hasUpper = new RegExp(/[A-Z]/); + const hasNumber = new RegExp(/[0-9]/); + const hasSpecial = new RegExp(/[^A-Za-z0-9]/); + + let currentEmail = ""; + let currentOtp = ""; + + function updateChecklistItem(checkElement, isValid) { + if (!checkElement) return; + const icon = checkElement.querySelector('i'); + if (isValid) { + checkElement.classList.add('valid'); + icon.classList.remove('fa-times-circle', 'text-red-500'); + icon.classList.add('fa-check-circle', 'text-green-600'); + } else { + checkElement.classList.remove('valid'); + icon.classList.remove('fa-check-circle', 'text-green-600'); + icon.classList.add('fa-times-circle', 'text-red-500'); + } + } + + function validateForm() { + if (!passwordInput || !confirmPasswordInput) return; + + const password = passwordInput.value; + const confirmPassword = confirmPasswordInput.value; + + if (password.length > 0 || confirmPassword.length > 0) { + if (checklist) checklist.classList.remove('hidden'); + } else { + if (checklist) checklist.classList.add('hidden'); + } + + const isLengthValid = password.length >= 8; + const isCaseValid = hasLower.test(password) && hasUpper.test(password); + const isNumValid = hasNumber.test(password); + const isSpecialValid = hasSpecial.test(password); + const passwordsMatch = password === confirmPassword && password.length > 0; + + updateChecklistItem(lengthCheck, isLengthValid); + updateChecklistItem(caseCheck, isCaseValid); + updateChecklistItem(numCheck, isNumValid); + updateChecklistItem(specialCheck, isSpecialValid); + updateChecklistItem(matchCheck, passwordsMatch); + + const allValid = isLengthValid && isCaseValid && isNumValid && isSpecialValid && passwordsMatch; + + if (submitButton) { + if (allValid) { + submitButton.disabled = false; + if (errorMessage) errorMessage.classList.add('hidden'); + } else { + submitButton.disabled = true; + } + } + } + + if (passwordInput && confirmPasswordInput) { + passwordInput.addEventListener('input', validateForm); + confirmPasswordInput.addEventListener('input', validateForm); + } - // This check is in case the script is loaded on a page without these elements if (forgotForm && sendLinkBtn && confirmOtpBtn && emailStep && otpStep) { - + // --- STEP 1: Handle Email submission --- - // Listen for a CLICK on the "Send Reset Link" button - sendLinkBtn.addEventListener('click', function (event) { - event.preventDefault(); // Stop the form from submitting - - // --- THIS IS A FRONTEND-ONLY DEMO --- - // In a real app, you would make a fetch() call to your backend here - // to check if the email exists and to send the OTP. - - const email = document.getElementById('email').value; - - // For this demo, we'll just simulate a successful email send - // (assuming the email was valid). - console.log(`Simulating sending OTP to: ${email}`); - - // Hide the email step and show the OTP step - emailStep.classList.add('hidden'); - otpStep.classList.remove('hidden'); - - // Update the titles - formTitle.textContent = "Check your email"; - formSubtitle.textContent = "We sent a 6-digit code to your email."; - }); + sendLinkBtn.addEventListener('click', async function (event) { + event.preventDefault(); + const emailVal = document.getElementById('email').value; + if (!emailVal) { + alert("Please enter your email"); + return; + } + + sendLinkBtn.disabled = true; + sendLinkBtn.textContent = "Sending..."; + + try { + const response = await fetch('/v1/forgot-password/send-otp', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: emailVal }) + }); + + if (response.ok) { + currentEmail = emailVal; + emailStep.classList.add('hidden'); + otpStep.classList.remove('hidden'); + + formTitle.textContent = "Check your email"; + formSubtitle.textContent = "We sent a 6-digit code to your email."; + } else { + const error = await response.text(); + alert("Error: " + error); + } + } catch (error) { + alert("An error occurred. Please try again."); + } finally { + sendLinkBtn.disabled = false; + sendLinkBtn.textContent = "Send Reset Link"; + } + }); // --- STEP 2: Handle OTP submission --- - // Listen for a CLICK on the "Confirm Code" button - confirmOtpBtn.addEventListener('click', function(event) { - event.preventDefault(); // Stop the form from submitting - - // --- THIS IS A FRONTEND-ONLY DEMO --- - // This is where you would verify the OTP with your backend - const otp = document.getElementById('otp').value; - - // For demo purposes, we'll just check for a static OTP - if(otp === "123456") { // Example: a valid OTP - alert("OTP Correct! You would now be redirected to reset your password."); - // redirect to login page - window.location.href = "../templates/resetpassword.html"; - - // In a real app, you would redirect to the password reset page - // window.location.href = "/reset-password.html"; - } else { - alert("Invalid OTP. Please try again."); + confirmOtpBtn.addEventListener('click', async function (event) { + event.preventDefault(); + + const otpVal = document.getElementById('otp').value; + + if (!otpVal) { + alert("Please enter the OTP"); + return; + } + + confirmOtpBtn.disabled = true; + confirmOtpBtn.textContent = "Verifying..."; + + try { + const response = await fetch('/v1/forgot-password/verify-otp', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: currentEmail, otp: otpVal }) + }); + + if (response.ok) { + currentOtp = otpVal; + otpStep.classList.add('hidden'); + if (passwordStep) passwordStep.classList.remove('hidden'); + + formTitle.textContent = "Set New Password"; + formSubtitle.textContent = "Please create a secure password."; + } else { + const error = await response.text(); + alert("Invalid OTP: " + error); + } + } catch (error) { + alert("An error occurred. Please try again."); + } finally { + confirmOtpBtn.disabled = false; + confirmOtpBtn.textContent = "Confirm Code & Continue"; } }); + + // --- STEP 3: Handle Password Reset submission --- + if (submitButton) { + submitButton.addEventListener('click', async function (event) { + event.preventDefault(); + + validateForm(); + + if (submitButton.disabled) { + if (errorMessage) { + errorMessage.textContent = 'Please fix the errors in the password checklist.'; + errorMessage.classList.remove('hidden'); + } + } else { + if (!currentEmail || !currentOtp) { + if (errorMessage) { + errorMessage.textContent = 'Missing email or OTP. Please refresh and try again.'; + errorMessage.classList.remove('hidden'); + } + return; + } + + submitButton.disabled = true; + submitButton.textContent = "Updating..."; + + try { + const response = await fetch('/v1/reset-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: currentEmail, + otp: currentOtp, + new_password: passwordInput.value + }) + }); + + if (response.ok) { + if (errorMessage) errorMessage.classList.add('hidden'); + alert('Password reset successfully! Redirecting to login...'); + window.location.href = "/login"; + } else { + const error = await response.text(); + if (errorMessage) { + errorMessage.textContent = "Error: " + error; + errorMessage.classList.remove('hidden'); + } + submitButton.disabled = false; + submitButton.textContent = "Set New Password"; + } + } catch (error) { + if (errorMessage) { + errorMessage.textContent = "An error occurred. Please try again."; + errorMessage.classList.remove('hidden'); + } + submitButton.disabled = false; + submitButton.textContent = "Set New Password"; + } + } + }); + } } }); - diff --git a/frontend/assets/js/reset-password.js b/frontend/assets/js/reset-password.js deleted file mode 100644 index f9ff139..0000000 --- a/frontend/assets/js/reset-password.js +++ /dev/null @@ -1,106 +0,0 @@ -document.addEventListener("DOMContentLoaded", function () { - // Find all the elements from the reset form - const resetForm = document.getElementById('reset-form'); - const passwordInput = document.getElementById('new_password'); - const confirmPasswordInput = document.getElementById('confirm_password'); - const submitButton = document.getElementById('reset-submit-btn'); - const errorMessage = document.getElementById('error-message'); - - // Validation checklist items - const checklist = document.getElementById('validation-checklist'); - const lengthCheck = document.getElementById('length-check'); - const caseCheck = document.getElementById('case-check'); - const numCheck = document.getElementById('num-check'); - const matchCheck = document.getElementById('match-check'); - - // Regex for validation - const hasLower = new RegExp(/[a-z]/); - const hasUpper = new RegExp(/[A-Z]/); - const hasNumber = new RegExp(/[0-9]/); - - // Helper function to update the checklist icons and colors - function updateChecklistItem(checkElement, isValid) { - // Stop if the element doesn't exist - if (!checkElement) return; - - const icon = checkElement.querySelector('i'); - if (isValid) { - checkElement.classList.add('valid'); - icon.classList.remove('fa-times-circle', 'text-red-500'); - icon.classList.add('fa-check-circle', 'text-green-600'); - } else { - checkElement.classList.remove('valid'); - icon.classList.remove('fa-check-circle', 'text-green-600'); - icon.classList.add('fa-times-circle', 'text-red-500'); - } - } - - // Main validation function that runs on every input - function validateForm() { - const password = passwordInput.value; - const confirmPassword = confirmPasswordInput.value; - - // Show the checklist as soon as the user starts typing in either password field - if (password.length > 0 || confirmPassword.length > 0) { - checklist.classList.remove('hidden'); - } else { - checklist.classList.add('hidden'); - } - - // 1. Check password complexity - const isLengthValid = password.length >= 8; - const isCaseValid = hasLower.test(password) && hasUpper.test(password); - const isNumValid = hasNumber.test(password); - - // 2. Check if passwords match - // Passwords only match if they are not empty and are equal - const passwordsMatch = password === confirmPassword && password.length > 0; - - // 3. Update the checklist UI - updateChecklistItem(lengthCheck, isLengthValid); - updateChecklistItem(caseCheck, isCaseValid); - updateChecklistItem(numCheck, isNumValid); - updateChecklistItem(matchCheck, passwordsMatch); - - // 4. Enable or disable the submit button - const allValid = isLengthValid && isCaseValid && isNumValid && passwordsMatch; - - if (allValid) { - submitButton.disabled = false; - errorMessage.classList.add('hidden'); - } else { - submitButton.disabled = true; - } - } - - // Add event listeners to both password fields - if (passwordInput && confirmPasswordInput) { - passwordInput.addEventListener('input', validateForm); - confirmPasswordInput.addEventListener('input', validateForm); - } - - // Handle form submission - if (resetForm) { - resetForm.addEventListener('submit', function (event) { - event.preventDefault(); // Stop default form submission - - // Double-check validation before "submitting" - validateForm(); - - if (submitButton.disabled) { - // Updated error message - errorMessage.textContent = 'Please fix the errors in the password checklist.'; - errorMessage.classList.remove('hidden'); - } else { - // --- THIS IS A FRONTEND-ONLY DEMO --- - // In a real app, you would make a fetch() call to your backend - // to set the new password. - errorMessage.classList.add('hidden'); - alert('Password reset successfully! Redirecting to login...'); - - // Redirect to login page after success - window.location.href = "paycard_login.html"; - } - }); - } -}); \ No newline at end of file diff --git a/frontend/templates/forgot-password.html b/frontend/templates/forgot-password.html index 2a11bbb..a949f04 100644 --- a/frontend/templates/forgot-password.html +++ b/frontend/templates/forgot-password.html @@ -105,7 +105,76 @@