Fix admin redirect by using isAdmin from auth context instead of user_metadata.role

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 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-01-04 19:04:10 +07:00
parent a423a6d31d
commit d6126d1943
9 changed files with 31 additions and 55 deletions

View File

@@ -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;
}

View File

@@ -23,7 +23,7 @@ export default function Auth() {
const [pendingUserId, setPendingUserId] = useState<string | null>(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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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([

View File

@@ -80,12 +80,10 @@ export default function AdminProducts() {
const [filterStatus, setFilterStatus] = useState<string>('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

View File

@@ -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 (

View File

@@ -47,9 +47,8 @@ export default function MemberAccess() {
const [selectedType, setSelectedType] = useState<string>('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([

View File

@@ -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