Skip to content

fix: modernise UserLogin, BackendLogin, Alert and Info to match new vault UI#326

Open
allamiro wants to merge 40 commits into
lixmal:masterfrom
allamiro:feat/modernise-login-pages
Open

fix: modernise UserLogin, BackendLogin, Alert and Info to match new vault UI#326
allamiro wants to merge 40 commits into
lixmal:masterfrom
allamiro:feat/modernise-login-pages

Conversation

@allamiro

Copy link
Copy Markdown
Contributor

Closes #325
Follows up #324 (UI redesign).

What was missed

PR #324 rewrote all vault-view components to use the new kp-* CSS design system, but three login-flow components still used Bootstrap-only markup, causing a visual inconsistency for htpasswd and LDAP backends.

Changes

UserLogin.js (htpasswd / LDAP)

  • form-controlkp-input
  • btn btn-block btn-lg btn-successkp-btn kp-btn-primary kp-btn-open
  • Added kp-login-icon tile (Shield icon), h4 title, subtitle, and kp-login-field labelled rows to match DBLogin

BackendLogin.js (HTTP db backend, template-driven)

  • Same input and button class upgrades
  • Backend icon rendered inside kp-login-icon tile; falls back to Shield icon
  • Each template field wrapped in kp-login-field with auto-detected IconKey / IconUser

Alert.js / Info.js

  • Removed Bootstrap alert-danger / alert-info and glyphicon spans
  • New kp-login-alert-error / kp-login-alert-info classes (rules added to vault.css)

NavBar.js

  • isVault now also excludes /user_login so the "Close Vault" dropdown item is correctly hidden on the htpasswd / LDAP login page

vault.css

  • Added .kp-login-alert, .kp-login-alert-error, .kp-login-alert-info rules (light + dark theme aware)

Testing

Verified build succeeds and all new CSS classes appear in the bundle. All three auth backends (OIDC, htpasswd, LDAP) route through DBLogin for the KeePass master password step — that page was already modernised in #324 and is unaffected here.

allamiro and others added 30 commits June 12, 2026 22:43
- 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.
allamiro and others added 10 commits June 13, 2026 12:53
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
UserLogin.js (htpasswd / LDAP backend):
- Replace form-control inputs and Bootstrap button with kp-input / kp-btn-primary
- Add Shield icon header, subtitle, and labelled field rows matching DBLogin style

BackendLogin.js (HTTP db backend, template-driven):
- Replace form-control inputs and Bootstrap button with kp-input / kp-btn-primary
- Render template icon inside kp-login-icon tile; fall back to ShieldIcon
- Wrap each template field in kp-login-field with auto-detected icon

Alert.js / Info.js:
- Remove Bootstrap alert-danger / alert-info classes and glyphicon spans
- Use new kp-login-alert / kp-login-alert-error / kp-login-alert-info classes

NavBar.js:
- Extend isVault exclusion to cover /user_login so Close Vault is hidden on
  the htpasswd / LDAP login page

vault.css:
- Add kp-login-alert, kp-login-alert-error, kp-login-alert-info rules
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.

fix: login pages (UserLogin, BackendLogin, Alert, Info) inconsistent with new vault UI

1 participant