Files
Reda Tarzalt 96e7d43b2f
Some checks failed
Run Check Script / check (pull_request) Failing after 38s
add auth to frontend through lib (#284)
Adds OIDC login support to the harmony-fleet-operator web dashboard using Zitadel SSO.

pkce was the recommended option for this since we don't need to hold on to any secret. We compute a value on server before sending the data to Zitadel who validates authenticity by recomputing the hash and comparing the two values.

pkce Auth flow

 1. User visits a protected dashboard route, like /devices.
 2. If no valid harmony_fleet_session cookie exists, the app redirects to /login.
 3. /login creates:
     - random state
     - random pkce_code_verifier
     - derived code_challenge = base64url(sha256(pkce_code_verifier))
 4. The app stores state and pkce_code_verifier in a temporary HTTP-only login-attempt cookie.
 5. The browser is redirected to Zitadel’s authorize endpoint with:
     - client_id
     - redirect_uri
     - scope
     - state
     - code_challenge
     - code_challenge_method=S256
 6. After SSO login, Zitadel redirects back to /auth/callback?code=...&state=....
 7. The callback handler:
     - parses the raw query into a strict success/failure enum
     - reads the temporary login-attempt cookie
     - validates returned state
     - exchanges code + pkce_code_verifier for tokens
     - validates the returned ID token using OIDC discovery/JWKS
     - creates a local harmony_fleet_session cookie
     - redirects to /
 8. Protected routes validate the local dashboard session cookie on each request.
 9. /logout clears the dashboard session cookie and redirects to /login.

---

 Auth middleware responses depending on request type:

 - normal browser request: redirect to /login
 - SSE request: 401 authentication required
 - HTMX request: 401 with HX-Redirect: /login (HTMX redirect is more idiomatic than through Axum for this)

Reviewed-on: #284
Reviewed-by: johnride <jg@nationtech.io>
Co-authored-by: Reda Tarzalt <tarzaltreda@gmail.com>
Co-committed-by: Reda Tarzalt <tarzaltreda@gmail.com>
2026-05-19 20:37:08 +00:00

107 lines
7.2 KiB
CSS

@import "tailwindcss";
@source "../src";
@source "../style";
/* ── CSS Custom Properties (default theme) ─────────────────────────── */
:root {
--bg: #07090c;
--bg-elev: #0c1018;
--bg-elev-2: #11151f;
--border: rgba(148, 163, 184, 0.08);
--border-strong: rgba(148, 163, 184, 0.16);
--accent: #f97316; /* orange-500 */
--accent-soft: rgba(249, 115, 22, 0.14);
--accent-fg: #fdba74;
--ok: #34d399;
--ok-soft: rgba(52, 211, 153, 0.13);
--warn: #fbbf24;
--warn-soft: rgba(251, 191, 36, 0.13);
--bad: #fb7185;
--bad-soft: rgba(251, 113, 133, 0.13);
--info: #60a5fa;
--info-soft: rgba(96, 165, 250, 0.13);
--row-h: 38px;
}
html, body { background: var(--bg); }
body { font-family: 'Inter', sans-serif; color: #e2e8f0; -webkit-font-smoothing: antialiased; }
::selection { background: var(--accent-soft); color: #fff; }
/* ── Scrollbar ──────────────────────────────────────────────────────── */
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: rgba(148,163,184,0.14); border-radius: 999px; border: 2px solid transparent; background-clip: content-box; }
*::-webkit-scrollbar-thumb:hover { background: rgba(148,163,184,0.28); background-clip: content-box; border: 2px solid transparent; }
/* ── Animations ─────────────────────────────────────────────────────── */
@keyframes ping-soft { 0% { transform: scale(1); opacity: .55; } 75%, 100% { transform: scale(2.4); opacity: 0; } }
.pulse-dot::after { content:''; position:absolute; inset:0; border-radius:9999px; background:currentColor; animation: ping-soft 1.8s cubic-bezier(0,0,.2,1) infinite; }
.pulse-dot { position: relative; }
@keyframes log-in { from { opacity:0; transform: translateY(2px); } to { opacity:1; transform: translateY(0); } }
.log-line { animation: log-in .22s ease-out both; }
@keyframes draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }
.spark-path { stroke-dasharray: 1; stroke-dashoffset: 1; animation: draw 1.4s ease-out forwards; }
@keyframes toast-in { from { opacity:0; transform: translateY(8px) scale(.98); } to { opacity:1; transform: translateY(0) scale(1); } }
.toast-in { animation: toast-in .25s cubic-bezier(.2,.7,.3,1) both; }
@keyframes toast-out { to { opacity:0; transform: translateY(-6px) scale(.98); } }
.toast-out { animation: toast-out .22s ease-in both; }
@keyframes roll-marquee { 0% { transform: translateX(-100%); } 100% { transform: translateX(400%); } }
/* ── Grid background ────────────────────────────────────────────────── */
.grid-bg {
background-image:
linear-gradient(rgba(148,163,184,0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(148,163,184,0.04) 1px, transparent 1px);
background-size: 32px 32px;
background-position: -1px -1px;
}
/* ── Density ────────────────────────────────────────────────────────── */
.density-compact { --row-h: 32px; }
.density-comfort { --row-h: 44px; }
/* ── Buttons ────────────────────────────────────────────────────────── */
.btn { display:inline-flex; align-items:center; gap:6px; padding:6px 10px; border-radius:7px; font-size:12px; font-weight:500; transition: all .15s; cursor: pointer; border: 1px solid transparent; }
.btn-primary { background: var(--accent); color: #0c0c0c; }
.btn-primary:hover { filter: brightness(1.1); }
.btn-ghost { background: transparent; color: #cbd5e1; border-color: var(--border-strong); }
.btn-ghost:hover { background: rgba(148,163,184,0.06); color:#f1f5f9; }
.btn-danger { background: rgba(244, 63, 94, 0.12); color: #fb7185; border-color: rgba(244,63,94,0.25); }
.btn-danger:hover { background: rgba(244, 63, 94, 0.2); color:#fda4af; }
/* ── Cards ──────────────────────────────────────────────────────────── */
.card { background: var(--bg-elev); border: 1px solid var(--border); border-radius: 10px; }
.card-flush { border-radius: 10px; overflow: hidden; }
/* ── Tables ─────────────────────────────────────────────────────────── */
.tbl { width: 100%; font-size: 13px; }
.tbl thead th { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.08em; color: #64748b; font-weight: 600; padding: 10px 14px; text-align: left; background: rgba(148,163,184,0.02); border-bottom: 1px solid var(--border); }
.tbl tbody td { padding: 0 14px; height: var(--row-h); border-bottom: 1px solid var(--border); color: #cbd5e1; }
.tbl tbody tr:hover { background: rgba(148,163,184,0.025); }
.tbl tbody tr.selected { background: var(--accent-soft); }
.tbl tbody tr.selected td { color: #f1f5f9; }
/* ── Inputs ─────────────────────────────────────────────────────────── */
.input { background: var(--bg-elev-2); border: 1px solid var(--border-strong); border-radius: 7px; padding: 6px 10px 6px 30px; font-size: 13px; color: #e2e8f0; outline: none; transition: border .15s; }
.input:focus { border-color: var(--accent); }
/* ── Chips ──────────────────────────────────────────────────────────── */
.chip { display:inline-flex; align-items:center; gap:6px; padding: 4px 9px; border-radius: 999px; font-size: 11px; font-weight: 500; border: 1px solid var(--border-strong); background: rgba(148,163,184,0.03); color:#cbd5e1; cursor:pointer; }
.chip.active { background: var(--accent-soft); color: var(--accent-fg); border-color: rgba(249,115,22,0.35); }
.chip:hover:not(.active) { color:#f1f5f9; background: rgba(148,163,184,0.07); }
/* ── Section title ──────────────────────────────────────────────────── */
.section-title { font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; font-weight: 600; color: #64748b; }
/* ── Progress bar ───────────────────────────────────────────────────── */
.progress-bg { background: rgba(148,163,184,0.1); }
/* ── Font helpers ───────────────────────────────────────────────────── */
.id-mono { font-family: 'JetBrains Mono', ui-monospace, monospace; white-space: nowrap; }