Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | 1x 12x 15x 5x 10x 10x 40x 40x 40x 40x 40x 40x 4x 40x 15x 15x 40x 1x 40x 4x 36x 4x 32x 15x 17x 1x 16x | import { useEffect } from "react";
import { useNavigate, useRouterState } from "@tanstack/react-router";
import { useAuth } from "../../lib/auth-context";
import { LoadingSpinner } from "./LoadingSpinner";
type AuthGuardProps = {
children: React.ReactNode;
};
// Paths where the ghost-session safety net should not fire (issue #581):
// legitimate authenticated-but-no-pro-yet states handled by their own flow.
const GHOST_SESSION_EXEMPT_PATHS = ["/onboarding", "/accept-terms"];
function isGhostSessionExemptPath(path: string): boolean {
return GHOST_SESSION_EXEMPT_PATHS.some((p) => path.startsWith(p));
}
// Map pro routes to their admin equivalents
function getAdminEquivalentRoute(proPath: string): string {
// Handle project detail pages: /projects/:id -> /admin/projects/:id
if (proPath.startsWith("/projects/")) {
return `/admin${proPath}`;
}
// Handle other pro routes
const routeMap: Record<string, string> = {
"/": "/admin",
"/projects": "/admin/projects",
"/analytics": "/admin/analytics",
"/profile": "/admin/pros",
"/team": "/admin/users",
};
return routeMap[proPath] || "/admin";
}
// Auth guard component - redirects to login if not authenticated
// Also redirects admin-only users (no pro access) to equivalent admin routes
export function AuthGuard({ children }: AuthGuardProps) {
const { isAuthenticated, isLoading, isAdmin, hasProAccess } = useAuth();
const navigate = useNavigate();
const routerState = useRouterState();
const currentPath = routerState.location.pathname;
useEffect(() => {
if (!isLoading && !isAuthenticated) {
navigate({ to: "/login" });
}
// Redirect admin-only users (no pro access) to equivalent admin route
if (!isLoading && isAuthenticated && isAdmin && !hasProAccess) {
const adminRoute = getAdminEquivalentRoute(currentPath);
navigate({ to: adminRoute });
}
// Issue #581 (reopen) safety net — a signed-in user with neither admin
// nor pro access is in a ghost state (deactivated team member, orphan
// role cleanup, etc.). Surface the revoked-access banner instead of an
// empty dashboard. Skip on /onboarding and /accept-terms, the two
// AuthGuard-wrapped standalone routes where a transient pre-pro state
// is legitimate and OnboardingGuard / the page itself handles redirect.
if (
!isLoading &&
isAuthenticated &&
!isAdmin &&
!hasProAccess &&
!isGhostSessionExemptPath(currentPath)
) {
navigate({ to: "/auth/sign-in", search: { error: "access_revoked" } });
}
}, [
isLoading,
isAuthenticated,
isAdmin,
hasProAccess,
currentPath,
navigate,
]);
if (isLoading) {
return <LoadingSpinner />;
}
if (!isAuthenticated) {
return <LoadingSpinner />; // Show spinner while redirecting
}
// Admin-only users should be redirected to admin equivalent
if (isAdmin && !hasProAccess) {
return <LoadingSpinner />; // Show spinner while redirecting
}
// Ghost session — see useEffect above
if (!isAdmin && !hasProAccess && !isGhostSessionExemptPath(currentPath)) {
return <LoadingSpinner />;
}
return <>{children}</>;
}
|