Browse Source

RHL-021 refactor(auth): enhance BranchGuard with branch validation and loading state

Code_Uwe 1 month ago
parent
commit
addee0cf41
1 changed files with 87 additions and 20 deletions
  1. 87 20
      components/auth/BranchGuard.jsx

+ 87 - 20
components/auth/BranchGuard.jsx

@@ -1,35 +1,102 @@
 "use client";
 
 import React from "react";
+import { Loader2 } from "lucide-react";
 
 import { useAuth } from "@/components/auth/authContext";
-import ForbiddenView from "@/components/system/ForbiddenView";
+import { getBranches } from "@/lib/frontend/apiClient";
 import {
-	getBranchAccess,
-	BRANCH_ACCESS,
-} from "@/lib/frontend/rbac/branchAccess";
-
-/**
- * BranchGuard (RHL-021)
- *
- * UI-side RBAC guard for branch-based routes.
- *
- * This guard assumes that AuthProvider already handled session checks and redirects.
- * Therefore:
- * - If the auth state is not authenticated yet, we render children (AuthProvider gating).
- * - If authenticated, we enforce branch-level RBAC for role="branch".
- */
+	decideBranchUi,
+	BRANCH_UI_DECISION,
+} from "@/lib/frontend/rbac/branchUiDecision";
+
+import ForbiddenView from "@/components/system/ForbiddenView";
+import NotFoundView from "@/components/system/NotFoundView";
+
+const BRANCH_LIST_STATE = Object.freeze({
+	IDLE: "idle",
+	LOADING: "loading",
+	READY: "ready",
+	ERROR: "error",
+});
+
+function BranchValidationLoading() {
+	return (
+		<div className="flex items-center gap-3 text-sm text-muted-foreground">
+			<Loader2 className="h-4 w-4 animate-spin" />
+			<span>Validating branch...</span>
+		</div>
+	);
+}
+
 export default function BranchGuard({ branch, children }) {
 	const { status, user } = useAuth();
 
-	const access = React.useMemo(() => {
-		if (status !== "authenticated") return BRANCH_ACCESS.ALLOWED;
-		return getBranchAccess(user, branch);
-	}, [status, user, branch]);
+	const isAuthenticated = status === "authenticated" && user;
+	const needsExistenceCheck =
+		isAuthenticated && (user.role === "admin" || user.role === "dev");
+
+	const [branchList, setBranchList] = React.useState({
+		status: BRANCH_LIST_STATE.IDLE,
+		branches: null,
+	});
+
+	React.useEffect(() => {
+		if (!needsExistenceCheck) return;
+
+		let cancelled = false;
+
+		setBranchList({ status: BRANCH_LIST_STATE.LOADING, branches: null });
+
+		(async () => {
+			try {
+				const res = await getBranches();
+				if (cancelled) return;
 
-	if (access === BRANCH_ACCESS.FORBIDDEN) {
+				const branches = Array.isArray(res?.branches) ? res.branches : [];
+				setBranchList({ status: BRANCH_LIST_STATE.READY, branches });
+			} catch (err) {
+				if (cancelled) return;
+
+				// Fail open: do not block navigation if validation fails.
+				console.error("[BranchGuard] getBranches failed:", err);
+				setBranchList({ status: BRANCH_LIST_STATE.ERROR, branches: null });
+			}
+		})();
+
+		return () => {
+			cancelled = true;
+		};
+
+		// IMPORTANT:
+		// - depend only on user identity + needsExistenceCheck
+		// - do NOT depend on branchList.status (would cancel itself when setting LOADING)
+	}, [needsExistenceCheck, user?.userId]);
+
+	if (!isAuthenticated) {
+		return children;
+	}
+
+	if (needsExistenceCheck && branchList.status === BRANCH_LIST_STATE.LOADING) {
+		return <BranchValidationLoading />;
+	}
+
+	const allowedBranches =
+		branchList.status === BRANCH_LIST_STATE.READY ? branchList.branches : null;
+
+	const decision = decideBranchUi({
+		user,
+		branch,
+		allowedBranches,
+	});
+
+	if (decision === BRANCH_UI_DECISION.FORBIDDEN) {
 		return <ForbiddenView attemptedBranch={branch} />;
 	}
 
+	if (decision === BRANCH_UI_DECISION.NOT_FOUND) {
+		return <NotFoundView />;
+	}
+
 	return children;
 }