W poprzednich lekcjach omówiliśmy podstawy uwierzytelniania w aplikacjach Next.js oraz różne strategie autoryzacji. Teraz skupimy się na praktycznej implementacji uwierzytelniania z popularnymi dostawcami zewnętrznymi - Google, GitHub i Facebook. Ta metoda jest niezwykle popularna, ponieważ oferuje użytkownikom szybkie i wygodne logowanie bez konieczności tworzenia nowego konta i zapamiętywania kolejnego hasła.
Logowanie z wykorzystaniem kont Google, GitHub czy Facebook przynosi kilka istotnych korzyści zarówno dla użytkowników, jak i dla nas - twórców aplikacji:
Przystąpmy więc do praktycznej implementacji logowania z tymi popularnymi dostawcami w aplikacji Next.js.
Zanim zaczniemy kodować, musimy utworzyć odpowiednie aplikacje OAuth u każdego z dostawców, aby uzyskać niezbędne klucze.
http://localhost:3000/api/auth/callback/google (dla środowiska deweloperskiego)https://twoja-domena.com/api/auth/callback/google (dla produkcji)http://localhost:3000http://localhost:3000/api/auth/callback/githublocalhost (dla środowiska deweloperskiego)twoja-domena.com (dla produkcji)http://localhost:3000/api/auth/callback/facebookTeraz, gdy mamy już klucze OAuth, możemy przystąpić do implementacji uwierzytelniania w naszej aplikacji Next.js. Będziemy używać biblioteki NextAuth.js (obecnie znanej również jako Auth.js).
Najpierw dodajmy nasze klucze OAuth do pliku
.env.local:1# Podstawowa konfiguracja NextAuth.js
2NEXTAUTH_URL=http://localhost:3000
3NEXTAUTH_SECRET=twój_tajny_klucz_do_szyfrowania
4
5# Google Provider
6GOOGLE_CLIENT_ID=twoje_google_client_id
7GOOGLE_CLIENT_SECRET=twoje_google_client_secret
8
9# GitHub Provider
10GITHUB_ID=twoje_github_client_id
11GITHUB_SECRET=twoje_github_client_secret
12
13# Facebook Provider
14FACEBOOK_CLIENT_ID=twoje_facebook_app_id
15FACEBOOK_CLIENT_SECRET=twoje_facebook_app_secretTeraz stwórzmy lub zaktualizujmy plik konfiguracyjny NextAuth.js:
1// app/api/auth/[...nextauth]/route.ts
2import NextAuth from "next-auth";
3import GoogleProvider from "next-auth/providers/google";
4import GitHubProvider from "next-auth/providers/github";
5import FacebookProvider from "next-auth/providers/facebook";
6import { PrismaAdapter } from "@auth/prisma-adapter";
7import { PrismaClient } from "@prisma/client";
8
9const prisma = new PrismaClient();
10
11export const authOptions = {
12 adapter: PrismaAdapter(prisma),
13 providers: [
14 GoogleProvider({
15 clientId: process.env.GOOGLE_CLIENT_ID!,
16 clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
17 // Możemy zdefiniować, jakie dane profilu chcemy otrzymać
18 profile(profile) {
19 return {
20 id: profile.sub,
21 name: profile.name,
22 email: profile.email,
23 image: profile.picture,
24 // Możemy dodać dodatkowe pola specyficzne dla naszej aplikacji
25 emailVerified: profile.email_verified,
26 };
27 },
28 }),
29 GitHubProvider({
30 clientId: process.env.GITHUB_ID!,
31 clientSecret: process.env.GITHUB_SECRET!,
32 profile(profile) {
33 return {
34 id: profile.id.toString(),
35 name: profile.name || profile.login,
36 email: profile.email,
37 image: profile.avatar_url,
38 // Dodatkowe pole specyficzne dla GitHub
39 username: profile.login,
40 };
41 },
42 }),
43 FacebookProvider({
44 clientId: process.env.FACEBOOK_CLIENT_ID!,
45 clientSecret: process.env.FACEBOOK_CLIENT_SECRET!,
46 profile(profile) {
47 return {
48 id: profile.id,
49 name: profile.name,
50 email: profile.email,
51 image: profile.picture.data.url,
52 };
53 },
54 }),
55 // Możemy również dodać uwierzytelnianie przez email/hasło
56 // dla użytkowników, którzy wolą tradycyjne logowanie
57 ],
58 callbacks: {
59 async session({ session, user }) {
60 // Dodaj ID użytkownika do sesji, aby łatwo identyfikować użytkownika
61 if (session.user) {
62 session.user.id = user.id;
63 }
64 return session;
65 },
66 async jwt({ token, user, account, profile }) {
67 // Jeśli mamy użytkownika (np. podczas logowania), dodaj jego ID do tokenu
68 if (user) {
69 token.uid = user.id;
70 }
71 // Możemy też dodać informacje o dostawcy uwierzytelniania
72 if (account) {
73 token.provider = account.provider;
74 }
75 return token;
76 },
77 },
78 pages: {
79 signIn: '/auth/login', // Niestandardowa strona logowania
80 signOut: '/auth/logout', // Niestandardowa strona wylogowania
81 error: '/auth/error', // Strona błędów uwierzytelniania
82 },
83 session: {
84 strategy: 'database', // Używamy bazy danych do przechowywania sesji
85 maxAge: 30 * 24 * 60 * 60, // 30 dni
86 },
87 debug: process.env.NODE_ENV === 'development',
88};
89
90const handler = NextAuth(authOptions);
91export { handler as GET, handler as POST };Teraz stwórzmy przyjazny interfejs logowania, który będzie oferował wszystkie skonfigurowane metody uwierzytelniania:
1// app/auth/login/page.tsx
2'use client';
3
4import { signIn, useSession } from 'next-auth/react';
5import { useRouter, useSearchParams } from 'next/navigation';
6import { useEffect, useState } from 'react';
7
8export default function LoginPage() {
9 const router = useRouter();
10 const searchParams = useSearchParams();
11 const callbackUrl = searchParams.get('callbackUrl') || '/dashboard';
12 const { status } = useSession();
13
14 const [email, setEmail] = useState('');
15 const [password, setPassword] = useState('');
16 const [isLoading, setIsLoading] = useState(false);
17 const [error, setError] = useState('');
18
19 // Jeśli użytkownik jest już zalogowany, przekieruj go
20 useEffect(() => {
21 if (status === 'authenticated') {
22 router.push(callbackUrl);
23 }
24 }, [status, router, callbackUrl]);
25
26 // Obsługa logowania przez email/hasło (jeśli skonfigurowano)
27 const handleEmailLogin = async (e: React.FormEvent) => {
28 e.preventDefault();
29 setIsLoading(true);
30 setError('');
31
32 try {
33 const result = await signIn('credentials', {
34 redirect: false,
35 email,
36 password,
37 });
38
39 if (result?.error) {
40 setError('Nieprawidłowy email lub hasło');
41 } else {
42 router.push(callbackUrl);
43 }
44 } catch (error) {
45 setError('Wystąpił błąd podczas logowania');
46 } finally {
47 setIsLoading(false);
48 }
49 };
50
51 if (status === 'loading') {
52 return (
53 <div className="flex justify-center items-center min-h-screen">
54 <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500" />
55 </div>
56 );
57 }
58
59 return (
60 <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded-lg shadow-lg">
61 <h1 className="text-2xl font-bold mb-6 text-center">Zaloguj się do aplikacji</h1>
62
63 {error && (
64 <div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
65 {error}
66 </div>
67 )}
68
69 <div className="space-y-4 mb-6">
70 <button
71 onClick={() => signIn('google', { callbackUrl })}
72 className="w-full flex items-center justify-center gap-3 p-3 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
73 disabled={isLoading}
74 >
75 <svg viewBox="0 0 24 24" width="20" height="20">
76 {/* Logo Google */}
77 <path
78 d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
79 fill="#4285F4"
80 />
81 <path
82 d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
83 fill="#34A853"
84 />
85 <path
86 d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
87 fill="#FBBC05"
88 />
89 <path
90 d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
91 fill="#EA4335"
92 />
93 </svg>
94 Kontynuuj z Google
95 </button>
96
97 <button
98 onClick={() => signIn('github', { callbackUrl })}
99 className="w-full flex items-center justify-center gap-3 p-3 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
100 disabled={isLoading}
101 >
102 <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
103 {/* Logo GitHub */}
104 <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
105 </svg>
106 Kontynuuj z GitHub
107 </button>
108
109 <button
110 onClick={() => signIn('facebook', { callbackUrl })}
111 className="w-full flex items-center justify-center gap-3 p-3 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
112 disabled={isLoading}
113 >
114 <svg viewBox="0 0 24 24" width="20" height="20" fill="#1877F2">
115 {/* Logo Facebook */}
116 <path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
117 </svg>
118 Kontynuuj z Facebook
119 </button>
120 </div>
121
122 {/* Opcjonalnie: formularz logowania email/hasło */}
123 <div className="relative my-6">
124 <div className="absolute inset-0 flex items-center">
125 <div className="w-full border-t border-gray-300"></div>
126 </div>
127 <div className="relative flex justify-center text-sm">
128 <span className="px-2 bg-white text-gray-500">
129 lub użyj emaila
130 </span>
131 </div>
132 </div>
133
134 <form onSubmit={handleEmailLogin} className="space-y-4">
135 <div>
136 <label htmlFor="email" className="block text-sm font-medium mb-1">
137 Email
138 </label>
139 <input
140 id="email"
141 type="email"
142 value={email}
143 onChange={(e) => setEmail(e.target.value)}
144 className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
145 required
146 />
147 </div>
148
149 <div>
150 <label htmlFor="password" className="block text-sm font-medium mb-1">
151 Hasło
152 </label>
153 <input
154 id="password"
155 type="password"
156 value={password}
157 onChange={(e) => setPassword(e.target.value)}
158 className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
159 required
160 />
161 </div>
162
163 <button
164 type="submit"
165 className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
166 disabled={isLoading}
167 >
168 {isLoading ? 'Logowanie...' : 'Zaloguj się'}
169 </button>
170 </form>
171
172 <div className="mt-6 text-center">
173 <p className="text-sm text-gray-600">
174 Nie masz jeszcze konta?{' '}
175 <a href="/auth/register" className="text-blue-600 hover:underline">
176 Zarejestruj się
177 </a>
178 </p>
179 </div>
180 </div>
181 );
182}Jeśli korzystamy z adaptera Prisma, musimy zapewnić odpowiedni schemat bazy danych:
1// prisma/schema.prisma
2datasource db {
3 provider = "postgresql" // lub inny dostawca bazy danych
4 url = env("DATABASE_URL")
5}
6
7generator client {
8 provider = "prisma-client-js"
9}
10
11model Account {
12 id String @id @default(cuid())
13 userId String
14 type String
15 provider String
16 providerAccountId String
17 refresh_token String? @db.Text
18 access_token String? @db.Text
19 expires_at Int?
20 token_type String?
21 scope String?
22 id_token String? @db.Text
23 session_state String?
24
25 user User @relation(fields: [userId], references: [id], onDelete: Cascade)
26
27 @@unique([provider, providerAccountId])
28}
29
30model Session {
31 id String @id @default(cuid())
32 sessionToken String @unique
33 userId String
34 expires DateTime
35 user User @relation(fields: [userId], references: [id], onDelete: Cascade)
36}
37
38model User {
39 id String @id @default(cuid())
40 name String?
41 email String? @unique
42 emailVerified DateTime?
43 image String?
44 accounts Account[]
45 sessions Session[]
46 // Dodatkowe pola specyficzne dla twojej aplikacji
47 username String? @unique
48 role String? @default("user")
49 createdAt DateTime @default(now())
50 updatedAt DateTime @updatedAt
51}
52
53model VerificationToken {
54 identifier String
55 token String @unique
56 expires DateTime
57
58 @@unique([identifier, token])
59}Aby uwierzytelnianie działało poprawnie w całej aplikacji, dodajmy
SessionProvider w głównym layoucie:1// app/providers.tsx
2'use client';
3
4import { SessionProvider } from "next-auth/react";
5import { ReactNode } from "react";
6
7export function Providers({ children }: { children: ReactNode }) {
8 return <SessionProvider>{children}</SessionProvider>;
9}
10
11// app/layout.tsx
12import { Providers } from "./providers";
13import "./globals.css";
14
15export const metadata = {
16 title: "Moja Aplikacja",
17 description: "Opis mojej aplikacji",
18};
19
20export default function RootLayout({
21 children,
22}: {
23 children: React.ReactNode;
24}) {
25 return (
26 <html lang="pl">
27 <body>
28 <Providers>{children}</Providers>
29 </body>
30 </html>
31 );
32}Teraz stwórzmy komponent nawigacyjny, który będzie pokazywał różne opcje w zależności od tego, czy użytkownik jest zalogowany:
1// components/Navigation.tsx
2'use client';
3
4import { signOut, useSession } from 'next-auth/react';
5import Link from 'next/link';
6import Image from 'next/image';
7
8export default function Navigation() {
9 const { data: session, status } = useSession();
10 const isLoading = status === 'loading';
11
12 return (
13 <nav className="bg-white shadow-sm">
14 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
15 <div className="flex justify-between h-16">
16 <div className="flex items-center">
17 <Link href="/" className="flex-shrink-0 flex items-center">
18 <span className="text-xl font-bold">MojaAplikacja</span>
19 </Link>
20 <div className="hidden sm:ml-6 sm:flex sm:space-x-8">
21 <Link
22 href="/"
23 className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
24 >
25 Strona główna
26 </Link>
27 <Link
28 href="/features"
29 className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
30 >
31 Funkcje
32 </Link>
33 <Link
34 href="/pricing"
35 className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
36 >
37 Cennik
38 </Link>
39 </div>
40 </div>
41
42 <div className="flex items-center">
43 {isLoading ? (
44 <div className="h-8 w-8 animate-pulse rounded-full bg-gray-200" />
45 ) : session ? (
46 <div className="relative ml-3">
47 <div className="flex items-center space-x-4">
48 <Link
49 href="/dashboard"
50 className="text-sm font-medium text-gray-700 hover:text-gray-800"
51 >
52 Dashboard
53 </Link>
54
55 <div className="flex items-center space-x-2">
56 {session.user?.image ? (
57 <Image
58 className="h-8 w-8 rounded-full"
59 src={session.user.image}
60 alt={session.user.name || "Avatar"}
61 width={32}
62 height={32}
63 />
64 ) : (
65 <div className="h-8 w-8 rounded-full bg-blue-600 flex items-center justify-center text-white text-sm">
66 {session.user?.name?.charAt(0) || "U"}
67 </div>
68 )}
69
70 <span className="text-sm font-medium text-gray-700">
71 {session.user?.name || session.user?.email?.split('@')[0]}
72 </span>
73 </div>
74
75 <button
76 onClick={() => signOut({ callbackUrl: '/' })}
77 className="text-sm font-medium text-gray-700 hover:text-gray-800"
78 >
79 Wyloguj
80 </button>
81 </div>
82 </div>
83 ) : (
84 <div className="space-x-4">
85 <Link
86 href="/auth/login"
87 className="text-gray-700 hover:text-gray-900 font-medium"
88 >
89 Zaloguj się
90 </Link>
91 <Link
92 href="/auth/register"
93 className="bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 font-medium"
94 >
95 Zarejestruj się
96 </Link>
97 </div>
98 )}
99 </div>
100 </div>
101 </div>
102 </nav>
103 );
104}Stwórzmy stronę Dashboard, która będzie dostępna tylko dla zalogowanych użytkowników:
1// app/dashboard/page.tsx
2import { getServerSession } from "next-auth/next";
3import { redirect } from "next/navigation";
4import { authOptions } from "../api/auth/[...nextauth]/route";
5
6export default async function DashboardPage() {
7 const session = await getServerSession(authOptions);
8
9 if (!session) {
10 redirect("/auth/login?callbackUrl=/dashboard");
11 }
12
13 return (
14 <div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
15 <div className="px-4 py-6 sm:px-0">
16 <h1 className="text-2xl font-bold mb-4">Dashboard</h1>
17
18 <div className="bg-white shadow overflow-hidden sm:rounded-lg">
19 <div className="px-4 py-5 sm:px-6">
20 <h3 className="text-lg leading-6 font-medium text-gray-900">
21 Informacje o profilu
22 </h3>
23 <p className="mt-1 max-w-2xl text-sm text-gray-500">
24 Szczegóły Twojego konta i ustawienia.
25 </p>
26 </div>
27 <div className="border-t border-gray-200">
28 <dl>
29 <div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
30 <dt className="text-sm font-medium text-gray-500">Imię i nazwisko</dt>
31 <dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
32 {session.user?.name || "Nie podano"}
33 </dd>
34 </div>
35 <div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
36 <dt className="text-sm font-medium text-gray-500">Adres email</dt>
37 <dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
38 {session.user?.email}
39 </dd>
40 </div>
41 <div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
42 <dt className="text-sm font-medium text-gray-500">Metoda logowania</dt>
43 <dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
44 {/* To pole wymaga rozszerzenia tokenu o informacje o dostawcy */}
45 Zewnętrzny dostawca uwierzytelniania
46 </dd>
47 </div>
48 </dl>
49 </div>
50 </div>
51 </div>
52 </div>
53 );
54}Zabezpieczmy określone ścieżki w naszej aplikacji za pomocą middleware Next.js:
1// middleware.ts
2import { NextResponse } from "next/server";
3import type { NextRequest } from "next/server";
4import { getToken } from "next-auth/jwt";
5
6export async function middleware(request: NextRequest) {
7 const token = await getToken({ req: request });
8
9 const isAuthPage = request.nextUrl.pathname.startsWith('/auth');
10
11 if (isAuthPage) {
12 if (token) {
13 return NextResponse.redirect(new URL('/dashboard', request.url));
14 }
15 return NextResponse.next();
16 }
17
18 if (!token) {
19 const url = new URL('/auth/login', request.url);
20 url.searchParams.set('callbackUrl', request.nextUrl.pathname);
21 return NextResponse.redirect(url);
22 }
23
24 return NextResponse.next();
25}
26
27// Określ ścieżki, dla których ma być stosowane middleware
28export const config = {
29 matcher: [
30 '/dashboard/:path*',
31 '/profile/:path*',
32 '/settings/:path*',
33 '/auth/:path*'
34 ],
35};Możemy modyfikować dane profilowe z dostawców zewnętrznych przed zapisaniem ich w naszej bazie danych:
1GoogleProvider({
2 clientId: process.env.GOOGLE_CLIENT_ID!,
3 clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
4 profile(profile) {
5 // Transformacja danych profilu
6 return {
7 id: profile.sub,
8 name: profile.name,
9 email: profile.email,
10 image: profile.picture,
11 // Dodatkowe pola specyficzne dla aplikacji
12 role: profile.email.endsWith('@twojafirma.com') ? 'admin' : 'user',
13 emailVerified: profile.email_verified ? new Date() : null,
14 };
15 },
16});NextAuth.js automatycznie obsługuje łączenie kont różnych dostawców, jeśli używają tego samego adresu email. Jeśli chcemy to dostosować, możemy zmodyfikować funkcję
linkAccount w callbackach:1callbacks: {
2 // ...inne callbacki
3 async linkAccount({ user, account, profile }) {
4 // Tutaj możemy wykonać niestandardową logikę łączenia kont
5 console.log(`Linking account: ${account.provider} for user ${user.id}`);
6 return true;
7 },
8}Możemy dodać dodatkowe kroki w procesie uwierzytelniania za pomocą callbacku
signIn:1callbacks: {
2 // ...inne callbacki
3 async signIn({ user, account, profile, email, credentials }) {
4 // Sprawdź, czy użytkownik może się zalogować
5 // Na przykład: sprawdź, czy email jest w domenie firmy
6 const allowedDomains = ['twojafirma.com', 'partner.com'];
7 const emailDomain = user.email?.split('@')[1];
8
9 if (emailDomain && !allowedDomains.includes(emailDomain)) {
10 return false; // Odmów dostępu
11 }
12
13 // Możemy też zaktualizować dane użytkownika przy każdym logowaniu
14 if (user.id) {
15 await prisma.user.update({
16 where: { id: user.id },
17 data: { lastLogin: new Date() }
18 });
19 }
20
21 return true; // Zezwól na dostęp
22 },
23}Stwórzmy niestandardową stronę błędów uwierzytelniania:
1// app/auth/error/page.tsx
2'use client';
3
4import { useSearchParams } from "next/navigation";
5import Link from "next/link";
6
7export default function AuthErrorPage() {
8 const searchParams = useSearchParams();
9 const error = searchParams.get('error');
10
11 let errorMessage = "Wystąpił błąd podczas próby uwierzytelnienia.";
12 let errorDescription = "Prosimy spróbować ponownie lub skontaktować się z administracją.";
13
14 // Mapuj kody błędów na przyjazne dla użytkownika komunikaty
15 switch (error) {
16 case "Configuration":
17 errorMessage = "Wystąpił błąd konfiguracji serwera.";
18 errorDescription = "Skontaktuj się z administratorem systemu.";
19 break;
20 case "AccessDenied":
21 errorMessage = "Brak dostępu.";
22 errorDescription = "Nie masz wystarczających uprawnień, aby się zalogować.";
23 break;
24 case "Verification":
25 errorMessage = "Link weryfikacyjny wygasł lub jest nieprawidłowy.";
26 errorDescription = "Spróbuj wysłać link weryfikacyjny ponownie.";
27 break;
28 case "OAuthSignin":
29 case "OAuthCallback":
30 case "OAuthCreateAccount":
31 case "EmailCreateAccount":
32 case "Callback":
33 errorMessage = "Wystąpił błąd podczas komunikacji z dostawcą uwierzytelniania.";
34 errorDescription = "Spróbuj ponownie za chwilę lub wybierz inną metodę logowania.";
35 break;
36 case "OAuthAccountNotLinked":
37 errorMessage = "Konto z tym adresem email już istnieje.";
38 errorDescription = "Zaloguj się metodą, której użyłeś wcześniej.";
39 break;
40 case "EmailSignin":
41 errorMessage = "Nie można wysłać emaila z linkiem do logowania.";
42 errorDescription = "Sprawdź, czy podany adres email jest poprawny.";
43 break;
44 case "CredentialsSignin":
45 errorMessage = "Nieprawidłowe dane logowania.";
46 errorDescription = "Sprawdź, czy podany email i hasło są poprawne.";
47 break;
48 case "SessionRequired":
49 errorMessage = "Wymagane zalogowanie.";
50 errorDescription = "Zaloguj się, aby uzyskać dostęp do tej strony.";
51 break;
52 default:
53 // Domyślny komunikat już ustawiony
54 break;
55 }
56
57 return (
58 <div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
59 <div className="max-w-md w-full space-y-8">
60 <div>
61 <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
62 {errorMessage}
63 </h2>
64 <p className="mt-2 text-center text-sm text-gray-600">
65 {errorDescription}
66 </p>
67 </div>
68 <div className="flex flex-col items-center space-y-4">
69 <Link
70 href="/auth/login"
71 className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none"
72 >
73 Wróć do strony logowania
74 </Link>
75 <Link
76 href="/"
77 className="text-sm text-blue-600 hover:text-blue-500"
78 >
79 Wróć do strony głównej
80 </Link>
81 </div>
82 </div>
83 </div>
84 );
85}W tym module omówiliśmy szczegółowo, jak zaimplementować uwierzytelnianie z popularnymi dostawcami zewnętrznymi (Google, GitHub, Facebook) w aplikacji Next.js. Poznaliśmy:
Dzięki tej implementacji nasza aplikacja Next.js zyskuje profesjonalny, wieloplatformowy system uwierzytelniania, który poprawia doświadczenie użytkownika i jednocześnie wzmacnia bezpieczeństwo naszej aplikacji.
W następnym module zajmiemy się szczegółowo sesjami użytkownika i zarządzaniem tokenami, co pozwoli nam lepiej zrozumieć, jak działają mechanizmy uwierzytelniania po zalogowaniu użytkownika.