Commit 541f0d571c

541f0d571cd6d178441c0967df41462886b98696

parent: 5a73145992

Verified · cmc ci/build: success

cmc <hello@cleberg.net> · 2026-08-30 16:09 UTC

web: the login page tells a new visitor how to register

/login explained how to mint a session over SSH and stopped there, so a
visitor without an account had nowhere to go from the page they land on.
The landing page carried the only signup link, and requireUser redirects
past it.

The page now branches on registration.mode: open and invite link to
/register and print the equivalent ssh register line, closed says the
instance is not accepting new accounts. Closed needs its own copy rather
than an omission, since /register 404s there.

Layout: unified · split

e2e/websignup_test.go +12
@@ -23,6 +23,11 @@ func TestWebSignup(t *testing.T) {
23 if status != 200 || !strings.Contains(body, "invite-only") || !strings.Contains(body, `name="key"`) { 23 if status != 200 || !strings.Contains(body, "invite-only") || !strings.Contains(body, `name="key"`) {
24 t.Fatalf("register form: %d\n%s", status, body) 24 t.Fatalf("register form: %d\n%s", status, body)
25 } 25 }
26 // The login page tells a brand-new visitor how to get an account.
27 status, body = inst.get(t, "/login")
28 if status != 200 || !strings.Contains(body, `href="/register"`) || !strings.Contains(body, "invite-only") {
29 t.Fatalf("login signup hint: %d\n%s", status, body)
30 }
26 31
27 // Invite issued over the admin path; redeemed through the browser. 32 // Invite issued over the admin path; redeemed through the browser.
28 inst.admin(t, "admin", "invite", "--email", "erin@example.test") 33 inst.admin(t, "admin", "invite", "--email", "erin@example.test")
@@ -76,4 +81,11 @@ func TestWebSignupClosedInstance(t *testing.T) {
76 if strings.Contains(body, `href="/register"`) { 81 if strings.Contains(body, `href="/register"`) {
77 t.Fatal("closed landing advertises signup") 82 t.Fatal("closed landing advertises signup")
78 } 83 }
84 _, body = inst.get(t, "/login")
85 if strings.Contains(body, `href="/register"`) {
86 t.Fatal("closed login page advertises signup")
87 }
88 if !strings.Contains(body, "not accepting new accounts") {
89 t.Fatalf("closed login page says nothing about accounts:\n%s", body)
90 }
79} 91}
internal/httpd/accounts.go +12 −9
@@ -60,22 +60,25 @@ func (s *Server) checkOrigin(h http.HandlerFunc) http.HandlerFunc {
60 } 60 }
61} 61}
62 62
63// renderLogin draws the login page. Mode carries the registration mode so
64// the page can tell a brand-new visitor how to get an account.
65func (s *Server) renderLogin(w http.ResponseWriter, errMsg string) {
66 s.render(w, "login.html", struct {
67 basePage
68 Mode string // closed | invite | open
69 Error string
70 }{basePage{Site: s.siteName(), Host: s.cfg.SiteHost()}, s.cfg.Registration.Mode, errMsg})
71}
72
63func (s *Server) login(w http.ResponseWriter, r *http.Request) { 73func (s *Server) login(w http.ResponseWriter, r *http.Request) {
64 token := r.URL.Query().Get("token") 74 token := r.URL.Query().Get("token")
65 if token == "" { 75 if token == "" {
66 s.render(w, "login.html", struct { 76 s.renderLogin(w, "")
67 basePage
68 Error string
69 }{basePage{Site: s.siteName(), Host: s.cfg.SiteHost()}, ""})
70 return 77 return
71 } 78 }
72 userID, err := s.st.ConsumeLoginToken(store.HashToken(token)) 79 userID, err := s.st.ConsumeLoginToken(store.HashToken(token))
73 if err != nil { 80 if err != nil {
74 s.render(w, "login.html", struct { 81 s.renderLogin(w, "that login link is invalid, expired, or already used — mint a new one")
75 basePage
76 Error string
77 }{basePage{Site: s.siteName(), Host: s.cfg.SiteHost()},
78 "that login link is invalid, expired, or already used — mint a new one"})
79 return 82 return
80 } 83 }
81 sessTok, sessHash, err := store.NewToken() 84 sessTok, sessHash, err := store.NewToken()
internal/web/templates/login.html +9
@@ -6,4 +6,13 @@
6with your registered key:</p> 6with your registered key:</p>
7<pre class="message">ssh git@{{.Host}} 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<h2>New here?</h2>
10{{if eq .Mode "open"}}<p><a href="/register">Create an account</a> — pick a username, paste your SSH
11public key, verify your email. Or from the terminal:</p>
12<pre class="message">ssh git@{{.Host}} register --username you --email you@example.org</pre>
13{{else if eq .Mode "invite"}}<p>This instance is invite-only. With a code from an admin,
14<a href="/register">create an account</a> — pick a username, paste your SSH public key. Or from
15the terminal:</p>
16<pre class="message">ssh git@{{.Host}} register --username you --invite &lt;code&gt;</pre>
17{{else}}<p>This instance is not accepting new accounts.</p>{{end}}
9{{end}} 18{{end}}