ThatAIguyA world of ideas

THE CONSCIOUSNESS ATLAS
Read. Experiment. Follow the evidence.

ConsciousnessLearn · Explore · Joseph Anady

Janady13/missionmatch / frontend/src/lib/AuthContext.tsx

Source identity, revision, and recovered contents.

GitHub sourceSource text recovered

Source identity

FieldRecorded value
Source typeGitHub source
Original recordJanady13/missionmatch / frontend/src/lib/AuthContext.tsx
DateNot specified in source
RepositoryJanady13/missionmatch
Revision720d47fae476b0d38477ae4401697d372b1ea2bc
Branchfeature/opportunity-to-outcome
Path or archive memberfrontend/src/lib/AuthContext.tsx
Original SHA-256198d2c866736a2818b3d4a70a41cd2526daf2d0a50e5136e7cc6bdacbd74d015
Git blob SHAbd9ff6dfa496a6a3ea2dd90788ca7f13e5c11312
Reading-copy SHA-256198d2c866736a2818b3d4a70a41cd2526daf2d0a50e5136e7cc6bdacbd74d015

Original source ↗

Recovered source transcript

import { useCallback, useEffect, useState, type ReactNode } from 'react' import { useQueryClient } from '@tanstack/react-query' import { setUnauthorizedHandler } from './api' import { authApi, type SessionPrincipal } from './auth' import { AuthContext } from './useAuth' // AuthProvider holds the validated session principal. On mount it asks GET /api/auth/session: a 200 // means an active MFA satisfied session (show the app), a 403 means none (show the login). refresh is // called after a successful login or MFA step to flip the gate. The context object and the useAuth // hook live in useAuth.ts so this file exports only a component (React Fast Refresh boundary). export function AuthProvider({ children }: { children: ReactNode }) { const [principal, setPrincipal] = useState<SessionPrincipal | null>(null) const [loading, setLoading] = useState(true) const qc = useQueryClient() // The query cache is keyed by resource, never by user or organization: ['candidates'], ['people'], // ['org','branding']. That is fine while one person is signed in and a cross tenant disclosure the // moment a second one is, because the QueryClient is a module singleton that outlives the session and // carries a 30 second staleTime. Signing out and signing in as someone else in the same tab rendered // the PREVIOUS account's candidates, client list and org branding from cache, and inside the stale // window did not even refetch. // // Clearing on BOTH boundaries is deliberate. On logout, so nothing survives for the next person at this // browser. On login, because a session can also change without a logout: an expired cookie, a session // revoked by an admin, or a different account authenticating against a tab that still holds the old // cache. Clearing only on logout leaves the path that skips it wide open. // // clear() rather than invalidateQueries(): invalidate marks data stale but KEEPS it, so it is still // returned to the next render while the refetch is in flight. For tenant data the old rows must be // gone, not merely refreshed. const clearTenantCache = useCallback(() => { qc.clear() qc.removeQueries() }, [qc]) const refresh = useCallback(async () => { try { const next = await authApi.session() setPrincipal((prev) => { // Only wipe when the identity actually changes. A routine refresh for the same user must not // discard a warm cache, or every MFA step and every window focus would refetch the whole app. if (prev && (prev.user_id !== next.user_id || prev.org_id !== next.org_id)) { clearTenantCache() } return next }) } catch { // Losing the session counts as an identity change. Without this the expiry path drops the principal // but keeps the cache, so the NEXT sign in sees a null previous identity, skips the comparison // above, and inherits the departed user's data anyway. That is the same leak reached by a different // route, and it is the likelier one in practice: sessions expire far more often than people log out. setPrincipal((prev) => { if (prev) clearTenantCache() return null }) } finally { setLoading(false) } }, [clearTenantCache]) const logout = useCallback(async () => { try { await authApi.logout() } catch { // The cookie is cleared server side on success; on any error we still drop local state. } setPrincipal(null) // After local state, so nothing can re render against a cleared cache while still believing it is // signed in and repopulate it from a query that has not yet been told the session is gone. clearTenantCache() }, [clearTenantCache]) useEffect(() => { void refresh() }, [refresh]) // THE SESSION CAN END WHILE THE PAGE IS STILL OPEN, and until this existed nothing noticed. // // Sessions idle out after fifteen minutes. A tab left open past that had every query start failing with // the server's "no session", which each screen rendered as a failure to load DATA, while this provider // went on holding a principal and the header went on showing the user signed in. The Command Center said // "Could not load mission readiness, no session" beside a Try again button that could never work. // // Dropping the principal here is what makes the app fall back to the sign in screen, which is the only // thing that actually helps. The tenant cache is cleared on the same path as logout, for the same reason: // the next person to authenticate in this tab must not be served the previous session's rows. useEffect(() => { setUnauthorizedHandler(() => { setPrincipal(null) clearTenantCache() }) return () => setUnauthorizedHandler(null) }, [clearTenantCache]) return <AuthContext.Provider value={{ principal, loading, refresh, logout }}>{children}</AuthContext.Provider> }

Search equations, builds, manuscript text, and recovered sources.