BranchGuard.jsx 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. "use client";
  2. import React from "react";
  3. import { Loader2 } from "lucide-react";
  4. import { useAuth } from "@/components/auth/authContext";
  5. import { getBranches } from "@/lib/frontend/apiClient";
  6. import {
  7. decideBranchUi,
  8. BRANCH_UI_DECISION,
  9. } from "@/lib/frontend/rbac/branchUiDecision";
  10. import ForbiddenView from "@/components/system/ForbiddenView";
  11. import NotFoundView from "@/components/system/NotFoundView";
  12. const BRANCH_LIST_STATE = Object.freeze({
  13. IDLE: "idle",
  14. LOADING: "loading",
  15. READY: "ready",
  16. ERROR: "error",
  17. });
  18. function BranchValidationLoading() {
  19. return (
  20. <div className="flex items-center gap-3 text-sm text-muted-foreground">
  21. <Loader2 className="h-4 w-4 animate-spin" />
  22. <span>Validating branch...</span>
  23. </div>
  24. );
  25. }
  26. export default function BranchGuard({ branch, children }) {
  27. const { status, user } = useAuth();
  28. const isAuthenticated = status === "authenticated" && user;
  29. const needsExistenceCheck =
  30. isAuthenticated && (user.role === "admin" || user.role === "dev");
  31. const [branchList, setBranchList] = React.useState({
  32. status: BRANCH_LIST_STATE.IDLE,
  33. branches: null,
  34. });
  35. React.useEffect(() => {
  36. if (!needsExistenceCheck) return;
  37. let cancelled = false;
  38. setBranchList({ status: BRANCH_LIST_STATE.LOADING, branches: null });
  39. (async () => {
  40. try {
  41. const res = await getBranches();
  42. if (cancelled) return;
  43. const branches = Array.isArray(res?.branches) ? res.branches : [];
  44. setBranchList({ status: BRANCH_LIST_STATE.READY, branches });
  45. } catch (err) {
  46. if (cancelled) return;
  47. // Fail open: do not block navigation if validation fails.
  48. console.error("[BranchGuard] getBranches failed:", err);
  49. setBranchList({ status: BRANCH_LIST_STATE.ERROR, branches: null });
  50. }
  51. })();
  52. return () => {
  53. cancelled = true;
  54. };
  55. // IMPORTANT:
  56. // - depend only on user identity + needsExistenceCheck
  57. // - do NOT depend on branchList.status (would cancel itself when setting LOADING)
  58. }, [needsExistenceCheck, user?.userId]);
  59. if (!isAuthenticated) {
  60. return children;
  61. }
  62. if (needsExistenceCheck && branchList.status === BRANCH_LIST_STATE.LOADING) {
  63. return <BranchValidationLoading />;
  64. }
  65. const allowedBranches =
  66. branchList.status === BRANCH_LIST_STATE.READY ? branchList.branches : null;
  67. const decision = decideBranchUi({
  68. user,
  69. branch,
  70. allowedBranches,
  71. });
  72. if (decision === BRANCH_UI_DECISION.FORBIDDEN) {
  73. return <ForbiddenView attemptedBranch={branch} />;
  74. }
  75. if (decision === BRANCH_UI_DECISION.NOT_FOUND) {
  76. return <NotFoundView />;
  77. }
  78. return children;
  79. }