fix: harden identity, live access, drafts and setup (review only)
Source-reviewed hardening changes and regression tests. UNBUILT / UNTESTED: not a deployable release. Go formatting, compilation, automated regression tests, full frontend validation and staging acceptance remain required. See docs/hardening/VALIDATION.md. Main remains the original clean snapshot; no upstream Git history imported.
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
// 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 { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from "react"
|
||||
import { Fragment, createContext, useCallback, useContext, useEffect, useRef, useState, type ReactNode } from "react"
|
||||
|
||||
import { fetchProfile, logout } from "@/lib/api/auth"
|
||||
import {
|
||||
@@ -11,6 +13,8 @@ import {
|
||||
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
|
||||
@@ -22,21 +26,41 @@ type SessionContextValue = {
|
||||
|
||||
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) {
|
||||
setSession(null)
|
||||
setReady(true)
|
||||
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,
|
||||
@@ -45,60 +69,76 @@ export function SessionProvider({ children }: { children: ReactNode }) {
|
||||
accessToken: profile.accessToken || stored.accessToken,
|
||||
expiresAt: profile.expiresAt || stored.expiresAt,
|
||||
}
|
||||
applySession(nextSession)
|
||||
writeSession(nextSession)
|
||||
setSession(nextSession)
|
||||
} catch (error) {
|
||||
if (!isCurrent()) return
|
||||
const errorCode = (error as Error & { errorCode?: number }).errorCode
|
||||
if (errorCode === 3000 || errorCode === 3002) {
|
||||
clearSession()
|
||||
setSession(null)
|
||||
applySession(null)
|
||||
} else {
|
||||
setSession(stored)
|
||||
applySession(stored)
|
||||
}
|
||||
} finally {
|
||||
setReady(true)
|
||||
if (isCurrent()) setReady(true)
|
||||
}
|
||||
}, [])
|
||||
}, [applySession, requestGuard])
|
||||
|
||||
const signOut = useCallback(async () => {
|
||||
if (signingOut) return
|
||||
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 logout()
|
||||
await pending
|
||||
} catch {
|
||||
clearSession()
|
||||
// Local sign-out already completed; a late error cannot clear a new login.
|
||||
} finally {
|
||||
setSession(null)
|
||||
setSigningOut(false)
|
||||
if (generation === signOutGenerationRef.current) {
|
||||
signOutPendingRef.current = false
|
||||
setSigningOut(false)
|
||||
}
|
||||
}
|
||||
}, [signingOut])
|
||||
|
||||
useEffect(() => {
|
||||
void refreshSession()
|
||||
}, [refreshSession])
|
||||
}, [applySession, requestGuard])
|
||||
|
||||
useEffect(() => {
|
||||
const syncSession = () => {
|
||||
setSession(readSession())
|
||||
setReady(true)
|
||||
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") syncSession()
|
||||
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 }}>
|
||||
{children}
|
||||
<Fragment key={accountKey(session)}>{children}</Fragment>
|
||||
</SessionContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user