package handlers import ( "errors" "go-backend/internal/auth" "go-backend/internal/services" "html/template" "log" "net/http" ) type UserResponse struct { ID string `json:"id"` Username string `json:"username"` Email string `json:"email"` } // PatchUserHandler applies a partial update to the logged-in user. A field // is only touched when its form key is present in the request body — an // omitted key leaves that column unchanged, while an empty value is treated // as an explicit (and rejected) attempt to blank out a required field. func (h *Handler) PatchUserHandler(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("session") if err != nil { http.Error(w, "Unauthorized, no session token", http.StatusUnauthorized) return } session, bump, err := h.auth.ValidateSessionToken(r.Context(), cookie.Value) if err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) return } if session == nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } if bump { http.SetCookie(w, auth.NewSessionCookie(cookie.Value, false)) } if err := r.ParseForm(); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } input := services.PatchUserInput{} if r.PostForm.Has("username") { v := r.PostFormValue("username") if v == "" { http.Error(w, "Username cannot be empty", http.StatusBadRequest) return } input.Username = &v } if r.PostForm.Has("email") { v := r.PostFormValue("email") if v == "" { http.Error(w, "Email cannot be empty", http.StatusBadRequest) return } input.Email = &v } if r.PostForm.Has("current_password") { v := r.PostFormValue("current_password") input.CurrentPassword = &v } if r.PostForm.Has("new_password") { v := r.PostFormValue("new_password") if v == "" { http.Error(w, "New password cannot be empty", http.StatusBadRequest) return } input.NewPassword = &v } user, err := h.users.PatchUser(r.Context(), session.UserID, input) if err != nil { switch { case errors.Is(err, services.ErrCurrentPasswordRequired), errors.Is(err, services.ErrInvalidCurrentPassword): http.Error(w, err.Error(), http.StatusBadRequest) default: log.Printf("Error patching user: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) } return } // The settings page targets the request's hx-target (a per-field error // container) by default, so a plain success body would land there. Any // field actually changed is instead pushed out-of-band to the element // that displays its value, leaving the error container's swap empty. data := patchUserSuccessData{PasswordChanged: input.NewPassword != nil} if input.Username != nil { data.Username = &user.Username } if input.Email != nil { data.Email = &user.Email } w.Header().Set("Content-Type", "text/html; charset=utf-8") patchUserSuccessTmpl.Execute(w, data) } type patchUserSuccessData struct { Username *string Email *string PasswordChanged bool } var patchUserSuccessTmpl = template.Must(template.New("patchUserSuccess").Parse(`` + `{{if .Username}}

Username: {{.Username}}

{{end}}` + `{{if .Email}}

Email: {{.Email}}

{{end}}` + `{{if .PasswordChanged}}Password updated.{{end}}`, ))