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:
141
src/App.tsx
141
src/App.tsx
@@ -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";
|
||||||
@@ -65,25 +66,131 @@ const App = () => (
|
|||||||
<Route path="/calendar" element={<CalendarPage />} />
|
<Route path="/calendar" element={<CalendarPage />} />
|
||||||
<Route path="/privacy" element={<Privacy />} />
|
<Route path="/privacy" element={<Privacy />} />
|
||||||
<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>
|
||||||
|
|||||||
64
src/components/ProtectedRoute.tsx
Normal file
64
src/components/ProtectedRoute.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user