Головна / Статті

Вход у Google за допомогою Auth.js та Prisma: поєднання посібників Next.js

Об’єднайте посібники Prisma та Auth.js для Next.js у один функціональний додаток із входом через Google, зберіганням даних користувачів та публікацій, прив’язаних до сеансу, включаючи виправлення, які пропускаються у документації.

Видавець: REACTAPP.TOP10 хв читання2404 слів

Джерела: Prisma + NextAuth setup for NextJS

Додавання облікових записів та збережених даних до проекту Next.js не вимагає окремого бекенду. Завдяки Auth.js (наступнику NextAuth) для аутентифікації, Prisma як ORM та хостованій базі даних Postgres, такій як Neon, один і той самий проект Next.js може обробляти процеси входу, сеанси та зберігання даних. Кожен інструмент має власний гід для Next.js, проте ці гіди розрізняються щодо структури папок, посилаються на застарілі стандарти та ніколи не показують, як усі елементи взаємопов’язані. Цей посібник слідує цим гідам у порядку та вказує на кожну необхідну корекцію, щоб у результаті ви отримали можливість входу через Google, зберігання користувачів у Postgres та прив’язку публікацій до авторизованого користувача.

Чому тримати аутентифікацію та дані всередині проекту Next.js

Типова конфігурація для продукту, який спочатку є переважно інструментом з боку клієнта, — це окремий бекенд, наприклад Django з Postgres та хостований постачальник ідентифікації, такий як Auth0. Це працює, але означає необхідність розгортання, моніторингу та оплати ще одного сервісу. Коли невеликому продукту потрібно додати щось на кшталт збережених робочих процесів, використання сервера Next.js для аутентифікації та доступу до бази даних допомагає знизити витрати на хостинг та кількість елементів, які потрібно підтримувати. Недоліком є те, що тепер вам доводиться самостійно керувати інтеграцією трьох бібліотек, про що й йдеться далі в цій статті.

Ці пакети швидко змінюються. Auth.js для Next.js досі публікується під маркером beta, а Prisma та Next.js змінили свої стандартні налаштування у останніх версіях. Розглядайте наведені нижче кроки як орієнтир та перевіряйте деталі у актуальній документації кожного проекту.

Встановлення Prisma без додаткових хостованих компонентів

Посібник Prisma для Next.js написаний з урахуванням Prisma Postgres — власної хостованої бази даних компанії. Якщо ви плануєте використовувати іншу версію Postgres, можна пропустити @prisma/extension-accelerate, адже він стосується лише цього хостованого рішення.

Для звичайної налаштування Postgres потрібно виконати два кроки інсталяції: CLI та tsx як залежності для розробки, а також клієнтську частину та dotenv як залежності під час виконання. У фрагменті коду обидві команди показані в одному рядку; виконуйте їх по черзі:

npm install prisma tsx --save-dev npm install @prisma/client dotenv

Далі ініціалізуйте Prisma у проекті:

npx prisma init

У посібнику також згадується прапорець --db для команди init. Цей прапорець створює інстанцію Prisma Postgres, тому його слід видалити, якщо ви використовуєте власну базу даних.

Щоб запустити базу даних локально без встановлення Postgres, Prisma може запустити для вас сервер розробки:

npx prisma dev

На етапі init вже було записано значення DATABASE_URL у ваш файл .env, яке вказує на цей локальний сервер. Якщо ви вже запускаєте Postgres локально або хочете вказати іншу гілку Neon, замініть це значення на власний рядок підключення. Щоб детальніше дізнатися про те, як Prisma 7 працює у проекті Next.js 16, включаючи новий файл конфігурації, перегляньте наш посібник з налаштування Prisma 7.

Створення прикладного додатку posts

Решта посібника Prisma створює невеликий блог із користувачами та публікаціями. Якщо ви додаєте Prisma до існуючого додатку, можете пропустити цей розділ та перейти до частини про автентифікацію. Проте варто пройти його, оскільки наступні кроки передбачають наявність цих моделей.

Спочатку додайте моделі до файлу schema.prisma, як описано у кроці зі схемою, а потім завантажте змінні середовища у конфігурації Prisma, як показано у кроці з dotenv.

Виправлення шляху імпорту клієнта

Під час заповнення бази даних скрипт для сімуляції імпортує дані безпосередньо з створеної папки prisma, що призводить до помилок. Створений код розголошує клієнта під ім’ям client, тому перший рядок імпорту у скрипті має вказувати саме на нього:

import { PrismaClient, Prisma } from "../app/generated/prisma/client";

Аліас @/app працює так само добре, як і відносний шлях ../app.

Та сама корекція застосовується під час налаштування спільного клієнта Prisma у файлі lib/prisma.ts:

import { PrismaClient } from '../app/generated/prisma/client'

У посібнику також чергуються конвенції src/app та просто app/ у кожному наступному кроці. Виберіть ту, яку насправді використовує ваш проект: якщо коренем є src/, шлях буде ../src/app/generated; якщо ні — ../app/generated. Більшість помилок типу „модуль не знайдено“ на цьому етапі виникають через змішування цих двох конвенцій.

Після налаштування клієнта додайте сторінки, описані у посібнику: запит головної сторінки, список публікацій, сторінку деталей публікації та форму для створення нових публікацій.

Додавання Auth.js

Існують два сайти документації до цієї бібліотеки: старіший сайт NextAuth та новіший довідник Auth.js для Next.js. Документація Auth.js описує поточну версію та є основною для слідування.

Встановіть пакет з його бета-каналу:

npm install next-auth@beta

Потім запустіть npx auth secret. Цей інструмент генерує випадкове значення AUTH_SECRET, яке Auth.js використовує для підписування та шифрування токенів та кукі, і записує його у файл .env.local. Оскільки цей проект вже зберігає свої змінні у файлі .env, перенесіть туди це значення та видаліть .env.local, щоб існував єдиний джерело конфігурації. Тримайте цей файл поза системою контролю версій.

Тепер дотримуйтесь кроків встановлення. Ключовим елементом є файл auth.ts, розташований у корені проекту. Він один раз викликає NextAuth та експортує чотири функції-допоміжники, які використовуються рештою додатку: HTTP-обработчики handlers, функції signIn, signOut та auth, яка читає поточну сесію:

import NextAuth from "next-auth"

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [],
})

Обробник маршруту у файлі ./app/api/auth/[...nextauth]/route.ts просто переекспортує обробники запитів GET та POST, щоб Auth.js міг обслуговувати свої кінцеві точки для входу, обробки повернень та керування сесіями. У наведеному уривку коментар та експорт опинилися на одній лінії; у вашому файлі розмістіть експорт на окремій лінії після імпорту:

import { handlers } from "@/auth" // Referring to the auth.ts we just created export const { GET, POST } = handlers

У посібнику з встановлення також додається файл middleware.ts. З того часу Next.js перейменував middleware на proxy (див. примітку Next.js щодо цієї зміни), тому файл, описаний у документації, не відповідає поточній версії Next.js. Вам він не потрібен для цього налаштування, оскільки кожна захищена сторінка та дія перевіряють сесію безпосередньо. Якщо згодом ви захочете захист на рівні запиту, адаптуйте його до конвенцій proxy.

Підключення Google як постачальника автентифікації

У Google Cloud Console створіть клієнта OAuth типу веб-додаток. Встановіть дозволений початковий адресний простір JavaScript як localhost:3000, а URI перенаправлення — як localhost:3000/api/auth/callback/google, що є маршрутом для повернення даних, який Auth.js надає для провайдера Google. Під час розгортання додайте також продакшн-адресний простір та маршрут повернення даних. Зберігайте ID клієнта та секрет, які надає Google, у вашому файлі середовища під іменами змінних, вказаними у документації Auth.js, а не у вихідному коді.

Посібник з OAuth додає компонент входу. Це форма, дія якої — це вбудована Server Action, що викликає функцію signIn:

import { signIn } from "@/auth"

export function SignIn() {
  return (
    <form
      action={async () => {
        "use server"
        await signIn()
      }}
    >
      <button type="submit">Sign in</button>
    </form>
  )
}

Якщо функція signIn() викликається без аргументів, вона направляє користувача на вбудовану сторінку, де перелічені всі налаштовані провайдери. Якщо налаштований лише Google, передача параметра "google" дозволяє пропустити цей проміжний екран.

Відповідний компонент для виходу з облікового запису, описаний у документації з керування сеансами, має таку саму структуру:

import { signOut } from "@/auth"

export function SignOut() {
  return (
    <form
      action={async () => {
        "use server"
        await signOut()
      }}
    >
      <button type="submit">Sign Out</button>
    </form>
  )
}

Розміщення кнопок у панелі навігації

У документації не пояснюється, куди саме слід розміщувати ці компоненти. Логічним місцем є панель навігації, яка сама є асинхронним серверним компонентом: вона викликає функцію auth() та відображає компонент SignOut, коли користувач усередині системи, а SignIn — в іншому випадку.

import Link from "next/link";
import { auth } from "../auth"
import SignIn from "./sign-in";
import SignOut from "./sign-out";

export default async function Navigation() {
    const session = await auth();

    return (
        <>
            <nav className="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-200">
                <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
                    <div className="flex items-center h-16">
                        {/* Logo */}
                        <Link href="/" className="flex items-center space-x-2">
                        <span className="text-xl font-semibold text-[#369b6f]">
                            Prisma + NextAuth + Neon
                        </span>
                        </Link>

                        {/* Spacer */}
                        <div className="flex-1"></div>

                        {/* Navigation Links */}
                        <div className="hidden md:flex items-center mr-8">
                        <Link
                            href="/posts"
                            className="text-gray-700 font-medium hover:text-[#369b6f] transition-all duration-200"
                        >
                            Posts
                        </Link>
                        </div>

                        {/* Auth Buttons */}
                        <div className="flex items-center space-x-2 sm:space-x-4">
                        {session?.user ? (
                            <SignOut/>
                        ) : (
                            <SignIn/>
                        )}
                        </div>
                    </div>
                </div>
            </nav>
        </>
    );
}

Зверніть увагу на імпорти тут: цей файл використовує стандартні імпорти для SignIn та SignOut, тоді як компоненти вище використовують іменовані експорти. Або змініть імпорти на import { SignIn } from "./sign-in", або експортуйте компоненти як стандартні; інакше процес будування завершиться невдачею.

Потім відобразіть навігацію у кореневому макеті, щоб вона була видима на кожній сторінці:

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
          <Navigation/>
          <main>
            {children}
          </main>
      </body>
    </html>
  );
}

Не забудьте імпортувати Navigation у верхній частині файлу app/layout.tsx. Після перезавантаження розробницького сервера над кожною сторінкою з’являється панель із кнопками входу та виходу. Пам’ятайте, що обидві кнопки використовують вбудовані Server Actions, тому вони працюють лише всередині Server Components. Client Component не може визначати серверні функції безпосередньо; у такому випадку потрібно перемістити ці дії до окремого файлу, позначеного "use server", та імпортувати їх.

Зберігання даних користувачів за допомогою Prisma adapter

На цьому етапі вхід працює, але жодна інформація про користувача не зберігається у вашій базі даних. Prisma adapter з’єднує Auth.js із вашим Prisma клієнтом, щоб дані користувачів, пов’язаних OAuth-акаунтів та сеансів зберігалися у Postgres. Встановіть його:

npm install @auth/prisma-adapter

Потім оновіть auth.ts, щоб передати адаптер вашого спільного клієнта Prisma та зареєструвати провайдера Google:

import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import { PrismaAdapter } from "@auth/prisma-adapter";
import prisma from "@/lib/prisma";

export const { handlers, signIn, signOut, auth } = NextAuth({
    adapter: PrismaAdapter(prisma),
    providers: [Google],
})

З налаштованим адаптером Auth.js за замовчуванням зберігає сесії в базі даних, а не у підписаному кукурі, що дозволяє скасовувати сесії з боку сервера, але це вимагає пошуку в базі даних при кожній перевірці сесії. Провайдер Google автоматично отримує свої облікові дані з змінних середовища, якщо вони відповідають конвенції іменування Auth.js.

Узгодження моделі користувача

Адаптер потребує певних таблиць та стовпців, які перелічені в схемі адаптера Prisma. Копіюйте їх у файл schema.prisma. Це створює конфлікт із прикладним додатком: посібник Prisma створив модель User із власним типом id, тоді як Auth.js очікує рядковий id разом із кількома додатковими полями. Для нового прикладного проекту найпростішим рішенням є заміна моделі та повне створення бази даних з нуля. Якщо ви інтегруєтеся в додаток, який вже має реальних користувачів, нічого не скидайте; сплануйте належну міграцію, яка буде відображати існуючих користувачів у новій структурі. Це окрема проблема, що виходить за межі цього посібника.

Щоб чисто перестворити базу даних, спочатку зупиніть повторне виконання процедури заповнення даними, оскільки ці дані стосуються старої структури користувачів. Видаліть запис про заповнення даними у файлі prisma.config.ts:

seed: `tsx prisma/seed.ts`

Потім скиньте базу даних, що призведе до видалення всіх таблиць та даних:

npx prisma migrate reset

Виконуйте це лише з базою даних розробки. Нарешті, створіть міграцію для нової схеми та застосуйте її:

npx prisma migrate dev

Знову запустіть додаток (npm run dev) та увійдіть через Google. У таблиці User має з’явитися новий рядок, а у таблиці Account — відповідний рядок для посилання Google. Щоб переглянути ці рядки, запустіть npx prisma studio — візуальний браузер баз даних, який постачається разом із Prisma.

Приписування постів до увійшового користувача

Приклад форми з посібника Prisma створює записи із жорстко заданим authorId. Тепер, коли існують сесії, сторінка може зчитати сесію та використовувати справжній ідентифікатор користувача замість нього. Наведена нижче сторінка викликає функцію auth(), відображає повідомлення анонімним відвідувачам та передає session.user.id до Prisma під час створення запису. Потім вона перевіряє список записів та перенаправляє на нього:

export default async function NewPost() {
  const session = await auth();

  async function createPost(formData: FormData) {
    "use server";

    if(!session?.user?.id){
      return;
    }

    const title = formData.get("title") as string;
    const content = formData.get("content") as string;

    await prisma.post.create({
      data: {
        title,
        content,
        authorId: session.user.id,
      },
    });

    revalidatePath("/posts");
    redirect("/posts");
  }

  if(!session?.user){
    return <div>You need to sign in to create a post</div>
  }

  ...

}

Є одна корекція, яку варто зробити перед тим, як цей код потрапить у продакшн. Дія зчитує значення session під час відображення сторінки через закриту функцію. Server Action — це публічний кінцевий пункт, який можна викликати безпосередньо, тому безпечнішим підходом є виклик auth() всередині тіла дії та перевірка результату там, щоб перевірка виконувалася при кожній подачі даних. Ми пояснюємо, чому це потрібно, у статті про те, чому Server Actions потребують авторизації всередині кожного тіла функції. Також доцільно перевіряти значення title та content, замість того щоб перетворювати їх за допомогою as string.

Підсумок

  • Дотримуйтесь офіційних посібників Prisma та Auth.js, але виправте імпорт клієнтської частини так, щоб він вказував на generated/prisma/client, та завжди дотримуйтесь консистентності щодо використання src/app чи app/.
  • Зберігайте всі секрети та рядки підключення у одному файлі середовища, який ніколи не підлягає комітуванню.
  • Пропустіть задокументований файл middleware.ts; поточний Next.js використовує конвенцію проксі, тож достатньо перевірок auth() на рівні сторінки.
  • Додавання адаптера Prisma змінює структуру об’єкта User, яка є необхідною. Для демонстрації можна скинути налаштування, але для існуючих даних потрібна справжня міграція.
  • Перевіряйте стан сесії всередині кожної Server Action, а не лише на сторінці, яка відображає форму.

Далі природними кроками є автентифікація з використанням Client Components та налаштування DATABASE_URL на хостований екземпляр Postgres для розгортання.