Protected Routes
Some routes should only be accessible to logged-in users. A ProtectedRoute component wraps them and redirects anonymous users to the login page.
ProtectedRoute component
Section titled “ProtectedRoute component”import { type ReactNode } from 'react'import { Navigate, useLocation } from 'react-router-dom'import { useAuth } from '../context/AuthContext'
interface ProtectedRouteProps { children: ReactNode}
export default function ProtectedRoute({ children }: ProtectedRouteProps) { const { isAuthenticated } = useAuth() const location = useLocation()
if (!isAuthenticated) { // Redirect to login, but remember where they were going return <Navigate to="/login" state={{ from: location }} replace /> }
return <>{children}</>}state={{ from: location }} passes the intended destination to the login page. After login, you can redirect back:
// In LoginPage.tsx — redirect back after loginconst location = useLocation()const from = (location.state as { from?: Location })?.from?.pathname || '/'
// After successful login:navigate(from, { replace: true })Applying ProtectedRoute in App.tsx
Section titled “Applying ProtectedRoute in App.tsx”<Route path="/create" element={ <ProtectedRoute> <CreatePost /> </ProtectedRoute>} />Routes that always require auth: create post, (future: settings).
Routes that don’t: feed, post detail, user profile, login, register.
Redirecting authenticated users away from auth pages
Section titled “Redirecting authenticated users away from auth pages”If a logged-in user visits /login, redirect them to the feed:
const { isAuthenticated } = useAuth()if (isAuthenticated) return <Navigate to="/" replace />Add this near the top of LoginPage and RegisterPage.
Loading state during auth check
Section titled “Loading state during auth check”With lazy localStorage initialization, isAuthenticated is correct immediately on render. No loading state is needed for auth in Bulletin (localStorage is synchronous).
If you used an async auth check (e.g., verifying the token with the server), you’d need a loading state:
if (authLoading) return <div>Checking auth...</div>if (!isAuthenticated) return <Navigate to="/login" replace />For Bulletin, this isn’t needed.
Authorization vs protected routes
Section titled “Authorization vs protected routes”Protected routes handle authentication (are you logged in?). Authorization (are you allowed to do this specific thing?) happens in the component or API:
// PostDetail.tsx — show delete button only to post ownerconst { user } = useAuth()const isAuthor = user?.userId === post.user_id
{isAuthor && ( <button onClick={handleDelete}>Delete post</button>)}The API enforces authorization too — DELETE /posts/:id checks AND user_id = ?.
Exercise
Section titled “Exercise”- Create
src/components/ProtectedRoute.tsx. - Wrap the
/createroute. - Visit
/createwhile logged out — confirm redirect to/login. - Log in — confirm redirect back to
/create. - Add the “redirect authenticated users” check to
LoginPageandRegisterPage.
ProtectedRoutewraps routes requiring authentication — redirects to/loginif not authed.- Pass
state={{ from: location }}soLoginPagecan redirect back after login. - Redirect authenticated users away from
/loginand/registerwith<Navigate to="/" replace />. - Authorization (ownership checks) happens in components and is enforced by the API.