"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" import { useNativeRuntime } from "@/lib/platform" import { ensureNativeAccessToken, loadNativeSession, } from "@/lib/auth/native-session" import { nativeLogout } from "@/lib/auth/native-auth" import { getRuntimeConfig } from "@/lib/runtime-config" import { hydrateNativeRuntimeConfig } from "@/lib/runtime-config/native" 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 native = useNativeRuntime() const syncSession = useCallback(async () => { if (native) { const token = await ensureNativeAccessToken() if (token) return true logout() return false } const data = await fetchSession() if (data && applySession(data)) return true logout() return false }, [applySession, logout, native]) useEffect(() => { let cancelled = false async function bootstrap() { if (!isOidcConfigured()) { setReady(true) return } if (native) { // Native: session lives in the OS secure store; no server selected yet // means the user must run the server picker first. await hydrateNativeRuntimeConfig() if (cancelled) return if (getRuntimeConfig()) { const ok = await loadNativeSession() if (!cancelled && ok) { await ensureNativeAccessToken() } } if (!cancelled) 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, native]) 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 // NativeAuthGate shows picker/login inline — avoid fighting redirects. if (native) 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() const native = useNativeRuntime() return async () => { if (native) { // Unregister the push device token before dropping the session. try { const { unregisterPushOnLogout } = await import("@/lib/native/push") await unregisterPushOnLogout() } catch { /* best effort */ } await nativeLogout() } else { 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") } }