MATIN MOLKARA
SERIES: آموزش Next js — PART 19 OF 28

قست بیستم - محافظت از مسیرها در Next.js

بعد از login، مهم‌ترین مرحله این است که مسیرها و صفحات ادمین را محافظت کنیم

محافظت از مسیرها در Next.js: ساخت صفحات فقط برای ادمین

تا اینجا مدل auth را طراحی کردیم و بعد هم ورود امن با bcrypt را ساختیم.
الان ادمین می‌تواند وارد شود و یک session ساده از طریق cookie دریافت کند. اما هنوز یک مسئله مهم باقی مانده است:

اگر کاربر مهمان مستقیماً آدرس /admin را وارد کند چه می‌شود؟

اگر هیچ محافظتی نداشته باشیم، ممکن است صفحه مدیریت برای همه قابل مشاهده باشد.
پس بعد از login، مهم‌ترین مرحله این است که مسیرها و صفحات ادمین را محافظت کنیم.

در این قسمت می‌خواهیم مشخص کنیم:

  • کدام مسیرها باید محافظت شوند؟
  • session را چطور بررسی کنیم؟
  • کاربر مهمان را چطور redirect کنیم؟
  • برای پنل ادمین چطور layout جداگانه بسازیم؟
  • در نبود دسترسی، چه تجربه کاربری مناسبی داشته باشیم؟

کدام مسیرها باید محافظت شوند؟

اول باید مسیرهای private را دقیق مشخص کنیم.

در این پروژه معمولاً همه این مسیرها باید فقط برای ادمین قابل دسترسی باشند:

  • /admin
  • /admin/projects
  • /admin/projects/new
  • /admin/projects/[id]/edit
  • /admin/posts
  • /admin/posts/new
  • /admin/posts/[id]/edit

در مقابل، این‌ها عمومی هستند:

  • /
  • /projects
  • /projects/[slug]
  • /blog
  • /blog/[slug]
  • /contact

پس هر چیزی که زیر /admin قرار می‌گیرد، باید محافظت شود.

در کنار صفحات، endpointهای مدیریتی هم باید private باشند:

  • POST /api/projects
  • PATCH /api/projects/[id]
  • DELETE /api/projects/[id]

نکته مهم این است:

محافظت از page route و محافظت از API route هر دو لازم‌اند.
اگر فقط صفحه را قفل کنیم ولی API را آزاد بگذاریم، باز هم امنیت کامل نداریم.


بررسی session

برای محافظت از مسیرها، اول باید بتوانیم session را بخوانیم.

فرض کن در login، این cookie را ست کرده‌ایم:

admin_session

حالا یک helper ساده برای خواندن کاربر فعلی می‌سازیم.

// lib/auth/session.ts

import { cookies } from 'next/headers'
import { prisma } from '@/lib/prisma'

export async function getCurrentAdminUser() {
  const cookieStore = await cookies()
  const session = cookieStore.get('admin_session')

  if (!session?.value) {
    return null
  }

  const user = await prisma.adminUser.findUnique({
    where: {
      id: session.value,
    },
  })

  if (!user) {
    return null
  }

  return user
}

این helper فعلاً ساده است:

  • cookie را می‌خواند
  • اگر cookie نبود، null برمی‌گرداند
  • اگر بود، کاربر را از دیتابیس پیدا می‌کند
  • اگر کاربر وجود داشت، آن را برمی‌گرداند

این helper بعداً هم در pageها و هم در APIها قابل استفاده است.


redirect کاربر مهمان

حالا می‌توانیم در صفحه ادمین، اگر کاربر وارد نشده بود، او را به صفحه login بفرستیم.

مثلاً در صفحه /admin:

// app/admin/page.tsx

import { redirect } from 'next/navigation'
import { getCurrentAdminUser } from '@/lib/auth/session'

export default async function AdminPage() {
  const user = await getCurrentAdminUser()

  if (!user) {
    redirect('/admin/login')
  }

  return (
    <main>
      <h1>Admin Dashboard</h1>
      <p>Welcome back, {user.name ?? user.email}</p>
    </main>
  )
}

در اینجا چون صفحه یک Server Component است، خیلی راحت می‌توانیم قبل از render شدن، session را بررسی کنیم.

اگر کاربر لاگین نکرده باشد:

redirect('/admin/login')

و در نتیجه اصلاً محتوای پنل برای او render نمی‌شود.

این یکی از تمیزترین روش‌ها در App Router است.


محافظت از همه صفحات ادمین با layout

اگر قرار باشد در هر صفحه ادمین این بررسی را دوباره تکرار کنیم، کد تکراری می‌شود.

برای همین بهتر است کل بخش /admin را با یک layout محافظت کنیم.

مثلاً:

app/admin/layout.tsx

کد:

import { ReactNode } from 'react'
import { redirect } from 'next/navigation'
import { getCurrentAdminUser } from '@/lib/auth/session'

type AdminLayoutProps = {
  children: ReactNode
}

export default async function AdminLayout({
  children,
}: AdminLayoutProps) {
  const user = await getCurrentAdminUser()

  if (!user) {
    redirect('/admin/login')
  }

  return (
    <div>
      <aside>
        <p>{user.name ?? user.email}</p>
        <nav>
          <a href="/admin">Dashboard</a>
          <a href="/admin/projects">Projects</a>
          <a href="/admin/posts">Posts</a>
        </nav>
      </aside>

      <main>{children}</main>
    </div>
  )
}

حالا هر صفحه‌ای که داخل /admin باشد، قبل از render شدن از این لایه عبور می‌کند.

این یعنی:

  • فقط یک‌بار check می‌نویسیم
  • کل ناحیه ادمین محافظت می‌شود
  • ساختار داشبورد منظم‌تر می‌شود

صفحه login باید از layout محافظت‌شده جدا باشد

یک نکته مهم اینجاست:

اگر /admin/login هم داخل همین layout قرار بگیرد، کاربر مهمان هرگز نمی‌تواند صفحه login را ببیند، چون قبل از آن redirect می‌شود.

پس بهتر است ساختار مسیرها را جدا کنیم.

مثلاً:

app/
  admin/
    login/
      page.tsx
    (dashboard)/
      layout.tsx
      page.tsx
      projects/
        page.tsx

در این ساختار:

  • /admin/login بیرون از layout محافظت‌شده است
  • بقیه صفحات ادمین داخل گروه (dashboard) قرار می‌گیرند

مثال:

/admin/login
/admin
/admin/projects
/admin/posts

اما از نظر فایل‌بندی:

app/admin/login/page.tsx
app/admin/(dashboard)/layout.tsx
app/admin/(dashboard)/page.tsx
app/admin/(dashboard)/projects/page.tsx

این یکی از تمیزترین ساختارها برای پنل ادمین در App Router است.


محافظت از APIهای ادمین

فقط pageها را نباید محافظت کنیم.
endpointها هم باید بررسی session داشته باشند.

مثلاً در POST /api/projects:

import { NextResponse } from 'next/server'
import { getCurrentAdminUser } from '@/lib/auth/session'

export async function POST(request: Request) {
  const user = await getCurrentAdminUser()

  if (!user) {
    return NextResponse.json(
      {
        success: false,
        error: 'Unauthorized',
      },
      { status: 401 }
    )
  }

  // ادامه منطق ساخت پروژه
}

همین الگو را برای PATCH و DELETE هم تکرار می‌کنیم.

این نکته بسیار مهم است:

حتی اگر صفحه ادمین محافظت شده باشد، API باید مستقل از UI هم محافظت شود.

چون هر کسی می‌تواند خارج از UI هم به endpoint درخواست بزند.


آیا از proxy هم استفاده کنیم؟

در Next.js جدید می‌توان از proxy.ts هم برای بعضی بررسی‌های اولیه استفاده کرد.
مثلاً برای جلوگیری سریع از دسترسی مهمان به /admin.

اما در این مرحله از آموزش، بهتر است اول مدل واضح و مستقیم را با layout و helper session یاد بگیریم.

یعنی:

  • محافظت صفحه با layout
  • محافظت API با check داخل Route Handler

بعداً اگر بخواهی، می‌توانی proxy را هم برای redirect سریع‌تر یا کنترل مرکزی‌تر اضافه کنی.
اما حتی در آن حالت هم نباید بررسی داخل API را حذف کنی.


layout اختصاصی ادمین

وقتی auth را اضافه می‌کنیم، معمولاً بهتر است برای ادمین یک layout جداگانه داشته باشیم.

چرا؟

چون UI ادمین با UI عمومی سایت متفاوت است.

مثلاً در layout ادمین معمولاً این بخش‌ها را داریم:

  • sidebar
  • topbar
  • اطلاعات ادمین
  • لینک خروج
  • منوی مدیریت پروژه‌ها و پست‌ها

نمونه ساده:

import Link from 'next/link'
import { ReactNode } from 'react'
import { redirect } from 'next/navigation'
import { getCurrentAdminUser } from '@/lib/auth/session'

type AdminLayoutProps = {
  children: ReactNode
}

export default async function AdminLayout({
  children,
}: AdminLayoutProps) {
  const user = await getCurrentAdminUser()

  if (!user) {
    redirect('/admin/login')
  }

  return (
    <div className="min-h-screen grid grid-cols-[240px_1fr]">
      <aside className="border-r p-6">
        <div className="mb-6">
          <p className="font-medium">{user.name ?? 'Admin'}</p>
          <p className="text-sm text-gray-400">{user.email}</p>
        </div>

        <nav className="space-y-3">
          <Link href="/admin">Dashboard</Link>
          <Link href="/admin/projects">Projects</Link>
          <Link href="/admin/posts">Posts</Link>
        </nav>
      </aside>

      <main className="p-8">{children}</main>
    </div>
  )
}

این ساختار هم از نظر UX بهتر است، هم از نظر معماری.


تجربه کاربری در عدم دسترسی

از نظر فنی می‌توانیم کاربر مهمان را مستقیم redirect کنیم.
این روش در بسیاری از مسیرهای private بهترین انتخاب است.

اما از نظر UX باید مشخص کنیم در هر سناریو چه رفتاری می‌خواهیم:

سناریوی ۱: کاربر مهمان وارد /admin می‌شود

بهترین رفتار:

redirect('/admin/login')

سناریوی ۲: کاربر login کرده ولی session نامعتبر شده

بهترین رفتار:

  • حذف session
  • redirect به login
  • نمایش پیام مناسب در صفحه login

سناریوی ۳: API بدون auth صدا زده می‌شود

بهترین رفتار:

{
  "success": false,
  "error": "Unauthorized"
}

با status:

401

سناریوی ۴: کاربر دسترسی کافی ندارد

اگر بعداً چند نقش تعریف کردی، باید از 403 Forbidden استفاده کنی.
اما فعلاً چون پروژه فقط ادمین دارد، بیشتر با 401 سروکار داریم.


جلوگیری از دسترسی ادمین به صفحه login بعد از ورود

یک بهبود خوب UX این است که اگر ادمین قبلاً login کرده، دیگر صفحه /admin/login را نبیند و مستقیم به داشبورد برود.

مثلاً در صفحه login:

// app/admin/login/page.tsx

import { redirect } from 'next/navigation'
import { getCurrentAdminUser } from '@/lib/auth/session'
import { AdminLoginForm } from '@/components/forms/admin-login-form'

export default async function AdminLoginPage() {
  const user = await getCurrentAdminUser()

  if (user) {
    redirect('/admin')
  }

  return (
    <main>
      <h1>Admin Login</h1>
      <AdminLoginForm />
    </main>
  )
}

این باعث می‌شود تجربه کاربری منطقی‌تر شود.


ساخت helper برای نیازهای بعدی

بعد از این قسمت، معمولاً خوب است دو helper مجزا داشته باشیم:

getCurrentAdminUser()
requireAdmin()

مثلاً:

// lib/auth/session.ts

import { redirect } from 'next/navigation'
import { cookies } from 'next/headers'
import { prisma } from '@/lib/prisma'

export async function getCurrentAdminUser() {
  const cookieStore = await cookies()
  const session = cookieStore.get('admin_session')

  if (!session?.value) {
    return null
  }

  return prisma.adminUser.findUnique({
    where: {
      id: session.value,
    },
  })
}

export async function requireAdmin() {
  const user = await getCurrentAdminUser()

  if (!user) {
    redirect('/admin/login')
  }

  return user
}

حالا در pageها می‌توانیم این‌طور استفاده کنیم:

const user = await requireAdmin()

این کد خواناتر می‌شود و تکرار کمتر می‌شود.


جمع‌بندی

در این قسمت، مسیرهای ادمین را برای کاربر احراز هویت‌نشده بستیم و ساختار محافظت از صفحات را طراحی کردیم.

یاد گرفتیم که:

  • همه مسیرهای زیر /admin باید محافظت شوند
  • endpointهای مدیریتی هم باید private باشند
  • برای بررسی auth، می‌توانیم session را از cookie بخوانیم
  • با یک helper مثل getCurrentAdminUser() می‌شود کار را تمیزتر کرد
  • در Server Component خیلی راحت می‌توانیم قبل از render شدن، کاربر را بررسی کنیم
  • با layout می‌توانیم کل ناحیه ادمین را یک‌جا محافظت کنیم
  • صفحه login باید بیرون از layout محافظت‌شده قرار بگیرد
  • در صورت نبود دسترسی، بهترین UX معمولاً redirect به login است

تا اینجا سه لایه مهم را ساختیم:

Auth Design
Login with bcrypt
Protected Admin Routes

یعنی حالا می‌توانی داشبورد مدیریتی را بسازیم، چون هم login داریم، هم session داریم، هم route protection.

// 0 comments
#Next.js#ProtectedRoute#آموزش

نظرات (0)

نظر خود را بنویسید