* feat(wireguard): WireGuard peer manager UI with QR, .conf download, and per-client exit node selection
- models/wg_peer.js — Redis-backed peer store with auto IP allocation (10.100.0.x)
- models/wg_site.js — Redis-backed exit node store (admin-managed sites)
- utils/wg_keys.js — X25519 keypair gen via Node crypto (no wg binary needed)
- utils/wg_conf.js — client wg0.conf renderer
- routes/wireguard.js — REST API: CRUD sites/peers, GET /conf, GET /qr (QRCode PNG)
- views/wireguard.ejs — full dark-mode UI: exit node table, peer table, QR modal,
.conf download, exit node picker per client
- conf/base.js — conf.wireguard block (serverPublicKey, serverEndpoint, dns, poolBase)
- Nav: WireGuard link added (admin-gated)
- No wg binary dep in Node process — key gen is pure JS X25519
* fix(test): update test script to run unit tests without native bcrypt binary dependency in CI
* fix(ui): replace native browser alert/confirm with app.messages in WireGuard view
- app_super_admin (cross-app, also recognized by sso-manager-node/proxy)
and a new app_jump_admin group are added to conf.auth: super admins are
full admins here same as app_sso_admin; jump admins get audit page/data
access without other admin rights (isJumpAdmin/requireJumpAdmin in
middleware/auth.js, wired into routes/api.js's audit-data gate and the
/audit page's client-side forceLogin -- previously the page shell
rendered for any logged-in user, only the data was gated).
- Dashboard: moved the stat boxes and Top hosts/Top users cards to the
Audit page (audit is now the admin-facing metrics home; dashboard stays
focused on "hosts I can reach"). Renamed "All hosts" to "My hosts".
- Host list now shows Last connection/Last failed connection columns and
highlights rows green (live session, from session_registry) or yellow
(most recent attempt failed) -- backed by new per-host last-success/
last-fail timestamps in models/metrics.js, populated by ssh_server.js
(which now attributes grammar/TUI connect failures to the resolved host
when one was found, not just aggregate counters) and surfaced through
GET /api/user/hosts (routes/user.js).
- Dashboard, Sessions, and Audit pages now wrap their content in
<div class="container mt-4">, matching sso-manager-node/proxy's width
instead of rendering full-bleed inside the fluid shell.
- Audit's nav entry now carries groups: ['admin'] (utils/ui.js), reusing
the existing synthetic-admin-group nav-gating convention -- the API
route was already server-side admin-gated, this closes the last gap by
hiding the nav link/page for non-admins too.
- app-base.js (byte-identical across the 3 apps): added
app.util.revealItem() and the --sw-content-offset sticky-positioning
variable, carried over from the same round of changes in
sso-manager-node/proxy. Not yet called anywhere in this app -- no
sticky/reveal use case here yet -- but keeps the shared file in sync.
views/top.ejs, views/bottom.ejs and public/lib/js/app-base.js are now
byte-identical across sso-manager-node, proxy and jump-host. Everything
per-app moved into utils/ui.js, exposed to every render as `ui` via
app.locals (nav items + their group gates, footer repo/docs/ToS links,
favicon, profile/logout targets, update-banner on/off + label).
Client framework changes:
- One gating model everywhere: app-base.js reveals .group-required-<cn>
for each of the current user user/me groups. sso-manager-node sends LDAP
DNs in memberOf, the OIDC clients send CNs in groups; both normalise to
CNs, and the clients isAdmin flag becomes a synthetic `admin` group, so
proxy nav-admin items are now group-required-admin.
- user/me is fetched once per page load and cached (app.auth.loadUser);
nav, forceLogin and group-required elements all read that one promise.
- isLoggedIn is dual-mode (Promise + node-style callback), so the async
and callback call styles both work from one shared top.ejs.
- forceLogin no longer uses $.holdReady (removed in jQuery 4): it redirects
to /login?redirect=<path>, and still enforces required groups.
- logOut only clears the session; the caller decides where to go next.
- post/put/delete are dual-mode Promise/callback, which also removes the
undefined `callback2` reference that threw on a non-function callback.
Dependencies: jquery ^4.0.0 and ejs ^3.1.10 in all three apps.
jump-host specifics:
- .group-required base rule added to styles.css (no gated nav items yet).
- #spa-shell drops its inline margin-top; styles.css already sets it, and
the shared shell adjusts it when a banner is shown.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>