إضافة المصادقة لتطبيق Next.js 15 باستخدام Auth.js v5: البريد الإلكتروني وOAuth والتحكم بالأدوار

AI Bot
بواسطة AI Bot ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

المصادقة هي حارس البوابة لكل تطبيق ويب جاد. سواء كنت تبني لوحة تحكم SaaS أو منصة تجارة إلكترونية أو أداة داخلية، فأنت بحاجة لنظام مصادقة آمن وقابل للصيانة. Auth.js v5 (المعروف سابقاً بـ NextAuth.js) هو المعيار الفعلي للمصادقة في بيئة Next.js — ومع إعادة كتابة الإصدار الخامس، أصبح أسرع وأبسط وأقوى من أي وقت مضى.

في هذا الدليل التعليمي، ستبني نظام مصادقة كاملاً من الصفر يشمل:

  • تسجيل الدخول عبر Google OAuth
  • المصادقة بالبريد الإلكتروني وكلمة المرور
  • إدارة الجلسات باستخدام JWT
  • حماية المسارات عبر middleware
  • التحكم بالوصول حسب الأدوار (RBAC)

لماذا Auth.js v5؟ يقدم الإصدار الخامس دعماً مدمجاً لـ App Router ومتوافقاً مع Edge Runtime وواجهة برمجية مبسطة وحماية CSRF مدمجة. إذا كنت تبدأ مشروع Next.js جديداً في 2026، فإن Auth.js v5 هو الخيار الموصى به.

المتطلبات المسبقة

قبل البدء، تأكد من توفر:

  • Node.js 18+ مثبت على جهازك
  • مشروع Next.js 15 (App Router)
  • حساب في Google Cloud Console (لـ OAuth)
  • معرفة أساسية بـ TypeScript و React Server Components

ما الذي ستبنيه

تطبيق Next.js 15 يتضمن:

  1. صفحة تسجيل دخول تدعم Google OAuth والبريد الإلكتروني/كلمة المرور
  2. لوحة تحكم محمية لا يمكن الوصول إليها إلا للمستخدمين المصادق عليهم
  3. لوحة إدارة مقيدة للمستخدمين ذوي دور admin
  4. Middleware يعيد توجيه المستخدمين غير المصادق عليهم تلقائياً

الخطوة 1: إعداد مشروع Next.js

إذا لم يكن لديك مشروع بعد، أنشئ واحداً:

npx create-next-app@latest my-auth-app --typescript --tailwind --app --src-dir
cd my-auth-app

ثبّت الحزم المطلوبة:

npm install next-auth@beta @auth/prisma-adapter @prisma/client prisma bcryptjs zod
npm install -D @types/bcryptjs

وظيفة كل حزمة:

الحزمةالغرض
next-auth@betaAuth.js v5 لـ Next.js
@auth/prisma-adapterحفظ المستخدمين والجلسات في قاعدة البيانات
prismaORM لقاعدة البيانات
bcryptjsتشفير كلمات المرور
zodالتحقق من صحة البيانات

الخطوة 2: إعداد Prisma

هيئ Prisma مع قاعدة البيانات المفضلة لديك (سنستخدم PostgreSQL):

npx prisma init --datasource-provider postgresql

حدّث ملف prisma/schema.prisma بنماذج Auth.js:

generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
 
model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  passwordHash  String?
  role          String    @default("user")
  accounts      Account[]
  sessions      Session[]
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
}
 
model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String? @db.Text
  access_token      String? @db.Text
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String? @db.Text
  session_state     String?
  user              User    @relation(fields: [userId], references: [id], onDelete: Cascade)
 
  @@unique([provider, providerAccountId])
}
 
model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}
 
model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime
 
  @@unique([identifier, token])
}

لاحظ حقلي passwordHash و role في نموذج User — هذه إضافات مخصصة لتسجيل الدخول بالبيانات الاعتمادية والتحكم بالأدوار.

شغّل عملية الترحيل:

npx prisma migrate dev --name init
npx prisma generate

أنشئ عميل Prisma في src/lib/prisma.ts:

import { PrismaClient } from "@prisma/client"
 
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}
 
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
 
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma

الخطوة 3: إعداد Auth.js

أنشئ ملف الإعداد الرئيسي لـ Auth.js في src/auth.ts:

import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import Credentials from "next-auth/providers/credentials"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/lib/prisma"
import bcrypt from "bcryptjs"
import { z } from "zod"
 
const signInSchema = z.object({
  email: z.string().email("عنوان بريد إلكتروني غير صالح"),
  password: z.string().min(8, "كلمة المرور يجب أن تكون 8 أحرف على الأقل"),
})
 
export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID!,
      clientSecret: process.env.AUTH_GOOGLE_SECRET!,
      profile(profile) {
        return {
          id: profile.sub,
          name: profile.name,
          email: profile.email,
          image: profile.picture,
          role: "user",
        }
      },
    }),
    Credentials({
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        const parsed = signInSchema.safeParse(credentials)
        if (!parsed.success) return null
 
        const { email, password } = parsed.data
 
        const user = await prisma.user.findUnique({
          where: { email },
        })
 
        if (!user || !user.passwordHash) return null
 
        const isValid = await bcrypt.compare(password, user.passwordHash)
        if (!isValid) return null
 
        return {
          id: user.id,
          name: user.name,
          email: user.email,
          image: user.image,
          role: user.role,
        }
      },
    }),
  ],
  session: {
    strategy: "jwt",
    maxAge: 30 * 24 * 60 * 60,
  },
  pages: {
    signIn: "/auth/signin",
    error: "/auth/error",
  },
  callbacks: {
    jwt({ token, user }) {
      if (user) {
        token.id = user.id
        token.role = (user as any).role ?? "user"
      }
      return token
    },
    session({ session, token }) {
      if (session.user) {
        session.user.id = token.id as string
        ;(session.user as any).role = token.role as string
      }
      return session
    },
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user
      const isOnDashboard = nextUrl.pathname.startsWith("/dashboard")
      const isOnAdmin = nextUrl.pathname.startsWith("/admin")
 
      if (isOnDashboard || isOnAdmin) {
        if (isLoggedIn) return true
        return false
      }
      return true
    },
  },
})

ما يحدث هنا:

  1. موفر Google يتعامل مع OAuth مع تعيين تلقائي للملف الشخصي
  2. موفر البيانات الاعتمادية يتحقق من البريد/كلمة المرور باستخدام Zod و bcrypt
  3. استدعاءات JWT تحفظ دور المستخدم في الرمز المميز
  4. استدعاء الجلسة يكشف الدور لمكونات العميل
  5. استدعاء التفويض يحمي مسارات /dashboard و /admin

الخطوة 4: إعداد مسار API

أنشئ مسار API لـ Auth.js في src/app/api/auth/[...nextauth]/route.ts:

import { handlers } from "@/auth"
 
export const { GET, POST } = handlers

هذا كل شيء — Auth.js v5 يجعل هذا نظيفاً للغاية.

الخطوة 5: إضافة Middleware لحماية المسارات

أنشئ src/middleware.ts:

export { auth as default } from "@/auth"
 
export const config = {
  matcher: [
    "/dashboard/:path*",
    "/admin/:path*",
    "/api/protected/:path*",
  ],
}

هذا الـ middleware يشغّل استدعاء authorized من إعدادات Auth.js على كل مسار مطابق. المستخدمون غير المصادق عليهم يُعاد توجيههم تلقائياً لصفحة تسجيل الدخول.

الخطوة 6: توسيع أنواع TypeScript

للحصول على تصنيف صحيح لـ session.user.role، أنشئ src/types/next-auth.d.ts:

import { DefaultSession } from "next-auth"
 
declare module "next-auth" {
  interface Session {
    user: {
      id: string
      role: string
    } & DefaultSession["user"]
  }
 
  interface User {
    role?: string
  }
}
 
declare module "next-auth/jwt" {
  interface JWT {
    id: string
    role: string
  }
}

الخطوة 7: بناء صفحة تسجيل الدخول

أنشئ صفحة تسجيل دخول مخصصة في src/app/auth/signin/page.tsx:

import { signIn, auth } from "@/auth"
import { redirect } from "next/navigation"
 
export default async function SignInPage() {
  const session = await auth()
  if (session) redirect("/dashboard")
 
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md w-full space-y-8 p-8 bg-white rounded-xl shadow-lg">
        <div className="text-center">
          <h1 className="text-3xl font-bold text-gray-900">مرحباً بعودتك</h1>
          <p className="mt-2 text-gray-600">سجّل الدخول إلى حسابك</p>
        </div>
 
        {/* Google OAuth */}
        <form
          action={async () => {
            "use server"
            await signIn("google", { redirectTo: "/dashboard" })
          }}
        >
          <button
            type="submit"
            className="w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
          >
            المتابعة مع Google
          </button>
        </form>
 
        <div className="relative">
          <div className="absolute inset-0 flex items-center">
            <div className="w-full border-t border-gray-300" />
          </div>
          <div className="relative flex justify-center text-sm">
            <span className="px-2 bg-white text-gray-500">أو المتابعة بالبريد الإلكتروني</span>
          </div>
        </div>
 
        {/* البريد الإلكتروني/كلمة المرور */}
        <form
          action={async (formData) => {
            "use server"
            await signIn("credentials", {
              email: formData.get("email"),
              password: formData.get("password"),
              redirectTo: "/dashboard",
            })
          }}
          className="space-y-4"
        >
          <div>
            <label htmlFor="email" className="block text-sm font-medium text-gray-700">
              البريد الإلكتروني
            </label>
            <input
              id="email"
              name="email"
              type="email"
              required
              className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
              placeholder="you@example.com"
            />
          </div>
          <div>
            <label htmlFor="password" className="block text-sm font-medium text-gray-700">
              كلمة المرور
            </label>
            <input
              id="password"
              name="password"
              type="password"
              required
              minLength={8}
              className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
            />
          </div>
          <button
            type="submit"
            className="w-full py-3 px-4 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors"
          >
            تسجيل الدخول
          </button>
        </form>
 
        <p className="text-center text-sm text-gray-600">
          ليس لديك حساب؟{" "}
          <a href="/auth/register" className="text-blue-600 hover:underline">
            أنشئ حساباً
          </a>
        </p>
      </div>
    </div>
  )
}

الخطوة 8: إنشاء نقطة نهاية التسجيل

أنشئ server action لتسجيل المستخدم في src/app/auth/register/actions.ts:

"use server"
 
import { prisma } from "@/lib/prisma"
import bcrypt from "bcryptjs"
import { z } from "zod"
 
const registerSchema = z.object({
  name: z.string().min(2, "الاسم يجب أن يكون حرفين على الأقل"),
  email: z.string().email("عنوان بريد إلكتروني غير صالح"),
  password: z.string().min(8, "كلمة المرور يجب أن تكون 8 أحرف على الأقل"),
})
 
export async function register(formData: FormData) {
  const parsed = registerSchema.safeParse({
    name: formData.get("name"),
    email: formData.get("email"),
    password: formData.get("password"),
  })
 
  if (!parsed.success) {
    return { error: parsed.error.errors[0].message }
  }
 
  const { name, email, password } = parsed.data
 
  const existingUser = await prisma.user.findUnique({
    where: { email },
  })
 
  if (existingUser) {
    return { error: "يوجد حساب بهذا البريد الإلكتروني بالفعل" }
  }
 
  const passwordHash = await bcrypt.hash(password, 12)
 
  await prisma.user.create({
    data: {
      name,
      email,
      passwordHash,
      role: "user",
    },
  })
 
  return { success: true }
}

الخطوة 9: بناء لوحة تحكم محمية

أنشئ لوحة تحكم في src/app/dashboard/page.tsx:

import { auth, signOut } from "@/auth"
import { redirect } from "next/navigation"
 
export default async function DashboardPage() {
  const session = await auth()
 
  if (!session?.user) redirect("/auth/signin")
 
  return (
    <div className="min-h-screen bg-gray-50 p-8">
      <div className="max-w-4xl mx-auto">
        <div className="flex items-center justify-between mb-8">
          <h1 className="text-3xl font-bold text-gray-900">لوحة التحكم</h1>
          <form
            action={async () => {
              "use server"
              await signOut({ redirectTo: "/" })
            }}
          >
            <button
              type="submit"
              className="px-4 py-2 text-sm text-red-600 border border-red-300 rounded-lg hover:bg-red-50 transition-colors"
            >
              تسجيل الخروج
            </button>
          </form>
        </div>
 
        <div className="bg-white rounded-xl shadow-sm p-6">
          <div className="flex items-center gap-4">
            {session.user.image && (
              <img
                src={session.user.image}
                alt=""
                className="w-16 h-16 rounded-full"
              />
            )}
            <div>
              <h2 className="text-xl font-semibold">{session.user.name}</h2>
              <p className="text-gray-600">{session.user.email}</p>
              <span className="inline-block mt-1 px-2 py-0.5 text-xs font-medium bg-blue-100 text-blue-800 rounded-full">
                {(session.user as any).role}
              </span>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}

الخطوة 10: تطبيق التحكم بالوصول حسب الأدوار

أنشئ صفحة خاصة بالمسؤول في src/app/admin/page.tsx:

import { auth } from "@/auth"
import { redirect } from "next/navigation"
 
export default async function AdminPage() {
  const session = await auth()
 
  if (!session?.user) redirect("/auth/signin")
  if ((session.user as any).role !== "admin") redirect("/dashboard")
 
  return (
    <div className="min-h-screen bg-gray-50 p-8">
      <div className="max-w-4xl mx-auto">
        <div className="flex items-center gap-3 mb-8">
          <span className="px-3 py-1 text-sm font-medium bg-red-100 text-red-800 rounded-full">
            المسؤولون فقط
          </span>
          <h1 className="text-3xl font-bold text-gray-900">لوحة الإدارة</h1>
        </div>
 
        <div className="bg-white rounded-xl shadow-sm p-6">
          <h2 className="text-lg font-semibold mb-4">إدارة المستخدمين</h2>
          <p className="text-gray-600">
            هذه الصفحة متاحة فقط للمستخدمين الذين يحملون دور المسؤول.
            يمكنك إضافة وظائف إدارة المستخدمين هنا.
          </p>
        </div>
      </div>
    </div>
  )
}

لفرض هذا على مستوى الـ middleware أيضاً، حدّث استدعاء authorized في src/auth.ts:

authorized({ auth, request: { nextUrl } }) {
  const isLoggedIn = !!auth?.user
  const isOnAdmin = nextUrl.pathname.startsWith("/admin")
 
  if (isOnAdmin) {
    if (!isLoggedIn) return false
    if ((auth?.user as any)?.role !== "admin") {
      return Response.redirect(new URL("/dashboard", nextUrl))
    }
    return true
  }
 
  if (nextUrl.pathname.startsWith("/dashboard")) {
    return isLoggedIn
  }
 
  return true
},

الخطوة 11: إعداد متغيرات البيئة

أنشئ ملف .env.local:

# قاعدة البيانات
DATABASE_URL="postgresql://user:password@localhost:5432/myauthdb"
 
# Auth.js
AUTH_SECRET="your-random-secret-here"
AUTH_URL="http://localhost:3000"
 
# Google OAuth
AUTH_GOOGLE_ID="your-google-client-id"
AUTH_GOOGLE_SECRET="your-google-client-secret"

الحصول على بيانات اعتماد Google OAuth

  1. اذهب إلى Google Cloud Console
  2. أنشئ مشروعاً جديداً أو اختر مشروعاً موجوداً
  3. انتقل إلى APIs & Services > Credentials
  4. انقر Create Credentials > OAuth client ID
  5. اختر Web application
  6. أضف رابط إعادة التوجيه: http://localhost:3000/api/auth/callback/google
  7. انسخ Client ID و Client Secret إلى ملف .env.local

أنشئ مفتاح auth السري:

npx auth secret

اختبار التطبيق

شغّل خادم التطوير:

npm run dev

اختبر التدفقات التالية:

  1. Google OAuth: انقر "المتابعة مع Google" في صفحة تسجيل الدخول
  2. التسجيل: أنشئ حساباً جديداً عبر /auth/register
  3. تسجيل الدخول بالبيانات الاعتمادية: سجّل الدخول بالبريد وكلمة المرور المسجلين
  4. المسارات المحمية: جرّب الوصول لـ /dashboard بدون تسجيل الدخول
  5. وصول المسؤول: غيّر دور مستخدم لـ admin في قاعدة البيانات وتحقق من وصول /admin

استكشاف الأخطاء وإصلاحها

خطأ "CSRF token mismatch"

تأكد أن AUTH_URL في .env.local يطابق الرابط الفعلي الذي تصل إليه.

إعادة توجيه Google OAuth لرابط خاطئ

تحقق أن رابط إعادة التوجيه في Google Cloud Console يطابق تماماً: http://localhost:3000/api/auth/callback/google

الجلسة null في مكونات العميل

استخدم مغلف SessionProvider في layout إذا كنت تحتاج الوصول للجلسة في مكونات العميل.

تسجيل الدخول بالبيانات الاعتمادية يرجع null

تأكد أن المستخدم لديه حقل passwordHash. المستخدمون الذين سجلوا عبر Google OAuth لن يكون لديهم كلمة مرور.

الخطوات التالية

بعد أن أصبح لديك أساس مصادقة صلب، فكّر في:

  • التحقق من البريد الإلكتروني: استخدم موفر البريد في Auth.js مع Resend أو Nodemailer
  • المصادقة الثنائية: أضف 2FA باستخدام TOTP
  • موفرون اجتماعيون إضافيون: أضف GitHub أو Discord أو Apple
  • تحديد معدل الطلبات: احمِ نقطة نهاية تسجيل الدخول من الهجمات
  • سجل المراجعة: تتبع أحداث تسجيل الدخول للمراقبة الأمنية

الخلاصة

لقد بنيت نظام مصادقة جاهزاً للإنتاج باستخدام Auth.js v5 و Next.js 15 يتضمن:

  • مصادقة مزدوجة: Google OAuth وبيانات اعتماد البريد الإلكتروني/كلمة المرور
  • مستخدمون مخزنون في قاعدة بيانات: Prisma مع PostgreSQL
  • جلسات JWT: إدارة جلسات سريعة وبدون حالة
  • حماية المسارات: تحكم بالوصول عبر middleware
  • RBAC: تفويض حسب الأدوار على مستوى الصفحة والـ middleware

واجهة Auth.js v5 المبسطة تجعل من السهل جداً إضافة مصادقة بمستوى المؤسسات لتطبيقات Next.js. الجمع بين Server Components و Server Actions والـ middleware يمنحك طبقات أمان متعددة دون التضحية بتجربة المطور.


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على 11 أساسيات Laravel 11: توليد الروابط.

ناقش مشروعك معنا

نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة

بناء مشروع SaaS متكامل باستخدام Next.js 15 و Stripe و Auth.js v5

تعلم كيفية بناء تطبيق SaaS جاهز للإنتاج باستخدام Next.js 15 مع نظام اشتراكات Stripe ومصادقة Auth.js v5. يغطي هذا الدليل التفصيلي إعداد المشروع وتسجيل الدخول عبر OAuth وخطط الأسعار ومعالجة Webhooks والمسارات المحمية.

35 د قراءة·