Files
nixos-router/pkgs/router-webui/tokens_ui.go
T
nilsandClaude Sonnet 5.5 b1fa7b1396 Add token expiry, edit notice, log allowlist, local-zone test; merge PLAN.md into SPECS.md
get_logs needs a unit from an allowlist (router-*, kea, unbound, networkd,
nftables, ddclient, acme, sshd, kernel). API tokens are valid for 1, 7, 30
(default) or 90 days, or without limit. The overview and the Changes page
say which token made the unapplied changes.

options.json (router://options) no longer holds the options' source paths:
they differ between builds of a release, so a router rebuilding itself
offline had to build the file and failed (found by the update test).

Web UI: panels flow into columns on wide screens and the width is no longer
capped; on phones the navigation is a menu button; the Advanced page uses
panels; VLAN dropdowns show their chosen names; the duplicate WireGuard
heading is gone.

basic: a subtest for wan -> local rules.

SPECS.md is now the only document (decisions, how each part works, and what
is not done yet); PLAN.md is gone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
2026-11-01 19:06:27 +01:00

66 lines
2.2 KiB
Go

package main
import (
"net/http"
"net/url"
"strings"
"time"
)
// How long a new token works (the form's choices; 0 is never).
var tokenLifetimes = map[string]time.Duration{
"1d": 24 * time.Hour, "7d": 7 * 24 * time.Hour, "30d": 30 * 24 * time.Hour, "90d": 90 * 24 * time.Hour, "never": 0,
}
type tokensData struct {
Tokens []Token
Audit []AuditEntry
URL string // the endpoint, for the example
Secret string // a token made just now: shown once
Name string
HasDoc bool
}
func (a *app) tokensData(r *http.Request) tokensData {
return tokensData{Tokens: a.tokens.List(), Audit: a.sys.RecentAudit(30), URL: "https://" + r.Host + "/mcp", HasDoc: a.optionsFile != ""}
}
func (a *app) tokensPage(w http.ResponseWriter, r *http.Request, sess session) {
a.render(w, "tokens", page{Title: "API access", CSRF: sess.csrf, LoggedIn: true, Flash: r.URL.Query().Get("msg"), Data: a.tokensData(r)})
}
func (a *app) tokenCreate(w http.ResponseWriter, r *http.Request, sess session) {
valid, ok := tokenLifetimes[r.PostFormValue("valid")]
if !ok {
valid = tokenLifetimes["30d"]
}
tok, secret, err := a.tokens.CreateFor(r.PostFormValue("name"), r.PostFormValue("scope"), valid)
p := page{Title: "API access", CSRF: sess.csrf, LoggedIn: true}
d := a.tokensData(r)
if err != nil {
p.Error = err.Error()
w.WriteHeader(http.StatusUnprocessableEntity)
} else {
d.Secret, d.Name = secret, tok.Name
p.Flash = "Token " + tok.Name + " created. Copy it now: it is not shown again."
}
p.Data = d
a.render(w, "tokens", p)
}
func (a *app) tokenRevoke(w http.ResponseWriter, r *http.Request, _ session) {
msg := "Token revoked."
if err := a.tokens.Revoke(strings.TrimSpace(r.PostFormValue("id"))); err != nil {
msg = "Could not revoke: " + err.Error()
}
http.Redirect(w, r, "/tokens?msg="+url.QueryEscape(msg), http.StatusSeeOther)
}
// changesPage shows what differs between the confirmed settings and the saved
// ones, for a person to review before applying.
func (a *app) changesPage(w http.ResponseWriter, r *http.Request, sess session) {
a.render(w, "changes", page{Title: "Changes", CSRF: sess.csrf, LoggedIn: true, Data: map[string]any{
"Diff": a.sys.pendingChanges(), "Unapplied": a.sys.Status().Unapplied, "TokenEdit": a.sys.Status().TokenEdit,
}})
}