Skip to content

Commit fdfd0a0

Browse files
authored
user & group management with permissions, audit trail, and security hardening, implements #23 (#24)
* feat: user management — authentication, permissions, login dialog Backend — Authentication &amp; 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 &amp; 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> * fix: security — address critical and high audit findings 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> * fix: medium audit fixes + camelCase throughout server code 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> * fix: UI state after logout/login without page refresh - 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> * splash screen with theme-aware SVG background, login dialog redesign - 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> * New UserGroupEditor component Initial version to edit users + groups. Signed-off-by: Mike Lischke <mike@lischke-online.de> * Portal-based stacking, SCSS restructuring, user-group colors 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> * Split big App.scss into smaller files Signed-off-by: Mike Lischke <mike@lischke-online.de> * User management dialog improvements - 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> * Group shared login and user/group management overhaul - 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> * New unit and e2e Tests for the user & group editor Signed-off-by: Mike Lischke <mike@lischke-online.de> * Add permission visualization and initial setup flow 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> * Temporary commit 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> * Group Access popup — entry-driven permissions, tree integration, tests - 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> * A fix bug fixes. Signed-off-by: Mike Lischke <mike@lischke-online.de> * security: fix auth vulnerabilities, add login audit trail - 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> --------- Signed-off-by: Mike Lischke <mike@lischke-online.de>
1 parent d9393b3 commit fdfd0a0

95 files changed

Lines changed: 19004 additions & 3958 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cspell.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"Colours",
2828
"Cuica",
2929
"Cuicas",
30+
"daisyui",
3031
"deserialise",
3132
"deserialisers",
3233
"Dois",
@@ -68,6 +69,7 @@
6869
"Tamborim",
6970
"tamborims",
7071
"Timbau",
72+
"Toggleable",
7173
"Três",
7274
"tuplet",
7375
"tuplets",
@@ -80,7 +82,11 @@
8082
"Handzeichen",
8183
"Lischke",
8284
"Millis",
85+
"OOOGGGWWW",
86+
"TINYINT",
87+
"VARCHAR",
8388
"alttext",
89+
"connectionrefused",
8490
"dasharray",
8591
"dashoffset",
8692
"fieldset",

eslint.config.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,10 @@ export default tseslint.config(
307307
"camelCase",
308308
],
309309
},
310+
{
311+
selector: "property",
312+
format: ["camelCase"],
313+
},
310314
{
311315
selector: "property",
312316
format: [

package-lock.json

Lines changed: 152 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@
2424
"playwright:install": "playwright install chromium",
2525
"generate-themes": "tsx build/generate-daisyui-themes.ts",
2626
"fix-svgs": "tsx build/fix-svg-attributes.ts",
27-
"start": "tsx src/server/backend.ts"
27+
"start": "tsx src/server/backend.ts",
28+
"start-with-dummy-secret": "JWT_SECRET=\"for-debugging\" tsx src/server/backend.ts"
2829
},
2930
"dependencies": {
3031
"@mediabunny/mp3-encoder": "1.39.1",
3132
"@vscode/codicons": "0.0.44",
3233
"classnames": "2.5.1",
3334
"daisyui": "5.5.19",
35+
"jsonwebtoken": "^9.0.3",
3436
"mediabunny": "1.39.1",
3537
"mime-types": "3.0.2",
3638
"mysql2": "3.16.0",
@@ -47,6 +49,7 @@
4749
"@stylistic/eslint-plugin": "5.10.0",
4850
"@tailwindcss/vite": "4.2.1",
4951
"@testing-library/preact": "3.2.4",
52+
"@types/jsonwebtoken": "^9.0.10",
5053
"@types/mime-types": "3.0.1",
5154
"@types/node": "25.5.0",
5255
"@types/pg": "8.16.0",

public/percussion-background.svg

Lines changed: 4111 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)