AuthProvider.jsx 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. "use client";
  2. import React from "react";
  3. import { useRouter } from "next/navigation";
  4. import { getMe } from "@/lib/frontend/apiClient";
  5. import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
  6. import { AuthProvider as AuthContextProvider } from "@/components/auth/authContext";
  7. /**
  8. * AuthProvider (RHL-020)
  9. *
  10. * Responsibilities:
  11. * - Run a session check via GET /api/auth/me.
  12. * - Store the result in AuthContext.
  13. * - Redirect to /login when unauthenticated (reason=expired, next=current URL).
  14. *
  15. * Important UX improvement:
  16. * - We no longer render full-screen "solo spinners" here.
  17. * - The AppShell stays visible and AuthGate renders the loading/error UI inside main content.
  18. *
  19. * Important performance/UX improvement:
  20. * - We do NOT re-check the session on every route change.
  21. * - The session check runs:
  22. * - once on mount
  23. * - and again only when the user hits "retry"
  24. *
  25. * @param {{ children: React.ReactNode }} props
  26. */
  27. export default function AuthProvider({ children }) {
  28. const router = useRouter();
  29. // Prevent double redirects in quick re-renders.
  30. const didRedirectRef = React.useRef(false);
  31. // Auth state exposed via context.
  32. const [auth, setAuth] = React.useState({
  33. status: "loading",
  34. user: null,
  35. error: null,
  36. });
  37. // Retry tick triggers a refetch without tying auth checks to route changes.
  38. const [retryTick, setRetryTick] = React.useState(0);
  39. const retry = React.useCallback(() => {
  40. setRetryTick((n) => n + 1);
  41. }, []);
  42. React.useEffect(() => {
  43. let cancelled = false;
  44. async function runSessionCheck() {
  45. setAuth({ status: "loading", user: null, error: null });
  46. try {
  47. const res = await getMe();
  48. if (cancelled) return;
  49. if (res?.user) {
  50. // Authenticated session.
  51. didRedirectRef.current = false;
  52. setAuth({ status: "authenticated", user: res.user, error: null });
  53. return;
  54. }
  55. // Unauthenticated session (frontend-friendly endpoint returns 200 + user:null).
  56. setAuth({ status: "unauthenticated", user: null, error: null });
  57. if (!didRedirectRef.current) {
  58. didRedirectRef.current = true;
  59. // Preserve the current URL as "next" so the user returns to the same page after login.
  60. const next =
  61. typeof window !== "undefined"
  62. ? `${window.location.pathname}${window.location.search}`
  63. : "/";
  64. const loginUrl = buildLoginUrl({
  65. reason: LOGIN_REASONS.EXPIRED,
  66. next,
  67. });
  68. router.replace(loginUrl);
  69. }
  70. } catch (err) {
  71. if (cancelled) return;
  72. setAuth({
  73. status: "error",
  74. user: null,
  75. error: "Sitzung konnte nicht geprüft werden. Bitte erneut versuchen.",
  76. });
  77. }
  78. }
  79. runSessionCheck();
  80. return () => {
  81. cancelled = true;
  82. };
  83. }, [router, retryTick]);
  84. return (
  85. <AuthContextProvider value={{ ...auth, retry }}>
  86. {children}
  87. </AuthContextProvider>
  88. );
  89. }