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:
+30
-6
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user