• בלוג
  • תבנית פרויקט: next, drizzle, auth0

תבנית פרויקט: next, drizzle, auth0

26/01/2025

רוצים להתחיל פרויקט React חדש שיתחבר לבסיס נתונים ויכיל מנגנון ניהול משתמשים מובנה? ברור, מי לא ירצה. במקום לשבור את השיניים על חיבור כל הכלים תנו לי לחסוך לכם עבודה ולשתף תבנית פשוטה שכתבתי שפשוט עובדת, מבוססת על דוגמה של auth0. זה הגיטהאב:

https://github.com/ynonp/next-drizzle-demo

בואו נראה מה יש בפנים.

1. ניהול משתמשים עם auth0

בשביל שהפרויקט יעבוד יש ליצור פרויקט ב auth0, ואז ליצור קובץ .env במבנה הבא:

AUTH0_SECRET=
AUTH0_BASE_URL=
AUTH0_ISSUER_BASE_URL=
AUTH0_CLIENT_ID=
AUTH0_CLIENT_SECRET=
DATABASE_URL=

ב AUTH0_SECRET כותבים מחרוזת אקראית שאפשר לקבל מהרצת הפקודה:

node -e "console.log(crypto.randomBytes(32).toString('hex'))"

את הערכים הבאים נמלא מהגדרות היישום ב auth0. בגדול auth0 מנהל עבורנו את כל תהליך חיבור ורישום משתמשים ומספק לנו מסכי התחברות, רישום, התחברות דרך צד-שלישי, מסך שכחתי סיסמה וכל מה שאפשר לדמיין. אתם רק צריכים ליצור משתמש שם ואז ליצור אפליקציה לפי ההוראות כאן:

https://auth0.com/docs/get-started/auth0-overview/create-applications

מבחינת הקוד בקובץ layout.tsx יש לנו את הקוד הבא:

import "../styles/styles.css";
import React, { PropsWithChildren } from "react";
import { PageLayout } from "@/components/page-layout";
import { siteMetadata } from "@/components/page-head";
import { PreloadResources } from "@/app/preload-resources";
import { UserProvider } from "@auth0/nextjs-auth0/client";

export const metadata = siteMetadata;

const RootLayout: React.FC<PropsWithChildren> = ({ children }) => {
  return (
    <html lang="en">
      <PreloadResources />
      <body>
        <UserProvider>
          <PageLayout>{children}</PageLayout>
        </UserProvider>
      </body>
    </html>
  );
};

export default RootLayout;

הקומפוננטה UserProvider אחראית לתקשורת עם auth0. בנוסף בתיקייה api/auth/[auth0] יש את הקובץ route.ts שאחראי על התקשורת עם auth0 וגם שם בעיקר השתמשתי בספריה שלהם:

import { handleAuth, handleLogin } from "@auth0/nextjs-auth0";

export const GET = handleAuth({
  login: handleLogin({
    returnTo: "/profile",
  }),
  signup: handleLogin({
    authorizationParams: {
      screen_hint: "signup",
    },
    returnTo: "/profile",
  }),
});

הקובץ middleware.ts שנמצא בתיקיית src מגדיר לנו את רשימת הנתיבים שפתוחים רק למשתמשים מחוברים ומאפשר לכתוב קוד שירוץ עבור כל משתמש שמתחבר:

// middleware.ts

import { withMiddlewareAuthRequired, getSession } from "@auth0/nextjs-auth0/edge";
import { NextResponse } from "next/server";

export default withMiddlewareAuthRequired(async function middleware(req) {
  // runs for authenticated users
  const res = NextResponse.next();
  const user = await getSession(req, res);
  res.cookies.set('hl', user?.language);
  return res;
});

export const config = {
  matcher: ["/protected", "/admin", "/profile"],
};

זה אומר שכל פעם שאנסה להיכנס לאחד הנתיבים ב config אני אנותב מיד למסך ההתחברות.

2. בסיס נתונים

החיבור לבסיס נתונים מבוצע עם drizzle ואני משתמש בדרייבר של neon כדי להתחבר לבסיס נתונים של neon בענן (כן גם שם צריך לפתוח פרויקט). אחרי שתפתחו פרויקט ב neon בקישור:

https://console.neon.tech/app/projects

תוכלו להעתיק את כתובת בסיס הנתונים לקובץ .env (הפריט האחרון שם) ואז להריץ משורת הפקודה:

npx drizzle-kit push

כדי ליצור את הטבלאות. כל הטבלאות מוגדרות בקובץ src/db/schema.ts וכרגע יש שם רק את:

import { integer, pgTable, varchar } from "drizzle-orm/pg-core";
export const usersTable = pgTable("users", {
  id: integer().primaryKey().generatedAlwaysAsIdentity(),
  name: varchar({ length: 255 }).notNull(),
  age: integer().notNull(),
  email: varchar({ length: 255 }).notNull().unique(),
});

הקובץ src/testdb.ts מראה איך להשתמש ב drizzle כדי ליצור נתונים בטבלאות.

3. איך הכל מתחבר

קובץ מאוד מעניין בפרויקט הוא src/services/people.service.ts:

'use server';

import { usersTable } from '../db/schema';
import { sql } from "drizzle-orm";
import { db } from "@/db/drizzle";


export async function getPeople() {
  const users = await db.select().from(usersTable);
  return users;
}

אין פה המון אבל מה שיש פה בהחלט מלהיב. הקובץ מגדיר פונקציה אסינכרונית אחת בשם getPeople שפונה לבסיס הנתונים. אני מפעיל את הפונקציה מתוך הקובץ app/public/page.tsx באופן הבא:

import { NextPage } from "next";
import React from "react";
import { getPeople } from "@/services/people.service";

const Public: NextPage = async () => {
  const people = await getPeople();

  return (
    <div className="content-layout">
      <h1 id="page-title" className="content__title">
        Public Page - list of people
      </h1>
      <div className="people">
        <ul>
        {people.map(user => (
          <li>{user.email}</li>
        ))}
        </ul>
        <hr />
      </div>
      <div className="content__body">
        <p id="page-description">
          <span>
            This page retrieves a <strong>public message</strong>.
          </span>
          <span>
            <strong>Any visitor can access this page.</strong>
          </span>
        </p>
      </div>
    </div>
  );
};

export default Public;

וכן זה הכח של next.js - מתוך קוד ריאקט שירוץ בצד שרת (Server Side Component) אנחנו פונים ישר לבסיס הנתונים, מושכים את המידע ושולחים את התוצאה לדפדפן. בעצם ה next מתחבר ישירות לבסיס הנתונים ואין צורך בשרת node או Backend אחר. בארכיטקטורה זו next.js הוא פריימוורק פיתוח Full Stack שמאפשר לנו לכתוב גם את קוד הפרונט בריאקט וגם את קוד צד השרת שקורא מידע מבסיס הנתונים או מעדכן שם.

בגלל שזה next אפשר להעלות את הפרויקט בלחיצת כפתור לשרתים של vercel וככה אנחנו מקבלים גם מנגנון CI/CD בחינם ויכולים להתמקד בפיתוח במקום בחיבור תשתיות.