Protected Routes
Prevent unauthorized access to pages and components.
React Router Protection
// components/auth/protected-route.tsx
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '@/contexts/auth-context';
interface ProtectedRouteProps {
children: React.ReactNode;
requiredRole?: string;
}
export function ProtectedRoute({ children, requiredRole }: ProtectedRouteProps) {
const { isAuthenticated, isLoading, user } = useAuth();
const location = useLocation();
if (isLoading) {
return <LoadingSpinner />;
}
if (!isAuthenticated) {
// Redirect to login with return URL
return <Navigate to="/login" state={{ from: location }} replace />;
}
if (requiredRole && user?.role !== requiredRole) {
return <Navigate to="/unauthorized" replace />;
}
return <>{children}</>;
}
// Usage in routes
import { Routes, Route } from 'react-router-dom';
function AppRoutes() {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
{/* Protected routes */}
<Route
path="/dashboard"
element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
}
/>
{/* Admin only */}
<Route
path="/admin"
element={
<ProtectedRoute requiredRole="admin">
<AdminPage />
</ProtectedRoute>
}
/>
</Routes>
);
}Layout-Based Protection
// layouts/protected-layout.tsx
import { Outlet, Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '@/contexts/auth-context';
export function ProtectedLayout() {
const { isAuthenticated, isLoading } = useAuth();
const location = useLocation();
if (isLoading) {
return <LoadingSpinner />;
}
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return (
<div className="protected-layout">
<Sidebar />
<main>
<Outlet />
</main>
</div>
);
}
// Routes with layout
function AppRoutes() {
return (
<Routes>
{/* Public routes */}
<Route path="/login" element={<LoginPage />} />
{/* All protected routes use ProtectedLayout */}
<Route element={<ProtectedLayout />}>
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/profile" element={<ProfilePage />} />
<Route path="/settings" element={<SettingsPage />} />
</Route>
</Routes>
);
}Next.js Middleware Protection
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const protectedPaths = ['/dashboard', '/profile', '/settings'];
const authPaths = ['/login', '/register'];
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')?.value;
const { pathname } = request.nextUrl;
// Redirect logged-in users away from auth pages
if (authPaths.some((path) => pathname.startsWith(path))) {
if (token) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
// Protect routes
if (protectedPaths.some((path) => pathname.startsWith(path))) {
if (!token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('returnUrl', pathname);
return NextResponse.redirect(loginUrl);
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*', '/settings/:path*', '/login', '/register'],
};Next.js Page-Level Protection
// pages/dashboard.tsx
import { GetServerSideProps } from 'next';
import { getSession } from '@/lib/auth';
export default function DashboardPage({ user }) {
return <Dashboard user={user} />;
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const session = await getSession(context.req);
if (!session) {
return {
redirect: {
destination: `/login?returnUrl=${context.resolvedUrl}`,
permanent: false,
},
};
}
return {
props: {
user: session.user,
},
};
};Next.js App Router Protection
// app/dashboard/layout.tsx
import { redirect } from 'next/navigation';
import { getSession } from '@/lib/auth';
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await getSession();
if (!session) {
redirect('/login');
}
return (
<div className="dashboard-layout">
<Sidebar user={session.user} />
<main>{children}</main>
</div>
);
}HOC Pattern (Higher-Order Component)
// hoc/with-auth.tsx
import { useRouter } from 'next/router';
import { useAuth } from '@/contexts/auth-context';
import { ComponentType, useEffect } from 'react';
interface WithAuthOptions {
requiredRole?: string;
redirectTo?: string;
}
export function withAuth<P extends object>(
WrappedComponent: ComponentType<P>,
options: WithAuthOptions = {}
) {
const { requiredRole, redirectTo = '/login' } = options;
return function WithAuthComponent(props: P) {
const { isAuthenticated, isLoading, user } = useAuth();
const router = useRouter();
useEffect(() => {
if (!isLoading && !isAuthenticated) {
router.push(`${redirectTo}?returnUrl=${router.asPath}`);
}
if (!isLoading && requiredRole && user?.role !== requiredRole) {
router.push('/unauthorized');
}
}, [isLoading, isAuthenticated, user, router]);
if (isLoading) {
return <LoadingSpinner />;
}
if (!isAuthenticated) {
return null;
}
if (requiredRole && user?.role !== requiredRole) {
return null;
}
return <WrappedComponent {...props} />;
};
}
// Usage
const ProtectedDashboard = withAuth(DashboardPage);
const AdminPanel = withAuth(AdminPage, { requiredRole: 'admin' });Client-Side Route Guard Hook
// hooks/use-require-auth.ts
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { useAuth } from '@/contexts/auth-context';
interface UseRequireAuthOptions {
redirectTo?: string;
requiredRole?: string;
}
export function useRequireAuth(options: UseRequireAuthOptions = {}) {
const { redirectTo = '/login', requiredRole } = options;
const { isAuthenticated, isLoading, user } = useAuth();
const router = useRouter();
useEffect(() => {
if (isLoading) return;
if (!isAuthenticated) {
router.push(`${redirectTo}?returnUrl=${router.asPath}`);
return;
}
if (requiredRole && user?.role !== requiredRole) {
router.push('/unauthorized');
}
}, [isLoading, isAuthenticated, user, router, redirectTo, requiredRole]);
return {
isLoading,
isAuthenticated,
user,
};
}
// Usage
function DashboardPage() {
const { isLoading, user } = useRequireAuth();
if (isLoading) {
return <LoadingSpinner />;
}
return <Dashboard user={user} />;
}Redirect After Login
// pages/login.tsx
import { useRouter } from 'next/router';
import { useAuth } from '@/contexts/auth-context';
function LoginPage() {
const router = useRouter();
const { login } = useAuth();
// Get return URL from query params
const returnUrl = (router.query.returnUrl as string) || '/dashboard';
const handleSubmit = async (data: LoginForm) => {
await login(data.email, data.password);
router.push(returnUrl);
};
return <LoginForm onSubmit={handleSubmit} />;
}Guest-Only Routes
// components/auth/guest-route.tsx
import { Navigate } from 'react-router-dom';
import { useAuth } from '@/contexts/auth-context';
interface GuestRouteProps {
children: React.ReactNode;
redirectTo?: string;
}
export function GuestRoute({ children, redirectTo = '/dashboard' }: GuestRouteProps) {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return <LoadingSpinner />;
}
if (isAuthenticated) {
return <Navigate to={redirectTo} replace />;
}
return <>{children}</>;
}
// Usage - redirect logged-in users away from login page
<Route
path="/login"
element={
<GuestRoute>
<LoginPage />
</GuestRoute>
}
/>Best Practices
- Server-side first - Always validate auth on the server
- Handle loading states - Show spinners during auth checks
- Preserve return URL - Redirect back after login
- Use middleware - Protect routes at the edge when possible
- Role-based access - Check permissions, not just authentication
- Graceful degradation - Show appropriate UI for unauthorized users