BranchGuard.jsx 2.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. "use client";
  2. import React from "react";
  3. import { useAuth } from "@/components/auth/authContext";
  4. import { getBranches } from "@/lib/frontend/apiClient";
  5. import {
  6. decideBranchUi,
  7. BRANCH_UI_DECISION,
  8. } from "@/lib/frontend/rbac/branchUiDecision";
  9. import ForbiddenView from "@/components/system/ForbiddenView";
  10. import NotFoundView from "@/components/system/NotFoundView";
  11. const BRANCH_LIST_STATE = Object.freeze({
  12. IDLE: "idle",
  13. LOADING: "loading",
  14. READY: "ready",
  15. ERROR: "error",
  16. });
  17. /**
  18. * BranchGuard
  19. *
  20. * UX improvement:
  21. * - We do NOT block rendering while admin/dev branch list is loading.
  22. * - Existence validation is applied once the list is READY.
  23. * - While LOADING/ERROR we fail open to avoid "solo spinner" screens.
  24. *
  25. * Security note:
  26. * - Backend RBAC remains authoritative.
  27. * - Branch users are enforced immediately (no existence check needed).
  28. */
  29. export default function BranchGuard({ branch, children }) {
  30. const { status, user } = useAuth();
  31. const isAuthenticated = status === "authenticated" && user;
  32. const needsExistenceCheck =
  33. isAuthenticated && (user.role === "admin" || user.role === "dev");
  34. const [branchList, setBranchList] = React.useState({
  35. status: BRANCH_LIST_STATE.IDLE,
  36. branches: null,
  37. });
  38. React.useEffect(() => {
  39. if (!needsExistenceCheck) return;
  40. let cancelled = false;
  41. setBranchList({ status: BRANCH_LIST_STATE.LOADING, branches: null });
  42. (async () => {
  43. try {
  44. const res = await getBranches();
  45. if (cancelled) return;
  46. const branches = Array.isArray(res?.branches) ? res.branches : [];
  47. setBranchList({ status: BRANCH_LIST_STATE.READY, branches });
  48. } catch (err) {
  49. if (cancelled) return;
  50. // Fail open: do not block navigation if validation fails.
  51. console.error("[BranchGuard] getBranches failed:", err);
  52. setBranchList({ status: BRANCH_LIST_STATE.ERROR, branches: null });
  53. }
  54. })();
  55. return () => {
  56. cancelled = true;
  57. };
  58. }, [needsExistenceCheck, user?.userId]);
  59. if (!isAuthenticated) return children;
  60. // Only apply existence validation when the list is READY.
  61. const allowedBranches =
  62. branchList.status === BRANCH_LIST_STATE.READY ? branchList.branches : null;
  63. const decision = decideBranchUi({
  64. user,
  65. branch,
  66. allowedBranches,
  67. });
  68. if (decision === BRANCH_UI_DECISION.FORBIDDEN) {
  69. return <ForbiddenView attemptedBranch={branch} />;
  70. }
  71. if (decision === BRANCH_UI_DECISION.NOT_FOUND) {
  72. return <NotFoundView />;
  73. }
  74. return children;
  75. }