Files
findyourpilot/.agent/skills/tanstack-start-best-practices/rules/auth-route-protection.md
2026-03-02 21:16:26 +01:00

4.5 KiB

auth-route-protection: Protect Routes with beforeLoad

Priority: HIGH

Explanation

Use beforeLoad in route definitions to check authentication before the route loads. This prevents unauthorized access, redirects to login, and can extend context with user data for child routes.

Bad Example

// Checking auth in component - too late, data may have loaded
function DashboardPage() {
  const user = useAuth()

  useEffect(() => {
    if (!user) {
      navigate({ to: '/login' })  // Redirect after render
    }
  }, [user])

  if (!user) return null  // Flash of content possible

  return <Dashboard user={user} />
}

// No protection on route
export const Route = createFileRoute('/dashboard')({
  loader: async () => {
    // Fetches sensitive data even for unauthenticated users
    return await fetchDashboardData()
  },
  component: DashboardPage,
})

Good Example: Route-Level Protection

// routes/_authenticated.tsx - Layout route for protected area
import { createFileRoute, redirect, Outlet } from '@tanstack/react-router'
import { getSessionData } from '@/lib/session.server'

export const Route = createFileRoute('/_authenticated')({
  beforeLoad: async ({ location }) => {
    const session = await getSessionData()

    if (!session) {
      throw redirect({
        to: '/login',
        search: {
          redirect: location.href,
        },
      })
    }

    // Extend context with user for all child routes
    return {
      user: session,
    }
  },
  component: AuthenticatedLayout,
})

function AuthenticatedLayout() {
  return (
    <div>
      <AuthenticatedNav />
      <main>
        <Outlet />  {/* Child routes render here */}
      </main>
    </div>
  )
}

// routes/_authenticated/dashboard.tsx
// This route is automatically protected by parent
export const Route = createFileRoute('/_authenticated/dashboard')({
  loader: async ({ context }) => {
    // context.user is guaranteed to exist
    return await fetchDashboardData(context.user.id)
  },
  component: DashboardPage,
})

function DashboardPage() {
  const data = Route.useLoaderData()
  const { user } = Route.useRouteContext()

  return <Dashboard data={data} user={user} />
}

Good Example: Role-Based Access

// routes/_admin.tsx
export const Route = createFileRoute('/_admin')({
  beforeLoad: async ({ context }) => {
    // context.user comes from parent _authenticated route
    if (context.user.role !== 'admin') {
      throw redirect({ to: '/unauthorized' })
    }
  },
  component: AdminLayout,
})

// File structure:
// routes/
//   _authenticated.tsx        # Requires login
//   _authenticated/
//     dashboard.tsx           # /dashboard - any authenticated user
//     settings.tsx            # /settings - any authenticated user
//     _admin.tsx              # Admin layout
//     _admin/
//       users.tsx             # /users - admin only
//       analytics.tsx         # /analytics - admin only

Good Example: Preserving Redirect URL

// routes/login.tsx
import { z } from 'zod'

export const Route = createFileRoute('/login')({
  validateSearch: z.object({
    redirect: z.string().optional(),
  }),
  component: LoginPage,
})

function LoginPage() {
  const { redirect } = Route.useSearch()
  const loginMutation = useMutation({
    mutationFn: login,
    onSuccess: () => {
      // Redirect to original destination or default
      navigate({ to: redirect ?? '/dashboard' })
    },
  })

  return <LoginForm onSubmit={loginMutation.mutate} />
}

// In protected routes
beforeLoad: async ({ location }) => {
  if (!session) {
    throw redirect({
      to: '/login',
      search: { redirect: location.href },
    })
  }
}

Good Example: Conditional Content Based on Auth

// Public route with different content for logged-in users
export const Route = createFileRoute('/')({
  beforeLoad: async () => {
    const session = await getSessionData()
    return { user: session?.user ?? null }
  },
  component: HomePage,
})

function HomePage() {
  const { user } = Route.useRouteContext()

  return (
    <div>
      <Hero />
      {user ? (
        <PersonalizedContent user={user} />
      ) : (
        <SignUpCTA />
      )}
    </div>
  )
}

Context

  • beforeLoad runs before route loading begins
  • Throwing redirect() prevents route from loading
  • Context from beforeLoad flows to loader and component
  • Child routes inherit parent's beforeLoad protection
  • Use pathless layout routes (_authenticated.tsx) for grouped protection
  • Store redirect URL in search params for post-login navigation