From 995d1e7a8fb165f9bd635c528b54fdd4938e97de Mon Sep 17 00:00:00 2001 From: sneak Date: Fri, 7 Aug 2026 19:48:55 +0700 Subject: [PATCH] Add NoCache middleware for authenticated pages (closes #61) --- internal/middleware/middleware.go | 20 +++++++++++++ internal/middleware/middleware_test.go | 39 ++++++++++++++++++++++++++ internal/server/routes.go | 4 +++ 3 files changed, 63 insertions(+) diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index d5fa46a..5f30912 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -265,6 +265,26 @@ func (s *Middleware) SecurityHeaders() func(http.Handler) http.Handler { } } +// NoCache returns middleware that instructs browsers and +// intermediary proxies not to cache the response. It sets +// Cache-Control: no-store and Pragma: no-cache (the latter for +// older HTTP/1.0 intermediaries). Apply it to authenticated pages +// so webhook configuration and captured event data are not stored +// by caches. +func (s *Middleware) NoCache() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func( + w http.ResponseWriter, + r *http.Request, + ) { + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Pragma", "no-cache") + + next.ServeHTTP(w, r) + }) + } +} + // MaxBodySize returns middleware that limits the request body size // for POST requests. If the body exceeds the given limit in // bytes, the server returns 413 Request Entity Too Large. This diff --git a/internal/middleware/middleware_test.go b/internal/middleware/middleware_test.go index 41ed0e8..05c5b68 100644 --- a/internal/middleware/middleware_test.go +++ b/internal/middleware/middleware_test.go @@ -387,6 +387,45 @@ func TestRequireAuth_UnauthenticatedSession_RedirectsToLogin( assert.Equal(t, "/pages/login", w.Header().Get("Location")) } +// --- NoCache Middleware Tests --- + +func TestNoCache_SetsHeaders(t *testing.T) { + t.Parallel() + + m, _ := testMiddleware(t, config.EnvironmentDev) + + var called bool + + handler := m.NoCache()(http.HandlerFunc( + func(w http.ResponseWriter, _ *http.Request) { + called = true + + w.WriteHeader(http.StatusOK) + }, + )) + + req := httptest.NewRequestWithContext( + context.Background(), + http.MethodGet, "/sources", nil, + ) + w := httptest.NewRecorder() + + handler.ServeHTTP(w, req) + + assert.True( + t, called, + "NoCache middleware should call the next handler", + ) + assert.Equal( + t, "no-store", + w.Header().Get("Cache-Control"), + ) + assert.Equal( + t, "no-cache", + w.Header().Get("Pragma"), + ) +} + // --- Helper Tests --- func TestIpFromHostPort(t *testing.T) { diff --git a/internal/server/routes.go b/internal/server/routes.go index adb9f16..1b6bedc 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -91,6 +91,7 @@ func (s *Server) setupRoutes() { func (s *Server) setupPageRoutes() { s.router.Route("/pages", func(r chi.Router) { r.Use(s.mw.CSRF()) + r.Use(s.mw.NoCache()) r.Use(s.mw.MaxBodySize(maxFormBodySize)) r.Group(func(r chi.Router) { @@ -106,6 +107,7 @@ func (s *Server) setupPageRoutes() { func (s *Server) setupUserRoutes() { s.router.Route("/user/{username}", func(r chi.Router) { r.Use(s.mw.CSRF()) + r.Use(s.mw.NoCache()) r.Use(s.mw.RequireAuth()) r.Get("/", s.h.HandleProfile()) }) @@ -114,6 +116,7 @@ func (s *Server) setupUserRoutes() { func (s *Server) setupSourceRoutes() { s.router.Route("/sources", func(r chi.Router) { r.Use(s.mw.CSRF()) + r.Use(s.mw.NoCache()) r.Use(s.mw.RequireAuth()) r.Use(s.mw.MaxBodySize(maxFormBodySize)) r.Get("/", s.h.HandleSourceList()) @@ -123,6 +126,7 @@ func (s *Server) setupSourceRoutes() { s.router.Route("/source/{sourceID}", func(r chi.Router) { r.Use(s.mw.CSRF()) + r.Use(s.mw.NoCache()) r.Use(s.mw.RequireAuth()) r.Use(s.mw.MaxBodySize(maxFormBodySize)) r.Get("/", s.h.HandleSourceDetail())