Fix WS offline on first login: sync token between api.ts and AuthContext

Two root causes fixed:
1. api.ts saveToken() updated only localStorage, not React state — WS kept
   reconnecting with the expired token. Now dispatches hotelsync:token-updated
   event; AuthContext listens and updates session state, triggering WS reconnect
   with the fresh token.
2. AuthContext mount effect could race with login(): if refresh failed while
   login() was concurrently setting a new token, catch() called setSession(null)
   and wiped the fresh session. Fixed with functional setSession updater that
   checks current state before clearing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-18 17:37:36 +03:00
parent cbd85c5c7e
commit bc12f8aabc
2 changed files with 34 additions and 7 deletions

View File

@@ -39,20 +39,45 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
.then(r => r.ok ? r.json() : null) .then(r => r.ok ? r.json() : null)
.then((data: { access_token?: string } | null) => { .then((data: { access_token?: string } | null) => {
if (data?.access_token) { if (data?.access_token) {
const s: AuthSession = { ...session, token: data.access_token } const tok = data.access_token
setSession(s) setSession(cur => {
if (!cur) return null
const s: AuthSession = { ...cur, token: tok }
localStorage.setItem('hotelsync-session', JSON.stringify(s)) localStorage.setItem('hotelsync-session', JSON.stringify(s))
return s
})
} else { } else {
setSession(null) // Guard: if login() set a fresh token while refresh was in-flight — keep it
setSession(cur => {
if (cur?.token) return cur
localStorage.removeItem('hotelsync-session') localStorage.removeItem('hotelsync-session')
return null
})
} }
}) })
.catch(() => { .catch(() => {
setSession(null) // Guard: same race-condition protection
setSession(cur => {
if (cur?.token) return cur
localStorage.removeItem('hotelsync-session') localStorage.removeItem('hotelsync-session')
return null
})
}) })
}, []) // eslint-disable-line react-hooks/exhaustive-deps }, []) // eslint-disable-line react-hooks/exhaustive-deps
// Keep React state in sync when api.ts refreshes the token (e.g. after 401 auto-refresh)
useEffect(() => {
const handler = (e: Event) => {
const { token } = (e as CustomEvent<{ token: string }>).detail
setSession(cur => {
if (!cur) return cur
return { ...cur, token }
})
}
window.addEventListener('hotelsync:token-updated', handler)
return () => window.removeEventListener('hotelsync:token-updated', handler)
}, [])
const login = async (email: string, password: string): Promise<User | null> => { const login = async (email: string, password: string): Promise<User | null> => {
try { try {
const { access_token, user } = await api.auth.login(email, password) const { access_token, user } = await api.auth.login(email, password)

View File

@@ -30,6 +30,8 @@ function saveToken(token: string) {
const parsed = JSON.parse(s) as Record<string, unknown> const parsed = JSON.parse(s) as Record<string, unknown>
parsed.token = token parsed.token = token
localStorage.setItem('hotelsync-session', JSON.stringify(parsed)) localStorage.setItem('hotelsync-session', JSON.stringify(parsed))
// Notify AuthContext so React state stays in sync (WS reconnects with fresh token)
window.dispatchEvent(new CustomEvent('hotelsync:token-updated', { detail: { token } }))
} catch { } catch {
// ignore // ignore
} }