diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index ac0dda1..dd6de8b 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -63,6 +63,7 @@ Phase 1: Jellyfin media index, SSH-based remote filesystem inspection, server mo - The Users tab may open a read-only detail drawer for a selected user, but any communication actions in that drawer should remain clearly disabled/placeholders until the workflow is implemented. - The frontend shell should use a polished two-row header with branding on the left, user/logout controls on the right, and primary navigation in a dedicated tab row beneath. - The frontend shell and primary pages should remain responsive and mobile-safe, with compact navigation, stacked controls on narrow screens, and reduced table column density where needed. +- The frontend should hydrate the API bearer token from persisted OIDC user storage immediately on reload so early requests do not race the auth provider lifecycle. - Backend startup should log a secret-safe configuration summary and request/activity diagnostics so configuration issues can be debugged without exposing API keys. ### Remote Filesystem over SSH @@ -127,6 +128,7 @@ Phase 1: Jellyfin media index, SSH-based remote filesystem inspection, server mo - Provide a dashboard tab with a compact Jellyfin media library overview and server resource overview. - Support OIDC login in the frontend using an OIDC client library, with backend JWT validation for protected API requests. +- Persist frontend OIDC auth state across tab reloads by storing the OIDC user and request state in browser localStorage. - Provide Docker Compose deployment files at the repository root for production and local development. - Backend Docker deployment should mount a private SSH key and a known_hosts file into the container rather than baking them into the image. - Production compose should also pass the root `.env` into the backend container so runtime auth settings like `OIDC_ISSUER_URL` are available there, not just at compose interpolation time. @@ -177,3 +179,5 @@ Phase 1: Jellyfin media index, SSH-based remote filesystem inspection, server mo - 2026-05-04: Backend Docker Compose now mounts a host SSH directory into `/root/.ssh` so Paramiko can use a private key and strict host-key checking without baking secrets into the image. - 2026-05-04: The frontend was adjusted to be more mobile-safe by making the app shell tabs scrollable, stacking header controls on narrow screens, and hiding low-priority table columns on smaller displays. - 2026-05-04: The app header was upgraded to a two-row branded layout with a left logo mark, right-side username/logout controls, and a separate navigation row. +- 2026-05-04: Frontend OIDC storage was switched from session-only defaults to localStorage-backed user/state stores so reloads keep the auth flow intact. +- 2026-05-04: API requests now fall back to the persisted OIDC user store for the bearer token so the first render after reload can avoid spurious 401s. diff --git a/frontend/src/auth.ts b/frontend/src/auth.ts index fd34f04..56f3ee1 100644 --- a/frontend/src/auth.ts +++ b/frontend/src/auth.ts @@ -1,5 +1,29 @@ +import { WebStorageStateStore } from "oidc-client-ts"; + let accessToken: string | null = null; +function getStoredAccessToken(): string | null { + if (typeof window === "undefined" || !isOidcConfigured()) { + return null; + } + + const authority = import.meta.env.VITE_OIDC_ISSUER as string; + const clientId = import.meta.env.VITE_OIDC_CLIENT_ID as string; + const raw = window.localStorage.getItem(`oidc.user:${authority}:${clientId}`); + if (!raw) { + return null; + } + + try { + const parsed = JSON.parse(raw) as { access_token?: unknown }; + return typeof parsed.access_token === "string" && parsed.access_token + ? parsed.access_token + : null; + } catch { + return null; + } +} + export function isOidcConfigured(): boolean { const enabled = (import.meta.env.VITE_OIDC_ENABLED ?? "true").toLowerCase() !== "false"; @@ -11,6 +35,10 @@ export function isOidcConfigured(): boolean { } export function getOidcConfig() { + const storage = new WebStorageStateStore({ + store: window.localStorage, + }); + return { authority: import.meta.env.VITE_OIDC_ISSUER as string, client_id: import.meta.env.VITE_OIDC_CLIENT_ID as string, @@ -23,6 +51,8 @@ export function getOidcConfig() { response_type: "code" as const, automaticSilentRenew: false, loadUserInfo: true, + stateStore: storage, + userStore: storage, onSigninCallback: () => { window.history.replaceState( {}, @@ -38,5 +68,5 @@ export function setAccessToken(token: string | null | undefined) { } export function getAccessToken(): string | null { - return accessToken; + return accessToken ?? getStoredAccessToken(); }