Files
agent-desk-ng/web/components/session-provider.tsx
mono 2f7a491666 feat: harden support, localize knowledge and add operator availability [skip ci]
Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.

Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.

Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.

Expected source tree: 0a4a7528e9.
2026-10-16 15:21:42 +00:00

151 lines
4.9 KiB
TypeScript

// Modified in the independent hardening fork; see docs/hardening/README.md.
// Modified for AgentDeskFixes: invalidate stale profile/sign-out work and clear private account state.
"use client"
import { Fragment, createContext, useCallback, useContext, useEffect, useRef, useState, type ReactNode } from "react"
import { fetchProfile, logout } from "@/lib/api/auth"
import {
AUTH_SESSION_CHANGED_EVENT,
AUTH_SESSION_EXPIRED_EVENT,
clearSession,
readSession,
writeSession,
type AuthSession,
} from "@/lib/auth"
import { createSessionRequestGuard } from "@/lib/session-request-guard"
import { resetAgentConversations } from "@/lib/stores/agent-conversations"
type SessionContextValue = {
session: AuthSession | null
ready: boolean
signingOut: boolean
refreshSession: () => Promise<void>
signOut: () => Promise<void>
}
const SessionContext = createContext<SessionContextValue | null>(null)
function accountKey(session: AuthSession | null) {
return session ? `${session.user.userType}:${session.user.id}` : "anonymous"
}
export function SessionProvider({ children }: { children: ReactNode }) {
const [session, setSession] = useState<AuthSession | null>(null)
const [ready, setReady] = useState(false)
const [signingOut, setSigningOut] = useState(false)
const [requestGuard] = useState(() => createSessionRequestGuard(readSession))
const identityRef = useRef<string | null>(null)
const signOutPendingRef = useRef(false)
const signOutGenerationRef = useRef(0)
const applySession = useCallback((next: AuthSession | null) => {
const nextIdentity = accountKey(next)
if (identityRef.current !== nextIdentity) {
resetAgentConversations()
identityRef.current = nextIdentity
}
setSession(next)
setReady(true)
}, [])
const refreshSession = useCallback(async () => {
const stored = readSession()
if (!stored) {
requestGuard.invalidate()
applySession(null)
return
}
const isCurrent = requestGuard.begin(stored)
try {
const profile = await fetchProfile()
if (!isCurrent()) return
const nextSession: AuthSession = {
...stored,
user: profile.user,
permissions: profile.permissions,
roles: profile.roles,
accessToken: profile.accessToken || stored.accessToken,
expiresAt: profile.expiresAt || stored.expiresAt,
}
applySession(nextSession)
writeSession(nextSession)
} catch (error) {
if (!isCurrent()) return
const errorCode = (error as Error & { errorCode?: number }).errorCode
if (errorCode === 3000 || errorCode === 3002) {
clearSession()
applySession(null)
} else {
applySession(stored)
}
} finally {
if (isCurrent()) setReady(true)
}
}, [applySession, requestGuard])
const signOut = useCallback(async () => {
if (signOutPendingRef.current) return
signOutPendingRef.current = true
const generation = ++signOutGenerationRef.current
setSigningOut(true)
requestGuard.invalidate()
// Start the request with the old token, then immediately remove local access.
const pending = logout()
clearSession()
applySession(null)
try {
await pending
} catch {
// Local sign-out already completed; a late error cannot clear a new login.
} finally {
if (generation === signOutGenerationRef.current) {
signOutPendingRef.current = false
setSigningOut(false)
}
}
}, [applySession, requestGuard])
useEffect(() => {
const syncSession = () => {
requestGuard.invalidate()
const current = readSession()
if (current && signOutPendingRef.current) {
signOutGenerationRef.current += 1
signOutPendingRef.current = false
setSigningOut(false)
}
applySession(current)
}
const syncOtherTab = (event: StorageEvent) => {
if (event.key === "agent-desk-session" || event.key === null) syncSession()
}
window.addEventListener(AUTH_SESSION_CHANGED_EVENT, syncSession)
window.addEventListener(AUTH_SESSION_EXPIRED_EVENT, syncSession)
window.addEventListener("storage", syncOtherTab)
applySession(readSession())
void refreshSession()
return () => {
requestGuard.invalidate()
signOutGenerationRef.current += 1
window.removeEventListener(AUTH_SESSION_CHANGED_EVENT, syncSession)
window.removeEventListener(AUTH_SESSION_EXPIRED_EVENT, syncSession)
window.removeEventListener("storage", syncOtherTab)
}
}, [applySession, refreshSession, requestGuard])
return (
<SessionContext.Provider value={{ session, ready, signingOut, refreshSession, signOut }}>
<Fragment key={accountKey(session)}>{children}</Fragment>
</SessionContext.Provider>
)
}
export function useSession() {
const context = useContext(SessionContext)
if (!context) throw new Error("useSession must be used within SessionProvider")
return context
}