237 lines
7.3 KiB
TypeScript
237 lines
7.3 KiB
TypeScript
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,
|
||
};
|
||
}
|