v0.281.0: 'Done' asks the probe first; sign-up closed after the first admin (decision 47); R-713 code-bound values refused, ${NAME|base64}
gates / gates (push) Successful in 23s
gates / gates (push) Successful in 23s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -48,6 +48,8 @@ const (
|
||||
gateAuthPath = "/__felhom_gate/auth"
|
||||
gateCallbackURI = "/__felhom_gate/cb"
|
||||
gateStartPath = "/__gate/start"
|
||||
// signupClosedPath: the sign-up block's replacePath target (internal/stacks/signup_block.go, decision 47).
|
||||
signupClosedPath = "/__felhom_gate/signup-closed"
|
||||
)
|
||||
|
||||
type gateState struct {
|
||||
@@ -285,6 +287,14 @@ func (s *Server) appSetupGateOpenHandler(w http.ResponseWriter, r *http.Request,
|
||||
escrowJSON(w, http.StatusNotFound, nil, s.msg(r, "escrow.unknown_app"))
|
||||
return
|
||||
}
|
||||
// v0.281.0: an app that can say whether its setup is done is ASKED first — the press never opens an app that
|
||||
// still says "not done" (measured 2026-09-29: uptime-kuma pressed before its setup answered anyone). An
|
||||
// unreadable status refuses too (fail closed). Without a probe the page's confirm says what the press does.
|
||||
if has, done, got, perr := s.stackMgr.SetupGateProbe(found.Name); has && (perr != nil || !done) {
|
||||
s.logger.Printf("[INFO] [web] setup gate %s: the household's press refused — the app's own status says not done (value %q, err %v)", found.Name, got, perr)
|
||||
escrowJSON(w, http.StatusConflict, nil, s.msg(r, "err.setup_gate.probe_not_done"))
|
||||
return
|
||||
}
|
||||
if err := s.stackMgr.OpenSetupGate(found.Name, stacks.SetupGateByHousehold); err != nil {
|
||||
if errors.Is(err, stacks.ErrSetupGateNotClosed) {
|
||||
escrowJSON(w, http.StatusConflict, nil, s.msg(r, "err.setup_gate.not_closed"))
|
||||
@@ -326,3 +336,52 @@ func (s *Server) stackBySlug(slug string) *stacks.Stack {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// appSignupWindowHandler is the household's "open sign-up for 15 minutes" (POST /apps/<slug>/signup-window, decision 47).
|
||||
func (s *Server) appSignupWindowHandler(w http.ResponseWriter, r *http.Request, slug string) {
|
||||
found := s.stackBySlug(slug)
|
||||
if found == nil {
|
||||
escrowJSON(w, http.StatusNotFound, nil, s.msg(r, "escrow.unknown_app"))
|
||||
return
|
||||
}
|
||||
until, err := s.stackMgr.OpenSignupWindow(found.Name)
|
||||
if err != nil {
|
||||
if errors.Is(err, stacks.ErrNoSignupBlock) {
|
||||
escrowJSON(w, http.StatusConflict, nil, s.msg(r, "err.setup_gate.no_signup_block"))
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[ERROR] [web] signup window %s: %v", found.Name, err)
|
||||
escrowJSON(w, http.StatusInternalServerError, nil, s.msg(r, "err.setup_gate.open_failed"))
|
||||
return
|
||||
}
|
||||
escrowJSON(w, http.StatusOK, map[string]any{"open_until": until}, "")
|
||||
}
|
||||
|
||||
// ServeSignupClosed answers the app's own sign-up address while it is closed (the sign-up block's replacePath
|
||||
// sends it here). A browser gets a page, anything else a 403 JSON. It holds no secret and changes nothing.
|
||||
func (s *Server) ServeSignupClosed(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
if !strings.Contains(r.Header.Get("Accept"), "text/html") || r.Method != http.MethodGet {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
_, _ = w.Write([]byte(`{"error":"sign-up is closed on this app; its admin adds new accounts"}`))
|
||||
return
|
||||
}
|
||||
host := strings.ToLower(r.Host)
|
||||
if i := strings.LastIndex(host, ":"); i != -1 {
|
||||
host = host[:i]
|
||||
}
|
||||
data := map[string]interface{}{"Host": host}
|
||||
if s.stackMgr != nil {
|
||||
if app, _, found := s.stackMgr.SetupGateHost(host); found {
|
||||
if st, ok := s.stackMgr.GetStack(app); ok {
|
||||
data["AppName"] = st.Meta.DisplayName
|
||||
}
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
if err := s.executeTemplateLang(w, r, "signupclosed", data); err != nil {
|
||||
s.logger.Printf("[ERROR] [web] signup-closed page: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user