From d6126d194370659432fee1578e0e437e187b0039 Mon Sep 17 00:00:00 2001 From: dwindown Date: Sun, 4 Jan 2026 19:04:10 +0700 Subject: [PATCH] Fix admin redirect by using isAdmin from auth context instead of user_metadata.role MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The root cause was that ProtectedRoute and Auth.tsx were checking user.user_metadata?.role, but the admin role is stored in the user_roles table, not in user metadata. Changes: - ProtectedRoute: Use isAdmin flag from useAuth context instead of user.user_metadata?.role - Auth.tsx: Use isAdmin flag for role-based redirect logic - Remove redundant auth checks from individual admin/member pages (ProtectedRoute handles it) - Add isAdmin to useEffect dependencies to ensure redirect happens after admin check completes This fixes the issue where admins were being redirected to /dashboard instead of /admin after login, because the role check was happening before the async admin role lookup completed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/components/ProtectedRoute.tsx | 17 +++++++---------- src/pages/Auth.tsx | 17 +++++++---------- src/pages/admin/AdminBootcamp.tsx | 8 +++----- src/pages/admin/AdminConsulting.tsx | 12 ++++-------- src/pages/admin/AdminEvents.tsx | 8 +++----- src/pages/admin/AdminProducts.tsx | 8 +++----- src/pages/admin/AdminSettings.tsx | 6 ------ src/pages/member/MemberAccess.tsx | 5 ++--- src/pages/member/MemberProfile.tsx | 5 ++--- 9 files changed, 31 insertions(+), 55 deletions(-) diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx index d8e5178..3417a9c 100644 --- a/src/components/ProtectedRoute.tsx +++ b/src/components/ProtectedRoute.tsx @@ -9,7 +9,7 @@ interface ProtectedRouteProps { } export function ProtectedRoute({ children, requireAdmin = false }: ProtectedRouteProps) { - const { user, loading: authLoading } = useAuth(); + const { user, loading: authLoading, isAdmin } = useAuth(); const navigate = useNavigate(); useEffect(() => { @@ -21,15 +21,12 @@ export function ProtectedRoute({ children, requireAdmin = false }: ProtectedRout return; } - // Check for admin role if required (only after user is loaded) - if (!authLoading && user && requireAdmin) { - const userRole = user.user_metadata?.role; - if (userRole !== 'admin') { - // Redirect non-admin users to member dashboard - navigate('/dashboard'); - } + // Check for admin role if required (only after user is loaded AND admin check is complete) + if (!authLoading && user && requireAdmin && !isAdmin) { + // Redirect non-admin users to member dashboard + navigate('/dashboard'); } - }, [user, authLoading, navigate, requireAdmin]); + }, [user, authLoading, isAdmin, navigate, requireAdmin]); // Show loading skeleton while checking auth if (authLoading) { @@ -53,7 +50,7 @@ export function ProtectedRoute({ children, requireAdmin = false }: ProtectedRout } // Don't render if admin access required but user is not admin - if (requireAdmin && user.user_metadata?.role !== 'admin') { + if (requireAdmin && !isAdmin) { return null; } diff --git a/src/pages/Auth.tsx b/src/pages/Auth.tsx index a23c459..adadbdb 100644 --- a/src/pages/Auth.tsx +++ b/src/pages/Auth.tsx @@ -23,7 +23,7 @@ export default function Auth() { const [pendingUserId, setPendingUserId] = useState(null); const [resendCountdown, setResendCountdown] = useState(0); const [isResendOTP, setIsResendOTP] = useState(false); // Track if this is resend OTP for existing user - const { signIn, signUp, user, sendAuthOTP, verifyAuthOTP, getUserByEmail } = useAuth(); + const { signIn, signUp, user, isAdmin, sendAuthOTP, verifyAuthOTP, getUserByEmail } = useAuth(); const navigate = useNavigate(); const location = useLocation(); @@ -35,10 +35,12 @@ export default function Auth() { sessionStorage.removeItem('redirectAfterLogin'); navigate(savedRedirect); } else { - navigate('/dashboard'); + // Default redirect based on user role (use isAdmin flag from context) + const defaultRedirect = isAdmin ? '/admin' : '/dashboard'; + navigate(defaultRedirect); } } - }, [user, navigate]); + }, [user, isAdmin, navigate]); // Countdown timer for resend OTP useEffect(() => { @@ -108,13 +110,8 @@ export default function Auth() { toast({ title: 'Error', description: error.message, variant: 'destructive' }); setLoading(false); } else { - // Get redirect from sessionStorage or use default - const savedRedirect = sessionStorage.getItem('redirectAfterLogin'); - const redirectTo = savedRedirect || '/dashboard'; - if (savedRedirect) { - sessionStorage.removeItem('redirectAfterLogin'); - } - navigate(redirectTo); + // Login successful - the useEffect watching 'user' will handle the redirect + // This ensures we have the full user metadata including role setLoading(false); } } else { diff --git a/src/pages/admin/AdminBootcamp.tsx b/src/pages/admin/AdminBootcamp.tsx index 3c45b52..4aba315 100644 --- a/src/pages/admin/AdminBootcamp.tsx +++ b/src/pages/admin/AdminBootcamp.tsx @@ -25,12 +25,10 @@ export default function AdminBootcamp() { const [searchQuery, setSearchQuery] = useState(''); useEffect(() => { - if (!authLoading) { - if (!user) navigate('/auth'); - else if (!isAdmin) navigate('/dashboard'); - else fetchBootcamps(); + if (user && isAdmin) { + fetchBootcamps(); } - }, [user, isAdmin, authLoading]); + }, [user, isAdmin]); const fetchBootcamps = async () => { const { data, error } = await supabase diff --git a/src/pages/admin/AdminConsulting.tsx b/src/pages/admin/AdminConsulting.tsx index c39e763..25999e6 100644 --- a/src/pages/admin/AdminConsulting.tsx +++ b/src/pages/admin/AdminConsulting.tsx @@ -79,15 +79,11 @@ export default function AdminConsulting() { const [notifyMember, setNotifyMember] = useState(true); useEffect(() => { - if (!authLoading) { - if (!user) navigate('/auth'); - else if (!isAdmin) navigate('/dashboard'); - else { - fetchSessions(); - fetchSettings(); - } + if (user && isAdmin) { + fetchSessions(); + fetchSettings(); } - }, [user, isAdmin, authLoading]); + }, [user, isAdmin]); const fetchSessions = async () => { // Fetch sessions with profile data diff --git a/src/pages/admin/AdminEvents.tsx b/src/pages/admin/AdminEvents.tsx index 4b4ab70..25e94f4 100644 --- a/src/pages/admin/AdminEvents.tsx +++ b/src/pages/admin/AdminEvents.tsx @@ -75,12 +75,10 @@ export default function AdminEvents() { const [blockForm, setBlockForm] = useState(emptyBlock); useEffect(() => { - if (!authLoading) { - if (!user) navigate('/auth'); - else if (!isAdmin) navigate('/dashboard'); - else fetchData(); + if (user && isAdmin) { + fetchData(); } - }, [user, isAdmin, authLoading]); + }, [user, isAdmin]); const fetchData = async () => { const [eventsRes, blocksRes, productsRes] = await Promise.all([ diff --git a/src/pages/admin/AdminProducts.tsx b/src/pages/admin/AdminProducts.tsx index 42afeb6..cb0b168 100644 --- a/src/pages/admin/AdminProducts.tsx +++ b/src/pages/admin/AdminProducts.tsx @@ -80,12 +80,10 @@ export default function AdminProducts() { const [filterStatus, setFilterStatus] = useState('all'); useEffect(() => { - if (!authLoading) { - if (!user) navigate('/auth'); - else if (!isAdmin) navigate('/dashboard'); - else fetchProducts(); + if (user && isAdmin) { + fetchProducts(); } - }, [user, isAdmin, authLoading]); + }, [user, isAdmin]); const fetchProducts = async () => { const { data, error } = await supabase diff --git a/src/pages/admin/AdminSettings.tsx b/src/pages/admin/AdminSettings.tsx index 521d8e1..00241b3 100644 --- a/src/pages/admin/AdminSettings.tsx +++ b/src/pages/admin/AdminSettings.tsx @@ -15,12 +15,6 @@ export default function AdminSettings() { const { user, isAdmin, loading: authLoading } = useAuth(); const navigate = useNavigate(); - useEffect(() => { - if (!authLoading) { - if (!user) navigate('/auth'); - else if (!isAdmin) navigate('/dashboard'); - } - }, [user, isAdmin, authLoading, navigate]); if (authLoading) { return ( diff --git a/src/pages/member/MemberAccess.tsx b/src/pages/member/MemberAccess.tsx index dddf80e..539f7c0 100644 --- a/src/pages/member/MemberAccess.tsx +++ b/src/pages/member/MemberAccess.tsx @@ -47,9 +47,8 @@ export default function MemberAccess() { const [selectedType, setSelectedType] = useState('all'); useEffect(() => { - if (!authLoading && !user) navigate('/auth'); - else if (user) fetchAccess(); - }, [user, authLoading]); + if (user) fetchAccess(); + }, [user]); const fetchAccess = async () => { const [accessRes, paidOrdersRes, consultingRes] = await Promise.all([ diff --git a/src/pages/member/MemberProfile.tsx b/src/pages/member/MemberProfile.tsx index d7b4228..edea454 100644 --- a/src/pages/member/MemberProfile.tsx +++ b/src/pages/member/MemberProfile.tsx @@ -35,9 +35,8 @@ export default function MemberProfile() { }); useEffect(() => { - if (!authLoading && !user) navigate('/auth'); - else if (user) fetchProfile(); - }, [user, authLoading]); + if (user) fetchProfile(); + }, [user]); const fetchProfile = async () => { const { data } = await supabase