carlos / platform Public

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>
Paul Campbell 21e0713de2f298fb50f7a48b5636e3ee67c0f61b parent 719e52e
4 files changed, +52 −15
  • internal/console/authcli.go +7 −4
  • internal/console/authcli_test.go +33 −4
  • internal/console/templates/cli_approve.html +2 −2
  • internal/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>