Add authentication protection to admin and member routes

CRITICAL SECURITY FIX: All admin and member routes now require authentication.

## Changes:
- Created ProtectedRoute component to enforce authentication
- Protected all member routes (/dashboard, /access, /orders, /profile)
- Protected all admin routes (/admin/*) with admin role check
- Added redirect-after-login functionality using sessionStorage
- Non-authenticated users accessing protected pages are redirected to /auth
- Non-admin users accessing admin pages are redirected to /dashboard

## Security Impact:
- Prevents unauthorized access to admin panel and member areas
- Users must login to access any protected functionality
- Admin routes additionally verify user role is 'admin'
- After login, users are redirected back to their intended page

🤖 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 15:24:34 +07:00
parent 47a645520c
commit aeeb02d36b
3 changed files with 202 additions and 20 deletions

View File

@@ -6,6 +6,7 @@ import { BrowserRouter, Routes, Route } from "react-router-dom";
import { AuthProvider } from "@/hooks/useAuth"; import { AuthProvider } from "@/hooks/useAuth";
import { CartProvider } from "@/contexts/CartContext"; import { CartProvider } from "@/contexts/CartContext";
import { BrandingProvider } from "@/hooks/useBranding"; import { BrandingProvider } from "@/hooks/useBranding";
import { ProtectedRoute } from "@/components/ProtectedRoute";
import Index from "./pages/Index"; import Index from "./pages/Index";
import Auth from "./pages/Auth"; import Auth from "./pages/Auth";
import ConfirmOTP from "./pages/ConfirmOTP"; import ConfirmOTP from "./pages/ConfirmOTP";
@@ -67,23 +68,129 @@ const App = () => (
<Route path="/terms" element={<Terms />} /> <Route path="/terms" element={<Terms />} />
{/* Member routes */} {/* Member routes */}
<Route path="/dashboard" element={<MemberDashboard />} /> <Route
<Route path="/access" element={<MemberAccess />} /> path="/dashboard"
<Route path="/orders" element={<MemberOrders />} /> element={
<Route path="/orders/:id" element={<OrderDetail />} /> <ProtectedRoute>
<Route path="/profile" element={<MemberProfile />} /> <MemberDashboard />
</ProtectedRoute>
}
/>
<Route
path="/access"
element={
<ProtectedRoute>
<MemberAccess />
</ProtectedRoute>
}
/>
<Route
path="/orders"
element={
<ProtectedRoute>
<MemberOrders />
</ProtectedRoute>
}
/>
<Route
path="/orders/:id"
element={
<ProtectedRoute>
<OrderDetail />
</ProtectedRoute>
}
/>
<Route
path="/profile"
element={
<ProtectedRoute>
<MemberProfile />
</ProtectedRoute>
}
/>
{/* Admin routes */} {/* Admin routes */}
<Route path="/admin" element={<AdminDashboard />} /> <Route
<Route path="/admin/products" element={<AdminProducts />} /> path="/admin"
<Route path="/admin/products/:id/curriculum" element={<ProductCurriculum />} /> element={
<Route path="/admin/bootcamp" element={<AdminBootcamp />} /> <ProtectedRoute requireAdmin>
<Route path="/admin/orders" element={<AdminOrders />} /> <AdminDashboard />
<Route path="/admin/members" element={<AdminMembers />} /> </ProtectedRoute>
<Route path="/admin/events" element={<AdminEvents />} /> }
<Route path="/admin/settings" element={<AdminSettings />} /> />
<Route path="/admin/consulting" element={<AdminConsulting />} /> <Route
<Route path="/admin/reviews" element={<AdminReviews />} /> path="/admin/products"
element={
<ProtectedRoute requireAdmin>
<AdminProducts />
</ProtectedRoute>
}
/>
<Route
path="/admin/products/:id/curriculum"
element={
<ProtectedRoute requireAdmin>
<ProductCurriculum />
</ProtectedRoute>
}
/>
<Route
path="/admin/bootcamp"
element={
<ProtectedRoute requireAdmin>
<AdminBootcamp />
</ProtectedRoute>
}
/>
<Route
path="/admin/orders"
element={
<ProtectedRoute requireAdmin>
<AdminOrders />
</ProtectedRoute>
}
/>
<Route
path="/admin/members"
element={
<ProtectedRoute requireAdmin>
<AdminMembers />
</ProtectedRoute>
}
/>
<Route
path="/admin/events"
element={
<ProtectedRoute requireAdmin>
<AdminEvents />
</ProtectedRoute>
}
/>
<Route
path="/admin/settings"
element={
<ProtectedRoute requireAdmin>
<AdminSettings />
</ProtectedRoute>
}
/>
<Route
path="/admin/consulting"
element={
<ProtectedRoute requireAdmin>
<AdminConsulting />
</ProtectedRoute>
}
/>
<Route
path="/admin/reviews"
element={
<ProtectedRoute requireAdmin>
<AdminReviews />
</ProtectedRoute>
}
/>
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
</Routes> </Routes>

View File

@@ -0,0 +1,64 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '@/hooks/useAuth';
import { Skeleton } from '@/components/ui/skeleton';
interface ProtectedRouteProps {
children: React.ReactNode;
requireAdmin?: boolean;
}
export function ProtectedRoute({ children, requireAdmin = false }: ProtectedRouteProps) {
const { user, loading: authLoading } = useAuth();
const navigate = useNavigate();
useEffect(() => {
if (!authLoading) {
if (!user) {
// Save current URL to redirect back after login
const currentPath = window.location.pathname + window.location.search;
sessionStorage.setItem('redirectAfterLogin', currentPath);
navigate('/auth');
return;
}
// Check for admin role if required
if (requireAdmin) {
const userRole = user.user_metadata?.role;
if (userRole !== 'admin') {
// Redirect non-admin users to member dashboard
navigate('/dashboard');
return;
}
}
}
}, [user, authLoading, navigate, requireAdmin]);
// Show loading skeleton while checking auth
if (authLoading) {
return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto space-y-4">
<Skeleton className="h-10 w-1/3" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-64 w-full" />
</div>
</div>
</div>
);
}
// Don't render children if user is not authenticated
if (!user) {
return null;
}
// Don't render if admin access required but user is not admin
if (requireAdmin && user.user_metadata?.role !== 'admin') {
return null;
}
return <>{children}</>;
}

View File

@@ -29,7 +29,14 @@ export default function Auth() {
useEffect(() => { useEffect(() => {
if (user) { if (user) {
navigate('/dashboard'); // Check if there's a saved redirect path
const savedRedirect = sessionStorage.getItem('redirectAfterLogin');
if (savedRedirect) {
sessionStorage.removeItem('redirectAfterLogin');
navigate(savedRedirect);
} else {
navigate('/dashboard');
}
} }
}, [user, navigate]); }, [user, navigate]);
@@ -101,8 +108,12 @@ export default function Auth() {
toast({ title: 'Error', description: error.message, variant: 'destructive' }); toast({ title: 'Error', description: error.message, variant: 'destructive' });
setLoading(false); setLoading(false);
} else { } else {
// Get redirect from URL state or use default // Get redirect from sessionStorage or use default
const redirectTo = (location.state as any)?.redirectTo || '/dashboard'; const savedRedirect = sessionStorage.getItem('redirectAfterLogin');
const redirectTo = savedRedirect || '/dashboard';
if (savedRedirect) {
sessionStorage.removeItem('redirectAfterLogin');
}
navigate(redirectTo); navigate(redirectTo);
setLoading(false); setLoading(false);
} }