Files
simplexcalc/internal/server/routes.go
T
clawbot f8ce8cef83 Seed from go-template-repo, renamed to simplexcalc
The template's files at a77fd30, without its history or LICENSE, after
script/rename simplexcalc.

Model: opus-5-5
2026-09-26 21:38:57 +00:00

88 lines
2.9 KiB
Go

package server
import (
"net/http"
"github.com/go-chi/chi/v5"
"sneak.berlin/go/simplexcalc/static"
)
// staticPrefix is where the embedded assets are mounted.
const staticPrefix = "/static/"
// staticCacheControl is a year, because the asset set is baked into the
// binary: a new build is a new deployment, and a deployment is the only
// thing that can change these bytes. Bump the path if that stops being
// true.
const staticCacheControl = "public, max-age=31536000, immutable"
// SetupRoutes builds the router. The middleware order is the contract
// of this file, and it is this way for reasons:
//
// 1. RequestID first, so every later line of log and every captured
// error can name the request it came from.
// 2. Recoverer next-to-outermost, so it covers every handler and every
// middleware below it. Above the logger, so a panic still produces
// a logged request line.
// 3. RequestLogger and Metrics before the work, so both see the final
// status of every request including the 500 the recoverer wrote.
// 4. SecurityHeaders before anything can write a body — the headers
// have to be set before the first Write, and a 404 or a panic
// response needs them as much as a page does.
// 5. Timeout and BodyLimit before any handler reads a body.
// 6. CSRF innermost of the global chain, wrapping only the routes that
// can change state.
//
// /metrics and the healthcheck sit outside CSRF (neither is
// state-changing, and a scraper has no token) and outside nothing else.
func (s *Server) SetupRoutes() {
r := chi.NewRouter()
r.Use(s.mw.RequestID())
r.Use(s.mw.Recoverer())
r.Use(s.mw.RequestLogger())
r.Use(s.mw.Metrics())
r.Use(s.mw.SecurityHeaders())
r.Use(s.mw.Timeout())
r.Use(s.mw.BodyLimit())
r.NotFound(s.h.NotFound())
r.MethodNotAllowed(s.h.MethodNotAllowed())
// Operational endpoints: no CSRF, no session, no HTML.
r.Get("/.well-known/healthcheck.json", s.h.Healthcheck())
r.Method(http.MethodGet, "/metrics", s.metrics.Handler())
r.Handle(staticPrefix+"*", s.staticHandler())
// Everything a browser drives, behind CSRF. Safe methods are
// unaffected by it except that they are issued a token.
r.Group(func(r chi.Router) {
r.Use(s.mw.CSRF())
r.Get("/", s.h.Index())
r.Post("/widgets", s.h.CreateWidget())
if s.params.Config.Debug {
// Only with DEBUG=true: a route that panics on demand is a
// denial-of-service primitive in production.
r.Get("/debug/panic", s.h.Panic())
}
})
s.router = r
}
// staticHandler serves the embedded assets, without directory listings
// and with a long cache lifetime.
func (s *Server) staticHandler() http.Handler {
fileServer := http.FileServer(filesOnly{inner: http.FS(static.FS)})
return http.StripPrefix(staticPrefix, http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", staticCacheControl)
fileServer.ServeHTTP(w, r)
},
))
}