Next.js: المشروع النهائي: المصادقة والإعداد

آخر تحديث: 2026-08-26

بناء مشروع SaaS متكامل يشبه تشييد مبنى — أولاً، تستخدم السقالات لإنشاء الهيكل الإنشائي؛ ثم تصمم أنظمة الكهرباء والسباكة (قاعدة البيانات)؛ وأخيراً، تركّب أقفال الأبواب (نظام المصادقة).

1. ما ستتعلمه



2. القصة الحقيقية لمهندس Full-Stack

(1) نقطة الألم: البداية الفوضوية لبناء منصة SaaS من الصفر

Alice هي مهندسة Full-Stack في شركة ناشئة تضم 50 موظفاً. قررت الشركة تطوير منصة تعاون داخلي للمشاريع تُسمى "TaskFlow" لخدمة 10,000 مستخدم، يدير كل منهم أكثر من 200 مهمة. حاولت Alice إعداد هيكل المشروع يدوياً — لكن التوجيه أصبح فوضوياً، وفشلت ترحيلات قاعدة البيانات بشكل متكرر، ورُفضت جميع حلول المصادقة الثلاثة التي اقترحتها. كان الفريق يهدر أربع ساعات يومياً في تهيئة البيئة، وتأخر المشروع أسبوعين عن الجدول الزمني.

(2) الحل باستخدام السقالات + إطار المصادقة

تهيئة بنقرة واحدة باستخدام create-next-app + نمذجة تعريفية باستخدام Prisma ORM + Auth.js جاهز للاستخدام.

BASH
npx create-next-app@latest taskflow --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
npx prisma init --datasource-provider postgresql
npx auth add

ثلاثة أسطر من التعليمات البرمجية تتعامل مع 80% من أعمال التهيئة.

(3) العائد

البعد الإعداد اليدوي حل TaskFlow
وقت التهيئة يومان ساعتان
تطوير حل المصادقة أسبوع واحد (JWT داخلي) 30 دقيقة (دمج Auth.js)
ترحيل قاعدة البيانات نصوص SQL يدوية Prisma Migrate (تعريفي)
إرشادات هيكل المشروع لكل فريق أسلوبه الخاص App Router: توجيه تقليدي
قابلية الصيانة منخفضة (بدون أمان الأنواع) عالية (TypeScript + أنواع Prisma)


3. تهيئة المشروع وإعداده

(1) اختيار أداة السقالات create-next-app

100%
graph LR
    A[create-next-app] --> B[TypeScript]
    A --> C[Tailwind CSS]
    A --> D[ESLint]
    A --> E[App Router]
    A --> F[src/ دليل]
    A --> G[@/ اسم مستعار]

    style A fill:#cce5ff
    style B fill:#d4edda
    style C fill:#d4edda
الخيار القيمة الوصف
TypeScript نعم أمان الأنواع — ضروري للإنتاج
ESLint نعم معايير الترميز
Tailwind CSS نعم يعمل مع shadcn/ui
دليل src/ نعم منفصل عن app/ لهيكل واضح
App Router نعم الإعداد الافتراضي في Next.js 16
import alias @/* مسار استيراد مختصر

▶ مثال: تهيئة مشروع TaskFlow

المخرجات:

TEXT 📖 للعرض فقط
رسم توضيحي لتدفق البيانات وهيكل المكونات.
BASH
# 1. إنشاء المشروع
npx create-next-app@latest taskflow --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"

# 2. الانتقال إلى دليل المشروع
cd taskflow

# 3. تثبيت shadcn/ui
npx shadcn@latest init -d

# 4. إضافة مكونات shadcn/ui الشائعة
npx shadcn@latest add button card input label select dialog dropdown-menu avatar badge separator

# 5. تشغيل خادم التطوير
npm run dev
TEXT 📖 للعرض فقط
✔ تم إنشاء المشروع في: taskflow/
✔ تمت تهيئة shadcn/ui بنجاح
✔ تم تثبيت مكون button
✔ تم تثبيت مكون card
✔ تم تثبيت مكون input
✔ تم تثبيت 7 مكونات إجمالاً

المخرجات:

TEXT 📖 للعرض فقط
✔ تم إنشاء المشروع في: taskflow/
✔ تمت تهيئة shadcn/ui بنجاح
✔ تم تثبيت مكون button
✔ تم تثبيت مكون card
✔ تم تثبيت مكون input
✔ تم تثبيت 7 مكونات إجمالاً

(2) هيكل دليل المشروع

TEXT 📖 للعرض فقط
taskflow/
├── src/
│   ├── app/                    # صفحات App Router
│   │   ├── (auth)/            # مجموعة مسارات المصادقة
│   │   ├── (dashboard)/       # مجموعة مسارات لوحة التحكم
│   │   ├── api/               # مسارات API
│   │   ├── layout.tsx         # التخطيط الجذر
│   │   └── page.tsx           # الصفحة الرئيسية
│   ├── components/            # المكونات المشتركة
│   │   ├── ui/               # مكونات shadcn/ui
│   │   └── forms/            # مكونات النماذج
│   ├── lib/                  # دوال مساعدة
│   │   ├── auth.ts           # إعداد Auth.js
│   │   ├── db.ts             # Prisma Client
│   │   └── utils.ts          # أدوات عامة
│   ├── prisma/               # Prisma Schema + الترحيل
│   │   ├── schema.prisma
│   │   └── seed.ts
│   └── middleware.ts         # Middleware حماية المسارات
├── public/                   # الموارد الثابتة
├── next.config.ts            # إعداد Next.js
├── tailwind.config.ts        # إعداد Tailwind
└── package.json


4. تصميم النماذج الخمسة في Prisma Schema

(1) رسم تخطيطي لعلاقات نماذج البيانات

100%
graph TB
    U[User] -->|ينتمي إلى| O[Organization]
    O -->|لديها العديد| P[Project]
    P -->|لديه العديد| T[Task]
    T -->|لديها العديد| C[Comment]
    U -->|مُسند إلى| T
    U -->|أنشأ| C
    U -->|أنشأ| P

    style U fill:#cce5ff
    style O fill:#d4edda
    style P fill:#ffeeba
    style T fill:#f8d7da
    style C fill:#d6d8db

(2) تعريف أنواع التعداد

اسم التعداد القيم الوصف
Role OWNER / ADMIN / MEMBER / VIEWER الأدوار التنظيمية
ProjectStatus ACTIVE / ARCHIVED / COMPLETED حالة المشروع
TaskStatus TODO / IN_PROGRESS / DONE حالة المهمة
TaskPriority LOW / MEDIUM / HIGH / URGENT أولوية المهمة

(3) مخطط Prisma الكامل

PRISMA
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

enum Role {
  OWNER
  ADMIN
  MEMBER
  VIEWER
}

enum ProjectStatus {
  ACTIVE
  ARCHIVED
  COMPLETED
}

enum TaskStatus {
  TODO
  IN_PROGRESS
  DONE
}

enum TaskPriority {
  LOW
  MEDIUM
  HIGH
  URGENT
}

model User {
  id             String   @id @default(cuid())
  name           String?
  email          String   @unique
  emailVerified  DateTime?
  image          String?
  passwordHash   String?
  organizationId String?
  organization   Organization? @relation(fields: [organizationId], references: [id])
  role           Role     @default(MEMBER)
  projects       Project[]
  assignedTasks  Task[]   @relation("TaskAssignee")
  comments       Comment[]
  createdAt      DateTime @default(now())
  updatedAt      DateTime @updatedAt

  accounts  Account[]
  sessions  Session[]
}

model Organization {
  id        String    @id @default(cuid())
  name      String
  slug      String    @unique
  inviteCode String   @unique @default(cuid())
  logo      String?
  users     User[]
  projects  Project[]
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
}

model Project {
  id          String        @id @default(cuid())
  name        String
  description String?
  status      ProjectStatus @default(ACTIVE)
  organizationId String
  organization Organization @relation(fields: [organizationId], references: [id])
  creatorId   String
  creator     User          @relation(fields: [creatorId], references: [id])
  tasks       Task[]
  color       String        @default("#3b82f6")
  createdAt   DateTime      @default(now())
  updatedAt   DateTime      @updatedAt
}

model Task {
  id          String       @id @default(cuid())
  title       String
  description String?
  status      TaskStatus   @default(TODO)
  priority    TaskPriority @default(MEDIUM)
  projectId   String
  project     Project      @relation(fields: [projectId], references: [id])
  assigneeId  String?
  assignee    User?        @relation("TaskAssignee", fields: [assigneeId], references: [id])
  creatorId   String
  creator     User         @relation(fields: [creatorId], references: [id])
  order       Int          @default(0)
  dueDate     DateTime?
  attachments Attachment[]
  comments    Comment[]
  createdAt   DateTime     @default(now())
  updatedAt   DateTime     @updatedAt
}

model Comment {
  id        String   @id @default(cuid())
  content   String
  taskId    String
  task      Task     @relation(fields: [taskId], references: [id])
  authorId  String
  author    User     @relation(fields: [authorId], references: [id])
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Attachment {
  id        String   @id @default(cuid())
  fileName  String
  fileUrl   String
  fileSize  Int
  mimeType  String
  taskId    String
  task      Task     @relation(fields: [taskId], references: [id])
  uploaderId String
  uploader  User     @relation(fields: [uploaderId], references: [id])
  createdAt DateTime @default(now())
}

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)
}

▶ مثال: تشغيل ترحيل قاعدة البيانات

BASH
# 1. إنشاء ترحيل
npx prisma migrate dev --name init

# 2. إنشاء Prisma Client
npx prisma generate

# 3. عرض قاعدة البيانات
npx prisma studio
TEXT 📖 للعرض فقط
✔ تم إنشاء Prisma Client (v6.x) في .\node_modules\@prisma\client
✔ قاعدة البيانات الآن متزامنة مع Prisma schema
✔ تم فتح Prisma Studio على http://localhost:5555


5. دمج مصادقة Auth.js

(1) مقارنة حلول المصادقة

الحل المزايا العيوب السيناريوهات المناسبة
Auth.js v5 موفري خدمة متعددين، جلسات مدمجة، آمن من حيث الأنواع منحنى تعلم متوسط مشاريع تتطلب مصادقة مرنة
Clerk واجهة مستخدم جاهزة، لا حاجة للتكوين مدفوع، احتكار المزود النماذج الأولية السريعة
Supabase Auth حصة مجانية سخية، مدمج مع قاعدة البيانات يتطلب التكامل مع منظومة Supabase حل مُدار بالكامل
JWT مُطور داخلياً تحكم كامل مخاطر أمنية عالية، تكاليف تطوير عالية غير موصى به

(2) إعداد Auth.js

TYPESCRIPT
// src/lib/auth.ts
import NextAuth from "next-auth"
import Credentials from "next-auth/providers/credentials"
import Google from "next-auth/providers/google"
import { compare } from "bcryptjs"
import { prisma } from "./db"

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    Credentials({
      name: "credentials",
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) {
          return null
        }

        const user = await prisma.user.findUnique({
          where: { email: credentials.email as string },
        })

        if (!user || !user.passwordHash) {
          return null
        }

        const isValid = await compare(
          credentials.password as string,
          user.passwordHash
        )

        if (!isValid) {
          return null
        }

        return {
          id: user.id,
          email: user.email,
          name: user.name,
          image: user.image,
        }
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id
      }
      return token
    },
    async session({ session, token }) {
      if (session.user) {
        session.user.id = token.id as string
      }
      return session
    },
  },
  pages: {
    signIn: "/auth/signin",
    error: "/auth/error",
  },
  session: {
    strategy: "jwt",
  },
})

▶ مثال: مسارات API لـ Auth.js

TYPESCRIPT
// src/app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/lib/auth"

export const { GET, POST } = handlers

المخرجات:

TEXT 📖 للعرض فقط
تم تنفيذ كود TypeScript بنجاح.
TYPESCRIPT
// src/app/api/auth/signup/route.ts
import { NextResponse } from "next/server"
import { hash } from "bcryptjs"
import { prisma } from "@/lib/db"

export async function POST(request: Request) {
  try {
    const { name, email, password, inviteCode } = await request.json()

    // التحقق من وجود المستخدم
    const existingUser = await prisma.user.findUnique({
      where: { email },
    })

    if (existingUser) {
      return NextResponse.json(
        { error: "Email already registered" },
        { status: 400 }
      )
    }

    const passwordHash = await hash(password, 12)

    let organizationId: string | null = null
    let role: "OWNER" | "ADMIN" | "MEMBER" | "VIEWER" = "MEMBER"

    // التحقق من رمز الدعوة
    if (inviteCode) {
      const org = await prisma.organization.findUnique({
        where: { inviteCode },
      })
      if (org) {
        organizationId = org.id
      }
    }

    const user = await prisma.user.create({
      data: {
        name,
        email,
        passwordHash,
        organizationId,
        role: organizationId ? "MEMBER" : "OWNER",
      },
    })

    // إنشاء مؤسسة جديدة إذا لم ينضم عبر رمز دعوة
    if (!organizationId) {
      const org = await prisma.organization.create({
        data: {
          name: `${name}'s Organization`,
          slug: email.split("@")[0],
          users: { connect: { id: user.id } },
        },
      })
      await prisma.user.update({
        where: { id: user.id },
        data: { organizationId: org.id, role: "OWNER" },
      })
    }

    return NextResponse.json(
      { message: "User created successfully" },
      { status: 201 }
    )
  } catch (error) {
    return NextResponse.json(
      { error: "Something went wrong" },
      { status: 500 }
    )
  }
}

(3) نظام رموز الدعوة للمؤسسات

TYPESCRIPT
// src/lib/invite.ts
import { prisma } from "./db"

// إنشاء رمز دعوة جديد
export async function generateInviteCode(organizationId: string) {
  const org = await prisma.organization.update({
    where: { id: organizationId },
    data: { inviteCode: crypto.randomUUID().slice(0, 8) },
  })
  return org.inviteCode
}

// التحقق من صحة رمز الدعوة
export async function validateInviteCode(code: string) {
  // TKFLW-XXXXXX format
  const fullCode = `TKFLW-${code.toUpperCase()}`
  const org = await prisma.organization.findUnique({
    where: { inviteCode: fullCode },
  })
  return org
}

▶ مثال: صفحات تسجيل الدخول والتسجيل

المخرجات:

TEXT 📖 للعرض فقط
يُحدّث سجل قاعدة البيانات.
TSX
// src/app/(auth)/auth/signin/page.tsx
import { AuthCard } from "@/components/auth/auth-card"
import { SignInForm } from "@/components/auth/sign-in-form"

export default function SignInPage() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <AuthCard
        title="Sign in to TaskFlow"
        description="Enter your credentials to continue"
      >
        <SignInForm />
      </AuthCard>
    </div>
  )
}

المخرجات:

TEXT 📖 للعرض فقط
يعرض واجهة مستخدم مكون SignInPage.
TSX
// src/components/auth/sign-in-form.tsx
"use client"

import { signIn } from "next-auth/react"
import { useRouter } from "next/navigation"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Card, CardContent } from "@/components/ui/card"

export function SignInForm() {
  const router = useRouter()
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)

  async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setIsLoading(true)
    setError(null)

    const formData = new FormData(event.currentTarget)

    const result = await signIn("credentials", {
      email: formData.get("email") as string,
      password: formData.get("password") as string,
      redirect: false,
    })

    if (result?.error) {
      setError("Invalid email or password")
      setIsLoading(false)
      return
    }

    router.push("/dashboard")
    router.refresh()
  }

  return (
    <Card>
      <CardContent className="pt-6">
        <form onSubmit={onSubmit} className="space-y-4">
          <div className="space-y-2">
            <Label htmlFor="email">Email</Label>
            <Input
              id="email"
              name="email"
              type="email"
              placeholder="alice@example.com"
              required
            />
          </div>
          <div className="space-y-2">
            <Label htmlFor="password">Password</Label>
            <Input
              id="password"
              name="password"
              type="password"
              placeholder="Enter your password"
              required
            />
          </div>
          {error && (
            <p className="text-sm text-red-500">{error}</p>
          )}
          <Button type="submit" className="w-full" disabled={isLoading}>
            {isLoading ? "Signing in..." : "Sign in"}
          </Button>
          <div className="relative">
            <div className="absolute inset-0 flex items-center">
              <span className="w-full border-t" />
            </div>
            <div className="relative flex justify-center text-xs uppercase">
              <span className="bg-background px-2 text-muted-foreground">
                Or continue with
              </span>
            </div>
          </div>
          <Button
            type="button"
            variant="outline"
            className="w-full"
            onClick={() => signIn("google", { callbackUrl: "/dashboard" })}
          >
            <svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
              <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4"/>
              <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
              <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
              <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
            </svg>
            Sign in with Google
          </Button>
        </form>
      </CardContent>
    </Card>
  )
}


6. Middleware لحماية المسارات

(1) سير عمل Middleware

100%
graph LR
    A[الطلب] --> B{Middleware}
    B -->|مسار عام| C[سماح]
    B -->|مسار محمي| D{هل توجد جلسة؟}
    D -->|نعم| C
    D -->|لا| E[إعادة توجيه /auth/signin]
    B -->|مسار API| F{رمز صالح؟}
    F -->|نعم| G[تمرير]
    F -->|لا| H[401 غير مصرح]

    style B fill:#ffeeba
    style C fill:#d4edda
    style E fill:#f8d7da

(2) تنفيذ Middleware

TYPESCRIPT
// src/middleware.ts
import { auth } from "@/lib/auth"
import { NextResponse } from "next/server"

export default auth((req) => {
  const { nextUrl } = req
  const isLoggedIn = !!req.auth
  const isApiRoute = nextUrl.pathname.startsWith("/api")
  const isAuthRoute = nextUrl.pathname.startsWith("/auth")
  const isDashboardRoute = nextUrl.pathname.startsWith("/dashboard")
  const isPublicRoute =
    nextUrl.pathname === "/" ||
    nextUrl.pathname.startsWith("/_next") ||
    nextUrl.pathname.startsWith("/static") ||
    nextUrl.pathname === "/api/auth/signup"

  // حماية مسارات API
  if (isApiRoute && !isPublicRoute && !isLoggedIn) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
  }

  // حماية مسارات لوحة التحكم
  if (isDashboardRoute && !isLoggedIn) {
    return NextResponse.redirect(new URL("/auth/signin", nextUrl))
  }

  // إعادة توجيه المستخدمين المسجلين من صفحات المصادقة
  if (isAuthRoute && isLoggedIn) {
    return NextResponse.redirect(new URL("/dashboard", nextUrl))
  }

  return NextResponse.next()
})

export const config = {
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico).*)",
  ],
}

▶ مثال: اختبار حماية المسارات باستخدام Middleware

BASH
# الوصول إلى المسارات المحمية (غير مسجل الدخول)
curl -v http://localhost:3000/dashboard 2>&1 | grep Location

# زيارة مسار API (غير مسجل الدخول)
curl -v http://localhost:3000/api/projects 2>&1
TEXT 📖 للعرض فقط
< Location: http://localhost:3000/auth/signin
< HTTP/1.1 302 Found

{"error":"Unauthorized"}

(3) استراتيجيات تكوين Matcher

النمط المسارات المطابقة المسارات المستثناة
/((?!_next/static).*) جميع المسارات الموارد الثابتة
/((?!api/auth).*) جميع المسارات API المصادقة
`/((?!_next/static _next/image).*)` جميع المسارات


7. تغذية قاعدة البيانات (Seeding)

(1) استراتيجية التغذية

100%
graph TB
    A[seed.ts] --> B[إنشاء مؤسسة]
    B --> C[إنشاء مستخدمين x5]
    C --> D[إنشاء مشاريع x5]
    D --> E[إنشاء مهام x50]
    E --> F[إنشاء تعليقات x40]
    F --> G[إنشاء مرفقات x10]

    style A fill:#cce5ff
    style G fill:#d4edda

▶ مثال: سكريبت التغذية الكامل

المخرجات:

TEXT 📖 للعرض فقط
رسم توضيحي لتدفق التغذية: إنشاء مؤسسة → مستخدمين → مشاريع → مهام → تعليقات.
TYPESCRIPT
// src/prisma/seed.ts
import { PrismaClient, Role, TaskStatus, TaskPriority } from "@prisma/client"
import { hash } from "bcryptjs"

const prisma = new PrismaClient()

async function main() {
  console.log("🌱 جاري تغذية قاعدة البيانات...")

  const passwordHash = await hash("password123", 12)

  // إنشاء مؤسسة
  const org = await prisma.organization.create({
    data: {
      name: "Acme Corp",
      slug: "acme-corp",
      inviteCode: "TKFLW-ACMECORP",
    },
  })

  // إنشاء مستخدمين
  const users = await Promise.all([
    prisma.user.create({
      data: {
        name: "Alice Wang",
        email: "alice@acme.com",
        passwordHash,
        organizationId: org.id,
        role: "OWNER",
        image: "https://api.dicebear.com/7.x/avataaars/svg?seed=alice",
      },
    }),
    prisma.user.create({
      data: {
        name: "Bob Chen",
        email: "bob@acme.com",
        passwordHash,
        organizationId: org.id,
        role: "ADMIN",
        image: "https://api.dicebear.com/7.x/avataaars/svg?seed=bob",
      },
    }),
    prisma.user.create({
      data: {
        name: "Charlie Liu",
        email: "charlie@acme.com",
        passwordHash,
        organizationId: org.id,
        role: "MEMBER",
        image: "https://api.dicebear.com/7.x/avataaars/svg?seed=charlie",
      },
    }),
    prisma.user.create({
      data: {
        name: "Diana Park",
        email: "diana@acme.com",
        passwordHash,
        organizationId: org.id,
        role: "MEMBER",
        image: "https://api.dicebear.com/7.x/avataaars/svg?seed=diana",
      },
    }),
    prisma.user.create({
      data: {
        name: "Eve Zhang",
        email: "eve@acme.com",
        passwordHash,
        organizationId: org.id,
        role: "VIEWER",
        image: "https://api.dicebear.com/7.x/avataaars/svg?seed=eve",
      },
    }),
  ])

  // إنشاء مشاريع
  const projectData = [
    { name: "Website Redesign", description: "Redesign the company website with modern UI", color: "#3b82f6" },
    { name: "Mobile App v2", description: "Version 2 of the mobile application", color: "#10b981" },
    { name: "API Integration", description: "Third-party API integrations", color: "#f59e0b" },
    { name: "Data Migration", description: "Migrate data from legacy system", color: "#ef4444" },
    { name: "Security Audit", description: "Q3 security audit and compliance", color: "#8b5cf6" },
  ]

  const projects = await Promise.all(
    projectData.map((p) =>
      prisma.project.create({
        data: {
          ...p,
          organizationId: org.id,
          creatorId: users[0].id,
        },
      })
    )
  )

  // عناوين المهام
  const taskTitles = [
    "Set up CI/CD pipeline", "Design system components", "Write unit tests",
    "Implement user authentication", "Create database schema", "API documentation",
    "Performance optimization", "Accessibility audit", "Mobile responsive layout",
    "Error handling middleware", "Data validation", "Search functionality",
    "Notification system", "File upload feature", "User dashboard",
    "Admin panel", "Reporting module", "Email templates",
    "WebSocket integration", "Cache layer", "Rate limiting",
    "Logging system", "Backup strategy", "Monitoring setup",
    "Load testing",
  ]

  const statuses: TaskStatus[] = ["TODO", "IN_PROGRESS", "DONE"]
  const priorities: TaskPriority[] = ["LOW", "MEDIUM", "HIGH", "URGENT"]

  // إنشاء مهام وتعليقات
  for (const project of projects.slice(0, 3)) {
    const taskCount = project.name === "Website Redesign" ? 20 : 15

    for (let i = 0; i < taskCount; i++) {
      const task = await prisma.task.create({
        data: {
          title: taskTitles[i % taskTitles.length],
          description: `Detailed description for task #${i + 1} in ${project.name}`,
          status: statuses[i % statuses.length],
          priority: priorities[i % priorities.length],
          projectId: project.id,
          assigneeId: users[i % users.length].id,
          creatorId: users[0].id,
          order: i,
          dueDate: new Date(Date.now() + (i + 1) * 86400000 * 7),
        },
      })

      // إضافة تعليق كل ثلاث مهام
      if (i % 3 === 0) {
        await prisma.comment.create({
          data: {
            content: `This task is progressing well. Need to review the implementation.`,
            taskId: task.id,
            authorId: users[(i + 1) % users.length].id,
          },
        })
      }
    }
  }

  const totalTasks = await prisma.task.count()
  const totalComments = await prisma.comment.count()

  console.log(`✅ اكتملت التغذية:
  - 1 مؤسسة (${org.name})
  - ${users.length} مستخدمين
  - ${projects.length} مشاريع
  - ${totalTasks} مهام
  - ${totalComments} تعليقات`)
}

main()
  .catch((e) => {
    console.error(e)
    process.exit(1)
  })
  .finally(async () => {
    await prisma.$disconnect()
  })

المخرجات:

TEXT 📖 للعرض فقط
🌱 جاري تغذية قاعدة البيانات...
`✅ اكتملت التغذية:
  - 1 مؤسسة (${org.name}
e
BASH
# إضافة سكريبت التغذية في package.json
# "prisma": { "seed": "tsx src/prisma/seed.ts" }

# تشغيل التغذية
npx prisma db seed
TEXT 📖 للعرض فقط
🌱 جاري تغذية قاعدة البيانات...
✅ اكتملت التغذية:
  - 1 مؤسسة (Acme Corp)
  - 5 مستخدمين
  - 5 مشاريع
  - 50 مهام
  - 16 تعليقات


8. مثال كامل: العملية الكاملة من تهيئة المشروع إلى المصادقة

BASH
# ============================================
# عملية تهيئة TaskFlow الكاملة
# ============================================

# 1. إنشاء المشروع
npx create-next-app@latest taskflow --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd taskflow

# 2. تثبيت التبعيات
npm install next-auth@beta @prisma/client @auth/prisma-adapter bcryptjs
npm install prisma tsx --save-dev
npm install @types/bcryptjs --save-dev

# 3. تهيئة shadcn/ui
npx shadcn@latest init -d
npx shadcn@latest add button card input label select dialog dropdown-menu avatar badge separator

# 4. تهيئة Prisma
npx prisma init --datasource-provider postgresql
# نسخ محتوى schema.prisma
npx prisma migrate dev --name init
npx prisma generate

# 5. تعيين متغيرات البيئة (.env)
cat > .env << EOF
DATABASE_URL="postgresql://postgres:password@localhost:5432/taskflow"
AUTH_SECRET="my-super-secret-key-change-in-production"
AUTH_URL="http://localhost:3000"
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
EOF

# 6. تشغيل التغذية
npx prisma db seed

# 7. تشغيل خادم التطوير
npm run dev

المخرجات المتوقعة (عند زيارة http://localhost:3000 في المتصفح):

TEXT 📖 للعرض فقط
→ إعادة توجيه تلقائي إلى /auth/signin (غير مسجل الدخول)
→ عرض نموذج تسجيل الدخول (Email + Password + Google)
→ تسجيل الدخول باستخدام alice@acme.com / password123
→ الانتقال إلى /dashboard
→ عرض الشريط الجانبي: Acme Corp · 5 مشاريع · 50 مهمة

❓ أسئلة شائعة

س لماذا نستخدم Auth.js بدلاً من Clerk؟
ج Auth.js (NextAuth v5) هو حل مفتوح المصدر يدعم أكثر من 80 مزود خدمة ويتجنب احتكار المزود. Clerk مناسب للنماذج الأولية السريعة لكنه يصبح مكلفاً جداً بمجرد البدء في الدفع. مع Auth.js، يمكن لـ TaskFlow التحكم الكامل في منطق المصادقة والتكامل بسلاسة مع Prisma.
س كيف أزامن قاعدة البيانات بعد تعديل Prisma schema؟
ج شغّل npx prisma migrate dev --name <وصف> لإنشاء ترحيل جديد؛ سيقوم Prisma تلقائياً بإنشاء وتنفيذ SQL. في بيئة التطوير، يمكنك استخدام npx prisma db push للمزامنة مباشرة (بدون إنشاء ملفات ترحيل).
س ما الغرض من البادئة في رمز الدعوة TKFLW-؟
ج تحدد البادئة الرمز كرمز دعوة TaskFlow لتجنب الخلط مع الأنظمة الأخرى. مثال على رمز دعوة كامل: TKFLW-ACMECORP. دمج البادئة مع slug المؤسسة يجعل رمز الدعوة سهل القراءة وفريداً.
س لماذا يطابق تكوين matcher في middleware جميع المسارات؟
ج تكوين matcher /((?!_next/static|_next/image|favicon.ico).*) يطابق جميع مسارات الموارد غير الثابتة، مما يسمح لـ middleware بإجراء فحوصات المصادقة على جميع مسارات الصفحات ومسارات API. توصي Next.js بإجراء فحوصات المسار داخل middleware بدلاً من تجزئتها بشكل مفرط في matcher.
س ما هو tsx في التغذية (Seeding)؟
ج tsx هو منفذ TypeScript (npm i tsx -D)، يمكنه تشغيل ملفات .ts مباشرة بدون ترجمة. توصي Prisma رسمياً بـ tsx كمشغل للتغذية. يمكنك أيضاً استخدام ts-node، لكن tsx أسرع ويوفر توافقاً أفضل.
س لماذا passwordHash اختياري في نموذج User؟
ج لأن تسجيل الدخول عبر Google OAuth مدعوم، ولا يحتاج مستخدمو OAuth إلى كلمة مرور. للمستخدمين الذين يسجلون الدخول باستخدام Credentials، يكون لـ passwordHash قيمة؛ أما لمستخدمي OAuth، فيكون passwordHash null. هذا يوضح مرونة تصميم المخطط.

📖 ملخص

📝 تمارين

  1. تمرين أساسي (⭐): اتبع الخطوات في هذا الدرس لتهيئة مشروع TaskFlow، وشغّل npm run dev بنجاح، واعرض صفحة تسجيل الدخول في المتصفح. التقط لقطة شاشة للنتائج واحفظها.

  2. تمرين متقدم (⭐⭐): أضف نموذج Label (نظام الوسوم) إلى Prisma Schema، وأنشئ علاقة متعدد إلى متعدد مع Task (عبر الجدول الوسيط TaskLabel)، ثم شغّل الترحيل وأنشئ الأنواع. نفذ مسار API CRUD للوسوم.

  3. تحدٍّ (⭐⭐⭐): وسّع إعداد Auth.js لإضافة مزود GitHub OAuth؛ نفذ منطق التحقق من الأدوار الموصوف في middleware.ts — فقط المستخدمون ذوو دوري OWNER و ADMIN يمكنهم الوصول إلى مسار /dashboard/admin؛ اكتب اختبارات وحدة للتحقق من منطق حماية الأدوار.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%