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
+24 -5
View File
@@ -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
}