feat(account): add editable phone and password change with validation
- Add editable phone field in /account General tab with UK phone validation - Create PUT /api/user/change-password endpoint in backend - Add zxcvbn password strength meter to change password modal - Add "passwords don't match" validation message to both /account and /register - Fix navbar logout reactivity with invalidateAll and $derived values - Fix a11y warnings: add labels, roles, and keyboard handlers - Remove unused CSS from account page
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
@@ -486,3 +487,69 @@ func ListAdminUsersHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type ChangePasswordRequest struct {
|
||||
CurrentPassword string `json:"current_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
||||
func ChangePasswordHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := mw.GetUserID(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var req ChangePasswordRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.CurrentPassword == "" || req.NewPassword == "" {
|
||||
http.Error(w, "current password and new password are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.NewPassword) < 8 {
|
||||
http.Error(w, "password must be at least 8 characters", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if len(req.NewPassword) > 72 {
|
||||
http.Error(w, "password must be less than 72 characters", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var passwordHash string
|
||||
err := db.DB.QueryRow(r.Context(), `SELECT password_hash FROM users WHERE id = $1`, userID).Scan(&passwordHash)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
http.Error(w, "user not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
log.Printf("Failed to fetch password hash for user %s: %v", userID, err)
|
||||
http.Error(w, "server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.CurrentPassword)); err != nil {
|
||||
http.Error(w, "current password is incorrect", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
newHash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
log.Printf("Failed to hash new password for user %s: %v", userID, err)
|
||||
http.Error(w, "server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = db.DB.Exec(r.Context(), `UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2`, string(newHash), userID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to update password for user %s: %v", userID, err)
|
||||
http.Error(w, "failed to update password", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
@@ -139,6 +139,7 @@ func main() {
|
||||
|
||||
r.Get("/user/profile", user.GetProfileHandler)
|
||||
r.Put("/user/profile", user.UpdateProfileHandler)
|
||||
r.Put("/user/change-password", user.ChangePasswordHandler)
|
||||
r.Delete("/user/account", user.DeleteAccountHandler)
|
||||
r.Get("/user/loyalty", user.GetLoyaltyHandler)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user