console: your settings at /settings; the cli tokens page moves in
/auth/cli/tokens 303s there, so Keymail notification links keep working.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 files changed,
+52
−15
internal/console/authcli.go+7 −4internal/console/authcli_test.go+33 −4internal/console/templates/cli_approve.html+2 −2internal/console/templates/{cli_tokens.html => settings.html}+10 −5
diff --git a/internal/console/authcli.go b/internal/console/authcli.go| index 6826f0d50..042725d5c 100644 |
| --- a/internal/console/authcli.go |
| +++ b/internal/console/authcli.go |
| @@ -205,7 +205,10 @@ func (s *Server) registerCLIRoutes() { |
| s.mux.HandleFunc("POST /auth/cli/poll", s.handleCLIPoll) |
| s.mux.HandleFunc("GET /auth/cli", s.handleCLIApprovePage) |
| s.mux.HandleFunc("POST /auth/cli/approve", s.handleCLIApprove) |
| - s.mux.HandleFunc("GET /auth/cli/tokens", s.handleCLITokens) |
| + s.mux.HandleFunc("GET /settings", s.handleCLITokens) |
| + s.mux.HandleFunc("GET /auth/cli/tokens", func(w http.ResponseWriter, r *http.Request) { |
| + http.Redirect(w, r, "/settings", http.StatusSeeOther) |
| + }) |
| s.mux.HandleFunc("POST /auth/cli/tokens/revoke", s.handleCLITokenRevoke) |
| s.mux.HandleFunc("GET /auth/cli/whoami", s.handleCLIWhoami) |
| s.mux.HandleFunc("POST /auth/cli/revoke", s.handleCLIRevoke) |
| @@ -528,7 +531,7 @@ func (s *Server) notifyCLIIssued(address, rawToken string) { |
| fmt.Sprint(int(CLITokenTTL/(24*time.Hour)))+" days.\n\n"+ |
| "If that was you running `carlos auth login`, nothing to do.\n\n"+ |
| "If it was NOT you — if you clicked a link somebody sent you — revoke it now:\n\n"+ |
| - s.cfg.Origin+"/auth/cli/tokens\n"); err != nil { |
| + s.cfg.Origin+"/settings\n"); err != nil { |
| log.Printf("console: could not notify %s of a CLI token issue: %v", address, err) |
| } |
| } |
| @@ -699,7 +702,7 @@ func (s *Server) handleCLITokens(w http.ResponseWriter, r *http.Request) { |
| p := cliPage{page: page{Title: "Command-line access", Address: addr, CSRF: raw}, |
| Tokens: toks, Console: s.cfg.Origin, Days: int(CLITokenTTL / (24 * time.Hour))} |
| p.Flash, p.FlashErr = s.takeFlash(w, r) |
| - s.renderCLI(w, http.StatusOK, "cli_tokens", p) |
| + s.renderCLI(w, http.StatusOK, "settings", p) |
| } |
| func (s *Server) handleCLITokenRevoke(w http.ResponseWriter, r *http.Request) { |
| @@ -726,7 +729,7 @@ func (s *Server) handleCLITokenRevoke(w http.ResponseWriter, r *http.Request) { |
| } else { |
| s.setFlash(w, "ok", "Revoked — that terminal can no longer act as you.") |
| } |
| - http.Redirect(w, r, "/auth/cli/tokens", http.StatusSeeOther) |
| + http.Redirect(w, r, "/settings", http.StatusSeeOther) |
| } |
| // handleCLIWhoami answers `carlos auth whoami`: who this token speaks as, and |
diff --git a/internal/console/authcli_test.go b/internal/console/authcli_test.go| index c7df17447..248de3b53 100644 |
| --- a/internal/console/authcli_test.go |
| +++ b/internal/console/authcli_test.go |
| @@ -161,7 +161,7 @@ func TestCLILoginEndToEnd(t *testing.T) { |
| } |
| // The owner can see it, and kill it, from the console. |
| - code, _, body = get(t, browser, fx.ts.URL+"/auth/cli/tokens") |
| + code, _, body = get(t, browser, fx.ts.URL+"/settings") |
| if code != http.StatusOK || !strings.Contains(body, "paul@laptop") { |
| t.Fatalf("tokens page = %d, body:\n%s", code, body) |
| } |
| @@ -266,7 +266,7 @@ func TestCLIIssuanceNotifiesTheUser(t *testing.T) { |
| if found == "" { |
| t.Fatalf("no issuance notice in the sealed sends: %v", fx.fk.sentBodies()) |
| } |
| - for _, want := range []string{"paul@laptop", "/auth/cli/tokens", "revoke"} { |
| + for _, want := range []string{"paul@laptop", "/settings", "revoke"} { |
| if !strings.Contains(found, want) { |
| t.Fatalf("issuance notice missing %q:\n%s", want, found) |
| } |
| @@ -486,6 +486,35 @@ func cliToken(t *testing.T, fx *consoleFixture, address, oauthCode string) strin |
| return tok |
| } |
| +// TestPersonalSettingsPage: the CLI-tokens page becomes the personal settings |
| +// page at /settings. The old URL keeps working as a redirect — Keymail |
| +// notification bodies already mailed link to it — and a signed-out visitor |
| +// gets exactly the protection handleCLITokens has always given this page. |
| +func TestPersonalSettingsPage(t *testing.T) { |
| + fx := newConsoleFixture(t) |
| + c := signInPaul(t, fx) |
| + status, _, body := get(t, c, fx.ts.URL+"/settings") |
| + if status != 200 { |
| + t.Fatalf("GET /settings = %d", status) |
| + } |
| + for _, want := range []string{"Your settings", "Command-line access"} { |
| + if !strings.Contains(body, want) { |
| + t.Fatalf("/settings missing %q", want) |
| + } |
| + } |
| + // The old URL keeps working — Keymail notification bodies link to it. |
| + status, loc, _ := get(t, c, fx.ts.URL+"/auth/cli/tokens") |
| + if status != http.StatusSeeOther || loc != "/settings" { |
| + t.Fatalf("/auth/cli/tokens = %d -> %q, want 303 -> /settings", status, loc) |
| + } |
| + // Signed out, the personal page must not render. |
| + a := newBrowser(t) |
| + status, loc, _ = get(t, a, fx.ts.URL+"/settings") |
| + if status != http.StatusFound || loc != "/" { |
| + t.Fatalf("signed-out /settings = %d -> %q, want 302 -> /", status, loc) |
| + } |
| +} |
| + |
| // TestCLITokensPageRendersAppTokenHonestly: an app token never expires |
| // (expires_at 0), and the page must SAY so rather than rendering the Unix |
| // epoch — "expired in 1970" is a row a reader dismisses instead of |
| @@ -502,8 +531,8 @@ func TestCLITokensPageRendersAppTokenHonestly(t *testing.T) { |
| Kind: cliKindApp, AccountID: 7, App: "tito", CreatedAt: now}, |
| }} |
| var buf bytes.Buffer |
| - if err := templates.ExecuteTemplate(&buf, "cli_tokens", p); err != nil { |
| - t.Fatalf("render cli_tokens: %v", err) |
| + if err := templates.ExecuteTemplate(&buf, "settings", p); err != nil { |
| + t.Fatalf("render settings: %v", err) |
| } |
| html := buf.String() |
| if strings.Contains(html, "1970") { |
diff --git a/internal/console/templates/cli_approve.html b/internal/console/templates/cli_approve.html| index 741918fa3..dae971dcc 100644 |
| --- a/internal/console/templates/cli_approve.html |
| +++ b/internal/console/templates/cli_approve.html |
| @@ -13,7 +13,7 @@ |
| </div> |
| <p>Approving gives that terminal your identity — every account you belong to — |
| for {{.Days}} days. You can revoke it at any time from |
| -<a href="/auth/cli/tokens">command-line access</a>, and a note goes to your |
| +<a href="/settings">command-line access</a>, and a note goes to your |
| Keymail inbox the moment it is issued.</p> |
| <form class="signin" method="post" action="/auth/cli/approve"> |
| <input type="hidden" name="csrf" value="{{.CSRF}}"> |
| @@ -49,7 +49,7 @@ which means it has to be on a screen in front of you.</p> |
| <p>Your terminal has the credential — it should say so within a couple of |
| seconds. It expires in {{.Days}} days, and a note is on its way to your Keymail |
| inbox.</p> |
| -<p><a href="/auth/cli/tokens">See every terminal signed in as you</a>, and |
| +<p><a href="/settings">See every terminal signed in as you</a>, and |
| revoke any you don't recognise.</p> |
| {{else}} |
| <p>Nothing was issued. That login attempt is dead and cannot be approved |
diff --git a/internal/console/templates/cli_tokens.html b/internal/console/templates/settings.html| similarity index 77% |
| rename from internal/console/templates/cli_tokens.html |
| rename to internal/console/templates/settings.html |
| index ef81bca12..c5f0f038b 100644 |
| --- a/internal/console/templates/cli_tokens.html |
| +++ b/internal/console/templates/settings.html |
| @@ -1,8 +1,13 @@ |
| -{{define "cli_tokens"}}{{template "top" .}} |
| -<h1>Command-line access</h1> |
| +{{define "settings"}}{{template "top" .}} |
| {{if .Flash}}<p class="banner">{{.Flash}}</p>{{end}} |
| -{{if .FlashErr}}<p class="error">{{.FlashErr}}</p>{{end}} |
| -<p class="lede">Every terminal that can act as {{.Address}}.</p> |
| +{{if .FlashErr}}<p class="banner error">{{.FlashErr}}</p>{{end}} |
| +<div class="pagehead"> |
| + <p class="eyebrow">{{.Address}}</p> |
| + <h1>Your settings</h1> |
| + <p class="lede">Yours across every account — not any one account's.</p> |
| +</div> |
| +<h2>Command-line access</h2> |
| +<p class="small">Every terminal that can act as {{.Address}}.</p> |
| {{if .Tokens}} |
| <div class="tablewrap"> |
| <table> |
| @@ -18,7 +23,7 @@ |
| <form method="post" action="/auth/cli/tokens/revoke"> |
| <input type="hidden" name="csrf" value="{{$.CSRF}}"> |
| <input type="hidden" name="id" value="{{.ID}}"> |
| - <button class="linkish" type="submit">Revoke</button> |
| + <button class="linkish danger" type="submit">Revoke</button> |
| </form> |
| </td> |
| </tr> |