Next.js: المشروع النهائي: المصادقة والإعداد
آخر تحديث: 2026-08-26
بناء مشروع SaaS متكامل يشبه تشييد مبنى — أولاً، تستخدم السقالات لإنشاء الهيكل الإنشائي؛ ثم تصمم أنظمة الكهرباء والسباكة (قاعدة البيانات)؛ وأخيراً، تركّب أقفال الأبواب (نظام المصادقة).
1. ما ستتعلمه
- تهيئة مشروع على مستوى المؤسسات باستخدام
create-next-app+ Tailwind + shadcn/ui - Prisma Schema: خمسة نماذج تصميم (User / Organization / Project / Task / Comment) وأنواع التعداد
- دمج Auth.js v5: حل مصادقة مزدوج باستخدام Credentials و Google OAuth
- نظام رموز الدعوة للمؤسسات وإدارة الأعضاء
- Middleware لحماية المسارات وإدارة الجلسات المركزية
- تغذية قاعدة البيانات: إنشاء 100 سجل نموذجي
2. القصة الحقيقية لمهندس Full-Stack
(1) نقطة الألم: البداية الفوضوية لبناء منصة SaaS من الصفر
Alice هي مهندسة Full-Stack في شركة ناشئة تضم 50 موظفاً. قررت الشركة تطوير منصة تعاون داخلي للمشاريع تُسمى "TaskFlow" لخدمة 10,000 مستخدم، يدير كل منهم أكثر من 200 مهمة. حاولت Alice إعداد هيكل المشروع يدوياً — لكن التوجيه أصبح فوضوياً، وفشلت ترحيلات قاعدة البيانات بشكل متكرر، ورُفضت جميع حلول المصادقة الثلاثة التي اقترحتها. كان الفريق يهدر أربع ساعات يومياً في تهيئة البيئة، وتأخر المشروع أسبوعين عن الجدول الزمني.
(2) الحل باستخدام السقالات + إطار المصادقة
تهيئة بنقرة واحدة باستخدام
create-next-app+ نمذجة تعريفية باستخدام Prisma ORM + Auth.js جاهز للاستخدام.
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
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
المخرجات:
رسم توضيحي لتدفق البيانات وهيكل المكونات.
# 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
✔ تم إنشاء المشروع في: taskflow/
✔ تمت تهيئة shadcn/ui بنجاح
✔ تم تثبيت مكون button
✔ تم تثبيت مكون card
✔ تم تثبيت مكون input
✔ تم تثبيت 7 مكونات إجمالاً
المخرجات:
✔ تم إنشاء المشروع في: taskflow/
✔ تمت تهيئة shadcn/ui بنجاح
✔ تم تثبيت مكون button
✔ تم تثبيت مكون card
✔ تم تثبيت مكون input
✔ تم تثبيت 7 مكونات إجمالاً
(2) هيكل دليل المشروع
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) رسم تخطيطي لعلاقات نماذج البيانات
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 الكامل
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)
}
▶ مثال: تشغيل ترحيل قاعدة البيانات
# 1. إنشاء ترحيل
npx prisma migrate dev --name init
# 2. إنشاء Prisma Client
npx prisma generate
# 3. عرض قاعدة البيانات
npx prisma studio
✔ تم إنشاء 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
// 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
// src/app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/lib/auth"
export const { GET, POST } = handlers
المخرجات:
تم تنفيذ كود 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) نظام رموز الدعوة للمؤسسات
// 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
}
▶ مثال: صفحات تسجيل الدخول والتسجيل
المخرجات:
يُحدّث سجل قاعدة البيانات.
// 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>
)
}
المخرجات:
يعرض واجهة مستخدم مكون SignInPage.
// 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
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
// 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
# الوصول إلى المسارات المحمية (غير مسجل الدخول)
curl -v http://localhost:3000/dashboard 2>&1 | grep Location
# زيارة مسار API (غير مسجل الدخول)
curl -v http://localhost:3000/api/projects 2>&1
< 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) استراتيجية التغذية
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
▶ مثال: سكريبت التغذية الكامل
المخرجات:
رسم توضيحي لتدفق التغذية: إنشاء مؤسسة → مستخدمين → مشاريع → مهام → تعليقات.
// 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()
})
المخرجات:
🌱 جاري تغذية قاعدة البيانات...
`✅ اكتملت التغذية:
- 1 مؤسسة (${org.name}
e
# إضافة سكريبت التغذية في package.json
# "prisma": { "seed": "tsx src/prisma/seed.ts" }
# تشغيل التغذية
npx prisma db seed
🌱 جاري تغذية قاعدة البيانات...
✅ اكتملت التغذية:
- 1 مؤسسة (Acme Corp)
- 5 مستخدمين
- 5 مشاريع
- 50 مهام
- 16 تعليقات
8. مثال كامل: العملية الكاملة من تهيئة المشروع إلى المصادقة
# ============================================
# عملية تهيئة 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 في المتصفح):
→ إعادة توجيه تلقائي إلى /auth/signin (غير مسجل الدخول)
→ عرض نموذج تسجيل الدخول (Email + Password + Google)
→ تسجيل الدخول باستخدام alice@acme.com / password123
→ الانتقال إلى /dashboard
→ عرض الشريط الجانبي: Acme Corp · 5 مشاريع · 50 مهمة
❓ أسئلة شائعة
npx prisma migrate dev --name <وصف> لإنشاء ترحيل جديد؛ سيقوم Prisma تلقائياً بإنشاء وتنفيذ SQL. في بيئة التطوير، يمكنك استخدام npx prisma db push للمزامنة مباشرة (بدون إنشاء ملفات ترحيل).TKFLW-ACMECORP. دمج البادئة مع slug المؤسسة يجعل رمز الدعوة سهل القراءة وفريداً./((?!_next/static|_next/image|favicon.ico).*) يطابق جميع مسارات الموارد غير الثابتة، مما يسمح لـ middleware بإجراء فحوصات المصادقة على جميع مسارات الصفحات ومسارات API. توصي Next.js بإجراء فحوصات المسار داخل middleware بدلاً من تجزئتها بشكل مفرط في matcher.npm i tsx -D)، يمكنه تشغيل ملفات .ts مباشرة بدون ترجمة. توصي Prisma رسمياً بـ tsx كمشغل للتغذية. يمكنك أيضاً استخدام ts-node، لكن tsx أسرع ويوفر توافقاً أفضل.passwordHash اختياري في نموذج User؟Credentials، يكون لـ passwordHash قيمة؛ أما لمستخدمي OAuth، فيكون passwordHash null. هذا يوضح مرونة تصميم المخطط.📖 ملخص
create-next-appاختيار الإطار: TypeScript + Tailwind + App Router + دليل src كنقطة انطلاق للمشاريع على مستوى المؤسسات- توفر shadcn/ui مكتبة مكونات واجهة مستخدم خالية من التبعيات يمكن تثبيتها حسب الطلب دون زيادة حجم الحزمة
- يستخدم Prisma Schema صيغة تعريفية لتعريف خمسة نماذج — User و Organization و Project و Task و Comment — بالإضافة إلى أنواع التعداد
- يدعم Auth.js v5 المصادقة المزدوجة عبر Credentials و Google OAuth؛ سياسة جلسة JWT لا تتطلب تخزين الجلسات في قاعدة البيانات
- ينفذ Middleware حماية المسارات العامة من خلال مساعد
auth()وتكوين matcher - يسمح نظام رموز الدعوة للمؤسسات (ببادئة
TKFLW-) لأعضاء الفريق بالانضمام بأنفسهم - تستخدم تغذية قاعدة البيانات مشغل tsx، الذي يمكنه إنشاء أكثر من 100 سجل تجريبي حقيقي
📝 تمارين
-
تمرين أساسي (⭐): اتبع الخطوات في هذا الدرس لتهيئة مشروع TaskFlow، وشغّل
npm run devبنجاح، واعرض صفحة تسجيل الدخول في المتصفح. التقط لقطة شاشة للنتائج واحفظها. -
تمرين متقدم (⭐⭐): أضف نموذج
Label(نظام الوسوم) إلى Prisma Schema، وأنشئ علاقة متعدد إلى متعدد معTask(عبر الجدول الوسيطTaskLabel)، ثم شغّل الترحيل وأنشئ الأنواع. نفذ مسار API CRUD للوسوم. -
تحدٍّ (⭐⭐⭐): وسّع إعداد Auth.js لإضافة مزود GitHub OAuth؛ نفذ منطق التحقق من الأدوار الموصوف في
middleware.ts— فقط المستخدمون ذوو دوريOWNERوADMINيمكنهم الوصول إلى مسار/dashboard/admin؛ اكتب اختبارات وحدة للتحقق من منطق حماية الأدوار.