"use client"; import React from "react"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { Loader2 } from "lucide-react"; import { getMe } from "@/lib/frontend/apiClient"; import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect"; import { AuthProvider as AuthContextProvider } from "@/components/auth/authContext"; import { Button } from "@/components/ui/button"; import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"; /** * AuthProvider (RHL-020) * * Best-practice approach (per Next.js docs): * - We use Next.js navigation hooks (usePathname/useSearchParams/useRouter) * to build a correct `next` parameter and redirect smoothly. * - IMPORTANT: When `useSearchParams()` is used, this component MUST be rendered * under a boundary for production builds where the route can be * statically prerendered. The Suspense boundary is added in the protected layout. * * Responsibilities: * - Run session check via GET /api/auth/me. * - If authenticated: provide { status: "authenticated", user } and render children. * - If unauthenticated: redirect to /login?reason=expired&next=. * - Show a loading UI while checking. * - Show an error UI with a retry button if the check fails. * * @param {{ children: React.ReactNode }} props */ export default function AuthProvider({ children }) { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); // Create a stable string representation of search params for effect deps. const search = searchParams ? searchParams.toString() : ""; // Prevent double redirects in quick re-renders. const didRedirectRef = React.useRef(false); // Auth state exposed via context. const [auth, setAuth] = React.useState({ status: "loading", user: null, error: null, }); // Retry tick for the error UI. const [retryTick, setRetryTick] = React.useState(0); React.useEffect(() => { let cancelled = false; async function runSessionCheck() { setAuth({ status: "loading", user: null, error: null }); try { const res = await getMe(); if (cancelled) return; // /api/auth/me: // - 200 { user: null } when unauthenticated // - 200 { user: {...} } when authenticated if (res?.user) { setAuth({ status: "authenticated", user: res.user, error: null }); return; } setAuth({ status: "unauthenticated", user: null, error: null }); if (!didRedirectRef.current) { didRedirectRef.current = true; // Build `next` from current URL (path + query). const next = search ? `${pathname}?${search}` : pathname; const loginUrl = buildLoginUrl({ reason: LOGIN_REASONS.EXPIRED, next, }); // Replace history so back button won't bounce into a protected page. router.replace(loginUrl); } } catch (err) { if (cancelled) return; // Do not redirect blindly on failures (backend might be down). setAuth({ status: "error", user: null, error: "Sitzung konnte nicht geprüft werden. Bitte erneut versuchen.", }); } } runSessionCheck(); return () => { cancelled = true; }; }, [router, pathname, search, retryTick]); // ----------------------------------------------------------------------- // Render gating // ----------------------------------------------------------------------- if (auth.status === "loading") { return (
Sitzung wird geprüft…
); } if (auth.status === "error") { return (
Sitzungsprüfung fehlgeschlagen {auth.error}
); } if (auth.status === "unauthenticated") { // Redirect happens in useEffect. Show a tiny placeholder meanwhile. return (
Weiterleitung zum Login…
); } // Authenticated return {children}; }