Skip to content

feat(ui): machine binding status, toggle and reset per token on the Users page - #264

Merged
CallMeTechie merged 3 commits into
masterfrom
feat/token-machine-binding-ui
Oct 6, 2026
Merged

CallMeTechie merged 3 commits into
masterfrom
feat/token-machine-binding-ui

Conversation

@CallMeTechie

@CallMeTechie CallMeTechie commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Why

Machine binding (licence feature machine_binding) binds an API token to the hardware fingerprint of the client device that first connects with it. The per-token UI for it (switch, status, reset) only existed in the old theme and went away in 66a4418 ("Aurora is the only theme"). Since then the admin could not see whether a token is bound, the individual mode had no way to switch binding on for a token, and a binding could not be released when someone got a new PC. On the Users page "binding" also only meant token→peer binding, which is a different thing.

What

Users page (edit user → API tokens)

Each client token (scopes client, client:* or full-access, or any token with a stored fingerprint) gets a Gerätebindung area:

  • Status: Gebunden an Gerät ab12cd34… seit 14.09.2026 / Noch nicht gebunden – wird beim nächsten Verbinden gebunden / Nicht aktiv (plus the stored device when the binding is off but a fingerprint is kept)
  • Effective state: für dieses Token aktiv, or durch globale Einstellung aktiv in global mode
  • Per-token switch: enabled only in individual mode. In global and off it is disabled and the reason is shown, with a link to Settings → Gerätebindung for off.
  • Bindung zurücksetzen… with a gc-dialog confirmation, shown only when a fingerprint is stored
  • Without the licence: Pro chip, lock note, and dimmed, disabled controls. The area stays visible.
  • One line above the list explains that this is not the token→peer binding

Page-local i18n island (#mb-users-i18n), same pattern as the 2FA strings on this page; CSS prefix mb-; the switch reuses .st-switch.

API

  • GET /api/v1/users/:id, GET /api/v1/users/unassigned-tokens, GET /api/v1/tokens: per token machine_fingerprint (shortened to 8 hex chars, the full hash no longer leaves the server), machine_binding_enabled, machine_bound_at, machine_binding_mode, machine_binding_active; top level machine_binding: { licensed, mode }
  • PUT /api/v1/tokens/:id/binding: returns 409 { code: 'binding_mode', mode } outside individual mode, so it no longer stores a flag that has no effect. Returns the updated token.
  • DELETE /api/v1/tokens/:id/binding: clears the fingerprint and machine_bound_at, and returns the updated token
  • Both routes now pass details/source/severity to activity.log correctly. Before this they used a 4-argument call, so the details were dropped.
  • The /tokens router is admin-session only, with no ownership check, so an admin can already switch or reset tokens owned by other users. This is now covered by a test, and the users router needed no change.
  • One shared rule tokens.isMachineBindingActive() / machineBindingState() is now used by the client routes (verifyMachineBinding), the enrollment redeem and the lists.

Data

  • Migration 88 add_machine_bound_at: api_tokens.machine_bound_at. It is set by bindMachineFingerprint (client request or setup code) and cleared by the reset. Tokens bound before this migration keep NULL, so the page shows no "seit".
  • The first binding logs machine_binding_bound once per binding. The UPDATE has a WHERE machine_fingerprint IS NULL guard, so concurrent first requests cannot double-log. machine_binding_reset now carries the short fingerprint. Both events fall into the existing machine_binding_ security category.

Settings → Gerätebindung

The description and hints are rewritten:

  • this binds a token to the device (hardware fingerprint) the client first connects from;
  • it is not the same as the peer binding;
  • status and reset are on the Benutzer page (linked);
  • the Windows client and the Android app from version 1.16 send the fingerprint;
  • older clients are rejected once binding is active for their token.

New keys are added in both de.json and en.json.

Dependency fix (not related to the feature)

Dependency Audit and Trivy failed because of new advisories that also affect master:

npm audit fix --package-lock-only bumps them to 2.0.8 and 4.1.2. These are patch releases inside the existing ranges, and package.json is unchanged. This is a separate commit.

Tests

tests/machine_binding_ui.test.js covers:

  • migration 88
  • bound_at and the machine_binding_bound event, logged once
  • first-use binding through verifyMachineBinding
  • list fields, and that the full hash is never in a response
  • global mode makes every token active
  • an admin toggles another user's token in individual mode
  • 409 in global and off
  • reset clears the fingerprint and bound_at and logs
  • behaviour without the licence (403, lists still return the state)
  • de/en key parity
  • /users and /settings render without raw keys (withoutScripts), and every island string the JS uses is translated

There is no users scenario under tests/e2e, so no browser scenario was extended.

🤖 Generated with Claude Code

https://claude.ai/code/session_016xX1efcZF1f6G9rhmaJNLD


Generated by Claude Code

Claude added 3 commits October 6, 2026 09:01
…sers page

The per-token device binding UI (switch, status, reset) only existed in the
old theme and disappeared with "Aurora is the only theme". Admins could not
see whether a token was bound, could not use the "individual" mode at all and
could not release a binding when someone got a new PC.

Users page (edit user → API tokens), for every client token:
- "Gerätebindung" area: bound to device ab12cd34… since <date> / not bound
  yet (binds on the next connection) / not active, with a stored device
  shown when the binding is off
- effective state incl. the global mode ("durch globale Einstellung aktiv");
  the per-token switch is only enabled in the "individual" mode, otherwise
  disabled with the reason (global, off with a link to the settings)
- "Bindung zurücksetzen…" with a confirmation dialog, only with a stored
  fingerprint
- without the licence: Pro chip, lock note, controls dimmed and disabled
- one line explaining that this is not the token→peer binding

API:
- token lists (GET /users/:id, /users/unassigned-tokens, /tokens) return
  machine_fingerprint shortened to 8 hex chars, machine_binding_enabled,
  machine_bound_at, machine_binding_mode, machine_binding_active, plus a
  top-level machine_binding { licensed, mode }
- PUT /tokens/:id/binding answers 409 (code binding_mode) outside the
  "individual" mode instead of storing a flag that does nothing; both
  binding routes return the updated token and log with proper details
- one shared rule for "is binding active" (tokens.isMachineBindingActive)
  used by the client routes, enrollment and the lists

Migration 88 adds api_tokens.machine_bound_at: set on the first binding
(client request or setup code), cleared by the reset. The first binding logs
machine_binding_bound (once per binding, race-safe UPDATE ... IS NULL).

Settings → Gerätebindung explains device vs. peer binding, links the Users
page for status and reset, and names the clients that send the fingerprint
(Windows client, Android app from 1.16).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016xX1efcZF1f6G9rhmaJNLD
… fix)

New advisories that also affect master: proxy-addr <= 2.0.7 (critical,
GHSA-jqcg-44mw-7w3h, IP spoofing via an IPv4-mapped IPv6 trust subnet; an
express dependency) and fast-copy 4.0.0-4.0.4 (moderate,
GHSA-jggr-w7fw-pc2j). Both are patch releases within the existing ranges,
applied with `npm audit fix --package-lock-only`; package.json is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016xX1efcZF1f6G9rhmaJNLD
The settings note claimed Android only sends it from app version 1.16;
older Android apps send it as well. Name scripts as the clients without one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016xX1efcZF1f6G9rhmaJNLD
@CallMeTechie
CallMeTechie merged commit 80ea9d4 into master Oct 6, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants