AuthProvider.jsx 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  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 / RHL-032)
  9. *
  10. * RHL-032 improvement:
  11. * - When revalidating while already authenticated, we keep:
  12. * - status="authenticated"
  13. * - user != null
  14. * - and only flip `isValidating=true`
  15. *
  16. * This prevents AuthGate from rendering the "Sitzung wird geprüft" content card
  17. * during fast navigations (branch switch), eliminating flicker.
  18. */
  19. export default function AuthProvider({ children }) {
  20. const router = useRouter();
  21. // Prevent double redirects in quick re-renders.
  22. const didRedirectRef = React.useRef(false);
  23. // Auth state exposed via context.
  24. const [auth, setAuth] = React.useState({
  25. status: "loading",
  26. user: null,
  27. error: null,
  28. isValidating: false,
  29. });
  30. // Retry tick triggers a refetch without tying auth checks to route changes.
  31. const [retryTick, setRetryTick] = React.useState(0);
  32. const retry = React.useCallback(() => {
  33. setRetryTick((n) => n + 1);
  34. }, []);
  35. React.useEffect(() => {
  36. let cancelled = false;
  37. async function runSessionCheck() {
  38. // If we already have a valid authenticated session, revalidate in the background:
  39. // keep content stable and only set isValidating=true.
  40. setAuth((prev) => {
  41. const hasUser = prev.status === "authenticated" && prev.user;
  42. if (hasUser) {
  43. return { ...prev, error: null, isValidating: true };
  44. }
  45. return {
  46. status: "loading",
  47. user: null,
  48. error: null,
  49. isValidating: false,
  50. };
  51. });
  52. try {
  53. const res = await getMe();
  54. if (cancelled) return;
  55. if (res?.user) {
  56. didRedirectRef.current = false;
  57. setAuth({
  58. status: "authenticated",
  59. user: res.user,
  60. error: null,
  61. isValidating: false,
  62. });
  63. return;
  64. }
  65. // Unauthenticated session (frontend-friendly endpoint returns 200 + user:null).
  66. setAuth({
  67. status: "unauthenticated",
  68. user: null,
  69. error: null,
  70. isValidating: false,
  71. });
  72. if (!didRedirectRef.current) {
  73. didRedirectRef.current = true;
  74. const next =
  75. typeof window !== "undefined"
  76. ? `${window.location.pathname}${window.location.search}`
  77. : "/";
  78. const loginUrl = buildLoginUrl({
  79. reason: LOGIN_REASONS.EXPIRED,
  80. next,
  81. });
  82. router.replace(loginUrl);
  83. }
  84. } catch (err) {
  85. if (cancelled) return;
  86. // If we were already authenticated, fail open:
  87. // keep the last known user/session and just end validating.
  88. setAuth((prev) => {
  89. const hasUser = prev.status === "authenticated" && prev.user;
  90. if (hasUser) {
  91. return { ...prev, isValidating: false };
  92. }
  93. // Initial load failed -> show error state (AuthGate handles it).
  94. return {
  95. status: "error",
  96. user: null,
  97. error:
  98. "Sitzung konnte nicht geprüft werden. Bitte erneut versuchen.",
  99. isValidating: false,
  100. };
  101. });
  102. }
  103. }
  104. runSessionCheck();
  105. return () => {
  106. cancelled = true;
  107. };
  108. }, [router, retryTick]);
  109. return (
  110. <AuthContextProvider value={{ ...auth, retry }}>
  111. {children}
  112. </AuthContextProvider>
  113. );
  114. }