Commit 2f3a60f2e9

2f3a60f2e906a6e466d35f0dc26021512142a1cf

parent: 97bab07d1a

Verified · cmc ci/build: success

cmc <hello@cleberg.net> · 2026-08-26 03:53 UTC

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".

Layout: unified · split

e2e/accounts_test.go +21
@@ -217,3 +217,24 @@ func TestViewOnlyHasNoLoginOnTheWire(t *testing.T) {
217 t.Fatalf("web login in view_only: exit %d, %s", code, errOut) 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.
224func 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 s.render(w, "login.html", struct { 66 s.render(w, "login.html", struct {
67 basePage 67 basePage
68 Error string 68 Error string
69 }{basePage{Site: s.siteName()}, ""}) 69 }{basePage{Site: s.siteName(), Host: s.cfg.SiteHost()}, ""})
70 return 70 return
71 } 71 }
72 userID, err := s.st.ConsumeLoginToken(store.HashToken(token)) 72 userID, err := s.st.ConsumeLoginToken(store.HashToken(token))
@@ -74,7 +74,7 @@ func (s *Server) login(w http.ResponseWriter, r *http.Request) {
74 s.render(w, "login.html", struct { 74 s.render(w, "login.html", struct {
75 basePage 75 basePage
76 Error string 76 Error string
77 }{basePage{Site: s.siteName()}, 77 }{basePage{Site: s.siteName(), Host: s.cfg.SiteHost()},
78 "that login link is invalid, expired, or already used — mint a new one"}) 78 "that login link is invalid, expired, or already used — mint a new one"})
79 return 79 return
80 } 80 }
@@ -226,7 +226,7 @@ func (s *Server) renderSignup(w http.ResponseWriter, errMsg, username string) {
226 Mode string // open | invite 226 Mode string // open | invite
227 Error string 227 Error string
228 Username string 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
232func (s *Server) signupSubmit(w http.ResponseWriter, r *http.Request) { 232func (s *Server) signupSubmit(w http.ResponseWriter, r *http.Request) {
@@ -248,7 +248,7 @@ func (s *Server) signupSubmit(w http.ResponseWriter, r *http.Request) {
248 Username string 248 Username string
249 Message string 249 Message string
250 Host string 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// issueCreateForm renders the new-issue form, prefilled from the repo's 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// basePage is what the layout needs on every page, repo or not. Page 36// basePage is what the layout needs on every page, repo or not. Page
37// structs embed it so the rail and the site name are always in scope. 37// structs embed it so the rail and the site name are always in scope.
38type basePage struct { 38type 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 Site string 41 Site string
42 Host string
40 Viewer string 43 Viewer string
41 Rail rail 44 Rail rail
42} 45}
@@ -45,7 +48,7 @@ type basePage struct {
45// resolved a viewer. 48// resolved a viewer.
46func (s *Server) base(r *http.Request) basePage { 49func (s *Server) base(r *http.Request) basePage {
47 if s.cfg.Web.Mode != "accounts" { 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 return s.baseFor(s.viewer(r)) 53 return s.baseFor(s.viewer(r))
51} 54}
@@ -53,7 +56,7 @@ func (s *Server) base(r *http.Request) basePage {
53// baseFor is base for a handler that already holds the viewer, so the 56// baseFor is base for a handler that already holds the viewer, so the
54// session lookup is not repeated. 57// session lookup is not repeated.
55func (s *Server) baseFor(viewer store.User) basePage { 58func (s *Server) baseFor(viewer store.User) basePage {
56 b := basePage{Site: s.siteName()} 59 b := basePage{Site: s.siteName(), Host: s.cfg.SiteHost()}
57 if viewer.ID == 0 { 60 if viewer.ID == 0 {
58 return b 61 return b
59 } 62 }
internal/web/templates/login.html +1 −1
@@ -4,6 +4,6 @@
4{{if .Error}}<p class="error" role="alert">{{.Error}}</p>{{end}} 4{{if .Error}}<p class="error" role="alert">{{.Error}}</p>{{end}}
5<p>Browser sessions are minted over SSH — there is no password. From a machine 5<p>Browser sessions are minted over SSH — there is no password. From a machine
6with your registered key:</p> 6with 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<p>then open the printed URL within five minutes.</p> 8<p>then open the printed URL within five minutes.</p>
9{{end}} 9{{end}}