Skip to content

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.

src/components/ProtectedRoute.tsx
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 login
const location = useLocation()
const from = (location.state as { from?: Location })?.from?.pathname || '/'
// After successful login:
navigate(from, { replace: true })
src/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:

src/pages/LoginPage.tsx
const { isAuthenticated } = useAuth()
if (isAuthenticated) return <Navigate to="/" replace />

Add this near the top of LoginPage and RegisterPage.

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.

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 owner
const { 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 = ?.

  1. Create src/components/ProtectedRoute.tsx.
  2. Wrap the /create route.
  3. Visit /create while logged out — confirm redirect to /login.
  4. Log in — confirm redirect back to /create.
  5. Add the “redirect authenticated users” check to LoginPage and RegisterPage.
  • ProtectedRoute wraps routes requiring authentication — redirects to /login if not authed.
  • Pass state={{ from: location }} so LoginPage can redirect back after login.
  • Redirect authenticated users away from /login and /register with <Navigate to="/" replace />.
  • Authorization (ownership checks) happens in components and is enforced by the API.