Address production-readiness review: clearer errors, safer votes, and ops hardening.

Distinguish auth/lookup failures, make votes idempotent on visible questions, bound shutdown, page admin users, LRU throttle, trusted-proxy CIDRs, avatar cleanup, versioned migrations, and session cleanup logging.
This commit is contained in:
2026-08-22 12:16:59 -07:00
parent 5bdaa8977f
commit 29b0536215
26 changed files with 612 additions and 146 deletions
+30 -6
View File
@@ -11,7 +11,6 @@ import (
"path"
"strings"
"github.com/google/uuid"
"golang.org/x/image/draw"
_ "golang.org/x/image/webp"
@@ -63,6 +62,7 @@ func (s *Server) handleProfile(w http.ResponseWriter, r *http.Request) {
}
avatarURL := ""
avatarKey := ""
file, hdr, err := r.FormFile("avatar")
if err == nil {
defer file.Close()
@@ -79,9 +79,10 @@ func (s *Server) handleProfile(w http.ResponseWriter, r *http.Request) {
s.renderProfile(w, r, u, "Avatar must be a JPEG, PNG, or WebP image.", state)
return
}
key := path.Join("avatars", u.ID, uuid.NewString()+ext)
prevURL := u.AvatarURL
avatarKey = path.Join("avatars", u.ID, "avatar"+ext)
url, upErr := s.cfg.Blob.Upload(r.Context(), blob.FileUpload{
Key: key,
Key: avatarKey,
Body: bytes.NewReader(body),
ContentType: contentType,
Size: int64(len(body)),
@@ -91,15 +92,25 @@ func (s *Server) handleProfile(w http.ResponseWriter, r *http.Request) {
return
}
avatarURL = url
u.State = state
u.AvatarURL = avatarURL
if err := s.store.SaveUserProfile(r.Context(), u); err != nil {
_ = s.cfg.Blob.Delete(r.Context(), avatarKey)
http.Error(w, "could not save profile", http.StatusInternalServerError)
return
}
if oldKey := avatarObjectKey(prevURL, u.ID); oldKey != "" && oldKey != avatarKey {
_ = s.cfg.Blob.Delete(r.Context(), oldKey)
}
s.sessions.Put(r.Context(), "flash", "Profile saved.")
http.Redirect(w, r, "/profile", http.StatusSeeOther)
return
} else if err != http.ErrMissingFile {
s.renderProfile(w, r, u, "Could not read avatar file.", state)
return
}
u.State = state
if avatarURL != "" {
u.AvatarURL = avatarURL
}
if err := s.store.SaveUserProfile(r.Context(), u); err != nil {
http.Error(w, "could not save profile", http.StatusInternalServerError)
return
@@ -108,6 +119,19 @@ func (s *Server) handleProfile(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/profile", http.StatusSeeOther)
}
func avatarObjectKey(publicURL, userID string) string {
marker := "/avatars/" + userID + "/"
i := strings.Index(publicURL, marker)
if i < 0 {
return ""
}
rest := publicURL[i+1:] // avatars/...
if q := strings.IndexAny(rest, "?#"); q >= 0 {
rest = rest[:q]
}
return rest
}
// prepareAvatar reads at most maxBytes, sniffs/decodes the image, resizes to a
// small avatar, and re-encodes so only bounded valid image bytes are stored.
func prepareAvatar(r io.Reader, maxBytes int64) (body []byte, ext, contentType string, err error) {