Front door: no-store both branches, and cover the anonymous one
Review (Fable) found a real caching defect in the previous commit. "/" now answers two different bodies keyed on a session cookie, but no-store was set only on the signed-in branch and nothing emitted Vary. A shared cache could store the anonymous front page and serve it to signed-in people. Both headers are now set before either branch runs. The session is read with sessions.Current, which uses what Middleware already resolved for this request instead of querying the database a second time. Tests gained the assertions the anonymous branch never had: it sets no cookie, it is no-store, and a cookie that does not resolve degrades to the front page rather than to a redirect loop or a partial catalogue. The colophon said "two fonts" while serving four files; it now says two typefaces, which is what was meant and what is true.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 files changed,
+25
−3
internal/home/directory.go+11 −2internal/home/directory_catalog_test.go+13 −0internal/home/static/site/index.html+1 −1
CI passed — run details
diff --git a/internal/home/directory.go b/internal/home/directory.go| index 7b38318..fefa080 100644 |
| --- a/internal/home/directory.go |
| +++ b/internal/home/directory.go |
| @@ -89,11 +89,20 @@ func (a *App) directoryRouter() http.Handler { |
| // The session is READ here, never required, so an expired cookie |
| // degrades to the front page rather than to a redirect loop. |
| r.Get("/", func(w http.ResponseWriter, r *http.Request) { |
| - if _, ok := a.sessions.From(r); !ok { |
| + // no-store covers BOTH branches, and it is set before either |
| + // runs. One URL now answers two different bodies keyed on a |
| + // cookie, so a shared cache that stored the anonymous 200 |
| + // would hand the front page to signed-in people. Vary: Cookie |
| + // says the same thing to a cache that honours it; no-store is |
| + // the half that does not depend on it honouring anything. |
| + w.Header().Set("Cache-Control", "no-store") |
| + w.Header().Add("Vary", "Cookie") |
| + // Current reads what Middleware already resolved for this |
| + // request, rather than asking the database a second time. |
| + if _, ok := sessions.Current(r); !ok { |
| a.handleIndex(w, r) |
| return |
| } |
| - w.Header().Set("Cache-Control", "no-store") |
| a.render(w, 200, "directory.html", map[string]any{"Apps": directoryCatalog(*a.runtime)}) |
| }) |
| r.Post("/signout", func(w http.ResponseWriter, r *http.Request) { |
diff --git a/internal/home/directory_catalog_test.go b/internal/home/directory_catalog_test.go| index 02ed273..54a0682 100644 |
| --- a/internal/home/directory_catalog_test.go |
| +++ b/internal/home/directory_catalog_test.go |
| @@ -1,6 +1,7 @@ |
| package home |
| import ( |
| + "net/http" |
| "strings" |
| "testing" |
| ) |
| @@ -25,6 +26,18 @@ func TestDirectoryCatalogRequiresSigninAndDoesNotInventLaunchDestinations(t *tes |
| t.Fatal("anonymous front page exposed catalogue", leak) |
| } |
| } |
| + noStore(t, anonymous) |
| + if len(anonymous.Result().Cookies()) != 0 { |
| + t.Fatal("front page set a cookie on an anonymous request") |
| + } |
| + // A cookie that does not resolve must degrade to the front page, not to |
| + // a redirect loop and not to a partial catalogue render. |
| + tampered := f.request("GET", "/", "", "", &http.Cookie{Name: f.p.directory.sessions.CookieName(), Value: "not-a-token"}) |
| + if tampered.Code != 200 || !strings.Contains(tampered.Body.String(), "office suite") || |
| + strings.Contains(tampered.Body.String(), "Choose a team") { |
| + t.Fatal("tampered cookie did not degrade to the front page", tampered.Code) |
| + } |
| + |
| cookie := f.session(t, chooserPerson) |
| page := f.request("GET", "/", "", "", cookie) |
| if page.Code != 200 { |
diff --git a/internal/home/static/site/index.html b/internal/home/static/site/index.html| index 0826a32..8eadc30 100644 |
| --- a/internal/home/static/site/index.html |
| +++ b/internal/home/static/site/index.html |
| @@ -237,7 +237,7 @@ |
| <li><a href="https://amadan.net/oficina/docs/-/releases/v0.1.0">Releases</a></li> |
| </ul> |
| </div> |
| - <p class="colophon">This page is one HTML file, one stylesheet and two fonts served |
| + <p class="colophon">This page is one HTML file, one stylesheet and two typefaces served |
| from our own machines. It runs no scripts, sets no cookies and calls nothing |
| third-party — which is why there is no banner asking your permission.</p> |
| </div> |