Some checks failed
check / check (push) Has been cancelled
CSRF ran before MaxBodySize, so the CSRF middleware parsed the form body
before any cap applied and an oversized request was read in full before
being rejected. MaxBodySize is now the first middleware in all four route
groups that parse forms, ahead of CSRF and RequireAuth.
An oversize request therefore gets 413 without the handler running and
without state changing, including the password-change route.
Note the ordering trade: an unauthenticated client now receives 413 rather
than an auth redirect on /user/{username}/password.
205 lines
5.1 KiB
Go
205 lines
5.1 KiB
Go
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)
|
|
}
|