diff --git a/backend/internal/auth/forgotPassword.go b/backend/internal/auth/forgotPassword.go
index ad01894..e1d2dc0 100644
--- a/backend/internal/auth/forgotPassword.go
+++ b/backend/internal/auth/forgotPassword.go
@@ -12,7 +12,10 @@ import (
"unicode"
"unicard-go/backend/internal/pkg/account"
+ jsonwrite "unicard-go/backend/internal/pkg/handler"
+ smtp "unicard-go/backend/internal/pkg/smtpbody"
+ "github.com/go-playground/validator/v10"
"gopkg.in/gomail.v2"
)
@@ -23,12 +26,17 @@ type OTPData struct {
// Forgot and Reset Password Request
type ForgotPasswordRequest struct {
- Email string `json:"email"`
- OTP string `json:"otp"`
- NewPassword string `json:"new_password"`
+ Email string `json:"email" validate:"required,email" db:"email"`
+ OTP string `json:"otp" validate:"required,numeric,len=6"`
+ NewPassword string `json:"new_password" validate:"required,min=8" db:"password_hash"`
}
var otpStore = make(map[string]OTPData)
+var validate *validator.Validate
+
+func init() {
+ validate = validator.New()
+}
// Forgot Password View
func (h *Handler) ForgotPasswordView(w http.ResponseWriter, r *http.Request) {
@@ -39,15 +47,13 @@ func (h *Handler) ForgotPasswordView(w http.ResponseWriter, r *http.Request) {
// Generate OTP Code
func generateOTP() string {
max := big.NewInt(1000000)
- n, err := rand.Int(rand.Reader, max)
- if err != nil {
- return "123456"
- }
+ n, _ := rand.Int(rand.Reader, max)
+
return fmt.Sprintf("%06d", n.Int64())
}
// Send OTP to email
-func sendEmailOTP(email, otp string) error {
+func sendEmailOTP(email, name, otp string) error {
smtpHost := os.Getenv("SMTP_HOST")
smtpPort := 587
smtpEmail := os.Getenv("SMTP_EMAIL")
@@ -55,77 +61,98 @@ func sendEmailOTP(email, otp string) error {
smtpPass := os.Getenv("SMTP_PASSWORD")
m := gomail.NewMessage()
- m.SetHeader("From", smtpSender, smtpEmail)
+ 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)
+ m.SetHeader("Subject", "Unicard Password Reset OTP")
- d := gomail.NewDialer(smtpHost, smtpPort, smtpSender, smtpPass)
- if err := d.DialAndSend(m); err != nil {
- log.Fatal(err)
- }
- fmt.Println("OTP SENT")
+ htmlBody := fmt.Sprintf(smtp.OTPCode(), name, otp)
- // 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")
+ m.SetBody("text/html", htmlBody)
- if os.Getenv("SMTP_HOST") == "" {
- fmt.Printf("SMTP credentials not set. Simulating email success.\n")
- return nil
- }
+ d := gomail.NewDialer(
+ smtpHost,
+ smtpPort,
+ smtpEmail,
+ smtpPass,
+ )
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 err
}
+
+ log.Printf("OTP sent successfully to %s", email)
return nil
}
// Forgot Password Send OTP
func (h *Handler) ForgotPasswordSendOTP(w http.ResponseWriter, r *http.Request) {
+ // get context from request
+ ctx := r.Context()
+
var req ForgotPasswordRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, "Invalid input", http.StatusBadRequest)
+ log.Println("Error decoding request:", err)
+ jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{
+ Success: false,
+ Message: "Invalid input",
+ })
return
}
exists, err := account.IsEmailExist(h.DB, req.Email)
if err != nil {
- http.Error(w, "System error", http.StatusInternalServerError)
+ log.Println("Error checking email existence:", err)
+ jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{
+ Success: false,
+ Message: "System error",
+ })
return
}
if !exists {
// 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."})
+ jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{
+ Success: true,
+ Message: "If the email is found, an OTP has been sent.",
+ })
return
}
+ // Fetch the user's name
+ var fullName string
+ err = h.DB.QueryRowContext(ctx, "SELECT full_name FROM users WHERE email = ?", req.Email).Scan(&fullName)
+ if err != nil {
+ fullName = "there" // Fallback if name is not found
+ }
+
+ // Generate OTP that valid for 5 minutes
otp := generateOTP()
otpStore[req.Email] = OTPData{
OTP: otp,
- Expiry: time.Now().Add(10 * time.Minute),
+ Expiry: time.Now().Add(5 * time.Minute),
}
- if err := sendEmailOTP(req.Email, otp); err != nil {
- fmt.Println("Error sending email:", err)
+ if err := sendEmailOTP(req.Email, fullName, otp); err != nil {
+ log.Println("Error sending email:", err)
http.Error(w, "Failed to send OTP", http.StatusInternalServerError)
return
}
- w.WriteHeader(http.StatusOK)
- json.NewEncoder(w).Encode(map[string]string{"message": "OTP sent successfully"})
+ jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{
+ Success: true,
+ 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)
+ log.Println("Error decoding request:", err)
+ jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{
+ Success: false,
+ Message: "Invalid input",
+ })
return
}
@@ -141,22 +168,31 @@ func (h *Handler) ForgotPasswordVerifyOTP(w http.ResponseWriter, r *http.Request
return
}
- w.WriteHeader(http.StatusOK)
- json.NewEncoder(w).Encode(map[string]string{"message": "OTP verified"})
+ jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{
+ Success: true,
+ Message: "OTP verified",
+ })
}
// 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)
+ log.Println("Error decoding request:", err)
+ jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{
+ Success: false,
+ Message: "Invalid input",
+ })
return
}
// Verify OTP again
data, ok := otpStore[req.Email]
if !ok || data.OTP != req.OTP {
- http.Error(w, "Invalid or expired OTP", http.StatusUnauthorized)
+ jsonwrite.WriteJSON(w, http.StatusUnauthorized, jsonwrite.APIResponse{
+ Success: false,
+ Message: "Invalid or expired OTP",
+ })
return
}
@@ -182,8 +218,10 @@ func (h *Handler) ResetPassword(w http.ResponseWriter, r *http.Request) {
// Clean up OTP
delete(otpStore, req.Email)
- w.WriteHeader(http.StatusOK)
- json.NewEncoder(w).Encode(map[string]string{"message": "Password updated successfully"})
+ jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{
+ Success: true,
+ Message: "Password updated successfully",
+ })
}
// Validate password helper function
@@ -191,40 +229,54 @@ 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
+
+ var (
+ hasUpper bool
+ hasLower bool
+ hasNumber bool
+ 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):
+
+ case unicode.IsNumber(c):
+ hasNumber = 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")
+
+ switch {
+ case !hasUpper:
+ return fmt.Errorf("password must contain at least one uppercase letter")
+
+ case !hasLower:
+ return fmt.Errorf("password must contain at least one lowercase letter")
+
+ case !hasNumber:
+ return fmt.Errorf("password must contain at least one number")
+
+ case !hasSpecial:
+ return fmt.Errorf("password must contain at least one special character")
}
+
return nil
}
// Update Password Handler
func (h *Handler) updatePassword(email, hashedPassword string) error {
- query := "UPDATE users SET password = ? WHERE email = ?"
+ query := "UPDATE users SET password_hash = ? WHERE email = ?"
_, err := h.DB.Exec(query, hashedPassword, email)
if err != nil {
- return fmt.Errorf("failed to update password: %w", err)
+ log.Printf("failed to update password: %v", err)
+ return err
}
return nil
}
diff --git a/backend/internal/pkg/smtpbody/smtp.go b/backend/internal/pkg/smtpbody/smtp.go
new file mode 100644
index 0000000..3051a7d
--- /dev/null
+++ b/backend/internal/pkg/smtpbody/smtp.go
@@ -0,0 +1,50 @@
+package smtp
+
+import (
+ "fmt"
+ "time"
+)
+
+func OTPCode() string {
+ return `
+
+
+
+ Password Reset OTP
+
+
+
+
+
+
+
Hello %s,
+
We received a request to reset your password. Please use the following One-Time Password (OTP) to proceed. This OTP is valid for 5 minutes.
+
+
If you did not request a password reset, please ignore this email or contact support if you have concerns.
+
Thank you,
The Unicard Team
+
+
+
+
+`
+}
+
+func Year() string {
+ return fmt.Sprintf("%d", time.Now().Year())
+}
diff --git a/frontend/assets/js/forgot-password.js b/frontend/assets/js/forgot-password.js
index 2b3cf86..2b5866e 100644
--- a/frontend/assets/js/forgot-password.js
+++ b/frontend/assets/js/forgot-password.js
@@ -17,7 +17,85 @@ document.addEventListener("DOMContentLoaded", function () {
// Password Elements
const passwordInput = document.getElementById('new_password');
const confirmPasswordInput = document.getElementById('confirm_password');
- const errorMessage = document.getElementById('error-message');
+
+ // Input Elements
+ const emailInput = document.getElementById('email');
+ const otpInputs = Array.from(document.querySelectorAll('.otp-digit'));
+
+ // Error Elements
+ const emailError = document.getElementById('email-error');
+ const otpError = document.getElementById('otp-error');
+ const newPasswordError = document.getElementById('new-password-error');
+ const confirmPasswordError = document.getElementById('confirm-password-error');
+
+ // Timer & Resend
+ const otpTimer = document.getElementById('otp-timer');
+ const resendOtpBtn = document.getElementById('resend-otp-btn');
+ let timerInterval;
+ let resendCount = 0;
+ const MAX_RESEND = 3;
+
+ function showFieldError(input, errorEl, msg) {
+ if (!errorEl) return;
+ errorEl.textContent = msg;
+ errorEl.classList.remove('hidden');
+ if (Array.isArray(input)) {
+ input.forEach(i => {
+ i.classList.remove('border-gray-300', 'focus:ring-blue-500', 'focus:border-blue-500');
+ i.classList.add('border-red-500', 'focus:ring-red-500', 'focus:border-red-500');
+ });
+ } else if (input) {
+ input.classList.remove('border-gray-300', 'focus:ring-blue-500', 'focus:border-blue-500');
+ input.classList.add('border-red-500', 'focus:ring-red-500', 'focus:border-red-500');
+ }
+ }
+
+ function hideFieldError(input, errorEl) {
+ if (!errorEl) return;
+ errorEl.classList.add('hidden');
+ if (Array.isArray(input)) {
+ input.forEach(i => {
+ i.classList.remove('border-red-500', 'focus:ring-red-500', 'focus:border-red-500');
+ i.classList.add('border-gray-300', 'focus:ring-blue-500', 'focus:border-blue-500');
+ });
+ } else if (input) {
+ input.classList.remove('border-red-500', 'focus:ring-red-500', 'focus:border-red-500');
+ input.classList.add('border-gray-300', 'focus:ring-blue-500', 'focus:border-blue-500');
+ }
+ }
+
+ function hideAllErrors() {
+ hideFieldError(emailInput, emailError);
+ hideFieldError(otpInputs, otpError);
+ hideFieldError(passwordInput, newPasswordError);
+ hideFieldError(confirmPasswordInput, confirmPasswordError);
+ }
+
+ function startTimer() {
+ let timeLeft = 300; // 5 minutes
+ if (resendOtpBtn) resendOtpBtn.disabled = true;
+ if (otpTimer) otpTimer.textContent = '05:00';
+
+ if (timerInterval) clearInterval(timerInterval);
+
+ timerInterval = setInterval(() => {
+ timeLeft--;
+ let minutes = Math.floor(timeLeft / 60);
+ let seconds = timeLeft % 60;
+ let formattedTime = `${minutes < 10 ? '0' : ''}${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
+ if (otpTimer) otpTimer.textContent = formattedTime;
+
+ if (timeLeft <= 0) {
+ clearInterval(timerInterval);
+ if (otpTimer) otpTimer.textContent = '00:00';
+ if (resendOtpBtn && resendCount < MAX_RESEND) {
+ resendOtpBtn.disabled = false;
+ } else if (resendOtpBtn && resendCount >= MAX_RESEND) {
+ if (otpTimer) otpTimer.textContent = 'Max attempts';
+ }
+ }
+ }, 1000);
+ }
// Validation Elements
const checklist = document.getElementById('validation-checklist');
@@ -39,13 +117,15 @@ document.addEventListener("DOMContentLoaded", function () {
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');
+ checkElement.classList.remove('text-gray-500', 'bg-gray-100');
+ checkElement.classList.add('text-green-700', 'bg-green-100');
+ icon.classList.remove('fa-circle');
+ icon.classList.add('fa-check');
} else {
- checkElement.classList.remove('valid');
- icon.classList.remove('fa-check-circle', 'text-green-600');
- icon.classList.add('fa-times-circle', 'text-red-500');
+ checkElement.classList.remove('text-green-700', 'bg-green-100');
+ checkElement.classList.add('text-gray-500', 'bg-gray-100');
+ icon.classList.remove('fa-check');
+ icon.classList.add('fa-circle');
}
}
@@ -55,12 +135,6 @@ document.addEventListener("DOMContentLoaded", function () {
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);
@@ -78,7 +152,7 @@ document.addEventListener("DOMContentLoaded", function () {
if (submitButton) {
if (allValid) {
submitButton.disabled = false;
- if (errorMessage) errorMessage.classList.add('hidden');
+ hideAllErrors();
} else {
submitButton.disabled = true;
}
@@ -86,8 +160,66 @@ document.addEventListener("DOMContentLoaded", function () {
}
if (passwordInput && confirmPasswordInput) {
- passwordInput.addEventListener('input', validateForm);
- confirmPasswordInput.addEventListener('input', validateForm);
+ passwordInput.addEventListener('input', () => { hideFieldError(passwordInput, newPasswordError); validateForm(); });
+ confirmPasswordInput.addEventListener('input', () => { hideFieldError(confirmPasswordInput, confirmPasswordError); validateForm(); });
+ }
+
+ if (emailInput) {
+ emailInput.addEventListener('input', () => hideFieldError(emailInput, emailError));
+ }
+
+ otpInputs.forEach((input, index) => {
+ input.addEventListener('input', (e) => {
+ input.value = input.value.replace(/[^0-9]/g, '');
+ hideFieldError(otpInputs, otpError);
+ if (input.value && index < otpInputs.length - 1) {
+ otpInputs[index + 1].focus();
+ }
+ });
+
+ input.addEventListener('keydown', (e) => {
+ if (e.key === 'Backspace' && !input.value && index > 0) {
+ otpInputs[index - 1].focus();
+ } else if (e.key === 'Enter') {
+ e.preventDefault();
+ if (confirmOtpBtn) confirmOtpBtn.click();
+ }
+ });
+
+ input.addEventListener('paste', (e) => {
+ e.preventDefault();
+ const pastedData = e.clipboardData.getData('text').replace(/[^0-9]/g, '').slice(0, 6);
+ for (let i = 0; i < pastedData.length; i++) {
+ if (otpInputs[i]) {
+ otpInputs[i].value = pastedData[i];
+ if (i < 5) otpInputs[i + 1].focus();
+ }
+ }
+ hideFieldError(otpInputs, otpError);
+ });
+ });
+
+ if (forgotForm) {
+ forgotForm.addEventListener('submit', (e) => e.preventDefault());
+
+ if (emailInput) {
+ emailInput.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ if (sendLinkBtn) sendLinkBtn.click();
+ }
+ });
+ }
+
+ const handlePasswordEnter = (e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ if (submitButton && !submitButton.disabled) submitButton.click();
+ }
+ };
+
+ if (passwordInput) passwordInput.addEventListener('keypress', handlePasswordEnter);
+ if (confirmPasswordInput) confirmPasswordInput.addEventListener('keypress', handlePasswordEnter);
}
if (forgotForm && sendLinkBtn && confirmOtpBtn && emailStep && otpStep) {
@@ -95,10 +227,14 @@ document.addEventListener("DOMContentLoaded", function () {
// --- STEP 1: Handle Email submission ---
sendLinkBtn.addEventListener('click', async function (event) {
event.preventDefault();
+ hideAllErrors();
- const emailVal = document.getElementById('email').value;
- if (!emailVal) {
- alert("Please enter your email");
+ const emailVal = emailInput.value;
+ const emailRegex = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/;
+
+ if (!emailVal || !emailRegex.test(emailVal)) {
+ // validateEmailDynamically will handle the UI
+ if (emailInput) emailInput.dispatchEvent(new Event('input'));
return;
}
@@ -119,12 +255,16 @@ document.addEventListener("DOMContentLoaded", function () {
formTitle.textContent = "Check your email";
formSubtitle.textContent = "We sent a 6-digit code to your email.";
+
+ // Reset resend count and start timer
+ resendCount = 0;
+ startTimer();
} else {
const error = await response.text();
- alert("Error: " + error);
+ showFieldError(emailInput, emailError, "Error: " + error);
}
} catch (error) {
- alert("An error occurred. Please try again.");
+ showFieldError(emailInput, emailError, "An error occurred. Please try again.");
} finally {
sendLinkBtn.disabled = false;
sendLinkBtn.textContent = "Send Reset Link";
@@ -134,11 +274,12 @@ document.addEventListener("DOMContentLoaded", function () {
// --- STEP 2: Handle OTP submission ---
confirmOtpBtn.addEventListener('click', async function (event) {
event.preventDefault();
+ hideAllErrors();
- const otpVal = document.getElementById('otp').value;
+ const otpVal = otpInputs.map(i => i.value).join('');
- if (!otpVal) {
- alert("Please enter the OTP");
+ if (!otpVal || otpVal.length !== 6) {
+ showFieldError(otpInputs, otpError, "Please enter a valid 6-digit OTP");
return;
}
@@ -153,6 +294,7 @@ document.addEventListener("DOMContentLoaded", function () {
});
if (response.ok) {
+ clearInterval(timerInterval);
currentOtp = otpVal;
otpStep.classList.add('hidden');
if (passwordStep) passwordStep.classList.remove('hidden');
@@ -161,34 +303,70 @@ document.addEventListener("DOMContentLoaded", function () {
formSubtitle.textContent = "Please create a secure password.";
} else {
const error = await response.text();
- alert("Invalid OTP: " + error);
+ showFieldError(otpInputs, otpError, "Invalid OTP: " + error);
}
} catch (error) {
- alert("An error occurred. Please try again.");
+ showFieldError(otpInputs, otpError, "An error occurred. Please try again.");
} finally {
confirmOtpBtn.disabled = false;
confirmOtpBtn.textContent = "Confirm Code & Continue";
}
});
+ // --- Handle Resend OTP ---
+ if (resendOtpBtn) {
+ resendOtpBtn.addEventListener('click', async function(e) {
+ e.preventDefault();
+ hideAllErrors();
+
+ if (resendCount >= MAX_RESEND) {
+ showFieldError(otpInputs, otpError, "Maximum resend attempts reached.");
+ return;
+ }
+
+ resendOtpBtn.disabled = true;
+ resendOtpBtn.textContent = "Sending...";
+
+ try {
+ const response = await fetch('/v1/forgot-password/send-otp', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email: currentEmail })
+ });
+
+ if (response.ok) {
+ resendCount++;
+ resendOtpBtn.textContent = "Resend";
+ startTimer();
+ otpInputs.forEach(i => i.value = '');
+ otpInputs[0].focus();
+ } else {
+ const error = await response.text();
+ showFieldError(otpInputs, otpError, "Error: " + error);
+ resendOtpBtn.textContent = "Resend";
+ resendOtpBtn.disabled = false;
+ }
+ } catch (error) {
+ showFieldError(otpInputs, otpError, "An error occurred. Please try again.");
+ resendOtpBtn.textContent = "Resend";
+ resendOtpBtn.disabled = false;
+ }
+ });
+ }
+
// --- STEP 3: Handle Password Reset submission ---
if (submitButton) {
submitButton.addEventListener('click', async function (event) {
event.preventDefault();
+ hideAllErrors();
validateForm();
if (submitButton.disabled) {
- if (errorMessage) {
- errorMessage.textContent = 'Please fix the errors in the password checklist.';
- errorMessage.classList.remove('hidden');
- }
+ showFieldError(passwordInput, newPasswordError, 'Please fix the errors in the password checklist.');
} else {
if (!currentEmail || !currentOtp) {
- if (errorMessage) {
- errorMessage.textContent = 'Missing email or OTP. Please refresh and try again.';
- errorMessage.classList.remove('hidden');
- }
+ showFieldError(passwordInput, newPasswordError, 'Missing email or OTP. Please refresh and try again.');
return;
}
@@ -207,23 +385,22 @@ document.addEventListener("DOMContentLoaded", function () {
});
if (response.ok) {
- if (errorMessage) errorMessage.classList.add('hidden');
- alert('Password reset successfully! Redirecting to login...');
- window.location.href = "/login";
+ hideAllErrors();
+ formTitle.textContent = "Success!";
+ formSubtitle.textContent = "Password reset successfully. Redirecting to login...";
+ if (passwordStep) passwordStep.classList.add('hidden');
+
+ setTimeout(() => {
+ window.location.href = "/login";
+ }, 2000);
} else {
const error = await response.text();
- if (errorMessage) {
- errorMessage.textContent = "Error: " + error;
- errorMessage.classList.remove('hidden');
- }
+ showFieldError(passwordInput, newPasswordError, "Error: " + error);
submitButton.disabled = false;
submitButton.textContent = "Set New Password";
}
} catch (error) {
- if (errorMessage) {
- errorMessage.textContent = "An error occurred. Please try again.";
- errorMessage.classList.remove('hidden');
- }
+ showFieldError(passwordInput, newPasswordError, "An error occurred. Please try again.");
submitButton.disabled = false;
submitButton.textContent = "Set New Password";
}
diff --git a/frontend/templates/forgot-password.html b/frontend/templates/forgot-password.html
index a949f04..777ff5a 100644
--- a/frontend/templates/forgot-password.html
+++ b/frontend/templates/forgot-password.html
@@ -56,6 +56,8 @@ Forgot Passwor