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:
@@ -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; }
|
||||
Executable
+20
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user