تطوير الويب

Auth.js v5 وClerk لـ Next.js 15: درس تكامل

5 min de lecture

السلسلة: هذا الدرس جزء من سلسلة React 19 وNext.js 15. اقرأ المقال الرئيسي.

هيمنتان في نظام Next.js 15: Auth.js v5 (تطوّر NextAuth مفتوح المصدر) لمن يريد إبقاء السيطرة، وClerk لمن يفضّل تفويض طبقة الهوية لخدمة مُدارة. هذا الدرس يركّب الاثنين على تطبيق ديمو ثم يعطي معايير الاختيار.

Auth.js v5 يطلب ساعتين إلى ثلاث للإعداد الأولي لكنه مجاني تشغيلياً ويُبقي كل شيء عندك. Clerk يُنشر في 15 دقيقة مع UI جاهز لكنه يصبح مدفوعاً فوق 10 000 مستخدم نشط شهرياً.

المتطلبات

  • Next.js 15.x مع App Router
  • قاعدة PostgreSQL متاحة
  • Prisma مثبَّت لمسار Auth.js
  • 90-120 دقيقة

المسار A — Auth.js v5

الخطوة 1 — التثبيت وملف الإعداد

pnpm add next-auth@beta @auth/prisma-adapter
pnpm add -D @types/bcrypt
pnpm add bcrypt zod
// src/auth.ts
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
import Credentials from 'next-auth/providers/credentials';
import { PrismaAdapter } from '@auth/prisma-adapter';
import { prisma } from '@/lib/prisma';
import bcrypt from 'bcrypt';
import { z } from 'zod';

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(prisma),
  session: { strategy: 'jwt' },
  pages: { signIn: '/login' },
  providers: [
    GitHub,
    Credentials({
      credentials: { email: {}, password: {} },
      async authorize(creds) {
        const schema = z.object({ email: z.string().email(), password: z.string().min(6) });
        const parsed = schema.safeParse(creds);
        if (!parsed.success) return null;
        const user = await prisma.user.findUnique({ where: { email: parsed.data.email } });
        if (!user || !user.passwordHash) return null;
        const ok = await bcrypt.compare(parsed.data.password, user.passwordHash);
        return ok ? { id: user.id, email: user.email, name: user.name } : null;
      },
    }),
  ],
});

strategy: 'jwt' يخزّن الجلسة في cookie موقَّع جانب العميل بدل قاعدة — أسرع وضروري إن أردت قراءة الجلسة من middleware Edge.

الخطوة 2 — متغيّرات البيئة وschema Prisma

# .env.local
AUTH_SECRET=...          # openssl rand -base64 32
AUTH_GITHUB_ID=...
AUTH_GITHUB_SECRET=...
DATABASE_URL=postgres://...
// prisma/schema.prisma — مقتطف
model User {
  id            String    @id @default(cuid())
  email         String    @unique
  name          String?
  image         String?
  passwordHash  String?
  emailVerified DateTime?
  accounts      Account[]
  sessions      Session[]
}

model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  user              User    @relation(fields: [userId], references: [id], onDelete: Cascade)
  @@unique([provider, providerAccountId])
}

pnpm prisma migrate dev --name init-auth لتطبيق Schema.

الخطوة 3 — Route handler وmiddleware

// src/app/api/auth/[...nextauth]/route.ts
export { GET, POST } from '@/auth';

// src/middleware.ts
import { auth } from '@/auth';
import { NextResponse } from 'next/server';

export default auth((req) => {
  const isProtected = req.nextUrl.pathname.startsWith('/compte');
  if (isProtected && !req.auth) {
    return NextResponse.redirect(new URL('/login', req.url));
  }
});

export const config = { matcher: ['/compte/:path*'] };

// src/app/compte/page.tsx
import { auth } from '@/auth';

export default async function Compte() {
  const session = await auth();
  return <p>Bonjour {session?.user?.name}</p>;
}

المسار B — Clerk

الخطوة 4 — التثبيت وClerkProvider

pnpm add @clerk/nextjs

# .env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
// src/app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <ClerkProvider>
      <html lang="fr">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  );
}

الخطوة 5 — مكوّنات جاهزة

// src/app/login/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs';

export default function LoginPage() {
  return <SignIn />;
}

// header
import { SignedIn, SignedOut, UserButton, SignInButton } from '@clerk/nextjs';

<header className="flex justify-between p-4 border-b">
  <a href="/">Boutique</a>
  <div>
    <SignedIn><UserButton /></SignedIn>
    <SignedOut><SignInButton /></SignedOut>
  </div>
</header>

الخطوة 6 — middleware وauth() جانب الخادم

// src/middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

const isProtected = createRouteMatcher(['/compte(.*)', '/admin(.*)']);

export default clerkMiddleware(async (auth, req) => {
  if (isProtected(req)) await auth.protect();
});

export const config = {
  matcher: ['/((?!_next|[^?]*\\.(?:html?|css|js|jpg|png|svg|gif|webp|ico)).*)', '/(api|trpc)(.*)', '/__clerk/(.*)'],
};

// src/app/compte/page.tsx
import { auth, currentUser } from '@clerk/nextjs/server';

export default async function Compte() {
  const { userId } = await auth();
  if (!userId) return <p>Non connecte</p>;
  const user = await currentUser();
  return <p>Bonjour {user?.firstName}</p>;
}

الخطوة 7 — كيف نفصل بين الاثنين

السؤال Auth.js Clerk
الميزانية الشهرية للـ auth؟ 0 يورو مجاناً حتى 10k MAU، ~25 USD/شهر بعدها
حجم المستخدمين المتوقَّع؟ مثالي > 50k مثالي < 50k
الحاجة إلى UI جاهز (sign-in، MFA، تنظيمات)؟ تُكتب يدوياً مقدَّم
سيادة بيانات المستخدم؟ كل شيء عندك مستضاف عند Clerk (USA أساساً)
الوقت المتاح للإعداد؟ 2-4 ساعات + صيانة 15 دقيقة + صفر صيانة
الحاجة لتخصيص flow auth؟ كاملة محدودة (لكن متنامية)

حالات نمطية: B2C عام جماهيري بحجم كبير ← Auth.js. B2B SaaS early-stage ← Clerk. منصة بمستخدمين حساسين (صحة، مالية) حيث تهمّ السيادة ← Auth.js. Side-project للإطلاق في عطلة نهاية الأسبوع ← Clerk.

الخطوة 8 — RBAC مع Auth.js

// src/auth.ts — تمديد
export const { handlers, auth, signIn, signOut } = NextAuth({
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        const dbUser = await prisma.user.findUnique({
          where: { id: user.id },
          select: { role: true },
        });
        token.role = dbUser?.role ?? 'user';
      }
      return token;
    },
    async session({ session, token }) {
      if (session.user) session.user.role = token.role as string;
      return session;
    },
  },
});

عرّض أنواع TypeScript بـ src/types/next-auth.d.ts. بعدها session.user.role متاح في كل التطبيق. للتحكم الدقيق (محرّر يعدّل مقالاته فقط)، اجمع الرول مع تحقّق ownership داخل Server Action.

الخطوة 9 — تنظيمات multi-tenant مع Clerk

// src/app/admin/page.tsx
import { auth } from '@clerk/nextjs/server';

export default async function Admin() {
  const { userId, orgId, orgRole } = await auth();
  if (!userId) return <p>Non connecte</p>;
  if (orgRole !== 'admin') return <p>Reserve aux admins de l'organisation</p>;
  return <p>Dashboard admin de l'organisation {orgId}</p>;
}

مكوّن <OrganizationSwitcher /> يسمح للمستخدم بالتبديل بين تنظيماته بلا إعادة تحميل. <OrganizationProfile /> يوفّر UI الإدارة (أعضاء، دعوات، إعدادات).

الخطوة 10 — استراتيجية هجرة

ابدأ على Clerk للتثبّت السريع من السوق، ثم هاجر إلى Auth.js إن برّر الحجم العمل. الهجرة قابلة لكنها تطلب خطة بأربع خطوات: تصدير المستخدمين عبر Clerk API، إنشاء سجلات مقابلة في القاعدة عبر PrismaAdapter، سكربت إعادة تحقق إيميل (hash كلمات السرّ من Clerk غير قابلة للتصدير، إعادة تعيين إلزامية للحسابات Credentials)، وتبديل الكود تدريجياً.

أخطاء شائعة

الخطأ السبب الحل
«AUTH_SECRET is required» عند الإقلاع متغيّر مفقود ولّد بـ openssl rand -base64 32
زر GitHub يُرجع 400 على callback URL callback سيئ في GitHub OAuth App تحقّق https://votre-domaine/api/auth/callback/github مُسجَّل
الجلسة دائماً null بعد login cookie غير مُرسَل في HTTPS في prod، تحقّق Secure، النطاق، AUTH_URL صحيح
Clerk: «Publishable key required» متغيّر بلا بادئة NEXT_PUBLIC_ بادئة NEXT_PUBLIC_ للمفتاح العام إلزامية
middleware Clerk يحجب الأصول الثابتة matcher واسع أعد matcher الدقيق من التوثيق
متغيّر AUTH_URL منسي في الإنتاج v5 يستخدم AUTH_URL لا NEXTAUTH_URL من v4 عرّفه في متغيّرات Vercel أو Coolify

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

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité