Slice 1 served dist/projects-chart.html unconditionally, leaking a 200 for any well-formed UUID guesser. Slice 2 resolves the project via ProjectService.GetByID before serving — ErrNotVisible (and any other visibility error) collapses to 404 + the standard notfound chrome, matching the JSON-API contract that already lives in writeServiceError. A genuine DB error logs through writeServiceError's existing path but still renders 404 chrome to the user (httpDevNullJSON wrapper discards the JSON body writeServiceError would otherwise emit, keeping the log side-effect intact). Test pins serveChartNotFound: 404 + non-empty body, degrading gracefully when dist/notfound.html is absent (test env). Closes Slice 1 edge case #2 flagged at m/paliad#35 issuecomment-7710. Design ref: docs/design-project-chart-2026-05-09.md §8.2.
75 lines
2.4 KiB
Go
75 lines
2.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"os"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"mgit.msbls.de/m/paliad/internal/services"
|
|
)
|
|
|
|
// t-paliad-177 — Project Timeline / Chart standalone page.
|
|
//
|
|
// Slice 1 served dist/projects-chart.html unconditionally and relied on
|
|
// the client's first API fetch to enforce visibility. That leaked a 200
|
|
// for any well-formed UUID a guesser tried (m/paliad#35 Slice 1 edge
|
|
// case #2). Slice 2 closes the leak — we resolve the project via
|
|
// ProjectService.GetByID *before* serving the shell so an inaccessible
|
|
// id returns 404 + the standard notfound chrome.
|
|
//
|
|
// Design ref: docs/design-project-chart-2026-05-09.md §8.2 + §12.
|
|
|
|
func handleProjectsChartPage(w http.ResponseWriter, r *http.Request) {
|
|
if !requireDB(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
id, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
serveChartNotFound(w)
|
|
return
|
|
}
|
|
if _, err := dbSvc.projects.GetByID(r.Context(), uid, id); err != nil {
|
|
// ErrNotVisible + any "not found" surface from the service collapses
|
|
// to the same outward 404 — never tell a guesser whether the id
|
|
// exists, only whether they can see it.
|
|
if errors.Is(err, services.ErrNotVisible) {
|
|
serveChartNotFound(w)
|
|
return
|
|
}
|
|
// Genuine errors (DB hiccup, etc.) — log via writeServiceError but
|
|
// also fall back to 404 page chrome for the user instead of a raw
|
|
// 500 string. The JSON path of writeServiceError handles /api/*
|
|
// only, so we keep its logging side-effect but render the HTML.
|
|
writeServiceError(httpDevNullJSON{}, err)
|
|
serveChartNotFound(w)
|
|
return
|
|
}
|
|
http.ServeFile(w, r, "dist/projects-chart.html")
|
|
}
|
|
|
|
func serveChartNotFound(w http.ResponseWriter) {
|
|
body, err := os.ReadFile("dist/notfound.html")
|
|
if err != nil {
|
|
http.Error(w, "404 page not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
w.WriteHeader(http.StatusNotFound)
|
|
_, _ = w.Write(body)
|
|
}
|
|
|
|
// httpDevNullJSON is a writer that discards everything writeServiceError
|
|
// would have emitted — we only want the log line, not a duplicate body
|
|
// before serveChartNotFound writes the real one.
|
|
type httpDevNullJSON struct{}
|
|
|
|
func (httpDevNullJSON) Header() http.Header { return http.Header{} }
|
|
func (httpDevNullJSON) Write(b []byte) (int, error) { return len(b), nil }
|
|
func (httpDevNullJSON) WriteHeader(int) {}
|