"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 type { PlatformUser } from "@/lib/auth/jwt-claims" const PUBLIC_PREFIXES = ["/login", "/auth/", "/api/auth/"] const REFRESH_LEAD_MS = 5 * 60 * 1000 const REFRESH_CHECK_MS = 60 * 1000 function isPublicPath(pathname: string) { if (pathname.startsWith("/drive/s/")) return true return PUBLIC_PREFIXES.some( (prefix) => pathname === prefix || pathname.startsWith(prefix) ) } type SessionPayload = { authenticated?: boolean accessToken?: string refreshToken?: string | null expiresAt?: number user?: PlatformUser | null } async function fetchSession(): Promise { try { const res = await fetch("/api/auth/session", { credentials: "include" }) if (!res.ok) return null return (await res.json()) as SessionPayload } catch { return null } } function canTrustPersistedAuth() { return useAuthStore.persist.hasHydrated() && useAuthStore.getState().isAuthenticated() } export function AuthProvider({ children }: { children: ReactNode }) { const pathname = usePathname() const router = useRouter() const login = useAuthStore((s) => s.login) const logout = useAuthStore((s) => s.logout) const isAuthenticated = useAuthStore((s) => s.isAuthenticated) const [ready, setReady] = useState( () => !isOidcConfigured() || canTrustPersistedAuth() ) const applySession = useCallback( (data: SessionPayload) => { if (data.authenticated && data.accessToken && data.expiresAt) { login( data.accessToken, data.refreshToken ?? "", data.expiresAt, data.user ?? null ) return true } return false }, [login] ) 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 } if (canTrustPersistedAuth()) { setReady(true) } const data = await fetchSession() if (cancelled) return if (data && applySession(data)) { setReady(true) return } if (data?.authenticated === false || !canTrustPersistedAuth()) { logout() } setReady(true) } if (!useAuthStore.persist.hasHydrated()) { const unsubHydrate = useAuthStore.persist.onFinishHydration(() => { if (useAuthStore.getState().isAuthenticated()) { setReady(true) } }) void bootstrap() return () => { cancelled = true unsubHydrate() } } void bootstrap() return () => { cancelled = true } }, [applySession, logout]) 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 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") } }