0bcb96c442
web/handlers/reach.go: GET /reach (list), GET /reach/new (form),
GET /reach/{id} (detail), POST /reach (atomic Reach+Stash create
per D-071, redirect 302). Labels "Create a Reach" (not the banned
legacy word). G-027 validation (non-empty, <=128, no path separators,
no template syntax). 3 Reach templates extend base.html.
reach_test.go: httptest for all 4 routes + atomic create + 400/409
error paths + rendered-HTML lexicon check on BOTH 200 and error bodies
(G-026). handlers/server.go: clone-per-page template pattern (avoids
content-block collision across pages). Coverage 81.2% on web/handlers.
---ci---
project: oy
phase: 1
milestone: v0.6
status: execute
---/ci---
91 lines
3.0 KiB
Go
91 lines
3.0 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
identitytypes "github.com/oy/openyield/x/identity/types"
|
|
stashtypes "github.com/oy/openyield/x/stash/types"
|
|
)
|
|
|
|
// registerReach wires the Reach signup routes (REQ-040) into the mux.
|
|
// Go 1.22 method-pattern routing: GET /reach (list), GET /reach/new (form),
|
|
// POST /reach (atomic create + redirect per D-071), GET /reach/{id} (detail).
|
|
func (s *Server) registerReach(mux *http.ServeMux) {
|
|
mux.HandleFunc("GET /reach", s.handleReachList)
|
|
mux.HandleFunc("GET /reach/new", s.handleReachNew)
|
|
mux.HandleFunc("POST /reach", s.handleReachCreate)
|
|
mux.HandleFunc("GET /reach/{id}", s.handleReachDetail)
|
|
}
|
|
|
|
// handleReachList renders all Reaches (seeded + created).
|
|
func (s *Server) handleReachList(w http.ResponseWriter, r *http.Request) {
|
|
reaches := s.Store.ListReaches()
|
|
s.render(w, "reach_list.html", map[string]any{"Reaches": reaches})
|
|
}
|
|
|
|
// handleReachNew renders the "Create a Reach" form. Lexicon-clean: "Create a
|
|
// Reach", NOT a legacy custodial-position label (REQ-012 bans that word).
|
|
func (s *Server) handleReachNew(w http.ResponseWriter, r *http.Request) {
|
|
s.render(w, "reach_new.html", nil)
|
|
}
|
|
|
|
// handleReachCreate handles the POST from the "Create a Reach" form. Calls
|
|
// store.CreateReach (atomic Reach + Stash per D-071). On validation error
|
|
// (G-027) returns 400 with a lexicon-clean message; on duplicate returns 409.
|
|
// On success redirects (302) to the new Reach detail page.
|
|
func (s *Server) handleReachCreate(w http.ResponseWriter, r *http.Request) {
|
|
holderID := r.FormValue("holder_id")
|
|
publicKey := r.FormValue("public_key")
|
|
reach, _, err := s.Store.CreateReach(holderID, publicKey)
|
|
if err != nil {
|
|
// G-026: rendered-HTML lexicon check scans error response bodies too;
|
|
// keep the error message lexicon-clean (no banned terms).
|
|
status := http.StatusBadRequest
|
|
if isDuplicate(err) {
|
|
status = http.StatusConflict
|
|
}
|
|
http.Error(w, "Could not create a Reach: "+err.Error(), status)
|
|
return
|
|
}
|
|
http.Redirect(w, r, "/reach/"+reach.HolderID, http.StatusFound)
|
|
}
|
|
|
|
// handleReachDetail renders one Reach + its associated Stash (BalanceGrain).
|
|
func (s *Server) handleReachDetail(w http.ResponseWriter, r *http.Request) {
|
|
id := r.PathValue("id")
|
|
reach, ok := s.Store.GetReach(id)
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
stash, _ := s.Store.GetStash(id)
|
|
s.render(w, "reach_detail.html", map[string]any{
|
|
"Reach": reach,
|
|
"Stash": stash,
|
|
})
|
|
}
|
|
|
|
// isDuplicate reports whether err is a duplicate-holder error from
|
|
// store.CreateReach. Kept as a string match to avoid exporting store errors.
|
|
func isDuplicate(err error) bool {
|
|
return err != nil && contains(err.Error(), "already has a Reach")
|
|
}
|
|
|
|
func contains(s, sub string) bool {
|
|
return len(s) >= len(sub) && (s == sub || indexOf(s, sub) >= 0)
|
|
}
|
|
|
|
func indexOf(s, sub string) int {
|
|
for i := 0; i+len(sub) <= len(s); i++ {
|
|
if s[i:i+len(sub)] == sub {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// Compile-time assertions that the handlers use the real x/*/types structs
|
|
// (D-067: the UI grounds in the real Go type definitions).
|
|
var _ identitytypes.Reach
|
|
var _ stashtypes.Stash
|