headscale nodes, users and pre-auth key management

domain routes in the sidecar: nodes with per-route approval done as a
read-modify-write (headscale's approve_routes replaces the whole set),
users enriched with node counts, and pre-auth keys.

pre-auth key secrets are revealed by call path, not by inspecting the
value. headscale masks keys created since 0.28, but returns older
plaintext ones in full from the list endpoint for backwards
compatibility, so listing would otherwise ship live secrets into the
browser's query cache. the list always nulls the secret; only creation
reveals it, and the ui shows it once with a copy affordance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 15:08:02 +00:00
co-authored by Claude Opus 5
parent adf922de30
commit 0d227c46a3
16 changed files with 1769 additions and 23 deletions
+19
View File
@@ -24,6 +24,25 @@ import { MIN_VERSION_LABEL } from './version';
// DELETE /_officer/servers/:id deregister; promotes the newest survivor if it was active
// POST /_officer/servers/:id/activate switch the active server
// GET /_officer/servers/:id/health probe: reachable? version? key still accepted?
//
// Everything below acts on the ACTIVE server. 409 when none is selected — see active.ts.
//
// GET /_officer/nodes nodes, normalized; ?user=<username> filters
// GET /_officer/nodes/:id one node
// DELETE /_officer/nodes/:id remove it from the tailnet
// POST /_officer/nodes/:id/rename {name}
// POST /_officer/nodes/:id/tags {tags} — 'tag:' prefix added if missing
// POST /_officer/nodes/:id/routes {routes} whole set, or {route,approved} single toggle (RMW here)
// POST /_officer/nodes/:id/expire expire its key, forcing re-auth (not a delete)
// GET /_officer/users users, each with a node count the admin API doesn't provide
// POST /_officer/users {name, displayName?, email?}
// POST /_officer/users/:id/rename {name}
// DELETE /_officer/users/:id refused upstream while the user still owns nodes
// GET /_officer/keys pre-auth keys, secrets masked, with a derived status
// POST /_officer/keys {userId, reusable?, ephemeral?, expirationDays?, aclTags?}
// → the ONLY response carrying the real secret
// POST /_officer/keys/:id/expire expire without deleting
// DELETE /_officer/keys/:id delete outright
// anything else 404
//
// There is deliberately NO transparent /api/v1/* passthrough. Headscale's REST shape changed repeatedly