feat(auth): auto-retry OIDC callback storage errors + 30s /auth/me heartbeat #413
No reviewers
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
psa-systems/mokosh-apps!413
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/MAPPS-355-auth-callback-retry-plus-heartbeat"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Two closely-coupled SPA auth-UX fixes.
/auth/callbackno longer shows the redSign-in failed / storage: no pending OIDC flow / Try againwall for the common transient cases. That error surfaces fromstorage::take_pending()when sessionStorage is empty - a user opening/auth/callbackin a new tab, refreshing after MAPPS-336 stripped the query string, or letting a pending flow expire (>1 week per MAPPS-338). Nothing an operator needs to look at; the user just needs a fresh flow. The callback now detects anystorage:prefixed FlowError, silentlywindow.location.replace("/login")(which kicksstart_login), and renders the sameSigning you in...copy the healthy exchange path shows so there is no visible red flash. CSRF (state mismatch), replay (nonce mismatch), token-endpoint, config, and network errors keep the manual screen because a silent retry could loop on them or mask a real problem.replace(notset_href) so the browser's back button never bounces the user back to the errored callback URL after the fresh flow finishes.use_auth_heartbeatmounted at the App root alongsideuse_token_refresh. Every 30 seconds it fires an authedGET /api/v1/auth/me. On a 410 response the shared fetch layer'shandle_response(added by MAPPS-348) callsnote_account_deleted, which flips theACCOUNT_DELETEDGlobalSignal and pops the terminalAccountDeletedOverlaysitting atAppLayoutroot. So the hook itself discards the response - it only needs to fire the request.Without this loop the SPA only discovers a soft-delete when the user happens to touch a page that fetches. A user idle on the dashboard after their Bunyip account was deleted could stare at stale UI for the full at+jwt TTL (15 min) before any request fired. 30s cadence puts the "you were signed out" overlay in front of them fast without meaningful cost (~120 requests per hour per active tab, well under the token-refresh loop's own cadence). Heartbeat skips itself when:
document.visibilityState == "hidden"), ORACCOUNT_DELETEDis already set (overlay is up).Cargo.toml: addVisibilityStateto theweb-sysfeature list sotab_is_hidden()compiles.Dependencies:
hooks::fetchalready exists (MAPPS-348 SPA merged as77aff3f). Until mokosh-server MAPPS-348 (PR #421) lands and returns 410 for tombstoned users, the heartbeat is quiet (200 on healthy sessions, no-op on 401); it becomes user-visible once the server side lands.use_token_refreshalready runs its own 30s loop next door; the auth-heartbeat piggybacks on the same tick shape (gloo_timers::future::TimeoutFuture) so the two loops interleave cleanly without a shared scheduler.#MAPPS-355
Two closely-coupled SPA auth-UX fixes. 1) `/auth/callback` no longer shows the red `Sign-in failed / storage: no pending OIDC flow / Try again` wall for the common transient cases. That error surfaces from `storage::take_pending()` when sessionStorage is empty - a user opening `/auth/callback` in a new tab, refreshing after MAPPS-336 stripped the query string, or letting a pending flow expire (>1 week per MAPPS-338). Nothing an operator needs to look at; the user just needs a fresh flow. The callback now detects any `storage:` prefixed FlowError, silently `window.location.replace("/login")` (which kicks `start_login`), and renders the same `Signing you in...` copy the healthy exchange path shows so there is no visible red flash. CSRF (`state mismatch`), replay (`nonce mismatch`), token-endpoint, config, and network errors keep the manual screen because a silent retry could loop on them or mask a real problem. `replace` (not `set_href`) so the browser's back button never bounces the user back to the errored callback URL after the fresh flow finishes. 2) New `use_auth_heartbeat` mounted at the App root alongside `use_token_refresh`. Every 30 seconds it fires an authed `GET /api/v1/auth/me`. On a 410 response the shared fetch layer's `handle_response` (added by MAPPS-348) calls `note_account_deleted`, which flips the `ACCOUNT_DELETED` GlobalSignal and pops the terminal `AccountDeletedOverlay` sitting at `AppLayout` root. So the hook itself discards the response - it only needs to fire the request. Without this loop the SPA only discovers a soft-delete when the user happens to touch a page that fetches. A user idle on the dashboard after their Bunyip account was deleted could stare at stale UI for the full at+jwt TTL (15 min) before any request fired. 30s cadence puts the "you were signed out" overlay in front of them fast without meaningful cost (~120 requests per hour per active tab, well under the token-refresh loop's own cadence). Heartbeat skips itself when: - no access token in the holder (unauthenticated / booting), OR - the tab is hidden (`document.visibilityState == "hidden"`), OR - `ACCOUNT_DELETED` is already set (overlay is up). `Cargo.toml`: add `VisibilityState` to the `web-sys` feature list so `tab_is_hidden()` compiles. Dependencies: - The 410 code path in `hooks::fetch` already exists (MAPPS-348 SPA merged as77aff3f). Until mokosh-server MAPPS-348 (PR #421) lands and returns 410 for tombstoned users, the heartbeat is quiet (200 on healthy sessions, no-op on 401); it becomes user-visible once the server side lands. - `use_token_refresh` already runs its own 30s loop next door; the auth-heartbeat piggybacks on the same tick shape (`gloo_timers::future::TimeoutFuture`) so the two loops interleave cleanly without a shared scheduler. #MAPPS-355