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:
+24
-5
@@ -3,11 +3,14 @@ package web
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -28,9 +31,9 @@ type Config struct {
|
||||
// posts the matching setup_secret. It is ignored once any admin exists.
|
||||
AdminSetupSecret string
|
||||
SecureCookie bool
|
||||
// TrustProxy enables X-Forwarded-For / RealIP only behind a known proxy.
|
||||
TrustProxy bool
|
||||
Blob blob.Uploader
|
||||
// TrustedProxies are CIDRs allowed to set X-Forwarded-For (direct peer).
|
||||
TrustedProxies []*net.IPNet
|
||||
Blob blob.Uploader
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
@@ -145,7 +148,7 @@ func New(st store.Store, sessionStore scs.Store, templateFS fs.FS, staticFS fs.F
|
||||
func (s *Server) Handler() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.RequestID)
|
||||
if s.cfg.TrustProxy {
|
||||
if len(s.cfg.TrustedProxies) > 0 {
|
||||
r.Use(middleware.RealIP)
|
||||
}
|
||||
r.Use(middleware.Logger)
|
||||
@@ -356,7 +359,17 @@ func (s *Server) handleQuestion(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
var ans *store.Answer
|
||||
if q.Answered {
|
||||
ans, _ = s.store.GetAnswer(r.Context(), q.ID)
|
||||
ans, err = s.store.GetAnswer(r.Context(), q.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
log.Printf("question %s marked answered but answer missing", q.ID)
|
||||
http.Error(w, "answer unavailable", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.Printf("get answer %s: %v", q.ID, err)
|
||||
http.Error(w, "could not load answer", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
s.exec(w, "question", questionPage{
|
||||
page: s.basePage(r, q.Title),
|
||||
@@ -385,11 +398,17 @@ func (s *Server) handleVote(w http.ResponseWriter, r *http.Request) {
|
||||
value = 1
|
||||
case "-1":
|
||||
value = -1
|
||||
case "0":
|
||||
value = 0
|
||||
default:
|
||||
http.Error(w, "invalid vote", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := s.store.Vote(r.Context(), u.ID, id, value); err != nil {
|
||||
if errors.Is(err, store.ErrHiddenOrMissing) {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, "could not vote", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user