Skip to content

feat(ui): redesigned users page with role clarity, member self-service area and per-device Pi-hole - #266

Merged
CallMeTechie merged 4 commits into
masterfrom
feat/users-redesign
Oct 6, 2026
Merged

CallMeTechie merged 4 commits into
masterfrom
feat/users-redesign

Conversation

@CallMeTechie

Copy link
Copy Markdown
Owner

Summary

Implements the approved Users page redesign, the member self-service area "Mein Bereich" and per-device Pi-hole statistics for members.

Users page (admin)

  • Header with the two role cards and Rollen vergleichen (role matrix dialog).
  • Zugänge ohne Besitzer banner + assign dialog (assignment goes through PATCH /tokens/:id, so the new owner's role caps the rights) and revoke.
  • List: search, filters with counts (Alle / Administratoren / Mitglieder / Deaktiviert), avatar, "Du" chip, role + "Deaktiviert" chips, sign-in column (Passwort / + 2FA / + Passkey; members "Nur über Geräte" / "Mein Bereich" / "Eingeladen"), owned peers, last activity (newer of last_login_at and token use, Intl.RelativeTimeFormat in the UI language). Card list below 768 px.
  • Detail panel (right on wide screens, below on narrow ones, ?user=<id>&tab=…) with five tabs: Übersicht (tiles, role card, display name/e-mail, danger zone with explanations for own account / last admin), Geräte & Zugänge (devices vs. API accesses, rights in words, binding, expiry, last use), "Was sieht ?" (new GET /api/v1/users/:id/visibility), Anmeldung & Sicherheit (password, 2FA reset, passkeys, sessions; for members the "Mein Bereich" invitation and the per-member "own device setup" switch), Aktivität (GET /api/v1/users/:id/activity).
  • Dialogs: create user, access wizard (Art & Besitzer → Rechte → Optionen → Code; replaces the old token wizard and the enrollment modal on this page; client-enrollment.js stays for the Peers page), edit access, new password, change role, delete (consequences computed by GET /api/v1/users/:id/delete-impact), invitation. Escape, × and the backdrop ask before discarding a changed form.

Fixed along the way

Admin password reset (the old modal's password was ignored), promotion leaving the '!' sentinel, English relative times, disabled users looking enabled, enable without confirmation, hidden delete side effects, errors only after clicking, wizard issues (presets not filtered by role, dead backup scope, no "no owner", disabled owners offered, no review step, unnamed steps, ae/ue spellings, English fallbacks), tokens not editable, MFA column ignoring passkeys, display name maxlength 200 vs 100, several error.users.* keys that were used but missing (raw keys), legacy activity.log calls in tokens.create/revoke whose options were silently ignored.

Role-aware navigation

Member sessions see only Mein Bereich, Konto & Sicherheit and the portal link; no bottom nav, FAB, topbar "Einstellungen", admin palette sources or admin SSE feed. Every admin page redirects a member to /me (generalises the old dashboard/settings → /profile redirect). APIs keep answering 403.

"Mein Bereich" (/me)

  • Opt-in per member: users.self_service_enabled. The admin creates a one-time link (/invite/:token, 32 random bytes, only SHA-256 stored, 72 h, single use, consumed atomically); mailed when SMTP and an address exist. The page applies the password policy. Switching it off, disabling, demoting or deleting the member removes password, 2FA, passkeys, invitations and sessions.
  • GET /api/v1/me, GET /me/devices, DELETE /me/devices/:tokenId, POST /me/enrollment, GET /me/services — session only, scoped to req.session.userId, ids from the client ignored, mutating endpoints rate-limited and logged.

Pi-hole per device

pihole (read) is in the member role cap; pihole:control never. New member devices default to off; the wizard/edit dialog offer "Pi-hole-Statistik". Enrollment defaults: DEFAULT_SCOPES (app scopes) for members, ADMIN_DEFAULT_SCOPES (+ pihole) for admins — both inside the cap. Member tokens only see their own device in /pihole/top-clients. Licence pihole_integration still gates it.

New endpoints

GET /users/:id/visibility, GET /users/:id/delete-impact, GET /users/:id/activity, POST /users/:id/password, POST /users/:id/role, GET|DELETE /users/:id/sessions[/:ref], DELETE /users/:id/passkeys/:pid, POST /users/:id/invite, DELETE /users/:id/self-service, PUT /users/:id/self-enroll, PATCH /tokens/:id, /api/v1/me/*, GET|POST /invite/:token, GET|POST /login/change-password.

Migration 89 users_self_service

users.self_service_enabled, self_enroll_enabled, must_change_password, password_changed_at; table user_invites. Existing members with a real password (e.g. demoted admins) keep their login (self_service_enabled = 1).

Security decisions

  • Session ids never leave the server; sessions are addressed by a truncated SHA-256 ref, always filtered by the account.
  • Login is refused for members without "Mein Bereich" (password, 2FA step and passkey path), same answer as a wrong password; existing sessions of such accounts are rejected by requireAuth.
  • must_change_password keeps the session anonymous until the new password is set.
  • Deleting a user removes them from route/RDP visibility lists, but a list that only named them is left as is, so the entry stays hidden instead of becoming public.
  • Members' own device setup needs a per-member flag (default off), app scopes only, new peer always owned by the member.

Deviations from the mockups

  • Visibility is computed with the same rules as the client API for admins too: today routes.user_ids also applies to admin devices, so the matrix/role texts say "„für alle“ + freigegebene" for admins instead of "alle", and the promote dialog says "Zugänge … nicht mehr auf App-Rechte begrenzt" instead of "sehen dann alle Dienste". The web UI is "alles" for admins as designed.
  • Pi-hole row in the matrix reflects the implemented per-device right instead of "heute immer aus".
  • Activity entries are shown as stored (English messages, like the logs page).
  • The "own device setup" permission is a per-member flag (no global settings key).
  • The gateway scope is not offered in the wizard (gateway tokens are managed on the Peers page).
  • The VPN-Tunnel right of a device cannot be unchecked in the edit dialog (the app could not connect without it).

Tests

tests/users_redesign.test.js (services, admin API, invite flow end to end, /me isolation/escalation, Pi-hole cap, nav guards, templates without raw keys in de/en), updated existing tests, new browser scenario tests/e2e/scenarios/07-users.js (added to the CI set). Full suite green locally.

🤖 Generated with Claude Code

https://claude.ai/code/session_016xX1efcZF1f6G9rhmaJNLD


Generated by Claude Code

…e area and per-device Pi-hole

Users page: list with search/filters and German relative times, detail
panel (Übersicht, Geräte & Zugänge, "Was sieht …?", Anmeldung &
Sicherheit, Aktivität) deep-linkable via ?user=&tab=, role comparison,
owner-less accesses, create user, a four-step access wizard (device code
that creates and assigns the peer, or script code / key), edit access
(PATCH /tokens/:id, rights capped by the owner's role), new password,
change role (promotion needs a password), delete with computed
consequences and type-to-confirm. Dirty dialogs ask before Escape or the
backdrop discard input.

Members: role-aware navigation, every admin page redirects to /me,
opt-in "Mein Bereich" via one-time invitation links (hashed, 72 h,
single use), /api/v1/me/* scoped to the session's own account,
must-change-password step at login, Pi-hole statistics as a per-device
right (read only) inside the member role cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016xX1efcZF1f6G9rhmaJNLD
Comment thread src/routes/index.js Fixed
Comment thread public/js/me.js Fixed
Comment thread src/routes/auth.js Fixed
Comment thread src/routes/index.js Fixed
- users.js / me.js: the DOM helper no longer sets arbitrary attribute
  names; a fixed list of literal attributes is allowed and href only takes
  same-origin paths or https URLs, so no text can become a handler, a
  script URL or markup.
- Login redirect: the target is the constant '/portal' prefix plus the
  remainder of the validated portal path, otherwise the account's home.
- "Change password at login": the pending state is stored on a fresh
  session id, and the pre-login session is abandoned before the account
  is signed in.
- Rate limits on GET / and GET /login/change-password.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016xX1efcZF1f6G9rhmaJNLD
Comment thread src/routes/index.js Fixed
Comment thread src/routes/index.js Fixed
Claude added 2 commits October 6, 2026 16:10
- GET /login/change-password only reads the pending session state; the
  session is regenerated on the successful POST before the account is
  signed in, and the login step that sets pendingPwChange regenerates too.
- GET / is back to the plain redirect to /dashboard; /dashboard sends
  members on to /me, so no user-derived target reaches res.redirect.
- users.js / me.js: error messages are always rendered through
  textContent, and the el() helper appends children via ParentNode.append,
  which inserts strings as Text nodes only.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016xX1efcZF1f6G9rhmaJNLD
@CallMeTechie
CallMeTechie merged commit 3841cb0 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.

3 participants