Render admin page errors in the normal layout (closes #382)
check / check (push) Successful in 3m12s
check / check (push) Successful in 3m12s
Every 400, 403, 404 and 500 on an admin page now answers with an error page in the normal layout: the status, one fixed line explaining it, and a link back to the webhook list, or to sign-in when nobody is signed in. Unknown paths reach it through the router's not-found handler, a refused form token through the CSRF middleware, and a panic through a recoverer each admin page route group installs first. Status codes are unchanged, and the page always sends Cache-Control: no-store. If the error page fails to render, the answer is the same status in plain text; if it panics, the answer is a 500. The receiver, the healthcheck and /metrics keep their plain answers. Model: opus-5-5
This commit was merged in pull request #408.
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
@@ -37,14 +36,14 @@ func (h *Handlers) HandlePasswordChange() http.HandlerFunc {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
h.log.Error("failed to parse form", "error", err)
|
||||
http.Error(w, "Bad request", http.StatusBadRequest)
|
||||
h.renderError(w, r, http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
successMessage, errorMessage, handled := h.applyPasswordChange(
|
||||
r.Context(),
|
||||
w,
|
||||
r,
|
||||
sessionUsername,
|
||||
// PostFormValue, not FormValue: the credential must
|
||||
// come from the body, never from the query string.
|
||||
@@ -66,12 +65,12 @@ func (h *Handlers) HandlePasswordChange() http.HandlerFunc {
|
||||
// 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 display on the profile page. On an internal failure it writes the
|
||||
// error page itself and returns handled=false, signalling the caller
|
||||
// to stop without re-rendering the page.
|
||||
func (h *Handlers) applyPasswordChange(
|
||||
ctx context.Context,
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
username, currentPassword, newPassword, confirmPassword string,
|
||||
) (string, string, bool) {
|
||||
// This endpoint verifies one password and hashes another, at
|
||||
@@ -79,15 +78,10 @@ func (h *Handlers) applyPasswordChange(
|
||||
// endpoint uses. The bound is per hash, not per endpoint: leaving
|
||||
// this path outside it would leave a hole in it. The slot is held
|
||||
// across both hashes.
|
||||
release, ok := h.mw.BeginPasswordVerification(ctx)
|
||||
release, ok := h.mw.BeginPasswordVerification(r.Context())
|
||||
if !ok {
|
||||
h.log.Warn("password verification capacity exhausted")
|
||||
http.Error(
|
||||
w,
|
||||
"The server is busy verifying credentials. "+
|
||||
"Please try again.",
|
||||
http.StatusServiceUnavailable,
|
||||
)
|
||||
h.renderError(w, r, http.StatusServiceUnavailable)
|
||||
|
||||
return "", "", false
|
||||
}
|
||||
@@ -103,7 +97,7 @@ func (h *Handlers) applyPasswordChange(
|
||||
).First(&user).Error
|
||||
if err != nil {
|
||||
h.serverError(
|
||||
w, "failed to load user for password change", err,
|
||||
w, r, "failed to load user for password change", err,
|
||||
)
|
||||
|
||||
return "", "", false
|
||||
@@ -113,7 +107,7 @@ func (h *Handlers) applyPasswordChange(
|
||||
currentPassword, user.Password,
|
||||
)
|
||||
if err != nil {
|
||||
h.serverError(w, "failed to verify password", err)
|
||||
h.serverError(w, r, "failed to verify password", err)
|
||||
|
||||
return "", "", false
|
||||
}
|
||||
@@ -132,7 +126,7 @@ func (h *Handlers) applyPasswordChange(
|
||||
|
||||
hashedPassword, err := database.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
h.serverError(w, "failed to hash new password", err)
|
||||
h.serverError(w, r, "failed to hash new password", err)
|
||||
|
||||
return "", "", false
|
||||
}
|
||||
@@ -141,7 +135,7 @@ func (h *Handlers) applyPasswordChange(
|
||||
"password", hashedPassword,
|
||||
).Error
|
||||
if err != nil {
|
||||
h.serverError(w, "failed to update password", err)
|
||||
h.serverError(w, r, "failed to update password", err)
|
||||
|
||||
return "", "", false
|
||||
}
|
||||
@@ -162,7 +156,7 @@ func (h *Handlers) profileOwnerOrDeny(
|
||||
) (string, string, bool) {
|
||||
requestedUsername := chi.URLParam(r, "username")
|
||||
if requestedUsername == "" {
|
||||
http.NotFound(w, r)
|
||||
h.renderError(w, r, http.StatusNotFound)
|
||||
|
||||
return "", "", false
|
||||
}
|
||||
@@ -172,7 +166,7 @@ func (h *Handlers) profileOwnerOrDeny(
|
||||
// unexpected retrieval error.
|
||||
sess, err := h.session.Get(r)
|
||||
if err != nil {
|
||||
h.serverError(w, "failed to get session", err)
|
||||
h.serverError(w, r, "failed to get session", err)
|
||||
|
||||
return "", "", false
|
||||
}
|
||||
@@ -180,10 +174,7 @@ func (h *Handlers) profileOwnerOrDeny(
|
||||
sessionUsername, ok := h.session.GetUsername(sess)
|
||||
if !ok {
|
||||
h.log.Error("authenticated session missing username")
|
||||
http.Error(
|
||||
w, "Internal server error",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
h.renderError(w, r, http.StatusInternalServerError)
|
||||
|
||||
return "", "", false
|
||||
}
|
||||
@@ -191,17 +182,14 @@ func (h *Handlers) profileOwnerOrDeny(
|
||||
sessionUserID, ok := h.session.GetUserID(sess)
|
||||
if !ok {
|
||||
h.log.Error("authenticated session missing user ID")
|
||||
http.Error(
|
||||
w, "Internal server error",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
h.renderError(w, r, http.StatusInternalServerError)
|
||||
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// Only allow users to act on their own profile.
|
||||
if requestedUsername != sessionUsername {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
h.renderError(w, r, http.StatusForbidden)
|
||||
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user