import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import type { User } from 'oidc-client-ts' import { isOidcConfigured, userManager } from './oidcClient' // Cookie management helper functions function setCookieFromUser(user: User) { if (!user?.access_token) return const isProduction = window.location.hostname !== 'localhost' const domain = isProduction ? '.orfl.xyz' : undefined const secure = isProduction const sameSite = isProduction ? 'None' : 'Lax' // Set cookie with JWT token from user const cookieValue = `fa_session=${user.access_token}; path=/; ${secure ? 'secure; ' : ''}samesite=${sameSite}${domain ? `; domain=${domain}` : ''}` document.cookie = cookieValue } function clearFaSessionCookie() { const isProduction = window.location.hostname !== 'localhost' const domain = isProduction ? '.orfl.xyz' : undefined // Clear cookie by setting expiration date in the past const cookieValue = `fa_session=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT${domain ? `; domain=${domain}` : ''}` document.cookie = cookieValue } type AuthContextValue = { user: User | null isLoading: boolean isConfigured: boolean login: () => Promise logout: () => Promise } const AuthContext = createContext(undefined) export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null) const [isLoading, setIsLoading] = useState(!!userManager) const callbackHandledRef = useRef(false) useEffect(() => { if (!userManager) { return } let cancelled = false userManager .getUser() .then((loadedUser) => { if (!cancelled) { setUser(loadedUser ?? null) if (loadedUser) { setCookieFromUser(loadedUser) } } }) .finally(() => { if (!cancelled) setIsLoading(false) }) return () => { cancelled = true } }, []) useEffect(() => { const manager = userManager if (!manager) return const handleLoaded = (nextUser: User) => { setUser(nextUser) setCookieFromUser(nextUser) } const handleUnloaded = () => { setUser(null) clearFaSessionCookie() } manager.events.addUserLoaded(handleLoaded) manager.events.addUserUnloaded(handleUnloaded) manager.events.addUserSignedOut(handleUnloaded) return () => { manager.events.removeUserLoaded(handleLoaded) manager.events.removeUserUnloaded(handleUnloaded) manager.events.removeUserSignedOut(handleUnloaded) } }, []) useEffect(() => { const manager = userManager if (!manager || callbackHandledRef.current) return const url = new URL(window.location.href) const hasAuthParams = url.searchParams.has('code') || url.searchParams.has('id_token') || url.searchParams.has('error') if (!hasAuthParams) return callbackHandledRef.current = true manager .signinRedirectCallback() .then((nextUser) => { setUser(nextUser ?? null) if (nextUser) { setCookieFromUser(nextUser) } }) .catch((error) => { console.error('Failed to complete sign-in redirect', error) }) .finally(() => { const cleanUrl = `${url.origin}${url.pathname}` window.history.replaceState({}, document.title, cleanUrl) }) }, []) const login = useCallback(async () => { const manager = userManager if (!manager) { console.warn('OIDC is not configured; set VITE_OIDC_* environment variables.') return } await manager.signinRedirect() }, []) const logout = useCallback(async () => { const manager = userManager if (!manager) { console.warn('OIDC is not configured; set VITE_OIDC_* environment variables.') return } try { await manager.signoutRedirect() } catch (error) { console.error('Failed to sign out via redirect, clearing local session instead.', error) await manager.removeUser() setUser(null) clearFaSessionCookie() } }, []) const value = useMemo( () => ({ user, isLoading, isConfigured: isOidcConfigured, login, logout, }), [isLoading, login, logout, user], ) return {children} } // eslint-disable-next-line react-refresh/only-export-components export function useAuth() { const context = useContext(AuthContext) if (!context) { throw new Error('useAuth must be used within an AuthProvider') } return context }