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