feat: rebuild web UI on the shared theta42 app stack; OIDC + local admin auth

The web management UI was a bespoke minimal theme with LDAP-bind login.
Rebuild it to match the SSO Manager and Proxy — same stack, same look/feel,
same auth model. The SSH bridge, audit, metrics, and access logic are
unchanged; this is purely the web layer.

Frontend (mirrors proxy/sso):
- Express + EJS with the shared top.ejs/bottom.ejs shell, Bootstrap 5,
  jQuery, jq-repeat, FontAwesome, Socket.IO, and the shared app-base.js /
  val.js client framework (copied verbatim). Vendor libs served from
  node_modules via /static-modules; app assets via /static.
- Dashboard / Sessions / Audit pages render in the common look/feel,
  loading data through the authenticated /api/* endpoints.

Auth (mirrors proxy):
- OIDC against the SSO (utils/oidc.js + routes/auth.js + models/oidc_state)
  plus a local anti-lockout admin (models/user_redis.js, bootstrapped from
  auth.adminUsers[0] / auth.localAdminPass). AuthToken sessions carry the
  group snapshot; middleware gates the data API on adminGroups or the local
  admin. New config: oidc{} + auth.adminUsers/localAdminPass.
- /api/user/me drives the client login state; "Log in with SSO" hidden when
  oidc.enabled is false.

Verified end to end: local admin login -> token -> /api/user/me isAdmin,
metrics/sessions/audit 200 with token / 401 without / 401 bad password;
static + page shells serve; 26 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 20:57:34 -04:00
parent eb8e5b409e
commit aa3b3ed515
38 changed files with 2550 additions and 374 deletions
-31
View File
@@ -1,31 +0,0 @@
:root { --bg:#0f1115; --panel:#181b22; --line:#272b34; --fg:#e6e8ec; --mut:#8b93a1; --acc:#4f9cf9; --bad:#ff6b6b; --ok:#4ec9a5; }
* { box-sizing: border-box; }
body { margin:0; font:14px/1.5 -apple-system,Segoe UI,Roboto,sans-serif; background:var(--bg); color:var(--fg); }
a { color:var(--acc); text-decoration:none; } a:hover { text-decoration:underline; }
.nav { display:flex; align-items:center; gap:16px; padding:12px 20px; background:var(--panel); border-bottom:1px solid var(--line); }
.brand { font-weight:600; } .brand small { color:var(--mut); font-weight:400; }
.nav .spacer { flex:1; } .nav .who { color:var(--mut); }
.wrap { max-width:1100px; margin:0 auto; padding:24px 20px; }
h1 { font-size:20px; margin:0 0 16px; } h2 { font-size:15px; margin:24px 0 8px; }
.tiles { display:flex; gap:16px; flex-wrap:wrap; }
.tile { background:var(--panel); border:1px solid var(--line); border-radius:10px; padding:16px 20px; min-width:150px; }
.tile .n { display:block; font-size:28px; font-weight:600; } .tile .l { color:var(--mut); }
.cols { display:grid; grid-template-columns:2fr 1fr; gap:24px; }
@media (max-width:800px){ .cols { grid-template-columns:1fr; } }
table { width:100%; border-collapse:collapse; margin-top:8px; }
th,td { text-align:left; padding:7px 10px; border-bottom:1px solid var(--line); }
th { color:var(--mut); font-weight:500; font-size:12px; text-transform:uppercase; letter-spacing:.03em; }
td.r,th.r { text-align:right; }
tr.bad td { color:var(--bad); }
.muted { color:var(--mut); }
.more { font-size:12px; font-weight:400; margin-left:8px; }
.foot { max-width:1100px; margin:0 auto; padding:16px 20px; color:var(--mut); font-size:12px; }
.filters { display:flex; gap:8px; margin-bottom:12px; flex-wrap:wrap; }
.filters input,.filters select,.login input { background:#0c0e12; border:1px solid var(--line); color:var(--fg); border-radius:7px; padding:7px 10px; }
button { background:var(--acc); color:#fff; border:0; border-radius:7px; padding:8px 14px; cursor:pointer; font:inherit; }
button.link { background:none; color:var(--acc); padding:0; }
.inline { display:inline; } .pager { display:flex; gap:16px; align-items:center; margin-top:16px; color:var(--mut); }
.center { display:grid; place-items:center; min-height:100vh; }
.card.login { background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:28px; width:320px; display:flex; flex-direction:column; gap:12px; }
.card.login h1 { margin:0 0 8px; } .card.login label { display:flex; flex-direction:column; gap:4px; font-size:13px; color:var(--mut); }
.card.login .hint { color:var(--mut); font-size:12px; margin:4px 0 0; } .err { color:var(--bad); margin:0; }
+20
View File
@@ -0,0 +1,20 @@
nav.navbar{
padding-left: 1em;
padding-right: 1em;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
#spa-shell {
margin-top: 4.5rem;
padding-bottom: 1em;
flex-grow: 1;
}
.card-title{
font-weight: bold;
}