package handlers import ( "net/http" "github.com/go-chi/chi" "sneak.berlin/go/webhooker/internal/database" ) // HandleProfile returns a handler for the user profile page func (h *Handlers) HandleProfile() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { sessionUserID, sessionUsername, ok := h.profileOwnerOrDeny(w, r) if !ok { return } h.renderProfile(w, r, sessionUserID, sessionUsername, "", "") } } // HandlePasswordChange returns a handler that lets an authenticated // user change their own password. It is served by the CSRF- and // auth-protected POST /password route under /user/{username}. func (h *Handlers) HandlePasswordChange() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { sessionUserID, sessionUsername, ok := h.profileOwnerOrDeny(w, r) if !ok { return } // The body size cap is enforced by the MaxBodySize // middleware, which runs before CSRF parses the form. err := r.ParseForm() if err != nil { h.log.Error("failed to parse form", "error", err) http.Error(w, "Bad request", http.StatusBadRequest) return } successMessage, errorMessage, handled := h.applyPasswordChange( w, sessionUsername, r.FormValue("current_password"), r.FormValue("new_password"), r.FormValue("confirm_password"), ) if !handled { return } h.renderProfile( w, r, sessionUserID, sessionUsername, successMessage, errorMessage, ) } } // applyPasswordChange verifies the current password and, on success, // persists a fresh hash for the user, reusing the same helpers that // bootstrap the admin user. It returns the success and error messages // to display on the profile page. On an internal failure it writes a // 500 response itself and returns handled=false, signalling the caller // to stop without re-rendering the page. func (h *Handlers) applyPasswordChange( w http.ResponseWriter, username, currentPassword, newPassword, confirmPassword string, ) (string, string, bool) { // Load the user row so we can verify the current password and // persist the new hash. var user database.User err := h.db.DB().Where( "username = ?", username, ).First(&user).Error if err != nil { h.serverError( w, "failed to load user for password change", err, ) return "", "", false } valid, err := database.VerifyPassword( currentPassword, user.Password, ) if err != nil { h.serverError(w, "failed to verify password", err) return "", "", false } if !valid { return "", "Current password is incorrect.", true } if newPassword == "" { return "", "New password must not be empty.", true } if newPassword != confirmPassword { return "", "New password and confirmation do not match.", true } hashedPassword, err := database.HashPassword(newPassword) if err != nil { h.serverError(w, "failed to hash new password", err) return "", "", false } err = h.db.DB().Model(&user).Update( "password", hashedPassword, ).Error if err != nil { h.serverError(w, "failed to update password", err) return "", "", false } h.log.Info("user changed password", "username", username) return "Password changed successfully.", "", true } // profileOwnerOrDeny resolves the session identity and enforces that a // user may only act on their own profile (the requested username in the // URL must equal the session username). On any failure it writes the // appropriate HTTP response and returns ok=false; callers must stop // when ok is false. func (h *Handlers) profileOwnerOrDeny( w http.ResponseWriter, r *http.Request, ) (string, string, bool) { requestedUsername := chi.URLParam(r, "username") if requestedUsername == "" { http.NotFound(w, r) return "", "", false } // RequireAuth middleware guarantees an authenticated session // before this handler runs, so we only need to guard against an // unexpected retrieval error. sess, err := h.session.Get(r) if err != nil { h.serverError(w, "failed to get session", err) return "", "", false } sessionUsername, ok := h.session.GetUsername(sess) if !ok { h.log.Error("authenticated session missing username") http.Error( w, "Internal server error", http.StatusInternalServerError, ) return "", "", false } sessionUserID, ok := h.session.GetUserID(sess) if !ok { h.log.Error("authenticated session missing user ID") http.Error( w, "Internal server error", http.StatusInternalServerError, ) return "", "", false } // Only allow users to act on their own profile. if requestedUsername != sessionUsername { http.Error(w, "Forbidden", http.StatusForbidden) return "", "", false } return sessionUserID, sessionUsername, true } // renderProfile renders the profile page for the given user, // optionally including a success or error message. func (h *Handlers) renderProfile( w http.ResponseWriter, r *http.Request, userID, username, successMessage, errorMessage string, ) { data := map[string]any{ "User": &UserInfo{ ID: userID, Username: username, }, "SuccessMessage": successMessage, "ErrorMessage": errorMessage, } h.renderTemplate(w, r, "profile.html", data) }