diff --git a/.gitignore b/.gitignore index dc928a4..afe833e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,10 @@ .env .unknown +# gitgore the compiled file +*.exe + +# gitgore the storage directory storage/* !storage/.gitkeep storage/**/* diff --git a/backend/cmd/app/config.yml b/backend/cmd/app/config.yml new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/backend/cmd/app/config.yml @@ -0,0 +1 @@ +{} diff --git a/backend/cmd/app/main.go b/backend/cmd/app/main.go index 0570e1b..cc9c133 100644 --- a/backend/cmd/app/main.go +++ b/backend/cmd/app/main.go @@ -88,8 +88,12 @@ func main() { mux.HandleFunc("POST /v1/forgot-password/verify-otp", authHandler.ForgotPasswordVerifyOTP) mux.HandleFunc("POST /v1/reset-password", authHandler.ResetPassword) mux.HandleFunc("GET /u/{username}", userHandler.ProfileView) + mux.HandleFunc("PATCH /u/{username}/profile/edit", userHandler.ProfileEdit) + mux.HandleFunc("POST /v1/user/{username}/profile/verify-password", userHandler.ProfileVerifyPassword) + mux.HandleFunc("PUT /u/{username}/profile/password", userHandler.ProfileChangePassword) mux.HandleFunc("GET /u/{username}/dashboard", userHandler.DashboardView) mux.HandleFunc("GET /u/{username}/card", userHandler.CardView) + mux.HandleFunc("GET /u/{username}/settings", userHandler.SettingsView) mux.HandleFunc("GET /u/{username}/topup", userHandler.TopUpView) // Your frontend calls this to get the Xendit URL mux.HandleFunc("POST /api/topup/create-session/{username}", userHandler.CreateXenditInvoice) diff --git a/backend/internal/user/customer_profile.go b/backend/internal/user/customer_profile.go index d45d8f1..f23a983 100644 --- a/backend/internal/user/customer_profile.go +++ b/backend/internal/user/customer_profile.go @@ -1,13 +1,59 @@ package user import ( + "context" + "encoding/json" + "errors" "fmt" + "log" "net/http" + "strings" + "unicard-go/backend/internal/pkg/account" + jsonwrite "unicard-go/backend/internal/pkg/handler" + + "golang.org/x/crypto/bcrypt" +) + +// Sentinel errors for password verification +var ( + ErrPasswordLookupFailed = errors.New("Failed to look up password hash") + ErrIncorrectPassword = errors.New("Incorrect password") ) +// ProfileUpdateRequest represents the expected payload for updating user profile information +type ProfileUpdateRequest struct { + Username string `json:"username,omitempty" db:"username"` + FullName string `json:"full_name,omitempty" db:"name"` + Email string `json:"email,omitempty" db:"email"` + Phone string `json:"phone_number,omitempty" db:"phone_number"` + CurrentPassword string `json:"current_password,omitempty"` + NewPassword string `json:"new_password,omitempty"` + ConfirmPassword string `json:"confirm_password,omitempty"` +} + +// verifyCurrentPassword checks the given password against the stored hash for username. +// On success, it returns the stored hash so callers can run further checks +// (e.g. preventing the new password from being the same as the current one). +func (h *Handler) verifyCurrentPassword(ctx context.Context, username, password string) (string, error) { + var currentHash string + err := h.DB.QueryRowContext(ctx, "SELECT password_hash FROM users WHERE username = ?", username).Scan(¤tHash) + if err != nil { + return "", fmt.Errorf("%w: %v", ErrPasswordLookupFailed, err) + } + + if err := bcrypt.CompareHashAndPassword([]byte(currentHash), []byte(password)); err != nil { + return "", ErrIncorrectPassword + } + + return currentHash, nil +} + +// ProfileView handles the display of the user's profile page +// It retrieves the username from the URL and renders the profile template func (h *Handler) ProfileView(w http.ResponseWriter, r *http.Request) { fmt.Println("Profile view is running...") + // Extract the username from the URL path username := r.PathValue("username") data := struct { Username string @@ -15,5 +61,206 @@ func (h *Handler) ProfileView(w http.ResponseWriter, r *http.Request) { Username: username, } + // Render the profile template with the username data h.Tpl.ExecuteTemplate(w, "profile.html", data) } + +// ProfileEdit renders the profile edit page (assumes router already filtered by method) +func (h *Handler) ProfileEdit(w http.ResponseWriter, r *http.Request) { + username := r.PathValue("username") + ctx := r.Context() + + var req ProfileUpdateRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Invalid request payload", + }) + return + } + + // PATCH — at least one field required + if req.FullName == "" && req.Email == "" && req.Phone == "" { + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "At least one field is required", + }) + return + } + + // Build query dynamically based on what was sent + fields := []string{} + args := []any{} + + if req.FullName != "" { + fields = append(fields, "name = ?") + args = append(args, req.FullName) + } + if req.Email != "" { + fields = append(fields, "email = ?") + args = append(args, req.Email) + } + if req.Phone != "" { + fields = append(fields, "phone_number = ?") + args = append(args, req.Phone) + } + + // Append username as last arg for WHERE clause + args = append(args, username) + + query := "UPDATE users SET " + strings.Join(fields, ", ") + " WHERE username = ?" + + _, err := h.DB.ExecContext(ctx, query, args...) + if err != nil { + log.Printf("ProfileEdit DB error: %v | query: %s | args: %v", err, query, args) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Failed to update profile", + }) + return + } + + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: true, + Message: "Profile updated successfully", + }) +} + +// ProfileVerifyPassword checks if the provided current password is correct +func (h *Handler) ProfileVerifyPassword(w http.ResponseWriter, r *http.Request) { + username := r.PathValue("username") + ctx := r.Context() + + var req struct { + CurrentPassword string `json:"current_password"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Invalid request payload", + }) + return + } + + if req.CurrentPassword == "" { + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Current password is required", + }) + return + } + + _, err := h.verifyCurrentPassword(ctx, username, req.CurrentPassword) + switch { + case errors.Is(err, ErrPasswordLookupFailed): + log.Printf("ProfileVerifyPassword lookup error: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Failed to verify current password", + }) + return + case errors.Is(err, ErrIncorrectPassword): + jsonwrite.WriteJSON(w, http.StatusUnauthorized, jsonwrite.APIResponse{ + Success: false, + Message: "Current password is incorrect", + }) + return + } + + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: true, + Message: "Password verified", + }) +} + +// ProfileChangePassword handles changing the user's password +func (h *Handler) ProfileChangePassword(w http.ResponseWriter, r *http.Request) { + username := r.PathValue("username") + + var req ProfileUpdateRequest + var ctx = r.Context() + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Invalid request payload", + }) + return + } + + if req.CurrentPassword == "" || req.NewPassword == "" || req.ConfirmPassword == "" { + log.Printf("One or more password fields are empty for user: %s", username) + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "All password fields are required", + }) + return + } + + if req.NewPassword != req.ConfirmPassword { + log.Printf("New password and confirm password do not match for user: %s", username) + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Password do not match", + }) + return + } + + // Verify current password + log.Printf("Verifying current password for user: %s", username) + currentHash, err := h.verifyCurrentPassword(ctx, username, req.CurrentPassword) + switch { + case errors.Is(err, ErrPasswordLookupFailed): + log.Printf("Error fetching current password hash: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Failed to verify current password", + }) + return + case errors.Is(err, ErrIncorrectPassword): + log.Printf("Current password verification failed for user: %s", username) + jsonwrite.WriteJSON(w, http.StatusUnauthorized, jsonwrite.APIResponse{ + Success: false, + Message: "Current password is incorrect", + }) + return + } + log.Printf("Current password verified for user: %s", username) + + // Prevent setting the same password again + if err := bcrypt.CompareHashAndPassword([]byte(currentHash), []byte(req.NewPassword)); err == nil { + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "New password must be different from current password", + }) + return + } + + // Hash the new password using the account package's HashPassword function + hashedPassword, err := account.HashPassword(req.NewPassword) + if err != nil { + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Failed to hash password", + }) + return + } + + // Save new password hash + query := "UPDATE users SET password_hash = ? WHERE username = ?" + _, err = h.DB.ExecContext(ctx, query, hashedPassword, username) + if err != nil { + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Failed to update password", + }) + return + } + + // Respond with success message + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: true, + Message: "Password updated successfully", + }) +} \ No newline at end of file diff --git a/backend/internal/user/settings.go b/backend/internal/user/settings.go new file mode 100644 index 0000000..4819a18 --- /dev/null +++ b/backend/internal/user/settings.go @@ -0,0 +1,20 @@ +package user + +import ( + "fmt" + "net/http" +) + +// SettingsView handles the display of the user's settings page +func (h *Handler) SettingsView(w http.ResponseWriter, r *http.Request) { + fmt.Println("Settings view is running...") + + username := r.PathValue("username") + data := struct { + Username string + }{ + Username: username, + } + + h.Tpl.ExecuteTemplate(w, "settings.html", data) +} diff --git a/docs/unicardv3.sql b/docs/unicardv3.sql index 1b82cc6..9bbdb81 100644 --- a/docs/unicardv3.sql +++ b/docs/unicardv3.sql @@ -129,11 +129,10 @@ CREATE TABLE top_ups ( gateway_cost DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Actual fee incurred from external payment providers (GCash, Maya, Bank) per transaction', net_gateway_fee DECIMAL(10, 2) GENERATED ALWAYS AS (convenience_fee - gateway_cost) STORED COMMENT 'Automatically calculated net revenue kept by the platform after 3rd party costs', total_charged DECIMAL(10, 2) GENERATED ALWAYS AS (amount + convenience_fee) STORED COMMENT 'Automatically calculated column representing the absolute total cash value collected from the external channel source', - payment_method ENUM('cash', 'gcash', 'maya', 'over_the_counter', 'stripe') NOT NULL COMMENT 'Drives system tracking to audit cash-drawer liquid positions against programmatic API callbacks', + payment_method ENUM('cash', 'gcash', 'maya', 'over_the_counter', 'xendit') NOT NULL COMMENT 'Drives system tracking to audit cash-drawer liquid positions against programmatic API callbacks', handled_by VARCHAR(50) NULL COMMENT 'Public user_id string identifier referencing the administrative staff member who manually accepted physical bills if OTC cash-loaded', status ENUM('pending', 'completed', 'failed') DEFAULT 'completed' COMMENT 'State pipeline tracker handling payment gateway processing exceptions or drops', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated clock timestamp mapping exactly when wallet balance credits were finalized', updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Tracks chronological life cycle changes, such as a top-up shifting from pending to completed', - FOREIGN KEY (card_number) REFERENCES cards(card_number), - FOREIGN KEY (handled_by) REFERENCES users(user_id) + FOREIGN KEY (card_number) REFERENCES cards(card_number) ) COMMENT='High-growth balance loader ledger maintaining immutable compliance auditing for all incoming ecosystem liquidity channels'; \ No newline at end of file diff --git a/frontend/assets/js/profile.js b/frontend/assets/js/profile.js index 4273f55..cc80812 100644 --- a/frontend/assets/js/profile.js +++ b/frontend/assets/js/profile.js @@ -1,6 +1,8 @@ document.addEventListener("DOMContentLoaded", function () { console.log("Profile page script loaded."); + const username = document.body.dataset.username; + // --- Profile Edit Elements --- const editProfileBtn = document.getElementById('edit-profile-btn'); const cancelEditBtn = document.getElementById('cancel-edit-btn'); @@ -11,6 +13,11 @@ document.addEventListener("DOMContentLoaded", function () { if (editProfileBtn && cancelEditBtn && profileActions && profileView && profileEditForm && saveProfileBtn) { editProfileBtn.addEventListener('click', () => { + // Pre-fill edit form with current values + document.getElementById('full_name').value = document.getElementById('profile-view-name').innerText.trim(); + document.getElementById('email').value = document.getElementById('profile-view-email').innerText.trim(); + document.getElementById('phone').value = document.getElementById('profile-view-phone').innerText.trim(); + profileView.classList.add('hidden'); profileEditForm.classList.remove('hidden'); editProfileBtn.classList.add('hidden'); @@ -25,27 +32,48 @@ document.addEventListener("DOMContentLoaded", function () { editProfileBtn.classList.remove('hidden'); }); - saveProfileBtn.addEventListener('click', (e) => { + saveProfileBtn.addEventListener('click', async (e) => { e.preventDefault(); - - // In a real app, send fetch() request here + const newName = document.getElementById('full_name').value; const newEmail = document.getElementById('email').value; const newPhone = document.getElementById('phone').value; - // Since dashboard.js manages the span values via ID, we just update the text content of the spans - const nameSpan = document.getElementById('profile-view-name'); - const emailSpan = document.getElementById('profile-view-email'); - const phoneSpan = document.getElementById('profile-view-phone'); - - if (nameSpan) nameSpan.innerText = newName; - if (emailSpan) emailSpan.innerText = newEmail; - if (phoneSpan) phoneSpan.innerText = newPhone; + try { + const response = await fetch(`/u/${username}/profile/edit`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + full_name: newName, + email: newEmail, + phone_number: newPhone + }) + }); - profileEditForm.classList.add('hidden'); - profileView.classList.remove('hidden'); - profileActions.classList.add('hidden'); - editProfileBtn.classList.remove('hidden'); + const result = await response.json(); + + if (!result.success) { + alert(result.message || 'Failed to update profile'); + return; + } + + const nameSpan = document.getElementById('profile-view-name'); + const emailSpan = document.getElementById('profile-view-email'); + const phoneSpan = document.getElementById('profile-view-phone'); + + if (nameSpan) nameSpan.innerText = newName; + if (emailSpan) emailSpan.innerText = newEmail; + if (phoneSpan) phoneSpan.innerText = newPhone; + + profileEditForm.classList.add('hidden'); + profileView.classList.remove('hidden'); + profileActions.classList.add('hidden'); + editProfileBtn.classList.remove('hidden'); + + } catch (err) { + console.error('Profile update error:', err); + alert('Network error, please try again.'); + } }); } @@ -55,7 +83,7 @@ document.addEventListener("DOMContentLoaded", function () { const confirmPasswordInput = document.getElementById('confirm_password'); const passwordSubmitBtn = document.getElementById('change-password-btn'); const passwordErrorMsg = document.getElementById('password-error-message'); - + // Checklist elements const checklist = document.getElementById('password-checklist'); const lengthCheck = document.getElementById('length-check'); @@ -88,44 +116,127 @@ document.addEventListener("DOMContentLoaded", function () { const password = newPasswordInput.value; const confirmPassword = confirmPasswordInput.value; - if (password.length > 0 || confirmPassword.length > 0) { - checklist.classList.remove('hidden'); - } else { - checklist.classList.add('hidden'); - } - - const isLengthValid = password.length >= 8; - const isCaseValid = hasLower.test(password) && hasUpper.test(password); - const isNumValid = hasNumber.test(password); const passwordsMatch = password === confirmPassword && password.length > 0; - - updateChecklistItem(lengthCheck, isLengthValid); - updateChecklistItem(caseCheck, isCaseValid); - updateChecklistItem(numCheck, isNumValid); - updateChecklistItem(matchCheck, passwordsMatch); - - const allValid = isLengthValid && isCaseValid && isNumValid && passwordsMatch; - passwordSubmitBtn.disabled = !allValid; + + passwordSubmitBtn.disabled = !passwordsMatch; } if (passwordForm) { + const verifyCurrentPasswordBtn = document.getElementById('verify-current-password-btn'); + const currentPasswordInput = document.getElementById('current_password'); + const newPasswordSection = document.getElementById('new-password-section'); + let currentPasswordVerified = false; + + if (verifyCurrentPasswordBtn) { + verifyCurrentPasswordBtn.addEventListener('click', async () => { + const currentPassword = currentPasswordInput.value; + if (!currentPassword) { + passwordErrorMsg.textContent = 'Please enter your current password.'; + passwordErrorMsg.classList.remove('hidden'); + return; + } + + passwordErrorMsg.classList.add('hidden'); + verifyCurrentPasswordBtn.disabled = true; + verifyCurrentPasswordBtn.textContent = 'Verifying...'; + + try { + const response = await fetch(`/v1/user/${username}/profile/verify-password`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ current_password: currentPassword }) + }); + + const result = await response.json(); + + if (!result.success) { + passwordErrorMsg.textContent = result.message || 'Current password verification failed.'; + passwordErrorMsg.classList.remove('hidden'); + verifyCurrentPasswordBtn.disabled = false; + verifyCurrentPasswordBtn.textContent = 'Verify'; + return; + } + + // Verification successful + currentPasswordVerified = true; + currentPasswordInput.disabled = true; // prevent changing it + verifyCurrentPasswordBtn.classList.add('hidden'); + newPasswordSection.classList.remove('hidden'); + + // Trigger validation to check if new inputs are valid (which they won't be yet, so button stays disabled) + validatePasswordForm(); + + } catch (err) { + console.error('Password verification error:', err); + passwordErrorMsg.textContent = 'Network error, please try again.'; + passwordErrorMsg.classList.remove('hidden'); + verifyCurrentPasswordBtn.disabled = false; + verifyCurrentPasswordBtn.textContent = 'Verify'; + } + }); + } + newPasswordInput.addEventListener('input', validatePasswordForm); confirmPasswordInput.addEventListener('input', validatePasswordForm); - passwordForm.addEventListener('submit', (e) => { + passwordForm.addEventListener('submit', async (e) => { e.preventDefault(); + + if (!currentPasswordVerified) { + passwordErrorMsg.textContent = 'Please verify your current password first.'; + passwordErrorMsg.classList.remove('hidden'); + return; + } + if (passwordSubmitBtn.disabled) { - passwordErrorMsg.textContent = 'Please fix the errors in the password checklist.'; + passwordErrorMsg.textContent = 'Passwords do not match.'; passwordErrorMsg.classList.remove('hidden'); - } else { - // --- FRONTEND-ONLY DEMO --- - // In a real app, you'd send a fetch() request to your backend - // to verify the *current* password and set the new one. + return; + } + + const currentPassword = currentPasswordInput.value; + const newPassword = newPasswordInput.value; + const confirmPassword = confirmPasswordInput.value; + + try { + const response = await fetch(`/u/${username}/profile/password`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + current_password: currentPassword, + new_password: newPassword, + confirm_password: confirmPassword + }) + }); + + const result = await response.json(); + + if (!result.success) { + passwordErrorMsg.textContent = result.message || 'Failed to change password'; + passwordErrorMsg.classList.remove('hidden'); + return; + } + passwordErrorMsg.classList.add('hidden'); alert('Password changed successfully!'); + + // Reset form state passwordForm.reset(); - checklist.classList.add('hidden'); + if (checklist) checklist.classList.add('hidden'); passwordSubmitBtn.disabled = true; + currentPasswordVerified = false; + currentPasswordInput.disabled = false; + if (verifyCurrentPasswordBtn) { + verifyCurrentPasswordBtn.classList.remove('hidden'); + verifyCurrentPasswordBtn.disabled = false; + verifyCurrentPasswordBtn.textContent = 'Verify'; + } + newPasswordSection.classList.add('hidden'); + + } catch (err) { + console.error('Password change error:', err); + passwordErrorMsg.textContent = 'Network error, please try again.'; + passwordErrorMsg.classList.remove('hidden'); } }); } @@ -140,7 +251,7 @@ document.addEventListener("DOMContentLoaded", function () { const deleteConfirmText = document.getElementById('delete-confirm-text'); if (deleteAccountBtn && deleteModal && deleteModalContent && deleteModalCloseBtn && deleteModalCancelBtn && deleteModalConfirmBtn && deleteConfirmText) { - + function openDeleteModal() { deleteModal.classList.remove('hidden'); setTimeout(() => { @@ -152,21 +263,19 @@ document.addEventListener("DOMContentLoaded", function () { function closeDeleteModal() { deleteModalContent.classList.add('scale-95', 'opacity-0'); - deleteModalContent.classList.remove('scale-100', 'opacity-1ci00'); + deleteModalContent.classList.remove('scale-100', 'opacity-100'); deleteModal.classList.add('hidden', 'opacity-0'); - - // Reset the form in the modal + deleteConfirmText.value = ''; deleteModalConfirmBtn.disabled = true; deleteModalConfirmBtn.classList.add('bg-gray-400', 'cursor-not-allowed'); deleteModalConfirmBtn.classList.remove('bg-red-600', 'hover:bg-red-700'); - + setTimeout(() => { deleteModal.classList.add('hidden'); }, 300); } - // Check if user has typed "DELETE" deleteConfirmText.addEventListener('input', () => { if (deleteConfirmText.value === 'DELETE') { deleteModalConfirmBtn.disabled = false; @@ -182,7 +291,7 @@ document.addEventListener("DOMContentLoaded", function () { deleteAccountBtn.addEventListener('click', openDeleteModal); deleteModalCloseBtn.addEventListener('click', closeDeleteModal); deleteModalCancelBtn.addEventListener('click', closeDeleteModal); - + deleteModal.addEventListener('click', (e) => { if (e.target === deleteModal) { closeDeleteModal(); @@ -191,7 +300,7 @@ document.addEventListener("DOMContentLoaded", function () { deleteModalConfirmBtn.addEventListener('click', () => { // --- FRONTEND-ONLY DEMO --- - // In a real app, send fetch() request to delete the user + // Not yet wired to backend alert('Account deleted. Redirecting...'); window.location.href = "paycard_login.html"; }); diff --git a/frontend/assets/js/settings.js b/frontend/assets/js/settings.js new file mode 100644 index 0000000..41dd57f --- /dev/null +++ b/frontend/assets/js/settings.js @@ -0,0 +1,120 @@ +document.addEventListener("DOMContentLoaded", function () { + console.log("Settings page script loaded."); + + const username = document.body.dataset.username; + if (!username) { + console.error("Username not found in body dataset."); + return; + } + + // --- Update Email --- + const updateEmailForm = document.getElementById('update-email-form'); + const updateEmailBtn = document.getElementById('update-email-btn'); + const emailErrorMsg = document.getElementById('settings-email-error'); + const emailSuccessMsg = document.getElementById('settings-email-success'); + + if (updateEmailForm) { + updateEmailForm.addEventListener('submit', async (e) => { + e.preventDefault(); + + emailErrorMsg.classList.add('hidden'); + emailSuccessMsg.classList.add('hidden'); + + const newEmail = document.getElementById('settings-email').value; + + try { + const response = await fetch(`/u/${username}/profile/edit`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: newEmail }) + }); + + const result = await response.json(); + + if (!result.success) { + emailErrorMsg.textContent = result.message || 'Failed to update email.'; + emailErrorMsg.classList.remove('hidden'); + return; + } + + emailSuccessMsg.textContent = 'Email updated successfully!'; + emailSuccessMsg.classList.remove('hidden'); + + } catch (err) { + console.error('Email update error:', err); + emailErrorMsg.textContent = 'Network error, please try again.'; + emailErrorMsg.classList.remove('hidden'); + } + }); + } + + // --- Update Password --- + const changePasswordForm = document.getElementById('settings-change-password-form'); + const passwordErrorMsg = document.getElementById('settings-password-error'); + const passwordSuccessMsg = document.getElementById('settings-password-success'); + + if (changePasswordForm) { + changePasswordForm.addEventListener('submit', async (e) => { + e.preventDefault(); + + passwordErrorMsg.classList.add('hidden'); + passwordSuccessMsg.classList.add('hidden'); + + const currentPassword = document.getElementById('settings_current_password').value; + const newPassword = document.getElementById('settings_new_password').value; + const confirmPassword = document.getElementById('settings_confirm_password').value; + + if (newPassword !== confirmPassword) { + passwordErrorMsg.textContent = "New password and confirm password do not match."; + passwordErrorMsg.classList.remove('hidden'); + return; + } + + try { + const response = await fetch(`/u/${username}/profile/password`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + current_password: currentPassword, + new_password: newPassword, + confirm_password: confirmPassword + }) + }); + + const result = await response.json(); + + if (!result.success) { + passwordErrorMsg.textContent = result.message || 'Failed to change password.'; + passwordErrorMsg.classList.remove('hidden'); + return; + } + + passwordSuccessMsg.textContent = 'Password updated successfully!'; + passwordSuccessMsg.classList.remove('hidden'); + changePasswordForm.reset(); + + } catch (err) { + console.error('Password change error:', err); + passwordErrorMsg.textContent = 'Network error, please try again.'; + passwordErrorMsg.classList.remove('hidden'); + } + }); + } + + // --- Mock Preferences Toggles --- + const toggles = ['toggle-2fa', 'toggle-email-notif', 'toggle-sms-notif', 'toggle-dark-mode']; + + toggles.forEach(toggleId => { + const toggle = document.getElementById(toggleId); + if (toggle) { + toggle.addEventListener('change', (e) => { + const settingName = toggleId.replace('toggle-', '').replace(/-/g, ' '); + const status = e.target.checked ? 'enabled' : 'disabled'; + // Simply log for now, as there is no backend for this yet + console.log(`${settingName} has been ${status}.`); + // Could display a toast notification here + }); + } + }); + +}); diff --git a/frontend/templates/customer/profile.html b/frontend/templates/customer/profile.html index 03cc26f..8ab86ef 100644 --- a/frontend/templates/customer/profile.html +++ b/frontend/templates/customer/profile.html @@ -12,7 +12,7 @@ -
+