Users, with their own settings, and a list of where each is signed in

The login is a user name and a password. The old single password becomes the account
named after the OS user on first boot, along with its sessions and the machine-wide look.
Settings gains Users (add, remove, reset a password — your own password confirms) and
Sessions (device, IP, last seen; revoke one or sign out everywhere else). Theme, accents
and the dashboard layout follow the account; the refresh rate, graph history and alerts
stay the machine's. Pairing a machine takes a user on it. Log lines name the actor.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-22 18:20:30 -07:00
co-authored by Claude Fable 5.1
parent db214ebc2b
commit 2b54a06821
15 changed files with 1059 additions and 170 deletions
+27 -27
View File
@@ -295,8 +295,33 @@ something long in one, switch away and back, refresh — it's all still there.
3.18/3.20 (OpenRC + doas, non-root), plus the four lab hosts. Not done: an OpenRC
backend for the Services tab; runit/s6/sysvinit service files; public hosting for
`install.sh` (the Gitea repo is private, so the one-liner needs `HAMLAB_TOKEN`).
- **Auth hardening** — WebAuthn passkeys (phone Face ID), TOTP fallback, session
list + revoke, optional NPM access list restricting to WireGuard/LAN.
- **Users, sessions and settings that follow the account** — **shipped 2026-09-22**
(`internal/api/users.go`, `config.User`, `store.Session`/`UserState`, `Login.tsx`,
`Settings.tsx`). The login is a user name and a password. `config.json` holds `users`
(name, argon2id hash, created, last seen); the old single `password_hash` becomes the
account named after the OS user the console runs as on first boot, its signed-in
sessions become that account's, and the machine-wide theme/accents/dashboard become
theirs — nothing to redo after the upgrade. A session carries its user, an ID, when it
was made and last seen, the IP and the browser; **Settings → Sessions** lists yours
("Safari on iPhone · 10.0.0.5 · seen 2 min ago"), revokes one, or signs out everywhere
else. **Settings → Users** lists accounts with their session counts, adds one, removes
one (their sessions and settings go too; never yourself, never the last), and resets
another's password (they are signed out); removing and resetting take your own
password first. Everyone signed in can do everything — a household console, not a
tenancy — and each machine keeps its own accounts: a hub reaches a node with its token,
so pairing (Machines → Add) now takes a user and password on the node, the user
optional while it has one account. Per account: `ui` (theme, accents) and `dashboard`
in `state.json` under `users.<name>`. Still the machine's: refresh rate (it is how
often every machine samples — it rides in the UI prefs object for the client but is
stored once, machine-wide), graph history, alert rules, terminals (the shells run as
one OS user anyway). A login attempt for a missing user takes as long as a wrong
password, so the form can't list accounts. Every write's log line now names the
account or token (`core@10.0.0.5 via …`), and Access tokens say who issued them. CLI:
`hamlab passwd [user]` (makes the account if new; reads stdin when piped), `hamlab
users`, `hamlab user-rm <user>`; `hamlab install` names the first account after the
service user. Not done: a read-only viewer role.
- **Auth hardening** — WebAuthn passkeys (phone Face ID), TOTP fallback, optional NPM
access list restricting to WireGuard/LAN.
- **Files tab** — **shipped 2026-09-18** (`internal/files`, `Files.tsx`): browse from
`/` with breadcrumbs, an editable path box, sort, hidden-file toggle and a filter;
rows show type icon, size, modified, perms and owner, link targets and broken links.
@@ -350,31 +375,6 @@ something long in one, switch away and back, refresh — it's all still there.
Downgrades are already refused by the installer's `vnum()` comparison; the updater
should use the same rule.
- **Users, with their own settings** — asked for 2026-09-22, not started. Today Ham Lab is
one shared password: `config.json` holds a single `password_hash`, a login mints a session
token in `state.json`, and everything a person can set — theme, accent colours, refresh
rate, dashboard layout and its widgets — is stored per *machine*, not per person
(`store.State.UI`, `.Dashboard`). So two people share one look, one layout, and one
audit trail. Wanted: a username and password field, an account each, and settings that
follow the account.
- **The login.** A `users` list in `config.json` (name, hash, created, last seen) beside
the existing hash, which becomes the first user on upgrade so nothing breaks. The
session already exists and is already a token → just carry which user it belongs to.
Argon2id or bcrypt as now; a login attempt must take the same time whether or not the
user exists, or the form becomes a way to enumerate accounts.
- **The settings.** `UI` and `Dashboard` move from one blob to one per user. Machine
settings — the sampling rate, the history window, alert rules — stay machine-wide:
they are facts about the box, not a preference, and the fleet is held to them.
- **Decisions to make when it is built:** whether anyone can do anything (simplest, and
honest for a household) or whether there is a read-only account — a viewer who can see
graphs but not open a terminal, remove a container or shut a machine down; who may add
and remove users; what happens to a user's terminals when their account is removed; and
whether the hub's users are the fleet's, pushed like the sampling rate, or each machine
keeps its own (each machine keeps its own is less surprising and less to go wrong —
a hub already authenticates to a node with a token, not a password).
- **Worth doing at the same time:** the log line every write already carries an IP should
carry the user, and Settings → Access tokens should say which account issued each one.
## Open questions
- Idle-session reaping policy (never / after N days) — leaning never; it's a