Вход в Google с помощью Auth.js и Prisma: согласование руководств Next.js
Соедините руководства Prisma и Auth.js для Next.js в одно рабочее приложение с возможностью входа через Google, сохранением данных пользователей и постов, связанных с сессией, включая исправления, которые пропущены в документации.
Источники: 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 по-прежнему выпускается в бета-версии, а как 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 локально или хотите указать другой сервер, замените этое значение на свою собственную строку подключения. Чтобы узнать больше о том, как 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 reference for 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 добавляет компонент входа. Это форма, действие которой представляет собой встроенную операцию сервера, вызывающую функцию 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. Теперь, когда существуют сессии, страница может считать информацию из сессии и использовать настоящий ID пользователя. Приведенная ниже страница вызывает функцию 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, а не только на странице, отображающей форму.
Дальнейшими логичными шагами будут вход в систему из клиентских компонентов и настройка DATABASE_URL на хостинговую инстанцу Postgres для развертывания.