مدونة بأستخدام NextAuth و Prisma (PostgreSQL)

Mohanad Alrwaihy

April 24, 2023

186

1

في هذا المنشور ، سنقوم بإنشاء مشروع Simple Blog Post باستخدام قاعدة بيانات NextAuth و PostgreSQL مع Prisma 💐

11 دقائق للقراءة

ما هو NextAuth؟

NextAuth هي واحدة من أفضل الأدوات للتعامل مع المصادقة عند استخدام Next.js لإنشاء مواقع ويب. لأنه سهل الاستخدام ، يقدم التكيف وأضافة الكثير من خيارات قواعد البيانات لحفظ المستخدمين واستمرار الجلسات.

خيارات المصادقة في NextAuth:

  1. موفرو OAuth - ( GitHub و Twitter و Google وما إلى ذلك ).
  2. موفر OAuth مخصص.
  3. استخدام البريد الإلكتروني - الروابط السحرية.
  4. استخدام طريقة تسجيل الدخول التقليدية - اسم المستخدم وكلمة المرور أو أي بيانات اعتماد عشوائية.

ميزات NextAuth

  • الأمان 🔒 - NextAuth تعزز عدم أستخدام طرق بأستخدام كلمات السر.
  • رموز تزوير طلب عبر المواقع ( CSRF ) على مسارات POST ( تسجيل الدخول ، تسجيل الخروج ).
  • تهدف سياسات ملفات تعريف الارتباط (Cookie) إلى أكثر السياسات تقييدًا.
  • رموز الويب JSON ( JWT ) مشفرة باستخدام A256GCM.

اقرأ المزيد عن NextAuth هنا.

تدعم NextAuth استراتيجيتين لجلسة المستخدم JWTs وجلسة قاعدة البيانات.

JWTs وجلسة قاعدة البيانات

يستخدم NextAuth الJWT بشكل أفتراضي لحفظ جلسة المستخدم. بينما يستخدم Database Session عند استخدام NextAuth مع قاعدة بيانات.

لدي منشور مدونة يتحدث عن JWTs و Database Session إذا كنت تريد معرفة المزيد عنه أنقر هنا.

معلومات عن Prisma

ما هو Prisma؟

بأختصار Prisma يصنف ك (Object Relation Mappin) ORM الذي يجعل العمل مع قواعد البيانات مهمة سهلة بسبب القدرة على إنشاء النماذج بطريقة نظيفة وترحيل النماذج إلى قاعدة البيانات ، ويوفر Type Safety عند التعامل مع TypeScript, والإكمال التلقائي.

مخطط Prisma

'مخطط Prisma بديهي ويتيح لك بناء جداول قاعدة البيانات الخاصة بك بطريقة يمكن قراءتها بواسطة الإنسان — مما يجعل تجربة نمذجة البيانات الخاصة بك ممتعة. يمكنك تحديد نماذجك يدويًا أو أستعمالها من قاعدة بيانات موجودة.'

قواعد بيانات Prisma

هذه هي قاعدة البيانات المدعومة:

  1. PostgreSQL
  2. MySQL
  3. SQLite
  4. SQL Server
  5. MongoDB
  6. CockroachDB

مشروع مدونة

سنقوم بإنشاء تطبيق بسيط Blog مع NextAuth و Prisma باستخدام Google OAuth Provider

هذا هو المظهر النهائي للمشروع 👇

  • المشروع النهائي - هنا
  • مستودع GitHub - هنا

بناء مشروع NextJS

POWERSHELL
npx create create-next-app@latest

سأقوم بتسمية التطبيق prisma_blog والتحقق من هذه الخيارات :

  • TypeScript ✅
  • TailwindCSS ✅
  • ESlint ✅

التعديلات الأساسية

بعد اكتمال التثبيت سأقوم بتنظيف ملف index.tsx و globals.css

index.tsxTSX
/* index.tsx */
export default function Home() {
  return (
    <div>
      <h1>Hello World</h1>
    </div>
  )
}
globals.cssCSS
/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
  .btn {
    @apply py-1 px-4 shadow-sm hover:shadow-md transition-all font-semibold rounded-md outline-none border-none ring-offset-2 ring-offset-neutral-950 tracking-wide focus-visible:ring-2 focus:scale-95 disabled:cursor-not-allowed disabled:opacity-75 disabled:shadow-none bg-teal-400 shadow-teal-800 hover:bg-teal-500 hover:shadow-teal-800 text-black ring-teal-400 disabled:hover:bg-teal-400;
  }

 .btn-red {
    @apply bg-red-400 shadow-red-800 hover:bg-red-500 hover:shadow-red-800 text-black ring-red-400 disabled:hover:bg-red-400;
  }

إضافة عنصر Layout.tsx الذي يعرض Nav.tsx و children في ملف app.tsx_:

Layout.tsxTSX
// Layout.tsx
import React from 'react'
import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'] })

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <div
      className={`flex min-h-screen flex-col items-start p-3 max-w-6xl mx-auto ${inter.className}`}
    >
      <Nav />
      {children}
    </div>
  )
}
_app.tsxTSX
// _app.tsx
import Layout from '@/components/Layout'
import '@/styles/globals.css'
import type { AppProps } from 'next/app'

export default function App({ Component, pageProps }: AppProps) {
  return (
    <Layout>
	 <Head>
        <title>Prisma Blog</title>
      </Head>
      <Component {...pageProps} />
    </Layout>
  )
}

هذا هيا الأعدادات الرئيسية لتطبيقنا. سيتم العثور على أي إضافة في Repository.

الصفحات 📃

ستكون هناك 3 صفحات في الموقع:

  • الصفحة الرئيسية - ./pages/index.tsx
  • مسودة - ./pages/draft.tsx
  • النشر - ./pages/post.tsx

أضف NextAuth

سوف أقوم بأتباع التعليمات في NextAuth

ابدأ بتثبيت next-auth:

POWERSHELL
npm install next-auth

إضافة NextAuth API Route:

pages/api/auth/[...nextauth].tsTSX
/* pages/api/auth/[...nextauth].ts */
import NextAuth from "next-auth"
import GoogleProvider from "next-auth/providers/google"

export const authOptions = {
  // Configure one or more authentication providers
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_ID || '',
      clientSecret: process.env.GOOGLE_SECRET || '',
    }),
    // ...add more providers here
  ],
}

export default NextAuth(authOptions)

إضافة معرف Google و Google Secret

نحن بحاجة إلى الحصول على GOOGLE_ID و GOOGLE_SECRET. للحصول على هذا ، نحتاج إلى التوجه إلى Google Cloud Console وإنشاء مشروع جديد.

إذا لم يكن لديك حساب Google Cloud ، يمكنك إنشاء حساب مجانًا ، عليك إضافة طريقة دفع لإنشاء الحساب ولكن لن تفرض أي رسوم إذا كنت تعرف ما تفعل 🙂 ( كن حذرًا لعدم استخدام أي خدمات حتى تقرأ رسوم الخدمة ) في حالتنا ، سنضيف OAuth 2.0 المجاني للاستخدام.

Iconنصيحة

إذا لم يكن لديك حساب Google Cloud ، يمكنك إنشاء حساب مجانًا ، عليك إضافة طريقة دفع لإنشاء الحساب ولكن لن تفرض أي رسوم إذا كنت تعرف ما تفعل 🙂 ( كن حذرًا لعدم استخدام أي خدمات حتى تقرأ رسوم الخدمة ) في حالتنا ، سنضيف OAuth 2.0 المجاني للاستخدام.

لإنشاء مشروع جديد ، يمكنك كتابة New Project في شريط البحث العلوي وتحديد الخيار الأول:

اختر أسم للمشروع واضغط على Create:

اكتب OAuth واختر Credentials:

في أعلى انقر Create Credentials ثم OAuth client ID:

يجب عليك تعديل اعدادات Consent Screen أولاً:

  • Configure consent screen
  • User type - External
  • Create

الآن علينا إضافة معلومات التطبيق 👇

  • اسم التطبيق - Prisma Blog
  • البريد الإلكتروني لدعم المستخدم - أي بريد إلكتروني
  • معلومات الاتصال بالمطور - بريدك الإلكتروني أو اي شيء
  • حفظ ومتابعة.

بمجرد انتهاء من تعديل Consent screen سوف نعود إلى Credentials ثم OAuth client ID.

هذا مثال على كيفية ملء الحقول المطلوبة 👇

  • نوع التطبيق - تطبيق ويب.
  • الاسم - يمكنك وضع أي اسم هنا ويمكنك إنشاء بيانات اعتماد OAuth متعددة لبيئات مختلفة.
  • أصول JavaScript المعتمدة - عنوان URL الأصلي للتطوير سيكون http://localhost:3000 تأكد من إضافة نطاقك الفعلي في الProduction.
  • الURIs المعتمدة لأعادة التوجيه - مع NextAuth سيكون عنوان URL الأساسي متبوعًا بـ BASE_URL/api/auth/callback/ provider في هذه الحالة يكون الموفر هو google.
  • انشاء!
  • بمجرد إنشائك سترى معرف العميل و سر العميل الذي نريد استخدامه في تطبيقنا 👍

إضافة Session Provider & useSession

SessionProvider 👇

_app.tsxTSX
// _app.tsx
import Layout from '@/components/Layout'
import '@/styles/globals.css'
import type { AppProps } from 'next/app'

import { SessionProvider } from 'next-auth/react'

export default function App({ Component, pageProps }: AppProps) {
  return (
    <SessionProvider session={pageProps.session}>
      <Layout>
        <Head>
          <title>Prisma Blog</title>
        </Head>
        <Component {...pageProps} />
      </Layout>
    </SessionProvider>
  )
}

في شريط Nav ، سنستخدم useSession للقيام بعملية تسجيل الدخول و تسجيل الخروج و عرض معلومات الجلسة 👇

Nav.tsxTSX
/* Nav.tsx */
import { signIn, signOut, useSession } from 'next-auth/react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import Button from './ui/Button'
  
export default function Nav() {
  const { data: session } = useSession()
  const asPath = useRouter().asPath

  function cn(...classes: string[]) {
  return classes.filter(Boolean).join(' ')
  }

  const navigation = [
    { title: 'Home', href: '/' },
    { title: 'Draft', href: '/draft' },
    { title: 'Post', href: '/post' },
  ]
  
  return (
    <nav className='p-5 mb-20 rounded-md shadow-lg bg-neutral-950 text-neutral-200 w-full'>
      <ul className='flex items-center gap-5 font-medium'>
        {navigation.map(({ title, href }) => (
          <li key={title}>
            <Link
              className={cn(
                'py-2 px-4 rounded-lg',
                asPath === href
                  ? 'text-teal-400 underline underline-offset-8 cursor-default'
                  : 'hover:underline underline-offset-4'
              )}
              href={href}
            >
              {title}
            </Link>
          </li>
        ))}
        <li className='ml-auto flex gap-2 text-sm'>
          {!session ? (
           <button className='btn' onClick={() => signIn('google')}>
              Sign in
            </button>
          ) : (
            <>
              <img
                src={session.user?.image || ''}
                alt={session.user?.name || 'User Avatar'}
                className='w-8 h-8 rounded-md cursor-pointer hover:ring ring-teal-400 mr-2'
              />
              <button className='btn' onClick={() => signOut()}>
                Sign Out
              </button>
            </>
          )}
        </li>
      </ul>
    </nav>
  )
}

الآن يمكننا تسجيل الدخول باستخدام Google وتسجيل الخروج وقراءة معلومات المستخدم الحالية!

إضافة Prisma

ابدأ بتثبيت prisma:

POWERSHELL
npm install prisma

تهيئة Prisma مع:

POWERSHELL
npx prisma init

هذا سوف ينشئ مجلد prisma مع ملف schema.prisma 👇

./prisma/schema.prismaTSX
/* ./prisma/schema.prisma */
generator client {
  provider = "prisma-client-js"
}

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

وسوف يقوم بإنشاء ملف env مع Connection String لقاعدة البيانات بأسم DATABASE_URL.

قاعدة بيانات PostgreSQL

يمكنك استخدام أي قاعدة بيانات تدعمها Prisma

سأقوم بإنشاء قاعدة بيانات PostgreSQL محلية تسمى prisma_blog مع psql 👇

POWERSHELL
# تسجيل الدخول لقاعدة البيانات بأستخدام اسم المستخدم postgres 👇
psql -U postgres 

# أنشاء قاعدة بيانات جديدة بأسم prisma_blog 👇
create database prisma_blog;

نحتاج إلى Connection String حتى نتمكن من استخدام قاعدة البيانات المحددة هذه مع Prisma 👇

POWERSHELL
# مثال للConnection String 👇
postgres://YourUserName:YourPassword@YourHostname:5432/YourDatabaseName

# للأتصال بقاعدة البيانات المحلية prisma_blog
# أنا لا أستخدم كلمة مرور ولكن تأكد من ادراجها إذا لزم الأمر
postgresql://postgres@localhost:5432/prisma_blog

يمكننا الآن استبدال الConnection String الموجودة تحت متغير البيئة DATABASE_URL إلى القيمة الصحيحة.

.envPOWERSHELL
# .env
DATABASE_URL="postgresql://postgres@localhost:5432/prisma_blog"

أستخدام Prisma Adapter في NextAuth

أبدء بتثبيت Prisma Adapter تحت أسم @next-auth/prisma-adapter:

POWERSHELL
npm install @next-auth/prisma-adapter

أضف Prisma Adapter إلى [...nextauth].ts:

[...nextauth].tsTSX
...
import { PrismaClient } from '@prisma/client'
import { PrismaAdapter } from '@next-auth/prisma-adapter'

const prisma = new PrismaClient()

export const authOptions = {
  adapter: PrismaAdapter(prisma),
  ...
}

نماذج Prisma (Models)

هناك فائدتان رئيسيتان لنماذج Prisma:

  • تمثيل الجداول الفعلية في قاعدة البيانات ( PostgreSQL ).
  • إضافة الأساس لـ Prisma Client API ( المستخدم للتواصل مع قاعدة البيانات. )

علينا إضافة العديد من النماذج لربط قاعدة البيانات الخاصة بنا بـ NextAuth وإضافة المنشورات لاحقًا.

جميع النماذج الضرورية:

./prisma/schema.prismaTSX
/* ./prisma/schema.prisma */
...

model User {
  id            String    @id @default(cuid())
  name          String?
  username      String?
  password      String?
  email         String   @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
  posts Post[]
}

model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime
  @@unique([identifier, token])
}

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

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String?
  published Boolean @default(false)
  author    User    @relation(fields: [authorId], references: [id])
  authorId  String
}

لدينا العديد من النماذج التي تم إنشاؤها هنا كل منها مفيد في جلستنا وإنشاء المنشورات:

  • المستخدم (User) - يستخدم هذا الجدول لحفظ معلومات المستخدم الجديدة باستخدام معرف فريد وبيانات اعتماد المستخدم الأخرى إذا كانت متوفرة ويمكن أن يكون للمنشورات التي أنشأها هذا المستخدم.
  • رمز التحقيق (Verification Token)
  • الحسابات (Account) - يستخدم لحفظ الأنواع المختلفة من الحسابات للمستخدمين الذين سجلوا في تطبيقك لأنه يمكن لنفس المستخدم أن يسجل بالطريقة التقليدية (اسم المستخدم وكلمة السر) وبأستخدام حسابات OAuth اخرى مثل (Google, GitHub, الى أخره)
  • الجلسة (Session) - جدول لجميع الجلسات الحالية المستخدمة مع المستخدمين في تطبيقك بمعرفهم الفريد ووقت انتهاء الصلاحية ومعرف المستخدم و رمز الجلسة Session Token التي يستخدمها المستخدمون لاستمرار الجلسة في التطبيق وطلب المعلومات من قاعدة البيانات.
  • المنشورات Post - هذا الجدول مخصص للمعلومات المطلوبة للمنشور مثل العنوان والمؤلف ومحتوى المنشور.

تهجير نماذج Prisma (Migrate)

باستخدام Prisma Migrate ، يمكننا إنشاء جداول PostgreSQL الفعلية وفقًا للنماذج التي تم إنشاؤها أعلاه.

توليد Prisma Migrate:

POWERSHELL
npx prisma migrate dev
  • أدخل init لاسم التهجير لتوضيح أن هذا هو أول تهجير لقاعدة البيانات.

سوف اقوم بالعودة لPSQL والبحث عن معلومات الجدوال 👇

pages/api/auth/[...nextauth].tsTSX
/* pages/api/auth/[...nextauth].ts */
import NextAuth from "next-auth"
import GoogleProvider from "next-auth/providers/google"

export const authOptions = {
  // Configure one or more authentication providers
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_ID || '',
      clientSecret: process.env.GOOGLE_SECRET || '',
    }),
    // ...add more providers here
  ],
}

export default NextAuth(authOptions)

سترى أيضًا مجلدًا جديدًا ضمن مجلد prisma يسمى التهجير (Migrate) والذي يحتوي على أكواد SQL لجميع عمليات التهجير التي قمت بها في مشروعك.

استخدام Prisma في التطبيق (Prisma Client)

قم بإنشاء ملف جديد داخل مجلد prisma باسم client.ts:

./prisma/client.tsTSX
/* ./prisma/client.ts */

import { PrismaClient } from '@prisma/client'

// PrismaClient is attached to the `global` object in development to prevent
// exhausting your database connection limit.
//
// Learn more:
// https://pris.ly/d/help/next-js-best-practices

const globalForPrisma = global as unknown as { prisma: PrismaClient }

export const prisma = globalForPrisma.prisma || new PrismaClient()

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

  

export default prisma

يستخدم هذا لمنع استنفاد حد الاتصال الأقصى بقاعدة البيانات.

أستديو Prisma

استخدم هذا الأمر لتشغيل أستديو Prisma:

POWERSHELL
npx prisma studio

الآن إذا قمت بتسجيل الدخول باستخدام Google سترى مستخدمًا جديدًا ظهر في قاعدة البيانات!

صحفات التطبيق ومسارات الAPI

مسار الAPI للمنشورات (Post)

للتفاعل مع قاعدة بيانات PostgreSQL باستخدام Prisma ، يمكننا جلب البيانات باستخدام getStaticProps أو getServerSideProps أو باستخدام ``مسارات API

في هذا المنشور ، سأستخدم مزيجًا من getServerSideProps و ``API

احرص على عدم جلب البيانات من مسار الAPI في getServerSideProps لأنها قد تؤدي إلى أخطاء في السيرفر في الProduction.

إنشاء مسار Post API مع هذا الكود

./api/post/index.tsTSX
/* ./api/post/index.ts */

import type { NextApiRequest, NextApiResponse } from 'next'
import { prisma } from './../../../prisma/client'
import { getServerSession } from 'next-auth'
import { authOptions } from '../auth/[...nextauth]'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === 'GET') {}
  if (req.method === 'POST') {}
  if (req.method === 'PATCH') {}
  if (req.method === 'DELETE') {}
}

نحتاج إلى getServerSession لمعرفة كيف إذا كان هناك مستخدم قام بتسجيل الدخول أم لا لتحديث منشور أو إنشاء منشور جديد.

قراءة المنشورات ( GET )

لقراءة جميع المنشورات في قاعدة البيانات ، يمكننا استخدام استعلام findMany مع الwhere لتحديد فقط المنشورات الي تم نشرها من المؤلف.

TSX
if (req.method === 'GET') {
    try {
      const data = await prisma.post.findMany({
        where: {
          published: true,
        },
      })  
      return res.status(200).json(data)
    } catch (err) {
      return res.status(500).json(err)
    }
  }

نشر منشور ( POST )

لإضافة منشور جديد ، نحتاج أولاً إلى التحقق من جلسة المستخدم واستخدام بريدهم الإلكتروني لإنشاء ( INSERT ) منشور جديد في قاعدة البيانات.

TSX
if (req.method === 'POST') {
    const { title, content } = req.body
    const session = await getServerSession(req, res, authOptions)

    if (session) {
      try {
        const result = await prisma.post.create({
          data: {
            title,
            content,
            author: { connect: { email: session?.user?.email } },
          },
        })
        res.json(result)
      } catch (err) {
        return res.status(500).json(err)
      }
    } else {
      res.status(401).send({ message: 'Unauthorized' })
    }
  }

تحديث منشور ( PATCH )

لتحديث حالة المنشور ، نحتاج إلى إرسال معرف المنشور وحالة المنشور هل هيا published او لا مع الطلب ثم تحديث المنشور ليتم نشره أم لا.

TSX
if (req.method === 'PATCH') {
    const { id, published } = req.body
    if (!id || published === '') {
      return res.status(401).send({ message: 'Unauthorized' })
    }
    const session = await getServerSession(req, res, authOptions)
    
    if (session) {
      try {
        const result = await prisma.post.update({
          where: { id },
          data: { published: !published },
        })
        res.json(result)
      } catch (err) {
        return res.status(500).json(err)
      }
    } else {
	 res.status(401).send({ message: 'Unauthorized' })
    } 
  }

أنا لا أقوم بتحديث عنوان أو محتوى المنشور هنا ولكن يمكنك أضافته هنا إذا كنت تريد!

حذف المنشور ( DELETE )

TSX
if (req.method === 'DELETE') {
    const { id } = req.query.id
    if (!id) res.status(401).send({ message: 'ID not found' })
    
    const session = await getServerSession(req, res, authOptions)
    if (session) {
      try {
        await prisma.post.delete({
          where: { id: Number(id) },
        })
        res.status(200).send('Delete Post Successfully')
      } catch (err) {
        return res.status(500).json(err)
      }
    }
    else {
	res.status(401).send({ message: 'Unauthorized' })
    }
  }

الصفحة الرئيسية

سأستخدم هوك useSWR من SWR بواسطة Vercel لجلب بيانات المنشورات في جهة العميل (Client Side)

يمكنك استخدام getServerSideProps مباشرة واستخدام عميل prisma للاستعلام عن بيانات النشر بدلاً من هذا النهج ، سيبدو مثل النهج في صفحة المسودات Draft في القسم التالي

تثبيت SWR

POWERSHELL
npm install swr
index.tsxTSX
import { Post } from "@prisma/client"
import { useSession } from 'next-auth/react'
import Link from 'next/link'
import useSWR, { Fetcher } from 'swr'

const fetcher: Fetcher<any, string> = (...args) =>
  fetch(...args).then((res) => res.json())
  
export default function Home() {
  const { data: session } = useSession()
  const { data: posts, isLoading, error } = useSWR('/api/post', fetcher)
  
 if (error)
    return (
      <div className='w-full p-5'>
        <h1>No Posts 🥲</h1>
      </div>
    )

  if (isLoading) return <div className='w-full p-5'>Loading...</div>
 
  return (
    <div className='w-full p-5'>
      {posts.map((post: Post) => (
        <div key={post.id} className='py-5 border-b border-teal-400 px-5'>
          <h1 className='font-bold text-2xl'>{post.title}</h1>
          <p className='text-lg mt-4'>{post.content}</p>
          {session?.user && post.authorId === session.user?.id && (
            <Link href='/draft' className='btn inline-block mt-4'>
              Edit
            </Link>
          )}
        </div>
      ))}
    </div>
  )
}

لم يتم أضافة معرف المستخدم (User ID) في الجلسة Session ، لذلك من أجل تضمينه ، سنستخدم دالة Session Callback لتمديد المعلومات المرسلة في الجلسة Session وإضافة معرف المستخدم 👇

pages/api/auth/[...nextauth].tsTSX
/* pages/api/auth/[...nextauth].ts */

const prisma = new PrismaClient()
export const authOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [...],
  
  callbacks: {
    async session({ session, user }: { session: Session; user: AdapterUser }) {
      session.user.id = user.id
      return session
    },
  },
}

صفحة المسودات

draft.tsxTSX
import { GetServerSidePropsContext } from 'next'
import { signIn, useSession } from 'next-auth/react'
import prisma from '../prisma/client'
import Router from 'next/router'
import { Post } from '@prisma/client'
import { authOptions } from './api/auth/[...nextauth]'
import { getServerSession } from 'next-auth'

export async function getServerSideProps({
  req,
  res,
}: GetServerSidePropsContext) {
  const session = await getServerSession(req, res, authOptions)
  if (!session) {
    return { props: { drafts: [] } }
  }

  const drafts = await prisma.post.findMany({
    where: { author: { email: session.user?.email } },
  })

  return {
    props: {
      drafts,
    },
  }
}

export default function Draft({ drafts }: { drafts: Post[] }) {
  const { data: session } = useSession()

  if (!session) {
    return (
     <button className='btn text-lg mx-auto' onClick={() => signIn('google')}>
        Sign In to see Drafts
      </button>
    )
  }

  async function handlePost(
    e: React.SyntheticEvent,
    id: number,
    published: boolean,
    del = false
  ) {
    e.preventDefault()
    
    try {
      await fetch(`/api/post${del ? `?id=${id}` : ''}`, {
        method: del ? 'DELETE' : 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: del ? '' : JSON.stringify({ id, published }),
      })
      await Router.push('/draft')
    } catch (err) {
      console.error(err)
    }
  }
  
  return (
    <div className='p-5 w-full'>
      {drafts.map((draft: any) => (
        <div
         key={draft.title}
          className='flex justify-between items-center border-b-2 pb-10 px-5'
        >
          <div>
            <h2 className='text-2xl font-bold text-teal-600 my-4'>
              {draft.title}
            </h2>
            <p>{draft.content}</p>
            {!draft.published ? (
              <button
                onClick={(e) => handlePost(e, draft.id, draft.published)}
                className='btn mt-4'
              >
                Publish
              </button>
            ) : (
              <button
                onClick={(e) => handlePost(e, draft.id, draft.published)}
                className='btn btn-red mt-4'
              >
                Unpublish
              </button>
            )}
          </div>
          <button
            onClick={(e) => handlePost(e, draft.id, draft.published, true)}
            className='btn btn-red'
          >
            Delete
          </button>
        </div>
      ))}
    </div>
  )
}

صفحة المنشورات

TSX
import { signIn, useSession } from 'next-auth/react'
import Router from 'next/router'
import { useState } from 'react'

export default function Post() {
  const [title, setTitle] = useState('')
  const [content, setContent] = useState('')
  
  const { data: session } = useSession() 
  if (!session) {
    return (
      <button className='btn mx-auto text-lg' onClick={() => signIn('google')}>
        Sign In to create Posts
      </button>
    )
  }
  
  async function handleSubmit(e: React.SyntheticEvent) {
    e.preventDefault()
    if (!title || !content) return
    
    try {
      const body = { title, content }
      const post = await fetch(`/api/post`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      })
      if (post.ok) await Router.push('/draft')
      throw new Error(post.statusText)
    } catch (err) {
      console.error(err)
    }
  }
  
  return (
    <form
      onSubmit={handleSubmit}
      className='flex flex-col gap-5 max-w-md w-full mx-auto justify-center'
    >
      <h1 className='text-xl font-bold text-teal-600 text-center'>
        Create New Draft
      </h1>
      <label htmlFor='title'>Title</label>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        type='text'
        id='title'
        autoFocus
        required
        className='border p-2 text-black'
      />
   
      <label htmlFor='content'>Content</label>
      <textarea
        value={content}
        required
        onChange={(e) => setContent(e.target.value)}
        id='content'
        className='border p-2 text-black'
      />
      
      <button className='btn'>
        Add Post
      </button>
    </form>
  )
}

الخاتمة

تمكنا من إنشاء مدونة باستخدام NextJS و NextAuth (مصادقة المستخدم) و Prisma ( قاعدة البيانات ) 😲

يمكن استخدام هذه الأدوات الثلاثة لإنشاء مشاريع Fullstack مع مصادقة وقاعدة بيانات آمنة!

NextJS

لقد استخدمت NextJS لفترة من الوقت ومن المميز في هذه الأداه انها لا تفشل في تقديم تجربة تطوير رائعة بالنسبة لي لإنشاء تطبيق جاهز للإنتاج Production مع أنواع مختلفة من طرق العرض مثل 👇

  • SSR (Server Side Rendering)
  • CSR (Client Side Rendering)
  • SSG (Static Site Generation)
  • ISR (Incremental Static Regeneration)

والقدرة على إنشاء واجهات برمجة تطبيقات مخصصة APIs ، مكون صورة مخصص Image Component ومجموعة من الخطافات والأساليب المفيدة وسهلة الاستخدام والفهم!

يمكنك قراءة معلومات عن طرق التقديم Render Methods من هنا.

NextAuth

المصادقة بNextAuth اعتقد انها أسهل طريقة لإنشاء مصادقة لتطبيق NextJS. لأنه سهل الاستخدام ومرن وآمن ويدعم أنواعًا متعددة من استراتيجيات التشفير ومحولات قاعدة البيانات!

NextAuth تنتقل لتصبح أداة جديدة تسمى الآن Auth.js والتي سيتم استخدامها مع أطر مختلفة أخرى غيرNextJS مثل SavelteKit و SolidJS

Prisma

تقدم Prisma تجربة رائعة للمطورين للتفاعل مع قواعد البيانات وتعديلها أو اختيار بين خيارات متعددة لقواعد البيانات.