الدليل الشامل لمصادقة Clerk في Next.js 15 مع المؤسسات والتحكم بالوصول

المقدمة
المصادقة هي واحدة من أهم الميزات في أي تطبيق ويب، لكن بناءها من الصفر معقد وعرضة للأخطاء ويستهلك الكثير من الوقت. برزت Clerk كمنصة المصادقة الرائدة لتطبيقات Next.js، حيث تقدم حلاً شاملاً يتجاوز بكثير تدفقات تسجيل الدخول والتسجيل البسيطة.
على عكس NextAuth.js أو تطبيقات JWT المخصصة، توفر Clerk خدمة مُدارة بالكامل مع مكونات واجهة مستخدم قابلة للتضمين، والمصادقة متعددة العوامل، وإدارة المؤسسات، والتحكم بالوصول المبني على الأدوار (RBAC)، ومعالجة الأحداث عبر webhooks — كل ذلك جاهز للاستخدام.
في هذا الدليل، ستقوم ببناء تطبيق SaaS متعدد المستأجرين مع Clerk و Next.js 15، مع تنفيذ المصادقة والتبديل بين المؤسسات والصلاحيات المبنية على الأدوار ومسارات API المحمية.
ما ستتعلمه
- إعداد Clerk في مشروع Next.js 15 App Router
- تنفيذ تدفقات التسجيل وتسجيل الدخول والملف الشخصي
- حماية المسارات بوسيط Clerk
- إنشاء وإدارة المؤسسات (تعدد المستأجرين)
- تنفيذ التحكم بالوصول المبني على الأدوار (RBAC)
- بناء مسارات API محمية
- معالجة webhooks لأحداث المستخدمين والمؤسسات
- تخصيص مكونات Clerk لتتوافق مع تصميمك
المتطلبات المسبقة
قبل البدء، تأكد من توفر:
- Node.js 18+ مثبت على جهازك
- معرفة أساسية بـ React و TypeScript
- إلمام بـ Next.js App Router
- حساب Clerk (الخطة المجانية متاحة على clerk.com)
- محرر أكواد (VS Code مُوصى به)
ما ستبنيه
ستقوم ببناء لوحة تحكم لإدارة المشاريع متعددة المستأجرين حيث:
- يمكن للمستخدمين التسجيل وتسجيل الدخول بالبريد الإلكتروني أو Google أو GitHub
- يمكن للمستخدمين إنشاء مؤسسات والانضمام إليها
- يمكن لمديري المؤسسات إدارة الأعضاء وتعيين الأدوار
- تظهر عناصر واجهة مختلفة حسب الدور (مدير، عضو، مشاهد)
- مسارات API محمية بناءً على المصادقة والأدوار
الخطوة 1: إنشاء مشروع Next.js
ابدأ بإنشاء مشروع Next.js 15 جديد مع TypeScript و Tailwind CSS:
npx create-next-app@latest clerk-saas-app --typescript --tailwind --eslint --app --src-dir
cd clerk-saas-appثبّت حزمة Clerk SDK لـ Next.js:
npm install @clerk/nextjsالخطوة 2: إعداد Clerk
إنشاء تطبيق Clerk
- اذهب إلى clerk.com وسجّل الدخول إلى لوحة التحكم
- انقر على Create application
- سمّه "SaaS Dashboard"
- فعّل طرق تسجيل الدخول المطلوبة (البريد الإلكتروني، Google، GitHub)
- انسخ مفاتيح API الخاصة بك
إعداد متغيرات البيئة
أنشئ ملف .env.local في جذر مشروعك:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_مفتاحك_العام
CLERK_SECRET_KEY=sk_test_مفتاحك_السري
# روابط إعادة التوجيه في Clerk
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboardاستبدل المفاتيح الوهمية بمفاتيح API الحقيقية من لوحة تحكم Clerk.
الخطوة 3: إضافة ClerkProvider
غلّف تطبيقك بـ ClerkProvider في layout الجذر. هذا يوفر سياق المصادقة لجميع المكونات:
// src/app/layout.tsx
import type { Metadata } from "next";
import { ClerkProvider } from "@clerk/nextjs";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "SaaS Dashboard",
description: "تطبيق SaaS متعدد المستأجرين مع مصادقة Clerk",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<ClerkProvider>
<html lang="ar" dir="rtl">
<body className={inter.className}>{children}</body>
</html>
</ClerkProvider>
);
}الخطوة 4: إعداد الوسيط لحماية المسارات
وسيط Clerk هو العمود الفقري لحماية المسارات. أنشئ ملف middleware.ts في جذر مشروعك:
// src/middleware.ts
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
const isPublicRoute = createRouteMatcher([
"/",
"/sign-in(.*)",
"/sign-up(.*)",
"/api/webhooks(.*)",
]);
export default clerkMiddleware(async (auth, request) => {
if (!isPublicRoute(request)) {
await auth.protect();
}
});
export const config = {
matcher: [
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
"/(api|trpc)(.*)",
],
};هذا التكوين يضمن:
- المسارات العامة (الصفحة الرئيسية، تسجيل الدخول، التسجيل، webhooks) متاحة بدون مصادقة
- جميع المسارات الأخرى تتطلب أن يكون المستخدم مسجل الدخول
- الملفات الثابتة لـ Next.js مستثناة من معالجة الوسيط
الخطوة 5: إنشاء صفحات المصادقة
صفحة تسجيل الدخول
// src/app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from "@clerk/nextjs";
export default function SignInPage() {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-50">
<SignIn
appearance={{
elements: {
rootBox: "mx-auto",
card: "shadow-lg border border-gray-200",
},
}}
/>
</div>
);
}صفحة التسجيل
// src/app/sign-up/[[...sign-up]]/page.tsx
import { SignUp } from "@clerk/nextjs";
export default function SignUpPage() {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-50">
<SignUp
appearance={{
elements: {
rootBox: "mx-auto",
card: "shadow-lg border border-gray-200",
},
}}
/>
</div>
);
}أجزاء المسار catch-all [[...sign-in]] و [[...sign-up]] تسمح لـ Clerk بمعالجة تدفقات المصادقة متعددة الخطوات مثل التحقق من البريد الإلكتروني و MFA ضمن نفس المسار.
الخطوة 6: بناء تخطيط لوحة التحكم
أنشئ تخطيط لوحة تحكم محمي مع شريط تنقل يعرض معلومات المستخدم والتبديل بين المؤسسات:
// src/app/dashboard/layout.tsx
import {
OrganizationSwitcher,
UserButton,
} from "@clerk/nextjs";
import Link from "next/link";
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen bg-gray-50">
<nav className="border-b bg-white px-6 py-3">
<div className="mx-auto flex max-w-7xl items-center justify-between">
<div className="flex items-center gap-6">
<Link href="/dashboard" className="text-xl font-bold">
لوحة التحكم
</Link>
<div className="flex items-center gap-4">
<Link
href="/dashboard"
className="text-sm text-gray-600 hover:text-gray-900"
>
المشاريع
</Link>
<Link
href="/dashboard/members"
className="text-sm text-gray-600 hover:text-gray-900"
>
الأعضاء
</Link>
<Link
href="/dashboard/settings"
className="text-sm text-gray-600 hover:text-gray-900"
>
الإعدادات
</Link>
</div>
</div>
<div className="flex items-center gap-4">
<OrganizationSwitcher
appearance={{
elements: {
rootBox: "flex items-center",
organizationSwitcherTrigger:
"rounded-md border px-3 py-1.5 text-sm",
},
}}
afterCreateOrganizationUrl="/dashboard"
afterSelectOrganizationUrl="/dashboard"
/>
<UserButton afterSignOutUrl="/" />
</div>
</div>
</nav>
<main className="mx-auto max-w-7xl px-6 py-8">{children}</main>
</div>
);
}مكون OrganizationSwitcher يتيح للمستخدمين إنشاء مؤسسات جديدة والتبديل بينها. مكون UserButton يوفر إدارة الملف الشخصي ووظيفة تسجيل الخروج.
الخطوة 7: عرض بيانات المستخدم والمؤسسة
أنشئ الصفحة الرئيسية للوحة التحكم التي تعرض البيانات بناءً على المستخدم والمؤسسة الحاليين:
// src/app/dashboard/page.tsx
import { auth, currentUser } from "@clerk/nextjs/server";
export default async function DashboardPage() {
const { orgId, orgRole } = await auth();
const user = await currentUser();
return (
<div className="space-y-8">
<div>
<h1 className="text-3xl font-bold">
مرحباً بعودتك، {user?.firstName || "مستخدم"}
</h1>
<p className="mt-1 text-gray-500">
إليك نظرة عامة على مساحة عملك.
</p>
</div>
{orgId ? (
<div className="rounded-lg border bg-white p-6 shadow-sm">
<h2 className="text-lg font-semibold">المؤسسة الحالية</h2>
<div className="mt-4 grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-500">معرّف المؤسسة</p>
<p className="font-mono text-sm">{orgId}</p>
</div>
<div>
<p className="text-sm text-gray-500">دورك</p>
<p className="font-medium capitalize">
{orgRole?.replace("org:", "")}
</p>
</div>
</div>
</div>
) : (
<div className="rounded-lg border border-dashed border-gray-300 bg-white p-8 text-center">
<h2 className="text-lg font-semibold">لم يتم اختيار مؤسسة</h2>
<p className="mt-2 text-gray-500">
أنشئ أو انضم إلى مؤسسة لبدء التعاون مع فريقك.
</p>
</div>
)}
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
<StatCard title="المشاريع" value="12" />
<StatCard title="أعضاء الفريق" value="8" />
<StatCard title="المهام النشطة" value="34" />
</div>
</div>
);
}
function StatCard({ title, value }: { title: string; value: string }) {
return (
<div className="rounded-lg border bg-white p-6 shadow-sm">
<p className="text-sm text-gray-500">{title}</p>
<p className="mt-2 text-3xl font-bold">{value}</p>
</div>
);
}لاحظ كيف يتم استدعاء auth() على جانب الخادم للحصول على معرّف المؤسسة الحالية ودورها. هذه البيانات متاحة بدون أي JavaScript على جانب العميل.
الخطوة 8: تفعيل المؤسسات في Clerk
قبل تنفيذ ميزات المؤسسات، فعّلها في لوحة تحكم Clerk:
- اذهب إلى Organizations في الشريط الجانبي للوحة تحكم Clerk
- انقر على Enable organizations
- تحت Roles، سترى الأدوار الافتراضية:
org:adminوorg:member - أضف دوراً مخصصاً:
org:viewerبصلاحيات محدودة
تعريف الصلاحيات المخصصة
في لوحة تحكم Clerk تحت Organizations، ثم Roles and Permissions:
أنشئ هذه الصلاحيات:
org:projects:create— إنشاء مشاريع جديدةorg:projects:read— عرض المشاريعorg:projects:update— تعديل المشاريعorg:projects:delete— حذف المشاريعorg:members:manage— إدارة أعضاء المؤسسة
عيّن الصلاحيات للأدوار:
| الصلاحية | المدير | العضو | المشاهد |
|---|---|---|---|
| org:projects:create | نعم | نعم | لا |
| org:projects:read | نعم | نعم | نعم |
| org:projects:update | نعم | نعم | لا |
| org:projects:delete | نعم | لا | لا |
| org:members:manage | نعم | لا | لا |
الخطوة 9: تنفيذ التحكم بالوصول المبني على الأدوار
التحقق من الصلاحيات على جانب الخادم
أنشئ دالة مساعدة للتحقق من الصلاحيات على الخادم:
// src/lib/auth.ts
import { auth } from "@clerk/nextjs/server";
import { redirect } from "next/navigation";
export async function requireAuth() {
const session = await auth();
if (!session.userId) {
redirect("/sign-in");
}
return session;
}
export async function requireOrg() {
const session = await requireAuth();
if (!session.orgId) {
redirect("/dashboard");
}
return session;
}
export async function checkPermission(permission: string): Promise<boolean> {
const session = await auth();
if (!session.userId || !session.orgId) {
return false;
}
const hasPermission = await session.has({ permission });
return hasPermission;
}مكونات واجهة مبنية على الأدوار
أنشئ مكوناً يعرض المحتوى بشكل شرطي بناءً على دور المستخدم:
// src/components/role-gate.tsx
"use client";
import { useAuth } from "@clerk/nextjs";
type RoleGateProps = {
children: React.ReactNode;
allowedRoles: string[];
fallback?: React.ReactNode;
};
export function RoleGate({
children,
allowedRoles,
fallback = null,
}: RoleGateProps) {
const { orgRole } = useAuth();
if (!orgRole || !allowedRoles.includes(orgRole)) {
return fallback;
}
return children;
}استخدام RoleGate
// src/app/dashboard/projects/page.tsx
import { auth } from "@clerk/nextjs/server";
import { RoleGate } from "@/components/role-gate";
export default async function ProjectsPage() {
const { orgId } = await auth();
if (!orgId) {
return (
<div className="text-center py-12">
<p className="text-gray-500">اختر مؤسسة لعرض المشاريع.</p>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">المشاريع</h1>
<RoleGate allowedRoles={["org:admin", "org:member"]}>
<button className="rounded-md bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700">
مشروع جديد
</button>
</RoleGate>
</div>
<div className="grid gap-4">
<ProjectCard
title="إعادة تصميم الموقع"
status="قيد التنفيذ"
members={4}
/>
<ProjectCard
title="تطبيق الهاتف v2"
status="التخطيط"
members={6}
/>
<ProjectCard
title="ترحيل API"
status="مكتمل"
members={3}
/>
</div>
<RoleGate
allowedRoles={["org:admin"]}
fallback={
<p className="text-sm text-gray-400">
المديرون فقط يمكنهم إدارة إعدادات المشاريع.
</p>
}
>
<div className="rounded-lg border border-red-200 bg-red-50 p-4">
<h3 className="font-semibold text-red-800">إجراءات المدير</h3>
<p className="mt-1 text-sm text-red-600">
أرشفة أو حذف أو نقل المشاريع.
</p>
</div>
</RoleGate>
</div>
);
}
function ProjectCard({
title,
status,
members,
}: {
title: string;
status: string;
members: number;
}) {
return (
<div className="flex items-center justify-between rounded-lg border bg-white p-4 shadow-sm">
<div>
<h3 className="font-semibold">{title}</h3>
<p className="text-sm text-gray-500">{members} أعضاء</p>
</div>
<span className="rounded-full bg-gray-100 px-3 py-1 text-xs font-medium">
{status}
</span>
</div>
);
}الخطوة 10: حماية مسارات API
مسار API محمي أساسي
// src/app/api/projects/route.ts
import { auth } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
export async function GET() {
const { userId, orgId } = await auth();
if (!userId) {
return NextResponse.json({ error: "غير مصرّح" }, { status: 401 });
}
if (!orgId) {
return NextResponse.json(
{ error: "لم يتم اختيار مؤسسة" },
{ status: 400 }
);
}
const projects = [
{ id: "1", name: "إعادة تصميم الموقع", orgId },
{ id: "2", name: "تطبيق الهاتف v2", orgId },
];
return NextResponse.json({ projects });
}
export async function POST(request: Request) {
const { userId, orgId } = await auth();
if (!userId) {
return NextResponse.json({ error: "غير مصرّح" }, { status: 401 });
}
if (!orgId) {
return NextResponse.json(
{ error: "لم يتم اختيار مؤسسة" },
{ status: 400 }
);
}
const session = await auth();
const canCreate = await session.has({ permission: "org:projects:create" });
if (!canCreate) {
return NextResponse.json({ error: "محظور" }, { status: 403 });
}
const body = await request.json();
const project = {
id: crypto.randomUUID(),
name: body.name,
orgId,
createdBy: userId,
};
return NextResponse.json({ project }, { status: 201 });
}الخطوة 11: معالجة Webhooks
ترسل Clerk webhooks لأحداث المستخدمين والمؤسسات. هذا ضروري لمزامنة البيانات مع قاعدة بياناتك.
تثبيت Svix للتحقق من Webhooks
npm install svixإنشاء نقطة نهاية Webhook
// src/app/api/webhooks/clerk/route.ts
import { Webhook } from "svix";
import { headers } from "next/headers";
import { WebhookEvent } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const SIGNING_SECRET = process.env.CLERK_WEBHOOK_SECRET;
if (!SIGNING_SECRET) {
throw new Error("متغير CLERK_WEBHOOK_SECRET مفقود");
}
const wh = new Webhook(SIGNING_SECRET);
const headerPayload = await headers();
const svixId = headerPayload.get("svix-id");
const svixTimestamp = headerPayload.get("svix-timestamp");
const svixSignature = headerPayload.get("svix-signature");
if (!svixId || !svixTimestamp || !svixSignature) {
return NextResponse.json(
{ error: "رؤوس svix مفقودة" },
{ status: 400 }
);
}
const payload = await request.json();
const body = JSON.stringify(payload);
let event: WebhookEvent;
try {
event = wh.verify(body, {
"svix-id": svixId,
"svix-timestamp": svixTimestamp,
"svix-signature": svixSignature,
}) as WebhookEvent;
} catch (err) {
console.error("فشل التحقق من webhook:", err);
return NextResponse.json(
{ error: "توقيع غير صالح" },
{ status: 400 }
);
}
switch (event.type) {
case "user.created": {
const { id, email_addresses, first_name, last_name } = event.data;
console.log("مستخدم جديد:", id);
// إدراج المستخدم في قاعدة البيانات
break;
}
case "user.updated": {
const { id } = event.data;
console.log("تحديث مستخدم:", id);
break;
}
case "user.deleted": {
const { id } = event.data;
console.log("حذف مستخدم:", id);
break;
}
case "organization.created": {
const { id, name, slug } = event.data;
console.log("مؤسسة جديدة:", name);
break;
}
case "organizationMembership.created": {
const { organization, public_user_data, role } = event.data;
console.log("عضو جديد في المؤسسة:", organization.id);
break;
}
default:
console.log("حدث webhook غير مُعالج:", event.type);
}
return NextResponse.json({ received: true });
}إعداد Webhook في Clerk
- اذهب إلى Webhooks في لوحة تحكم Clerk
- انقر على Add endpoint
- أدخل الرابط:
https://نطاقك.com/api/webhooks/clerk - اختر الأحداث:
user.created،user.updated،user.deleted،organization.created،organizationMembership.created - انسخ Signing Secret وأضفه إلى
.env.local:
CLERK_WEBHOOK_SECRET=whsec_سرّك_للwebhookللتطوير المحلي، استخدم ngrok:
npx ngrok http 3000الخطوة 12: تخصيص مكونات Clerk
يمكن تخصيص مكونات Clerk بالكامل لتتوافق مع تصميم تطبيقك.
إعداد السمة العامة
// src/app/layout.tsx
import { ClerkProvider } from "@clerk/nextjs";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<ClerkProvider
appearance={{
variables: {
colorPrimary: "#2563eb",
colorBackground: "#ffffff",
colorInputBackground: "#f9fafb",
colorInputText: "#111827",
borderRadius: "0.5rem",
fontFamily: "Inter, sans-serif",
},
elements: {
formButtonPrimary:
"bg-blue-600 hover:bg-blue-700 text-sm font-medium",
card: "shadow-md border border-gray-100",
headerTitle: "text-xl font-bold",
headerSubtitle: "text-gray-500",
socialButtonsBlockButton:
"border border-gray-200 hover:bg-gray-50",
formFieldInput:
"border border-gray-300 focus:border-blue-500 focus:ring-blue-500",
footerActionLink: "text-blue-600 hover:text-blue-700",
},
}}
>
<html lang="ar" dir="rtl">
<body>{children}</body>
</html>
</ClerkProvider>
);
}الخطوة 13: صفحة إدارة الأعضاء
ابنِ صفحة مخصصة لإدارة أعضاء المؤسسة:
// src/app/dashboard/members/page.tsx
import { auth, clerkClient } from "@clerk/nextjs/server";
import { redirect } from "next/navigation";
import { RoleGate } from "@/components/role-gate";
import { InviteMemberForm } from "./invite-form";
export default async function MembersPage() {
const { orgId, userId } = await auth();
if (!orgId) {
redirect("/dashboard");
}
const client = await clerkClient();
const memberships =
await client.organizations.getOrganizationMembershipList({
organizationId: orgId,
});
return (
<div className="space-y-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">أعضاء الفريق</h1>
<p className="text-gray-500">
إدارة من لديه صلاحية الوصول لهذه المؤسسة.
</p>
</div>
<RoleGate allowedRoles={["org:admin"]}>
<InviteMemberForm orgId={orgId} />
</RoleGate>
</div>
<div className="overflow-hidden rounded-lg border bg-white shadow-sm">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500">
العضو
</th>
<th className="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500">
الدور
</th>
<th className="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500">
تاريخ الانضمام
</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
الإجراءات
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{memberships.data.map((member) => (
<tr key={member.id}>
<td className="whitespace-nowrap px-6 py-4">
<div className="flex items-center gap-3">
<img
src={member.publicUserData?.imageUrl}
alt=""
className="h-8 w-8 rounded-full"
/>
<div>
<p className="font-medium">
{member.publicUserData?.firstName}{" "}
{member.publicUserData?.lastName}
</p>
<p className="text-sm text-gray-500">
{member.publicUserData?.identifier}
</p>
</div>
</div>
</td>
<td className="whitespace-nowrap px-6 py-4">
<span className="inline-flex rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800">
{member.role.replace("org:", "")}
</span>
</td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
{new Date(member.createdAt).toLocaleDateString("ar-SA")}
</td>
<td className="whitespace-nowrap px-6 py-4 text-left">
<RoleGate allowedRoles={["org:admin"]}>
{member.publicUserData?.userId !== userId && (
<button className="text-sm text-red-600 hover:text-red-800">
إزالة
</button>
)}
</RoleGate>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}نموذج الدعوة (مكون عميل)
// src/app/dashboard/members/invite-form.tsx
"use client";
import { useOrganization } from "@clerk/nextjs";
import { useState } from "react";
export function InviteMemberForm({ orgId }: { orgId: string }) {
const { organization } = useOrganization();
const [email, setEmail] = useState("");
const [role, setRole] = useState("org:member");
const [isLoading, setIsLoading] = useState(false);
const [message, setMessage] = useState("");
async function handleInvite(e: React.FormEvent) {
e.preventDefault();
setIsLoading(true);
setMessage("");
try {
await organization?.inviteMember({
emailAddress: email,
role: role as "org:admin" | "org:member",
});
setMessage("تم إرسال الدعوة بنجاح!");
setEmail("");
} catch (error) {
setMessage("فشل إرسال الدعوة. يرجى المحاولة مرة أخرى.");
} finally {
setIsLoading(false);
}
}
return (
<form onSubmit={handleInvite} className="flex items-end gap-3">
<div>
<label className="block text-sm font-medium text-gray-700">
البريد الإلكتروني
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="زميل@شركة.com"
className="mt-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
الدور
</label>
<select
value={role}
onChange={(e) => setRole(e.target.value)}
className="mt-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
>
<option value="org:member">عضو</option>
<option value="org:admin">مدير</option>
</select>
</div>
<button
type="submit"
disabled={isLoading}
className="rounded-md bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700 disabled:opacity-50"
>
{isLoading ? "جارٍ الإرسال..." : "دعوة"}
</button>
{message && (
<p className="text-sm text-green-600">{message}</p>
)}
</form>
);
}الخطوة 14: إضافة المصادقة متعددة العوامل
تدعم Clerk المصادقة متعددة العوامل (MFA) مباشرة. فعّلها في لوحة التحكم:
- اذهب إلى User and Authentication ثم Multi-factor في لوحة تحكم Clerk
- فعّل Authenticator application (TOTP)
- اختيارياً فعّل التحقق عبر SMS
يمكن للمستخدمين تفعيل MFA من مكون UserProfile:
// src/app/dashboard/settings/page.tsx
import { UserProfile } from "@clerk/nextjs";
export default function SettingsPage() {
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold">إعدادات الحساب</h1>
<p className="text-gray-500">
إدارة ملفك الشخصي وأمانك وتفضيلاتك.
</p>
</div>
<UserProfile
appearance={{
elements: {
rootBox: "w-full",
card: "shadow-sm border border-gray-200 w-full",
navbar: "border-r border-gray-200",
},
}}
/>
</div>
);
}اختبار التطبيق
1. تشغيل خادم التطوير
npm run dev2. اختبار تدفق المصادقة
- انتقل إلى
http://localhost:3000— يجب أن ترى الصفحة الرئيسية العامة - انقر على التسجيل وأنشئ حساباً جديداً
- تحقق من بريدك الإلكتروني (استخدم وضع الاختبار في Clerk للتحقق الفوري)
- بعد تسجيل الدخول، يجب أن تُعاد توجيهك إلى
/dashboard
3. اختبار ميزات المؤسسات
- انقر على
OrganizationSwitcherفي شريط التنقل - أنشئ مؤسسة جديدة باسم "شركة أكمي"
- ادعُ عضواً ببريد إلكتروني مختلف
- بدّل بين الحساب الشخصي والمؤسسة
4. اختبار RBAC
- كمدير، تحقق من ظهور زر "مشروع جديد" وإجراءات المدير
- سجّل الدخول بحساب عضو وتحقق من الوصول المحدود
- حاول الوصول مباشرة لمسار API
/api/projects— يجب أن يتطلب المصادقة
استكشاف الأخطاء وإصلاحها
"auth() أرجع userId فارغ"
تأكد من إعداد الوسيط بشكل صحيح وأن المسار غير مُعلّم كعام. تحقق أيضاً من ضبط CLERK_SECRET_KEY بشكل صحيح.
"ميزات المؤسسات لا تظهر"
فعّل المؤسسات في لوحة تحكم Clerk تحت Organizations. هذه الميزة غير مفعّلة افتراضياً.
"فشل التحقق من توقيع webhook"
تحقق مرة أخرى من CLERK_WEBHOOK_SECRET في ملف .env.local. عند استخدام ngrok، تأكد من توجيه webhook في Clerk إلى رابط ngrok وليس localhost.
"أخطاء CORS في مسارات API"
وسيط Clerk يعالج CORS تلقائياً للمسارات المصادق عليها. إذا كنت تستدعي من مصدر خارجي، اضبط رؤوس CORS بشكل صريح في مسار API.
الخطوات التالية
الآن بعد أن لديك نظام مصادقة يعمل بالكامل، فكّر في:
- تكامل قاعدة البيانات: اربط معرّفات مستخدمي Clerk بقاعدة بياناتك باستخدام Drizzle ORM أو Prisma
- الفوترة: أضف Stripe لإدارة الاشتراكات لكل مؤسسة
- سجلات التدقيق: تتبع جميع أحداث المصادقة والمؤسسات
- بيانات مخصصة: أضف بيانات وصفية مخصصة لجلسات المستخدمين
- SSO/SAML: فعّل تسجيل الدخول الموحد للمؤسسات الكبيرة
الخلاصة
في هذا الدليل، قمت ببناء نظام مصادقة كامل مع Clerk و Next.js 15 يتضمن:
- تسجيل المستخدمين وتسجيل الدخول بمزودين متعددين
- إدارة المؤسسات لتعدد المستأجرين
- التحكم بالوصول المبني على الأدوار مع صلاحيات مخصصة
- مسارات API محمية مع التحقق من الصلاحيات
- معالجة webhooks لمزامنة قاعدة البيانات
- مكونات واجهة مصادقة مخصصة
- دعم المصادقة متعددة العوامل
تزيل Clerk الحاجة لبناء وصيانة بنية مصادقة معقدة، مما يتيح لك التركيز على الميزات الأساسية لتطبيقك. تكاملها العميق مع Next.js App Router و Server Components يجعلها خياراً طبيعياً لتطبيقات React الحديثة.
مزيج المؤسسات و RBAC يجعل هذا الإعداد مثالياً لتطبيقات SaaS حيث التعاون الجماعي والتحكم بالوصول أساسيان. مع البنية التحتية المُدارة من Clerk، تحصل على أمان بمستوى المؤسسات دون عبء التشغيل للحلول المستضافة ذاتياً.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

بناء وكيل ذكاء اصطناعي مستقل باستخدام Agentic RAG و Next.js
تعلم كيف تبني وكيل ذكاء اصطناعي يقرر بشكل مستقل متى وكيف يسترجع المعلومات من قواعد البيانات المتجهية. دليل عملي شامل باستخدام Vercel AI SDK و Next.js مع أمثلة قابلة للتنفيذ.

بناء محرك بحث دلالي بالذكاء الاصطناعي مع Next.js 15 و OpenAI و Pinecone
تعلّم كيف تبني محرك بحث دلالي متقدّم باستخدام Next.js 15 و OpenAI Embeddings و قاعدة بيانات Pinecone المتجهية. يغطي هذا الدليل الشامل من الإعداد إلى النشر مع Server Actions وواجهة بحث تفاعلية.

بناء تطبيق متكامل باستخدام Appwrite Cloud و Next.js 15
تعلّم كيفية بناء تطبيق ويب متكامل باستخدام Appwrite Cloud كخدمة خلفية و Next.js 15 مع App Router. يغطي هذا الدليل المصادقة وقواعد البيانات وتخزين الملفات والميزات الفورية.