"use client" import { useCallback, useEffect, useState, type ReactNode } from "react" import { usePathname, useRouter } from "next/navigation" import { useAuthStore, AUTH_STORAGE_KEY, LEGACY_AUTH_KEYS } from "@/lib/api/auth-store" import { isOidcConfigured } from "@/lib/auth/oidc-config" import { fetchSession, applySessionToStore, type SessionPayload, } from "@/lib/auth/session-sync" import { isSessionExpired, useSessionGuardStore, } from "@/lib/auth/session-guard-store" import { isAuthPublicPath } from "@/lib/auth/public-paths" const REFRESH_LEAD_MS = 5 * 60 * 1000 const REFRESH_CHECK_MS = 60 * 1000 function isPublicPath(pathname: string) { return isAuthPublicPath(pathname) } export function AuthProvider({ children }: { children: ReactNode }) { const pathname = usePathname() const router = useRouter() const logout = useAuthStore((s) => s.logout) const isAuthenticated = useAuthStore((s) => s.isAuthenticated) const [ready, setReady] = useState(() => !isOidcConfigured()) const applySession = useCallback( (data: SessionPayload) => applySessionToStore(data), [] ) const syncSession = useCallback(async () => { const data = await fetchSession() if (data && applySession(data)) return true logout() return false }, [applySession, logout]) useEffect(() => { let cancelled = false async function bootstrap() { if (!isOidcConfigured()) { setReady(true) return } const data = await fetchSession() if (cancelled) return if (data && applySession(data)) { setReady(true) return } const hadMemoryAuth = useAuthStore.getState().isAuthenticated() logout() if (hadMemoryAuth && !isPublicPath(pathname) && !isSessionExpired()) { useSessionGuardStore.getState().setExpired() } setReady(true) } if (!useAuthStore.persist.hasHydrated()) { const unsubHydrate = useAuthStore.persist.onFinishHydration(() => { void bootstrap() }) return () => { cancelled = true unsubHydrate() } } void bootstrap() return () => { cancelled = true } }, [applySession, logout, pathname]) useEffect(() => { if (!ready || !isOidcConfigured()) return const interval = setInterval(() => { const { accessToken, expiresAt } = useAuthStore.getState() if (!accessToken || !expiresAt) return if (Date.now() >= expiresAt - REFRESH_LEAD_MS) { void syncSession() } }, REFRESH_CHECK_MS) return () => clearInterval(interval) }, [ready, syncSession]) useEffect(() => { if (!ready || !isOidcConfigured()) return if (isPublicPath(pathname)) return if (isAuthenticated()) return let cancelled = false void syncSession().then((ok) => { if (cancelled || ok) return if (useSessionGuardStore.getState().status === "expired") return const returnTo = encodeURIComponent(pathname) router.replace(`/login?returnTo=${returnTo}`) }) return () => { cancelled = true } }, [ready, pathname, isAuthenticated, router, syncSession]) return <>{children} } export function useAuthLogout() { const logout = useAuthStore((s) => s.logout) const router = useRouter() return async () => { await fetch("/api/auth/logout", { method: "POST", credentials: "include" }) logout() if (typeof window !== "undefined") { localStorage.removeItem(AUTH_STORAGE_KEY) for (const legacy of LEGACY_AUTH_KEYS) { localStorage.removeItem(legacy) } } router.replace("/login") } }