inogen/app/components/auth/global-route-guard.tsx

237 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect } from "react";
import { useAuth } from "~/contexts/auth-context";
import { useLocation, useNavigate } from "react-router";
import toast from "react-hot-toast";
interface GlobalRouteGuardProps {
children: React.ReactNode;
}
// Define protected routes that require authentication
const PROTECTED_ROUTES = [
"/dashboard",
"/dashboard/projects",
"/dashboard/teams",
"/dashboard/reports",
"/dashboard/settings",
"/profile",
"/settings",
"/admin",
];
// Define public routes that don't require authentication
const PUBLIC_ROUTES = [
"/",
"/login",
"/forgot-password",
"/reset-password",
"/404",
"/unauthorized",
];
// Define routes that authenticated users shouldn't access
const AUTH_RESTRICTED_ROUTES = [
"/login",
"/forgot-password",
"/reset-password",
];
// Define exact routes (for root path handling)
const EXACT_ROUTES = ["/", "/login", "/dashboard", "/404", "/unauthorized"];
export function GlobalRouteGuard({ children }: GlobalRouteGuardProps) {
const { isAuthenticated, isLoading, token, user, validateToken } = useAuth();
const location = useLocation();
const navigate = useNavigate();
useEffect(() => {
// Don't do anything while authentication is loading
if (isLoading) return;
const currentPath = location.pathname;
// Check if current route is protected
const isProtectedRoute = PROTECTED_ROUTES.some(
(route) => currentPath === route || currentPath.startsWith(route + "/"),
);
// Check if current route is auth-restricted (like login page)
const isAuthRestrictedRoute = AUTH_RESTRICTED_ROUTES.some(
(route) => currentPath === route || currentPath.startsWith(route + "/"),
);
// Check if current route is a known public route
const isPublicRoute = PUBLIC_ROUTES.some(
(route) => currentPath === route || currentPath.startsWith(route + "/"),
);
// Check if it's an exact route match
const isExactRoute = EXACT_ROUTES.includes(currentPath);
// Case 1: User accessing protected route without authentication
if (isProtectedRoute && !isAuthenticated) {
// if just logged out, don't show another auth-required toast
const justLoggedOut = sessionStorage.getItem("justLoggedOut") === "1";
if (!justLoggedOut) {
toast.remove("auth-required");
toast.error("برای دسترسی به این صفحه باید وارد شوید", {
id: "auth-required",
});
} else {
sessionStorage.removeItem("justLoggedOut");
}
// Save the intended destination for after login
const returnTo = encodeURIComponent(currentPath + location.search);
navigate(`/login?returnTo=${returnTo}`, { replace: true });
return;
}
// Case 2: User accessing protected route with expired/invalid token
if (isProtectedRoute && isAuthenticated && (!token || !token.accessToken)) {
toast.remove("session-expired");
toast.error("جلسه کاری شما منقضی شده است. لطفاً دوباره وارد شوید", {
id: "session-expired",
});
// Clear invalid auth data
localStorage.removeItem("auth_user");
localStorage.removeItem("auth_token");
navigate("/login", { replace: true });
return;
}
// Case 3: Authenticated user trying to access auth-restricted routes
if (isAuthRestrictedRoute && isAuthenticated && token?.accessToken) {
// Get return URL from query params, default to dashboard
const searchParams = new URLSearchParams(location.search);
const returnTo = searchParams.get("returnTo");
const redirectPath =
returnTo && returnTo !== "/login" ? returnTo : "/dashboard";
navigate(redirectPath, { replace: true });
return;
}
// Case 4: Handle root path redirection
if (currentPath === "/") {
if (isAuthenticated && token?.accessToken) {
navigate("/dashboard", { replace: true });
} else {
navigate("/login", { replace: true });
}
return;
}
// Case 5: Unknown/404 routes
const isKnownRoute = isProtectedRoute || isPublicRoute || isExactRoute;
if (
!isKnownRoute &&
!currentPath.includes("/404") &&
!currentPath.includes("/unauthorized")
) {
// If user is authenticated, show authenticated 404
if (isAuthenticated && token?.accessToken) {
navigate("/404", { replace: true });
} else {
// If user is not authenticated, redirect to login
toast.remove("not-found-login");
toast.error("صفحه مورد نظر یافت نشد. لطفاً وارد شوید", {
id: "not-found-login",
});
navigate("/login", { replace: true });
}
return;
}
// Case 6: Validate token for protected routes periodically
if (isProtectedRoute && isAuthenticated && token?.accessToken) {
const checkTokenValidity = async () => {
try {
const isValid = await validateToken();
if (!isValid) {
toast.remove("session-expired-soft");
toast.error("جلسه کاری شما منقضی شده است", {
id: "session-expired-soft",
});
navigate("/unauthorized?reason=token-expired", { replace: true });
}
} catch (error) {
console.error("Token validation failed:", error);
navigate("/unauthorized?reason=token-expired", { replace: true });
}
};
// Only check token validity every 30 seconds to avoid excessive API calls
const now = Date.now();
const lastCheck = parseInt(
sessionStorage.getItem("lastTokenCheck") || "0",
);
if (now - lastCheck > 30000) {
// 30 seconds
sessionStorage.setItem("lastTokenCheck", now.toString());
checkTokenValidity();
}
}
}, [
isLoading,
isAuthenticated,
token,
user,
location.pathname,
location.search,
navigate,
]);
// Note: periodic validation is handled in the block above and the auth context; avoid duplicating it here.
// Show loading screen while checking authentication
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-500 mx-auto mb-4"></div>
<p className="text-gray-600 dark:text-gray-400 font-persian">
در حال بررسی احراز هویت...
</p>
</div>
</div>
);
}
// Render children if all checks pass
return <>{children}</>;
}
// Hook to check if user can access a specific route
export function useRouteAccess() {
const { isAuthenticated, token } = useAuth();
const location = useLocation();
const canAccessRoute = (path: string): boolean => {
const isProtectedRoute = PROTECTED_ROUTES.some(
(route) => path === route || path.startsWith(route + "/"),
);
if (isProtectedRoute) {
return isAuthenticated && !!token?.accessToken;
}
return true; // Public routes are accessible to everyone
};
const getCurrentRouteAccess = () => {
return canAccessRoute(location.pathname);
};
return {
canAccessRoute,
getCurrentRouteAccess,
isAuthenticated,
hasValidToken: !!token?.accessToken,
};
}