Commit 2f3a60f2e9
2f3a60f2e906a6e466d35f0dc26021512142a1cf
parent: 97bab07d1a
Verified · cmc ci/build: success
cmc <hello@cleberg.net> · 2026-08-26T03:53:31Z
web: the login page names the host, not the display title
basePage carries both now: Site is the display name, Host is what a command
or URL must use. Setting [web] title made the login page print
"ssh git@GitBay web login".
e2e/accounts_test.go
+21
| @@ -217,3 +217,24 @@ func TestViewOnlyHasNoLoginOnTheWire(t *testing.T) { |
| 217 | 217 | t.Fatalf("web login in view_only: exit %d, %s", code, errOut) |
| 218 | 218 | } |
| 219 | 219 | } |
| 220 | |
| 221 | // TestTitleIsNotAHostname pins the split between the instance's display name |
| 222 | // and its hostname: the login page prints a command to paste into a terminal, |
| 223 | // so it must name the host even when the operator has set a display title. |
| 224 | func TestTitleIsNotAHostname(t *testing.T) { |
| 225 | inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\ntitle = \"GitBay\"\n") |
| 226 | |
| 227 | status, body := inst.get(t, "/login") |
| 228 | if status != 200 { |
| 229 | t.Fatalf("/login = %d", status) |
| 230 | } |
| 231 | if strings.Contains(body, "ssh git@GitBay") { |
| 232 | t.Fatal("login page tells you to ssh to the display title") |
| 233 | } |
| 234 | if !strings.Contains(body, "ssh git@gitbay.test web login") { |
| 235 | t.Fatalf("login page does not name the host:\n%s", body) |
| 236 | } |
| 237 | if !strings.Contains(body, "GitBay") { |
| 238 | t.Fatal("login page dropped the display title entirely") |
| 239 | } |
| 240 | } |
internal/httpd/accounts.go
+4 −4
| @@ -66,7 +66,7 @@ func (s *Server) login(w http.ResponseWriter, r *http.Request) { |
| 66 | 66 | s.render(w, "login.html", struct { |
| 67 | 67 | basePage |
| 68 | 68 | Error string |
| 69 | | }{basePage{Site: s.siteName()}, ""}) |
| 69 | }{basePage{Site: s.siteName(), Host: s.cfg.SiteHost()}, ""}) |
| 70 | 70 | return |
| 71 | 71 | } |
| 72 | 72 | userID, err := s.st.ConsumeLoginToken(store.HashToken(token)) |
| @@ -74,7 +74,7 @@ func (s *Server) login(w http.ResponseWriter, r *http.Request) { |
| 74 | 74 | s.render(w, "login.html", struct { |
| 75 | 75 | basePage |
| 76 | 76 | Error string |
| 77 | | }{basePage{Site: s.siteName()}, |
| 77 | }{basePage{Site: s.siteName(), Host: s.cfg.SiteHost()}, |
| 78 | 78 | "that login link is invalid, expired, or already used — mint a new one"}) |
| 79 | 79 | return |
| 80 | 80 | } |
| @@ -226,7 +226,7 @@ func (s *Server) renderSignup(w http.ResponseWriter, errMsg, username string) { |
| 226 | 226 | Mode string // open | invite |
| 227 | 227 | Error string |
| 228 | 228 | Username string |
| 229 | | }{basePage{Site: s.siteName()}, s.cfg.SiteHost(), s.cfg.Registration.Mode, errMsg, username}) |
| 229 | }{basePage{Site: s.siteName(), Host: s.cfg.SiteHost()}, s.cfg.SiteHost(), s.cfg.Registration.Mode, errMsg, username}) |
| 230 | 230 | } |
| 231 | 231 | |
| 232 | 232 | func (s *Server) signupSubmit(w http.ResponseWriter, r *http.Request) { |
| @@ -248,7 +248,7 @@ func (s *Server) signupSubmit(w http.ResponseWriter, r *http.Request) { |
| 248 | 248 | Username string |
| 249 | 249 | Message string |
| 250 | 250 | Host string |
| 251 | | }{basePage{Site: s.siteName()}, username, msg, s.cfg.SiteHost()}) |
| 251 | }{basePage{Site: s.siteName(), Host: s.cfg.SiteHost()}, username, msg, s.cfg.SiteHost()}) |
| 252 | 252 | } |
| 253 | 253 | |
| 254 | 254 | // issueCreateForm renders the new-issue form, prefilled from the repo's |
internal/httpd/page.go
+5 −2
| @@ -36,7 +36,10 @@ func (r rail) Empty() bool { return len(r.Pinned) == 0 && len(r.Reviews) == 0 } |
| 36 | 36 | // basePage is what the layout needs on every page, repo or not. Page |
| 37 | 37 | // structs embed it so the rail and the site name are always in scope. |
| 38 | 38 | type basePage struct { |
| 39 | // Site is the instance's display name; Host is the name a command or |
| 40 | // URL must use. Anything copy-pasteable takes Host. |
| 39 | 41 | Site string |
| 42 | Host string |
| 40 | 43 | Viewer string |
| 41 | 44 | Rail rail |
| 42 | 45 | } |
| @@ -45,7 +48,7 @@ type basePage struct { |
| 45 | 48 | // resolved a viewer. |
| 46 | 49 | func (s *Server) base(r *http.Request) basePage { |
| 47 | 50 | if s.cfg.Web.Mode != "accounts" { |
| 48 | | return basePage{Site: s.siteName()} |
| 51 | return basePage{Site: s.siteName(), Host: s.cfg.SiteHost()} |
| 49 | 52 | } |
| 50 | 53 | return s.baseFor(s.viewer(r)) |
| 51 | 54 | } |
| @@ -53,7 +56,7 @@ func (s *Server) base(r *http.Request) basePage { |
| 53 | 56 | // baseFor is base for a handler that already holds the viewer, so the |
| 54 | 57 | // session lookup is not repeated. |
| 55 | 58 | func (s *Server) baseFor(viewer store.User) basePage { |
| 56 | | b := basePage{Site: s.siteName()} |
| 59 | b := basePage{Site: s.siteName(), Host: s.cfg.SiteHost()} |
| 57 | 60 | if viewer.ID == 0 { |
| 58 | 61 | return b |
| 59 | 62 | } |
internal/web/templates/login.html
+1 −1
| @@ -4,6 +4,6 @@ |
| 4 | 4 | {{if .Error}}<p class="error" role="alert">{{.Error}}</p>{{end}} |
| 5 | 5 | <p>Browser sessions are minted over SSH — there is no password. From a machine |
| 6 | 6 | with your registered key:</p> |
| 7 | | <pre class="message">ssh git@{{.Site}} web login</pre> |
| 7 | <pre class="message">ssh git@{{.Host}} web login</pre> |
| 8 | 8 | <p>then open the printed URL within five minutes.</p> |
| 9 | 9 | {{end}} |