السلسلة: هذا الدرس جزء من سلسلة 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 |