Files
igny8/frontend/src/components/auth/ProtectedRoute.tsx
IGNY8 VPS (Salman) 9f85ce4f52 sadasd
2025-12-08 18:22:10 +00:00

150 lines
5.1 KiB
TypeScript

import { useEffect, ReactNode, useState } from "react";
import { Navigate, useLocation } from "react-router-dom";
import { useAuthStore } from "../../store/authStore";
import { useErrorHandler } from "../../hooks/useErrorHandler";
import { trackLoading } from "../common/LoadingStateMonitor";
interface ProtectedRouteProps {
children: ReactNode;
}
/**
* ProtectedRoute component - guards routes requiring authentication
* Redirects to /signin if user is not authenticated
*/
export default function ProtectedRoute({ children }: ProtectedRouteProps) {
const { isAuthenticated, loading, user, logout } = useAuthStore();
const location = useLocation();
const { addError } = useErrorHandler('ProtectedRoute');
const [showError, setShowError] = useState(false);
const [errorMessage, setErrorMessage] = useState<string>('');
const PLAN_ALLOWED_PATHS = [
'/account/plans',
'/account/purchase-credits',
'/account/settings',
'/account/team',
'/account/usage',
'/billing',
'/payment',
];
const isPlanAllowedPath = PLAN_ALLOWED_PATHS.some((prefix) =>
location.pathname.startsWith(prefix)
);
// Track loading state
useEffect(() => {
trackLoading('auth-loading', loading);
}, [loading]);
// Validate account + plan whenever auth/user changes
useEffect(() => {
console.log('[ProtectedRoute] Auth state changed:', {
isAuthenticated,
hasUser: !!user,
hasAccount: !!user?.account,
pathname: location.pathname
});
if (!isAuthenticated) {
console.log('[ProtectedRoute] Not authenticated, will redirect to signin');
return;
}
if (!user?.account) {
console.error('[ProtectedRoute] User has no account, logging out');
setErrorMessage('This user is not linked to an account. Please contact support.');
logout();
return;
}
console.log('[ProtectedRoute] Auth validation passed');
}, [isAuthenticated, user, logout]);
// Immediate check on mount: if loading is true, reset it immediately
useEffect(() => {
if (loading) {
console.warn('ProtectedRoute: Loading state is true on mount, resetting immediately');
useAuthStore.setState({ loading: false });
}
}, []);
// Safety timeout: if loading becomes true and stays stuck, show error
useEffect(() => {
if (loading) {
const timeout1 = setTimeout(() => {
setErrorMessage('Authentication check is taking longer than expected. This may indicate a network or server issue.');
setShowError(true);
addError(new Error('Auth loading stuck for 3 seconds'), 'ProtectedRoute');
}, 3000);
const timeout2 = setTimeout(() => {
console.error('ProtectedRoute: Loading state stuck for 5 seconds, forcing reset');
useAuthStore.setState({ loading: false });
setShowError(false);
}, 5000);
return () => {
clearTimeout(timeout1);
clearTimeout(timeout2);
};
} else {
setShowError(false);
}
}, [loading, addError]);
// Show loading state while checking authentication
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-900">
<div className="text-center max-w-md px-4">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-brand-500 mb-4"></div>
<p className="text-lg font-medium text-gray-800 dark:text-white mb-2">Loading...</p>
{showError && (
<div className="mt-4 p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
<p className="text-sm text-yellow-800 dark:text-yellow-200 mb-3">
{errorMessage}
</p>
<button
onClick={() => {
useAuthStore.setState({ loading: false });
setShowError(false);
window.location.reload();
}}
className="px-4 py-2 text-sm bg-yellow-600 text-white rounded hover:bg-yellow-700"
>
Retry or Reload Page
</button>
</div>
)}
</div>
</div>
);
}
// Redirect to signin if not authenticated
if (!isAuthenticated) {
console.log('[ProtectedRoute] Redirecting to /signin - not authenticated');
return <Navigate to="/signin" state={{ from: location }} replace />;
}
// If authenticated but missing an active plan, keep user inside billing/onboarding
const accountStatus = user?.account?.status;
const accountInactive = accountStatus && ['suspended', 'cancelled'].includes(accountStatus);
const pendingPayment = accountStatus === 'pending_payment';
const isPrivileged = user?.role === 'developer' || user?.is_superuser;
if (!isPrivileged) {
if (pendingPayment && !isPlanAllowedPath) {
return <Navigate to="/account/plans" state={{ from: location }} replace />;
}
if (accountInactive && !isPlanAllowedPath) {
return <Navigate to="/account/plans" state={{ from: location }} replace />;
}
}
return <>{children}</>;
}