Skip to content

user & group management with permissions, audit trail, and security hardening, implements #23 - #24

Merged
mike-lischke merged 16 commits into
mainfrom
features/user-management
Jun 30, 2026
Merged

user & group management with permissions, audit trail, and security hardening, implements #23#24
mike-lischke merged 16 commits into
mainfrom
features/user-management

Conversation

@mike-lischke

Copy link
Copy Markdown
Owner
  • UserGroupEditor: user/group CRUD, shared group passwords, member management,
    "My Groups" mode, group login flow, admin badges with per-user colors.
  • PermissionEditor: drag-and-drop Read/Write zones, entry-driven updates,
    World write-blocked, Admins filtered. PermIndicator in score tree.
  • Double-click: cellDblClick instead of manual event.detail check.
    Auto-select tree entry when Group Access menu opens.
  • Security: handleRefresh server-side auth context (no client headers),
    admin-only testConnection/listUsers, adminId reassignment restricted,
    body size limits (10MB JSON, 50MB upload).
  • login_audit table: immutable audit trail for all auth events with IP tracking.
  • Tests: 5 unit + 6 e2e for PermissionEditor, UserGroupEditor unit tests,
    login audit enum tests. Fixed stale selectors in print/settings/users-groups e2e.

Copilot AI review requested due to automatic review settings June 30, 2026 14:44
@mike-lischke mike-lischke linked an issue Jun 30, 2026 that may be closed by this pull request
@mike-lischke

Copy link
Copy Markdown
Owner Author

Quite a few UI enhancements are included in this patch, as they are needed to fully provide feedback to the user for certain events.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

Backend — Authentication & Authorisation
────────────────────────────────────────
- New src/server/auth.ts: scrypt password hashing, JWT sign/verify,
  Linux-style rwx permission system with owner/group/world bits,
  inheritance (scores inherit from parent folders), admin bypass
- New API endpoints (17): login, refresh, logout, whoami,
  listUsers, createUser, updateUser, deleteUser,
  listGroups, createGroup, updateGroup, deleteGroup,
  addUserToGroup, removeUserFromGroup, listGroupMembers,
  getPermissions, setPermissions
- Permission checks on all existing mutation endpoints
  (addScoreFolder, addScore, renameEntry, updateScore, delete, move)
- New DB tables: users, groups, user_groups, permissions
  (MySQL + PostgreSQL adapters, backend-db.sql)
- Admin seed: auto-creates admin/admin on first start when no users exist
- Dependency: jsonwebtoken (scrypt uses Node.js built-in crypto)

Frontend — Auth State & Login Dialog
────────────────────────────────────
- ScoreBookDataModel: access/refresh token management, login/logout/
  restoreSession, auto-401-refresh in fetchApi, capabilities getter
  replacing the old canWriteScores boolean field
- Requisitions: new authChanged event for UI updates on auth changes
- LoginDialog: uses ValueDialog with password fields, Enter-to-submit
  (username→focus password, password→trigger accept), error re-display
- App.tsx: calls restoreSession() on startup, shows LoginDialog when
  no valid session exists, Continue Anonymously support

Dialog Architecture Refactoring
───────────────────────────────
- ValueDialog extended: password fields, custom button labels,
  errorMessage display, hideActions option, isDefault button flag,
  Spinner entry type, dismiss()/triggerAccept() public methods
- New StatusDialog: composes ValueDialog for status-only modals
  (no user input), simplified API: show()/update()/dismiss()
- BackendDisconnectedDialog: rewritten to use StatusDialog,
  configurable dialogId prop

Tests
─────
- New tests/server/auth.spec.ts: 10 tests (JWT round-trip,
  refresh/access token separation, permission bit math)
- New tests/core/ScoreBookDataModel-auth.spec.ts: 8 tests
  (auth state, login/logout, capabilities, Authorization header)
- New tests/e2e/login-flow.spec.ts: 4 tests (dialog appears,
  anonymous continue, successful login, failed login error)
- tests/e2e/helpers.ts: setupAuthenticatedSession() and
  setupAnonymousSession() helpers; routeApi defaults to authenticated
- tests/e2e/backend-disconnect.spec.ts: adapted to login dialog
  and StatusDialog id changes

Signed-off-by: Mike Lischke <mike@lischke-online.de>
Auth enforcement on unauthenticated endpoints:
- handleSetup: require admin if users already exist (prevents DB reconfiguration)
- handleClearAll: admin-only (was unauthenticated, trivial GET wipe)
- handleUploadInstrumentImage: admin-only

Permission fixes:
- Root-level writes (parentId === -1) now require authenticated user
- buildCapabilities: sync, simple admin check; removed broken per-feature
  permission queries and the ?? operator precedence bug
- verifyPassword: JSON.parse of stored hash inside try/catch

Token and credential hardening:
- JWT_SECRET: mandatory env variable, throws on startup if absent
  (no hardcoded fallback secret)
- Admin seed: log without plaintext password
- Refresh cookie: added Secure flag

Tests:
- tests/setup.ts: JWT_SECRET set for test environment

Signed-off-by: Mike Lischke <mike@lischke-online.de>
Raw error messages replaced with generic responses (audit #13):
- console.error with convertErrorToString, sendError with generic text

Refresh token rotation (audit #11):
- Replaced JWT refresh tokens with random token + SHA-256 hash
- DB: refresh_token_hash column in users table (MySQL + PostgreSQL,
  migration for existing databases)
- createRefreshToken(): 32 random bytes, returns raw + hash
- verifyAndRotateRefreshToken(): compares hash, rotates on success,
  clears all hashes on mismatch (stolen token detection)
- login stores hash, refresh returns new raw token in cookie

Read-permission filtering in listScoreFolderContent (audit #9):
- Folders and scores filtered post-query via checkPermission(R)

camelCase throughout server code:
- All SELECT queries use AS to map snake_case columns to camelCase
- Removed unused IUserRow, IGroupRow; IPermissionRow fields camelCase
- Upload JSON response keys changed to camelCase

Signed-off-by: Mike Lischke <mike@lischke-online.de>
- Add AppPhase enum for explicit UI phases (Checking, Setup, AdminSetup, Login, Running)
- Add AdminSetupDialog for first-time install with no admin user
- Add user dropdown menu with sign-out in the toolbar
- Clear stale StatusBar items (score stats, notification icon) on logout
- Use fresh array for scoreLib on re-initialize so TreeGrid detects the change
- Backend: add whoami endpoint, support JWT_SECRET env var for local dev

Signed-off-by: Mike Lischke <mike@lischke-online.de>
- Replace ValueDialog-based LoginDialog with Dialog-based layout styled like SettingsDialog
- Add splash screen overlay with percussion-background.svg mask, auto-adapts to theme
- Show splash during Setup, AdminSetup, Login phases; fade out on Running
- Add logo.svg watermark (top-right, desaturated, wobble animation every 5s)
- Center ProgressIndicator in a rounded card during Checking phase
- Button component passes type attribute to native <button> element
- Fix Sign In from Running: transition through Login phase for proper splash display
- Fix continue-anonymous from Running: return directly without reinitializing
- Fix stale StatusBar items and score lib after logout/login without page refresh
- Tests updated

Signed-off-by: Mike Lischke <mike@lischke-online.de>
Initial version to edit users + groups.

Signed-off-by: Mike Lischke <mike@lischke-online.de>
Replace native <dialog> with a Portal-based Dialog and introduce
a reusable Portal component.
Portals render into managed host divs in document.body with automatic
z-index stacking — fixing all overlay/backdrop ordering issues.

New/refactored components:
- Portal (src/components/ui/framework/Portal.tsx)
  Static portalStack, incrementing z-index, topmost-Escape-only,
  configurable background opacity, click-outside, mouse-event blocking.
- Dialog rewritten to build on Portal instead of native <dialog>.
  Restores legacy action-button value→onClose forwarding.
- Popup builds on Portal with auto-flip positioning via existing
  computeContentPosition() — replaces naive manual positioning.
- SCSS split from monolithic component-styles.scss into 25 partials
  under styles/, loaded via a single @use index.

Features:
- Group colors: random color on creation, editable via inline
  <input type=color> in TagInput badges with auto light/dark text.
- UserGroupEditor: inline form moved to Popup anchored to the
  clicked button; pending groups held until Save; ConfirmDialog
  replaces native confirm() for deletes.
- ConvertErrorToString import cleanup in backend.ts.

Fixes:
- All 285 unit tests and 73 e2e tests green.
- TagInput: .badge→.du-badge, add stopPropagation on remove btn.
- SettingsDialog tests: query document.body (Portal renders there).
- LoginDialog: add onClick for anonymous button (lost in Dialog rewrite).
- print-dialog/backend-setup e2e: remove native dialog element selectors.
- Coding-preference: remove all _-prefixed params and this.props/state
  direct access from authored files.

Signed-off-by: Mike Lischke <mike@lischke-online.de>
Signed-off-by: Mike Lischke <mike@lischke-online.de>
- Simpler handling for canceling a dialog/popup.
- Remove is_admin. That right is determined by the group the user is in (here Admin group).
- Anonymous login and admin group always exist and cannot be changed.

Signed-off-by: Mike Lischke <mike@lischke-online.de>
- Add group shared password login (DB, backend, data model, UI)
  - New columns: groups.password_hash, groups.admin_id, users/groups.last_login
  - Group login endpoint authenticates as anonymous with group permissions
  - LoginDialog tab switcher for User/Group login with group dropdown
  - Group info shown in user menu, "Sign Out" returns to login screen
- Redesign UserGroupEditor with settings-card/settings-row pattern
  - Users and Groups cards with headings and compact add buttons
  - Inline popup forms for create/edit user, group password, create group
  - Delete group with member count warning and password notice
  - Self-delete logs out and returns to splash/login screen
  - Form validation errors shown inside popups, not main dialog
  - Anonymous user filtered from list
- Popup arrow: rotated square with border matching popup, slight rounding
- Input component: showPasswordToggle prop (eye icon for reveal)
- Dropdown: keyboard navigation (↑↓/Enter), skip non-interactive items,
  disabled support, auto-focus first item on open
- Dialog: closeOnBackdropClick prop, ConfirmDialog support
- Fix arrangement player crash when arrangement is undefined during init
- Prevent Admins group from getting a shared password
- Name collision checks between users and groups
- Various lint fixes, JSDoc cleanup

Signed-off-by: Mike Lischke <mike@lischke-online.de>
Signed-off-by: Mike Lischke <mike@lischke-online.de>
Backend:
- Add getPermissionSummary() returning isOwner/isGroup/isWorld/permBits
- Include perm data in listScoreFolderContent response
- Create default group + assign permissions on first admin creation
- Support custom group name from admin setup dialog
- Preserve group-login context across page reloads via sessionStorage
- Sync users/groups table definitions with backend-db.sql in both adapters

Frontend:
- Add PermMatrix component (3×2 dot grid: Owner/Group/World × Read/Write)
- Render permission matrix in Score Library tree (right of kebab menu)
- Hide matrix for anonymous/world-only access
- Add "Show permission matrix" toggle in Settings dialog (default on)
- Live tree update on settings change via requisitions
- Rewrite AdminSetupDialog to use standard Dialog pattern
  - Two blocks: Admin User + Initial Group
  - "Finish Installation" title and button
  - Group name field with validation (whitespace-only rejected)
  - Enter key on last field triggers submit
- Fix ValueDialog: suppress empty decline button, wire Enter on last field
- Fix default button primary-color styling
- Remove dead PermissionGlyph component

Tests:
- auth.spec.ts: IPermissionSummary permBits tests
- PermMatrix.spec.tsx: 6 tests for 3×2 dot rendering
- settings-perm-matrix.spec.ts: e2e test for toggle flow

Signed-off-by: Mike Lischke <mike@lischke-online.de>
This commit exists only to avoid having to push a big commit later, after the coming rework.

Signed-off-by: Mike Lischke <mike@lischke-online.de>
- PermissionEditor now receives the data model entry directly (not entityType/ID).
  saveChanges mutates entry.perm as single source of truth; no server round-trip
  needed. Removed originalReadIds — diffs computed against entry.perm.groupIds.
- Auto-select score tree entry when Group Access menu item is clicked.
- Single-row tree update via handlePermChanged(entry) → row.reformat().
- World group blocked from Write zone (init, drop, save). Blocked cursor feedback
  via dragGroupId tracking in handleDragOverWrite.
- Admins group excluded from group pool.
- canWrite no longer overwritten in saveChanges — remains user-relative from
  backend (PermIndicator now shows current user's write capability, not group).
- Double-click handling: cellDblClick on column definition instead of manual
  event.detail check in onRowClick. Removed dead handleScoreTreeRowClick.
- CSS: .perm-drop-zone and .perm-group-pool use display:flex (parent flex, not
  child inline-flex). PermIndicator moved left of kebab button. user-select:none
  on score tree entries.
- Added ResizeObserver mock to test setup.
- Unit tests (5) and e2e tests (6) for PermissionEditor.
- Fixed pre-existing e2e selector rot: .settings-card→.form-card,
  .permMatrix→.permIndicator, .settings-row→.form-row, popup input specifier.

Signed-off-by: Mike Lischke <mike@lischke-online.de>
Signed-off-by: Mike Lischke <mike@lischke-online.de>
- handleRefresh: store auth_type/group_id server-side in users table instead of
  trusting client-controlled x-auth-type/x-group-id headers. Prevents privilege
  escalation via forged group membership.
- handleTestConnection: added admin auth check (was unauthenticated SSRF oracle).
- handleListUsers: restricted to admins only (was any authenticated user).
- handleUpdateGroup: only full admins may reassign group adminId.
- body size limits: readJsonBody 10MB, readRawBody 50MB. Oversized requests
  destroy the socket.
- login_audit table: tracks login, group_login, refresh, logout events with
  user_id, group_id, ip_address, timestamp. Replaces users.last_login.
- recordLoginAudit() helper + LoginAuditEvent enum in auth.ts.
- getClientIp() respects x-forwarded-for header.
- Tests: 2 new auth spec tests for LoginAuditEvent enum.

Signed-off-by: Mike Lischke <mike@lischke-online.de>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@mike-lischke
mike-lischke merged commit fdfd0a0 into main Jun 30, 2026
9 checks passed
@mike-lischke

Copy link
Copy Markdown
Owner Author

My fault, I should have done the CSS refactoring separately.

@mike-lischke mike-lischke self-assigned this Jul 1, 2026
@mike-lischke
mike-lischke deleted the features/user-management branch July 4, 2026 13:39
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.

[FR] We need a solid user management with detailed access rights

2 participants