feat: modern three-panel UI redesign with dark theme and icon picker#324
Open
allamiro wants to merge 39 commits into
Open
feat: modern three-panel UI redesign with dark theme and icon picker#324allamiro wants to merge 39 commits into
allamiro wants to merge 39 commits into
Conversation
- bind as the found user before unbinding: unbind terminates the connection, so the credential check always failed - check the result of the service account bind - reject empty passwords, which would otherwise result in an anonymous bind and bypass the password check - build the search filter safely: empty filter now matches on the login attribute only, the invalid default filter '()' is removed - include the search filter in the 'no users found' error to make config mistakes diagnosable from the logs - document that the configured filter is ANDed with the login attribute match and that no placeholder substitution takes place - fix default ldap port (339 -> 389)
The error path logged "user login from '{}': " with the error
interpolated where the username should be, dropping the actual message.
Log the read failure itself at error level instead.
Four error paths returned "success": true, which clients could misread as a successful operation: failed to store key, failed to store db, failed to retrieve session, failed to close db.
…uth testing (#4) Both services are commented out by default; uncomment to enable. Includes matching config.yml snippets and one-time Keycloak setup notes. The OpenLDAP service is seeded with testuser/testpass via env vars.
… in search (#5) LDAP attribute names are case-insensitive (RFC 4512), but servers return them in their own canonical casing: openldap returns 'cn' for a requested 'CN', so the case-sensitive HashMap lookup failed with 'CN attribute not found' after successful credential verification. The search also only requested the CN attribute, so the login attribute (e.g. uid) was never returned and its lookup failed next. Verified end-to-end against openldap 2.6: valid credentials log in, wrong/empty passwords are rejected.
KeePass::get_file was todo!(), so any authenticated user requesting an entry attachment triggered a runtime panic. The endpoint now returns 501 Not Implemented with an error JSON until attachment download is implemented.
The HTTP db backend streamed whatever the server returned into the kdbx parser: a 404/500 error page surfaced as a confusing 'keepass db initialization failed'. Reads, keyfile reads and writes now check the response status via error_for_status.
… headers (#8) - NotFoundError type distinguishes missing kdbx objects from real server faults; affected handlers now return 404 instead of 500 - icon Cache-Control used semicolons and 's-max-age', which browsers ignore; now: public, max-age=31536000, s-maxage=31536000, immutable - icon content type is sniffed from magic bytes (png/jpeg/gif/svg) instead of always claiming image/png
actix-session recommends rotating the session on privilege changes. set_user_session now calls session.renew() before storing the user and csrf token.
Empty uri/base_dn/login_attribute and non-ldap(s/i) uri schemes were accepted silently and only failed at login time, where backend errors are masked as 'username or password incorrect'. The LDAP backend now implements validate_config like the other auth backends.
Expired encrypted databases of inactive users were never removed, so the in-memory cache grew without bound. store() now drops all expired entries before inserting (resolves the 'expire old entries' TODO).
…est (#12) A new Client was constructed for every request, defeating connection pooling and TLS session reuse. The client is now created once in Http::new.
The plaintext was extended by 16 zero bytes before AES-GCM encryption and the last 16 bytes stripped after decryption. GCM is a stream mode and needs no padding; the constant was also named LENGTH_IV while the actual GCM nonce is 12 bytes. The roundtrip only worked because both sides applied the same offset.
X-Frame-Options: DENY, X-Content-Type-Options: nosniff and Referrer-Policy: no-referrer via DefaultHeaders middleware, registered outermost so even short-circuited responses (auth middleware 401s) carry them. CSP is left out for now since the index page relies on inline script injection.
The clippy 0.0.302 crate is a years-old relic (clippy ships as a rustup component); cargo itself warns it is an invalid dependency. The new CI job gates on the bug-catching lint groups (correctness, suspicious); style lints are left for a dedicated cleanup.
The LDAP backend lowercases UserInfo.id while htpasswd used it as-is. The id keys the in-memory db cache, so case variations of the same user created separate cache entries. htpasswd now lowercases like LDAP; OIDC subjects are case-sensitive opaque strings and stay as-is (documented on the trait).
- drop the x86-only RUSTFLAGS (sandybridge/sse) from the Dockerfile and workflows: they made arm64 builds impossible and are redundant, the aes crate uses runtime cpu feature detection - publish linux/amd64 + linux/arm64 images via buildx/qemu - new unauthenticated /health endpoint and a --health-check flag on the binary (scratch has no shell or curl), wired up as HEALTHCHECK - build dependencies in their own docker layer so source changes don't recompile the whole dependency tree - fix stray semicolon in ENV RUST_BACKTRACE
File::open opens read-only, so any attempt to save the database through the filesystem backend failed with EBADF. Use File::create. The save path itself (KeePass::to_backend) is still unused by the API; wiring it up is part of the write-support feature work.
In-memory throttle keyed by client ip + username: 5 free failures, then lockouts of 2s doubling up to 15 minutes. Successful login resets the counter; stale entries are pruned opportunistically. Blocked attempts return 429.
#20) The key store was hard-wired to linux-keyutils, making native runs on macOS/Windows impossible. The kernel keyring remains the store on linux; other platforms get an in-memory store (zeroized on drop, with expiry). linux-keyutils is now a linux-only target dependency, and the http layer matches on a crate-local KeyUnavailableError instead of linux_keyutils::KeyError.
…ytes (#21) The linux keyring retrieval read into a fixed 32-byte buffer, so any secret of a different length was truncated or zero-padded, breaking the roundtrip and diverging from the in-memory store. Use read_to_vec which sizes from the actual payload (resolves the buffer size TODO). Addresses the cubic review finding on PR #20.
realip_remote_addr honors Forwarded/X-Forwarded-For, which clients can forge to evade login throttling when the server is directly exposed. The rate limiter now uses the tcp peer address unless the new trust_proxy_headers config option is enabled (for deployments behind a reverse proxy that sets these headers). Addresses the cubic review finding on PR #19.
…alm import (#23) - .env.example carries all example credentials for the optional compose auth services; users copy it to .env (now gitignored) - the keycloak service imports tests/keycloak/realm.json on startup: realm 'keepass', confidential client 'keepass4web' with the /callback_user_auth redirect URI, and user testuser/testpass - OIDC is testable without any manual admin console setup - the openldap service reads its seed credentials from .env - compose comments updated with the cp .env.example step and the matching config.yml snippets
Provider metadata (including JWKS) is now cached for 10 minutes and re-fetched on demand. Previously the metadata was fetched once at startup and cached forever, so a provider that was down at boot would crash the app, and signing-key rotations would permanently break token verification. - init() does a best-effort warm-up but no longer exits on failure; discovery is retried on the first login request (fixes lixmal#314) - MetadataCache (RwLock<Option<CachedMetadata>>) replaces the plain Box in AuthCache; double-checked locking avoids thundering herds on expiry - stale copy is served while the provider is transiently unreachable, so an expired TTL during a brief outage does not disrupt active users - ID token verification failure triggers one forced metadata refresh and a retry, recovering transparently from JWKS key rotation (fixes lixmal#313) - get_login_type / get_logout_type made async to carry the cache through; all other backends updated accordingly (no behaviour change) - unit test: discovery_is_lazy_and_recovers (mockito)
feat(oidc): lazy discovery with TTL cache and key-rotation retry
The Linux kernel keyring (keyctl/add_key/request_key) is blocked by the default Docker seccomp profile, so users running the container on Docker Desktop for Mac without --security-opt seccomp=seccomp/keyring.json would get a runtime error even though the binary compiled fine. Add `use_keyring: bool` (default true) to Config. When false, the in-memory key store is used even on Linux, making the seccomp profile optional. - SecretKey::store / retrieve take use_keyring; dispatch picks keyring or memory at runtime on Linux, always uses memory on other platforms - memory module is now always compiled (was guarded by cfg(not(linux))) - config.yml documents the option and its Docker-on-macOS use case - keepass tests pass use_keyring=false so they do not require a real keyring Fixes lixmal#227
feat: use_keyring config option for container-runtime compatibility
config.example.yml is a copy-and-edit reference showing all backends with realistic, testable values: - LDAP section pre-filled with the docker-compose OpenLDAP test values (uri, base_dn, bind, filter match .env.example exactly) - OIDC section pre-filled with the Keycloak test values (issuer, client_id, client_secret match tests/keycloak/realm.json and .env.example) - All options documented inline with context and caveats config.yml is now a minimal htpasswd configuration. LDAP and OIDC sections are removed; a header comment points to config.example.yml for other backends. The file works out of the box once a .htpasswd file is created. Fixes lixmal#317
docs: add config.example.yml; slim default config.yml to htpasswd
Keycloak previously mapped host:8081 → container:8080, meaning both services used port 8080 internally. This caused confusion when reading config examples because the keepass app and Keycloak issuer URL both referenced :8080. Change Keycloak to use port 8180 end-to-end (--http-port=8180, ports 8180:8180) so the port assignment is unambiguous in all contexts: keepass app → 8080 Keycloak → 8180 Update all references in docker-compose.yml, config.example.yml, and .env.example. The inside-compose issuer and outside-compose issuer now use the same port (8180), removing the two-URL comment.
fix: move Keycloak to port 8180 to avoid confusion with keepass (8080)
Closes lixmal#319. - Add dedicated Docker Compose quick-start sections for htpasswd, LDAP (OpenLDAP), and OIDC (Keycloak) with copy-paste commands - Document .env.example and the optional service blocks in docker-compose.yml - Point readers to config.example.yml for full per-option reference - Note use_keyring: false for Docker Desktop (macOS / Windows) - Update CONFIGURATION section to mention config.example.yml - Remove stale x86-only RUSTFLAGS from Classic build instructions
…database, entry CRUD Fixes lixmal#320 OIDC Docker split-brain fix: - Add optional `discovery_url` config field so the app fetches OIDC metadata from an internal container URL (e.g. http://keycloak:8180) while validating tokens against the external issuer URL the browser uses (e.g. http://localhost:8180). - After fetching the discovery doc, rewrite server-to-server endpoint URLs (token_endpoint, jwks_uri, userinfo_endpoint, etc.) to use the internal host. Browser-facing URLs (authorization_endpoint, end_session_endpoint) are left unchanged so redirects work. - Fetch the JWKS explicitly from the rewritten jwks_uri: raw JSON deserialization of the discovery document does not populate the JWKS because openidconnect marks the field #[serde(skip)]. - Update Keycloak test realm to allow localhost:8088 redirect URIs. Auto-create database: - If the configured .kdbx path does not exist when the user first unlocks the vault, create a new empty KDBX4 database with the provided master password and write it to the configured path. Entry CRUD + save to disk: - POST /api/v1/entry — create entry in a group - PUT /api/v1/entry — update entry fields - DELETE /api/v1/entry — delete entry - POST /api/v1/save_db — flush in-memory database to the .kdbx file (requires re-entering the master password) - Add modify_db() utility: decrypt cached database, run a closure, re-encrypt with a fresh key, revoke the old key, store the result. - Frontend: New Entry inline form in the group panel, Delete button per entry row, Save button in the navbar with a password modal.
…try form - EntryGroup was missing its own UUID, causing group_id to be sent as "undefined" when creating a new entry (UUID parse error on the server) - search_entries returns Uuid::nil() as the sentinel group id so the frontend can suppress the New Entry button on search result views - New entry password field gains a show/hide eye toggle and a generate button (crypto.getRandomValues, 20-char mixed charset)
- POST /api/v1/group: create a named subgroup under any parent - PUT /api/v1/group: rename an existing group - Depth rule: root accepts unlimited child groups; any non-root group is capped at 2 subgroups (enforced in KeePass::create_group); the "Add Group" button is hidden in the UI when depth >= 2 - Entry edit: inline pre-filled edit form per entry row; blank password keeps the existing protected value - Viewport tracks selected group depth via findGroupDepth tree walk and passes it to GroupViewer as groupDepth prop
- Replace Bootstrap vault layout with flat three-panel design: sidebar (groups), center (entry cards), detail/form panel - New CSS design system (vault.css) with CSS custom properties, supporting both light and dark themes; toggle persists in localStorage - Entry cards replace table rows; inline forms moved to dedicated right panel (EntryForm component) - New SVG icon set (Icons.js) used throughout, replacing glyphicons - Icon picker on entry create/edit: all 69 standard KeePass icons selectable from a grid; icon saved to the KeePass database - NavBar rewritten: inline search, user dropdown, theme toggle button - TreeViewer/TreeNode rewritten with kp-tree-* classes; root group creation via sidebar "+" button - NodeViewer rewritten: kp-node-field rows, reveal/hide protected fields via React state (no DOM mutation), copy-to-clipboard feedback - DBLogin page modernised: custom file picker, kp-input styled fields - Backend: create_entry and update_entry now accept optional icon field - Fix save_db error display: use HTTPError.msg not jQuery responseJSON
This was referenced Jun 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #323
What this changes
Replaces the Bootstrap-grid vault view with a purpose-built flat UI — no Bootstrap in the vault, only for login pages.
Layout
Three fixed panels:
CSS design system (
js/style/vault.css)--kp-*custom properties — one place to rethemelocalStorageIcon picker
entry.icon_id)create_entry/update_entryaccept optionalicon: Option<usize>Component changes
js/style/vault.cssjs/scripts/EntryForm.jsjs/scripts/Icons.jspublic/preview.htmlNavBar.jsTreeViewer.js/TreeNode.jskp-tree-*classes, expand/collapseGroupViewer.jsNodeViewer.jsViewport.jsrightPanelstateDBLogin.jssrc/keepass/keepass.rscreate_entry/update_entryaccepticonsrc/server/route/keepass.rsNewEntry/UpdateEntryhaveiconfieldTesting
Tested locally with the Docker Compose stack (OIDC via Keycloak). The
public/preview.htmlfile can be opened directly in any browser to preview the layout without the backend.