"use client" import { useCallback, useEffect, useState, type ReactNode } from "react" import { usePathname, useRouter } from "next/navigation" import { useQueryClient } from "@tanstack/react-query" import { useAuthStore } from "@/lib/api/auth-store" import { isOidcConfigured } from "@/lib/auth/oidc-config" import { clearClientAuthState } from "@/lib/auth/clear-client-auth-state" import { fetchSession, applySessionToStore, type SessionPayload, } from "@/lib/auth/session-sync" import { useSessionGuardStore, } from "@/lib/auth/session-guard-store" import { isAuthPublicPath } from "@/lib/auth/public-paths" import { getOidcEndSessionPath, POST_LOGOUT_PATH } from "@/lib/auth/oidc-config" 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 authenticated = 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 if (useAuthStore.getState().isAuthenticated()) 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 } if (useAuthStore.getState().isAuthenticated()) { setReady(true) return } logout() setReady(true) } if (!useAuthStore.persist.hasHydrated()) { const unsubHydrate = useAuthStore.persist.onFinishHydration(() => { void bootstrap() }) return () => { cancelled = true unsubHydrate() } } void bootstrap() return () => { cancelled = true } }, [applySession, logout, 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 (authenticated) return // NativeAuthGate shows picker/login inline — avoid fighting redirects. if (native) return let cancelled = false void syncSession().then((ok) => { if (cancelled || ok) return if (useAuthStore.getState().isAuthenticated()) return if (useSessionGuardStore.getState().status === "expired") return const returnTo = encodeURIComponent(pathname) router.replace(`/login?returnTo=${returnTo}`) }) return () => { cancelled = true } }, [ready, pathname, authenticated, router, syncSession, native]) return <>{children} } export function useAuthLogout() { const router = useRouter() const queryClient = useQueryClient() 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() clearClientAuthState(queryClient) router.replace(POST_LOGOUT_PATH) return } await fetch("/api/auth/logout", { method: "POST", credentials: "include" }) clearClientAuthState(queryClient) if (typeof window !== "undefined") { // Clear Ultimail session then terminate Authentik SSO (provider invalidation flow). window.location.assign(getOidcEndSessionPath()) } } }