Logowanie przez Google z użyciem Auth.js i Prisma: łączenie wskazówek Next.js
Połącz przewodniki Prisma i Auth.js dla Next.js w jedną działającą aplikację z logowaniem przez Google, przechowywaniem użytkowników oraz postów powiązanych z sesją, włączając poprawki, których pomijają dokumentacje.
Dodawanie kont i zapisanych danych do aplikacji Next.js nie wymaga oddzielnego backendu. Dzięki Auth.js (następcy NextAuth) do autoryzacji, Prisma jako ORM oraz hostowanemu bazie danych Postgres takiej jak Neon, ta sama aplikacja Next.js może obsługiwać logowanie, sesje oraz przechowywanie danych. Każde z tych narzędzi ma własny przyzwoity przewodnik dla Next.js, ale różnią się one pod względem struktury folderów, odwołują się do przestarzałych konwencji i nigdy nie pokazują, jak poszczególne elementy ze sobą współpracują. Ten przewodnik postępuje zgodnie z tymi instrukcjami i wskazuje na każdą konieczną korektę, dzięki czemu ostatecznie uzyskasz możliwość logowania przez Google, przechowywanie użytkowników w bazie Postgres oraz publikacje przypisywane zalogowanemu użytkownikowi.
Dlaczego trzymać mechanizmy autoryzacji i dane wewnątrz aplikacji Next.js
Powszechnym rozwiązaniem dla produktu, który początkowo jest głównie narzędziem po stronie klienta, jest oddzielny backend – na przykład Django z Postgresem oraz usługa zarządzania tożsamościami taką jak Auth0. To działa, ale oznacza konieczność zarządzania drugą usługą, monitorowania jej oraz płacenia za nią. Gdy mały produkt wymaga dodatków takich jak zapisywane procesy pracy, ponowne wykorzystanie serwera Next.js do autoryzacji i dostępu do bazy danych pomaga obniżyć koszty hostingu oraz liczbę elementów wymagających konserwacji. Wadą jest to, że musisz samodzielnie zajmować się integracją trzech bibliotek, o czym będzie mowa dalej w tym artykule.
Pakiety te zmieniają się szybko. Auth.js dla Next.js nadal jest publikowany pod tagiem beta, a zarówno Prisma, jak i Next.js zmieniły swoje domyślne ustawienia w najnowszych majorowych wersjach. Traktuj poniższe kroki jako orientacyjny plan i sprawdź szczegóły w aktualnej dokumentacji każdego projektu.
Instalacja Prismy bez dodatkowych usług hostowanych
Przewodnik Prisma dla Next.js jest napisany z myślą o Prisma Postgres, własnej usłudze bazy danych hostowanej przez tę firmę. Jeśli planujesz użyć innej wersji Postgres, możesz pominąć @prisma/extension-accelerate, ponieważ ma on znaczenie tylko w kontekście tej hostowanej usługi.
Dla zwykłej konfiguracji Postgres potrzebujesz dwóch instalacji: CLI oraz tsx jako zależności rozwojowe, a także klienta wraz z dotenv jako zależności runtime. W przykładzie oba polecenia znajdują się na jednej linii; należy je wykonać po kolei:
npm install prisma tsx --save-dev npm install @prisma/client dotenv
Następnie zainicjuj Prisma w projekcie:
npx prisma init
W przewodniku wspomniano również o flagi --db dla polecenia init. Ta flaga tworzy instancję Prisma Postgres, więc należy ją pominąć, gdy używasz własnej bazy danych.
Aby uruchomić bazę danych lokalnie bez instalowania Postgres, Prisma może uruchomić dla ciebie serwer rozwojowy:
npx prisma dev
Krok init już zapisał wartość DATABASE_URL w pliku .env, która wskazuje na ten lokalny serwer. Jeśli już uruchamiasz Postgres lokalnie lub chcesz wskazać na gałąź Neon, zastąp tę wartość własnym łańcuchem połączenia. Aby lepiej zobaczyć, jak Prisma 7 funkcjonuje w projekcie Next.js 16, w tym nowy plik konfiguracyjny, zapoznaj się z naszym przewodnikiem instalacji Prisma 7.
Budowanie przykładowej aplikacji posts
Pozostała część przewodnika Prisma dotyczy tworzenia małego bloga z użytkownikami i wpisami. Jeśli dodajesz Prisma do istniejącego aplikacji, możesz przejść bezpośrednio do sekcji dotyczącej autoryzacji. Mimo to warto kontynuować, ponieważ późniejsze kroki zakładają już istnienie tych modeli.
Najpierw dodaj modele do pliku schema.prisma zgodnie z instrukcjami dotyczącymi schematu, a następnie załaduj zmienne środowiskowe w konfiguracji Prisma, jak pokazano w kroku dotyczącym pliku dotenv.
Poprawa ścieżki importu klienta wygenerowanego przez Prisma
Gdy tworzysz próbne dane w bazie danych, skrypt siewny, tak jak jest napisany, importuje elementy bezpośrednio z wygenerowanej folderu prisma, co powoduje błąd. Wygenerowany kod udostępnia klienta pod nazwą client, dlatego pierwsza linia importu w skrypcie siewnym musi wskazywać właśnie na tę nazwę:
import { PrismaClient, Prisma } from "../app/generated/prisma/client";
Alias @/app działa równie dobrze jak ścieżka względna ../app.
To samo korekty dotyczy w przypadku konfiguracji współdzielonego klienta Prisma w pliku lib/prisma.ts:
import { PrismaClient } from '../app/generated/prisma/client'
Przewodnik również przechodzi między konwencjami src/app a zwykłym app/ w kolejnych krokach. Wybierz tę, której faktycznie używa twój projekt: jeśli korzystasz z katalogu src/, ścieżka to ../src/app/generated; w przeciwnym razie ../app/generated. Większość błędów typu „module not found” na tym etapie wynika z mieszania tych dwóch konwencji.
Gdy klient będzie już skonfigurowany, dodaj strony opisane w przewodniku: zapytanie do strony głównej, listę postów, stronę szczegółową posta oraz formularz do tworzenia nowych postów.
Dodawanie Auth.js
Istnieją dwa strony dokumentacyjne dla tej biblioteki: starsza strona NextAuth oraz nowszy dokumentacja Auth.js dla Next.js. Dokumentacja Auth.js opisuje aktualną wersję i jest tą, której należy się trzymać.
Zainstaluj pakiet z jego kanału beta:
npm install next-auth@beta
Następnie uruchom npx auth secret. Generuje to losowy AUTH_SECRET, który Auth.js używa do podpisywania i szyfrowania tokenów oraz plików cookie, a następnie zapisuje go do pliku .env.local. Ponieważ ten projekt już przechowuje swoje zmienne w pliku .env, przenieś tam tę wartość i usuń .env.local, aby istniał tylko jeden źródło konfiguracji. Unikaj umieszczania tego pliku w systemie kontroli wersji.
Następnie postępuj zgodnie z krokami instalacji. Kluczowym elementem jest plik auth.ts znajdujący się w korzeniu projektu. Tworzy on jednorazowy wywołanie NextAuth i eksportuje cztery funkcje pomocnicze używane przez resztę aplikacji: obsługi HTTP handlers, funkcje signIn, signOut oraz auth, która odczytuje aktualną sesję:
import NextAuth from "next-auth"
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [],
})
Obsługa trasy w pliku ./app/api/auth/[...nextauth]/route.ts po prostu ponownie eksportuje obsługi GET i POST, aby Auth.js mógł obsłużyć swoje punkty końcowe do logowania, obsługi powrotów i zarządzania sesją. W tym fragmencie komentarz oraz eksport znajdują się na jednej linii; w swoim pliku umieść eksport na osobnej linii po importie:
import { handlers } from "@/auth" // Referring to the auth.ts we just created export const { GET, POST } = handlers
Przewodnik instalacyjny zawiera również plik middleware.ts. Next.js przemianował później pojęcie middleware na proxy (zobacz notatkę Next.js na temat tej zmiany), więc plik opisany w dokumencie nie jest zgodny z aktualną wersją Next.js. Nie jest on potrzebny przy tej konfiguracji, ponieważ każda chroniona strona i akcja sprawdza samą sesję. Jeśli później chcesz zabezpieczenia na poziomie żądania, musisz dostosować je do konwencji proxy.
Łączenie Google jako dostawcy logowania
W Google Cloud Console utwórz klienta OAuth typu aplikacja internetowa. Ustaw uprawniony źródło JavaScript na localhost:3000, a adres URI przekierowania na localhost:3000/api/auth/callback/google – to jest ścieżka przekierowania, którą Auth.js udostępnia dla dostawcy Google. Podczas wdrażania dodaj również źródło produkcyjne oraz ścieżkę przekierowania. Przechowuj identyfikator klienta oraz sekret dostarczone przez Google w pliku środowiskowym pod nazwami zmiennych określonymi w dokumentacji Auth.js, a nie w kodzie źródłowym.
Następnie przewodnik OAuth dodaje komponent logowania. Jest to formularz, którego akcja to wbudowana Server Action wywołująca metodę signIn:
import { signIn } from "@/auth"
export function SignIn() {
return (
<form
action={async () => {
"use server"
await signIn()
}}
>
<button type="submit">Sign in</button>
</form>
)
}
Kiedy signIn() jest wywoływany bez argumentów, kieruje użytkownika na wbudowaną stronę przedstawiającą wszystkie skonfigurowane dostawcy. Jeśli skonfigurowany jest tylko Google, podanie wartości "google" omija tę pośrednią stronę.
Odpowiednia komponenta do wylogowania, dostępna w dokumentacji dotyczącej zarządzania sesjami, ma podobną strukturę:
import { signOut } from "@/auth"
export function SignOut() {
return (
<form
action={async () => {
"use server"
await signOut()
}}
>
<button type="submit">Sign Out</button>
</form>
)
}
Dokumentacja nie precyzuje, gdzie powinny znajdować się te komponenty. Naturalnym miejscem jest pasek nawigacyjny, który sam w sobie jest asynchronicznym komponentem serwerowym: wywołuje funkcję auth() i renderuje komponent SignOut, gdy użytkownik jest zalogowany, a SignIn w przeciwnym razie.
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>
</>
);
}
Zwróć uwagę na importy: ten plik używa domyślnych importów dla SignIn i SignOut, podczas gdy powyższe komponenty wykorzystują nazwane eksporty. Należy albo zmienić importy na import { SignIn } from "./sign-in", albo eksportować komponenty jako domyślne; w przeciwnym razie budowa się nie uda.
Następnie wyświetl menu nawigacyjne w rozkładzie głównym, aby pojawiło się na każdej stronie:
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<Navigation/>
<main>
{children}
</main>
</body>
</html>
);
}
Pamiętaj, aby na początku pliku app/layout.tsx zaimportować moduł Navigation. Po ponownym uruchomieniu serwera deweloperskiego nad każdą stroną pojawi się pasek z przyciskami logowania i wylogowania. Należy pamiętać, że oba przyciski opierają się na inline Server Actions, więc działają tylko wewnątrz Server Components. Client Component nie może definiować funkcji serwerowych w formie inline; w takim przypadku należy przenieść te funkcje do osobnego pliku oznaczonego tagiem "use server" i je tam zaimportować.
Zachowywanie danych użytkowników za pomocą adaptera Prisma
W tym momencie logowanie działa, ale żadne informacje o użytkowniku nie są przechowywane w bazie danych. Adapter Prisma łączy Auth.js z klientem Prisma, dzięki czemu dane użytkowników oraz powiązane konta i sesje OAuth są zapisywane w Postgresie. Aby to zainstalować:
npm install @auth/prisma-adapter
Następnie zaktualizuj plik auth.ts, aby przekazać adapter wspólnego klienta Prisma oraz zarejestrować dostawcę 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],
})
Gdy adapter jest skonfigurowany, Auth.js domyślnie przechowuje sesje w bazie danych zamiast w podpisanym pliku cookie, co umożliwia cofnięcie sesji po stronie serwera, ale wiąże się z koniecznością wyszukiwania w bazie danych przy każdej sprawdzie sesji. Dostawca Google automatycznie pobiera swoje dane uwierzytelniające z zmiennych środowiskowych, o ile spełniają one konwencję nazewniczą Auth.js.
Synchronizacja modelu użytkownika
Adapter wymaga określonych tabel i kolumn, które są wymienione w schemacie adaptera Prisma. Skopiuj je do pliku schema.prisma. To powoduje konflikt z przykładowym aplikacją: przewodnik Prisma stworzył model User z własnym typem id, podczas gdy Auth.js oczekuje identyfikatora w formie ciągu znaków oraz kilku dodatkowych pól. W przypadku nowego projektu przykładowego najprostszym rozwiązaniem jest zastąpienie modelu i odbudowa bazy danych od zera. Jeśli integrujesz się z aplikacją, która już posiada rzeczywistych użytkowników, nie resetuj niczego; zaplanuj odpowiednią migrację, która przypisze istniejących użytkowników do nowej struktury. To jest odrębny problem, poza zakresem tego przewodnika.
Aby odbudować wszystko czysto, najpierw zatrzymaj ponowne uruchomienie procedury seedowania, ponieważ dane seedowe odnoszą się do starej struktury użytkowników. Usuń wpis seedowania z pliku prisma.config.ts:
seed: `tsx prisma/seed.ts`
Następnie sformatuj bazę danych, co usunie wszystkie tabele i dane:
npx prisma migrate reset
Tę operację należy wykonać tylko na bazie danych rozwojowej. Na koniec utwórz migrację dla nowej struktury bazy i ją zastosuj:
npx prisma migrate dev
Zainstaluj aplikację ponownie (npm run dev) i zaloguj się przez Google. W tabeli User powinna pojawić się nowa wiersz, a w tabeli Account – odpowiadająca jej wiersz dla linku Google. Aby przejrzeć te wiersze, uruchom npx prisma studio – wizualny przeglądarkę bazy danych dostarczany wraz z Prismą.
Przykładowa forma z przewodnika Prisma tworzy wpisy z sztywno zdefiniowanym authorId. Teraz, gdy istnieją sesje, strona może je odczytać i zamiast tego użyć rzeczywistego identyfikatora użytkownika. Poniższa strona wywołuje auth(), pokazuje komunikat anonimowym odwiedzającym i przekazuje session.user.id do Prisma podczas tworzenia wpisu. Następnie ponownie waliduje listę wpisów i kieruje na nią użytkownika:
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>
}
...
}
Jedna poprawka jest konieczna, zanim to trafi choćby w pobliże środowiska produkcyjnego. Operacja ta odczytuje wartość session z renderowanej strony za pośrednictwem zamknięcia. Server Action to publiczny punkt końcowy, który można wywołać bezpośrednio, więc bezpieczniejszym podejściem jest wywołanie auth() wewnątrz ciała operacji i sprawdzenie tam wyniku, co zapewnia sprawdzanie przy każdej wysyłce danych. Wyjaśniamy dlaczego w dlaczego Server Actions wymagają uprawnień wewnątrz każdego ciała funkcji. Dobrym pomysłem jest również walidacja wartości title i content, zamiast ich przekształcania za pomocą as string.
Podsumowanie
- Następuj instrukcjom oficjalnych przewodników Prisma i Auth.js, ale popraw zapis importu klienta tak, aby wskazywał na
generated/prisma/client, i zachowuj spójność co do używaniasrc/appzamiastapp/. - Zachowaj wszystkie hasła i łącza połączenia w jednym pliku środowiskowym, który nigdy nie jest komitowany.
- Pomij dokumentowany plik
middleware.ts; obecny Next.js wykorzystuje konwencję proxy, a wystarczające są tutaj sprawdzeniaauth()na poziomie strony. - Dodanie adaptera Prisma zmienia wymagany kształt obiektu
User. Szybkie sformatowanie nadaje się do demonstracji, ale istniejące dane wymagają prawdziwej migracji.
Dalszymi naturalnymi krokami są logowanie z komponentów klienta oraz skierowanie DATABASE_URL na hostowaną instancję Postgres w celu implementacji.
Powiązane materiały
- Ustawianie Prisma 7 w projekcie Next.js 16 z TypeScript — Dowiedz się, jak zainstalować, skonfigurować i przenieść Prisma 7 z PostgreSQL do aplikacji Next.js 16, a następnie stworzyć klienta typu singleton, aby uniknąć wycieków połączeń.
- Autoryzacja biometryczna na iOS i Android z użyciem kluczy przypisanych do hardwaru — Dowiedz się, dlaczego sama funkcja Face ID nie dostarcza żadnych informacji twojemu serwerowi, oraz jak wykorzystać klucze Secure Enclave i Android Keystore do podpisywania jednorazowych wyzwań od serwera.