Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Implementacja Clerk w Next.js

W nowoczesnych aplikacjach, niezawodna i bezpieczna obsługa uwierzytelniania oraz zarządzania użytkownikami stanowi fundament dobrego doświadczenia użytkownika. Clerk to kompleksowe rozwiązanie, które upraszcza ten aspekt tworzenia aplikacji, oferując gotowe komponenty i API do zarządzania tożsamością użytkowników w aplikacjach Next.js.

Czym jest Clerk?

Clerk to kompleksowa platforma zarządzania tożsamością (Identity-as-a-Service), zaprojektowana specjalnie pod kątem aplikacji Next.js i React. W przeciwieństwie do innych rozwiązań, Clerk oferuje pełny pakiet narzędzi do uwierzytelniania, zarządzania użytkownikami i zarządzania sesjami.

Główne zalety Clerk:

  1. Gotowe komponenty UI - pre-stylowane komponenty do logowania, rejestracji, weryfikacji dwustopniowej i więcej
  2. Natywne wsparcie dla Next.js App Router - zoptymalizowane do pracy z najnowszymi wersjami Next.js
  3. Wieloplatformowa obsługa - wsparcie dla wielu urządzeń i platform
  4. Wieloczynnikowe uwierzytelnianie - wbudowana obsługa 2FA
  5. Zarządzanie organizacjami i teamami - funkcje dla aplikacji B2B
  6. Zgodność z GDPR i innymi regulacjami - wbudowane mechanizmy ochrony prywatności

Instalacja i konfiguracja

Rozpocznijmy od instalacji pakietów Clerk w projekcie Next.js:

1npm install @clerk/nextjs
2# lub
3yarn add @clerk/nextjs
4# lub
5pnpm add @clerk/nextjs

Konfiguracja kluczy API

Po utworzeniu konta na clerk.com i nowej aplikacji, otrzymasz klucze API. Dodaj je do zmiennych środowiskowych w pliku

.env.local
:

1NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
2CLERK_SECRET_KEY=sk_test_...

Jeśli korzystasz z GitHub lub innego systemu kontroli wersji, upewnij się, że plik

.env.local
jest dodany do
.gitignore
.

Owinięcie aplikacji w provider Clerk

Aby Clerk działał w całej aplikacji, musimy dodać

ClerkProvider
w głównym layoucie:

1// app/layout.tsx
2import { ClerkProvider } from '@clerk/nextjs';
3 
4export default function RootLayout({
5  children,
6}: {
7  children: React.ReactNode;
8}) {
9  return (
10    <ClerkProvider>
11      <html lang="pl">
12        <body>{children}</body>
13      </html>
14    </ClerkProvider>
15  );
16}

Konfiguracja ścieżek publicznych i chronionych

Ważnym krokiem jest określenie, które ścieżki są publiczne (dostępne dla niezalogowanych użytkowników), a które chronione (wymagające zalogowania). Można to zrobić za pomocą middleware:

1// middleware.ts
2import { authMiddleware } from "@clerk/nextjs";
3 
4export default authMiddleware({
5  publicRoutes: ["/", "/contact", "/about", "/api/public"],
6});
7 
8export const config = {
9  matcher: ["/((?!.+\.[\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
10};

W powyższym przykładzie strony główna, kontaktowa i "o nas" są publiczne, a wszystkie inne wymagają zalogowania.

Komponenty uwierzytelniania

Clerk dostarcza gotowe komponenty do najczęstszych operacji związanych z uwierzytelnianiem.

SignIn i SignUp

Zaimplementowanie stron logowania i rejestracji jest bardzo proste:

1// app/sign-in/[[...sign-in]]/page.tsx
2import { SignIn } from "@clerk/nextjs";
3 
4export default function SignInPage() {
5  return (
6    <div className="flex justify-center items-center min-h-screen">
7      <SignIn
8        appearance={{
9          elements: {
10            formButtonPrimary: "bg-blue-500 hover:bg-blue-600 text-white",
11            footerActionLink: "text-blue-500 hover:text-blue-600"
12          }
13        }}
14      />
15    </div>
16  );
17}
18
19// app/sign-up/[[...sign-up]]/page.tsx
20import { SignUp } from "@clerk/nextjs";
21 
22export default function SignUpPage() {
23  return (
24    <div className="flex justify-center items-center min-h-screen">
25      <SignUp
26        appearance={{
27          elements: {
28            formButtonPrimary: "bg-blue-500 hover:bg-blue-600 text-white",
29            footerActionLink: "text-blue-500 hover:text-blue-600"
30          }
31        }}
32      />
33    </div>
34  );
35}

Zwróć uwagę na wzorzec

[[...sign-in]]
w nazwie folderu - podwójne nawiasy kwadratowe oznaczają opcjonalny parametr catch-all, co pozwala Clerk obsługiwać różne stany uwierzytelniania.

UserButton - przycisk profilu użytkownika

Dodaj przycisk profilu użytkownika do nagłówka, który wyświetla menu z opcjami zarządzania kontem:

1// components/Header.tsx
2import { UserButton } from "@clerk/nextjs";
3
4export function Header() {
5  return (
6    <header className="flex justify-between items-center p-4 bg-white shadow">
7      <div className="font-bold text-xl">Moja aplikacja</div>
8      <UserButton afterSignOutUrl="/" />
9    </header>
10  );
11}

Dostęp do informacji o użytkowniku

W komponentach po stronie klienta możemy użyć hooków Clerk, aby uzyskać informacje o zalogowanym użytkowniku:

1// components/Profile.tsx
2'use client';
3
4import { useUser } from "@clerk/nextjs";
5
6export function Profile() {
7  const { user, isLoaded } = useUser();
8  
9  if (!isLoaded) {
10    return <div>Ładowanie...</div>;
11  }
12  
13  return (
14    <div className="p-4">
15      <h1 className="text-2xl font-bold mb-4">Twój profil</h1>
16      <div className="mb-4">
17        <img 
18          src={user?.imageUrl} 
19          alt="Zdjęcie profilowe" 
20          className="w-16 h-16 rounded-full"
21        />
22      </div>
23      <p><strong>Imię:</strong> {user?.firstName}</p>
24      <p><strong>Nazwisko:</strong> {user?.lastName}</p>
25      <p><strong>Email:</strong> {user?.primaryEmailAddress?.emailAddress}</p>
26    </div>
27  );
28}

W komponentach po stronie serwera, możemy użyć funkcji pomocniczych:

1// app/dashboard/page.tsx
2import { currentUser } from "@clerk/nextjs";
3
4export default async function DashboardPage() {
5  const user = await currentUser();
6  
7  if (!user) {
8    return <div>Nie jesteś zalogowany</div>;
9  }
10  
11  return (
12    <div className="p-4">
13      <h1 className="text-2xl font-bold mb-4">Panel użytkownika</h1>
14      <p>Witaj, {user.firstName}! Oto Twój panel użytkownika.</p>
15    </div>
16  );
17}

Zarządzanie organizacjami

Jedną z mocnych stron Clerk jest wbudowana obsługa organizacji (teamów), co jest szczególnie przydatne w aplikacjach B2B.

Komponent selekcji organizacji

1// components/OrganizationSwitcher.tsx
2import { OrganizationSwitcher } from "@clerk/nextjs";
3
4export function OrgSwitcher() {
5  return (
6    <OrganizationSwitcher
7      appearance={{
8        elements: {
9          organizationSwitcherTrigger: "p-2 border rounded-md"
10        }
11      }}
12    />
13  );
14}

Tworzenie nowej organizacji

1// app/organizations/new/page.tsx
2import { CreateOrganization } from "@clerk/nextjs";
3
4export default function NewOrganizationPage() {
5  return (
6    <div className="flex justify-center items-center min-h-screen">
7      <CreateOrganization
8        appearance={{
9          elements: {
10            formButtonPrimary: "bg-blue-500 hover:bg-blue-600 text-white",
11          }
12        }}
13      />
14    </div>
15  );
16}

Dostęp do danych organizacji

1// app/dashboard/organization/page.tsx
2import { currentUser, clerkClient } from "@clerk/nextjs";
3import { redirect } from "next/navigation";
4
5export default async function OrganizationDashboard() {
6  const user = await currentUser();
7  
8  if (!user) {
9    return redirect("/sign-in");
10  }
11  
12  // Pobierz aktywną organizację
13  const org = await clerkClient.organizations.getOrganization({
14    organizationId: user.organizationId || ""
15  });
16  
17  if (!org) {
18    return (
19      <div className="p-4">
20        <h1 className="text-2xl font-bold mb-4">Nie wybrano organizacji</h1>
21        <p>Wybierz organizację, aby zobaczyć jej dashboard.</p>
22      </div>
23    );
24  }
25  
26  return (
27    <div className="p-4">
28      <h1 className="text-2xl font-bold mb-4">Dashboard organizacji: {org.name}</h1>
29      <p>Członkowie: {org.membersCount}</p>
30      <p>Utworzono: {new Date(org.createdAt).toLocaleDateString()}</p>
31    </div>
32  );
33}

Zabezpieczanie API Routes

W Next.js App Router możemy łatwo zabezpieczać trasy API używając funkcji pomocniczych Clerk:

1// app/api/protected/route.ts
2import { auth } from "@clerk/nextjs";
3import { NextResponse } from "next/server";
4
5export async function GET() {
6  const { userId } = auth();
7  
8  if (!userId) {
9    return new NextResponse(JSON.stringify({ error: "Unauthorized" }), {
10      status: 401,
11      headers: { "Content-Type": "application/json" }
12    });
13  }
14  
15  return NextResponse.json({
16    message: "To są chronione dane tylko dla zalogowanych użytkowników"
17  });
18}

Dostosowywanie wyglądu

Clerk oferuje rozbudowane opcje dostosowywania wyglądu. Można to zrobić na trzy sposoby:

1. Za pomocą prop appearance

1import { SignIn } from "@clerk/nextjs";
2
3export default function SignInPage() {
4  return (
5    <SignIn
6      appearance={{
7        elements: {
8          formButtonPrimary: "bg-gradient-to-r from-blue-500 to-purple-500 text-white",
9          card: "shadow-xl border-0",
10          socialButtonsBlockButton: "border border-gray-300 hover:bg-gray-100"
11        }
12      }}
13    />
14  );
15}

2. Za pomocą dostawcy wyglądu

1// app/layout.tsx
2import { ClerkProvider } from '@clerk/nextjs';
3
4const appearance = {
5  elements: {
6    formButtonPrimary: "bg-blue-500 hover:bg-blue-600 text-white",
7    footerActionLink: "text-blue-500 hover:text-blue-600",
8    card: "shadow-md"
9  },
10  variables: {
11    colorPrimary: "#3B82F6"
12  }
13};
14
15export default function RootLayout({
16  children,
17}: {
18  children: React.ReactNode;
19}) {
20  return (
21    <ClerkProvider appearance={appearance}>
22      <html lang="pl">
23        <body>{children}</body>
24      </html>
25    </ClerkProvider>
26  );
27}

3. Za pomocą niestandardowych stylów CSS

Można również utworzyć plik CSS z własnymi stylami:

1/* styles/clerk.css */
2.cl-card {
3  border-radius: 12px;
4  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
5}
6
7.cl-formButtonPrimary {
8  background: linear-gradient(45deg, #3B82F6, #6366F1);
9  border-radius: 6px;
10  transition: transform 0.2s;
11}
12
13.cl-formButtonPrimary:hover {
14  transform: translateY(-2px);
15}

A następnie zaimportować do aplikacji:

1// app/layout.tsx
2import "../styles/clerk.css";

Webhook i integracja z bazą danych

W większości aplikacji będziesz chciał synchronizować dane użytkowników Clerk z własną bazą danych. Najlepszym sposobem na to jest użycie webhooków:

Konfiguracja endpointu webhook

1// app/api/webhooks/clerk/route.ts
2import { WebhookEvent } from "@clerk/nextjs/server";
3import { headers } from "next/headers";
4import { NextResponse } from "next/server";
5import { Webhook } from "svix";
6
7export async function POST(req: Request) {
8  const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET;
9  
10  if (!WEBHOOK_SECRET) {
11    throw new Error('Brak CLERK_WEBHOOK_SECRET');
12  }
13
14  // Pobierz nagłówki
15  const headerPayload = await headers();
16  const svix_id = headerPayload.get("svix-id");
17  const svix_timestamp = headerPayload.get("svix-timestamp");
18  const svix_signature = headerPayload.get("svix-signature");
19  
20  if (!svix_id || !svix_timestamp || !svix_signature) {
21    return new NextResponse(JSON.stringify({ error: "Brak wymaganych nagłówków" }), {
22      status: 400
23    });
24  }
25
26  // Pobierz treść żądania
27  const payload = await req.json();
28  const body = JSON.stringify(payload);
29  
30  // Zweryfikuj webhook
31  const webhook = new Webhook(WEBHOOK_SECRET);
32  let event: WebhookEvent;
33  
34  try {
35    event = webhook.verify(body, {
36      "svix-id": svix_id,
37      "svix-timestamp": svix_timestamp,
38      "svix-signature": svix_signature,
39    }) as WebhookEvent;
40  } catch (err) {
41    console.error('Błąd weryfikacji webhooka:', err);
42    return new NextResponse(JSON.stringify({ error: "Nieprawidłowy podpis" }), {
43      status: 400
44    });
45  }
46  
47  // Obsługa różnych typów zdarzeń
48  const { type } = event;
49  
50  switch (type) {
51    case 'user.created': {
52      const { id, email_addresses, username, first_name, last_name } = event.data;
53      const primaryEmail = email_addresses?.[0]?.email_address;
54      
55      // Tutaj dodaj logikę zapisania użytkownika w bazie danych
56      // Na przykład używając Prisma:
57      // await prisma.user.create({
58      //   data: {
59      //     clerkId: id,
60      //     email: primaryEmail,
61      //     username: username || "",
62      //     firstName: first_name || "",
63      //     lastName: last_name || ""
64      //   }
65      // });
66      
67      break;
68    }
69    
70    case 'user.updated': {
71      const { id, email_addresses, username, first_name, last_name } = event.data;
72      const primaryEmail = email_addresses?.[0]?.email_address;
73      
74      // Tutaj zaktualizuj użytkownika w bazie danych
75      break;
76    }
77    
78    case 'user.deleted': {
79      const { id } = event.data;
80      
81      // Tutaj usuń użytkownika z bazy danych
82      break;
83    }
84    
85    // Obsłuż inne typy zdarzeń, takie jak organization.created, organization.updated, itp.
86  }
87  
88  return NextResponse.json({ message: "Webhook odebrany" }, { status: 200 });
89}

Zaawansowane funkcje

Wieloczynnikowe uwierzytelnianie (2FA)

Clerk oferuje wbudowane wsparcie dla 2FA. Możesz włączyć tę funkcję w panelu administracyjnym Clerk.

Logowanie bezhasłowe

Możesz skonfigurować logowanie za pomocą linków magic link (bez hasła):

1// app/sign-in/magic-link/page.tsx
2import { SignIn } from "@clerk/nextjs";
3
4export default function MagicLinkPage() {
5  return (
6    <div className="flex justify-center items-center min-h-screen">
7      <SignIn path="/sign-in/magic-link" />
8    </div>
9  );
10}

Wieloetapowe logowanie

Możesz utworzyć niestandardowy proces logowania z wieloma etapami:

1// components/CustomSignIn.tsx
2'use client';
3
4import { useSignIn } from "@clerk/nextjs";
5import { useState } from "react";
6
7export function CustomSignIn() {
8  const { isLoaded, signIn, setActive } = useSignIn();
9  const [email, setEmail] = useState("");
10  const [password, setPassword] = useState("");
11  const [verificationCode, setVerificationCode] = useState("");
12  const [stage, setStage] = useState("email");
13  const [error, setError] = useState("");
14  
15  if (!isLoaded) {
16    return <div>Ładowanie...</div>;
17  }
18  
19  async function handleSubmitEmail(e: React.FormEvent) {
20    e.preventDefault();
21    setError("");
22    
23    try {
24      await signIn.create({
25        identifier: email
26      });
27      
28      await signIn.prepareFirstFactor({
29        strategy: "email_code",
30        email
31      });
32      
33      setStage("verification");
34    } catch (err: any) {
35      setError(err.errors?.[0]?.message || "Wystąpił błąd");
36    }
37  }
38  
39  async function handleVerifyCode(e: React.FormEvent) {
40    e.preventDefault();
41    setError("");
42    
43    try {
44      const result = await signIn.attemptFirstFactor({
45        strategy: "email_code",
46        code: verificationCode
47      });
48      
49      if (result.status === "complete") {
50        await setActive({ session: result.createdSessionId });
51        // Przekierowanie po zalogowaniu
52        window.location.href = "/dashboard";
53      }
54    } catch (err: any) {
55      setError(err.errors?.[0]?.message || "Kod nieprawidłowy");
56    }
57  }
58  
59  return (
60    <div className="p-4 max-w-sm mx-auto">
61      <h1 className="text-2xl font-bold mb-4 text-center">Logowanie</h1>
62      
63      {error && (
64        <div className="mb-4 p-2 bg-red-100 text-red-700 rounded-md">
65          {error}
66        </div>
67      )}
68      
69      {stage === "email" ? (
70        <form onSubmit={handleSubmitEmail} className="space-y-4">
71          <div>
72            <label htmlFor="email" className="block mb-1">Email</label>
73            <input
74              id="email"
75              type="email"
76              value={email}
77              onChange={(e) => setEmail(e.target.value)}
78              required
79              className="w-full p-2 border rounded-md"
80            />
81          </div>
82          
83          <button 
84            type="submit" 
85            className="w-full bg-blue-500 text-white p-2 rounded-md hover:bg-blue-600"
86          >
87            Kontynuuj
88          </button>
89        </form>
90      ) : (
91        <form onSubmit={handleVerifyCode} className="space-y-4">
92          <div>
93            <label htmlFor="code" className="block mb-1">Kod weryfikacyjny</label>
94            <input
95              id="code"
96              type="text"
97              value={verificationCode}
98              onChange={(e) => setVerificationCode(e.target.value)}
99              required
100              className="w-full p-2 border rounded-md"
101            />
102          </div>
103
104          <button
105            type="submit"
106            className="w-full bg-blue-500 text-white p-2 rounded-md hover:bg-blue-600"
107          >
108            Weryfikuj
109          </button>
110          
111          <button 
112            type="button" 
113            className="w-full text-blue-500 hover:text-blue-600"
114            onClick={() => setStage("email")}
115          >
116            Wróć
117          </button>
118        </form>
119      )}
120    </div>
121  );
122}

Wady i ograniczenia Clerk

Choć Clerk oferuje wiele korzyści, warto być świadomym kilku ograniczeń:

  1. Koszt - w przeciwieństwie do Auth.js, Clerk działa na modelu freemium i dla większych aplikacji wymaga płatnej subskrypcji
  2. Zależność od zewnętrznej usługi - dane uwierzytelniania są przechowywane na serwerach Clerk
  3. Mniejsza kontrola - masz mniejszą kontrolę nad implementacją szczegółów uwierzytelniania
  4. Kustomizacja - mimo wielu opcji dostosowywania, czasami natywne rozwiązanie może oferować więcej swobody

Kiedy wybrać Clerk zamiast Auth.js?

Przy podejmowaniu decyzji warto rozważyć następujące czynniki:

  1. Wybierz Clerk, gdy:

    • Potrzebujesz gotowego, kompletnego rozwiązania uwierzytelniania
    • Chcesz szybko wdrożyć funkcje uwierzytelniania z minimalnym wysiłkiem
    • Potrzebujesz zarządzania organizacjami i teamami
    • Potrzebujesz zaawansowanych funkcji jak 2FA, magic links, etc.
    • Nie chcesz zarządzać infrastrukturą uwierzytelniania
  2. Wybierz Auth.js, gdy:

    • Potrzebujesz większej kontroli nad procesem uwierzytelniania
    • Preferujesz rozwiązanie open-source
    • Nie chcesz polegać na zewnętrznym serwisie
    • Masz ograniczony budżet dla większego projektu
    • Musisz przechowywać dane uwierzytelniania we własnej bazie danych

Podsumowanie

Clerk to potężne narzędzie, które może znacząco przyspieszyć implementację uwierzytelniania w aplikacjach Next.js. Oferuje bogaty zestaw funkcji, które mogłyby wymagać tygodni pracy do samodzielnej implementacji. Dla wielu zespołów deweloperskich oszczędność czasu i gotowe rozwiązania bezpieczeństwa przeważają nad kosztami i mniejszą kontrolą.

Jednak wybór między Clerk, Auth.js, czy własnym rozwiązaniem uwierzytelniania powinien być oparty na konkretnych wymaganiach projektu, dostępnym budżecie i potrzebach biznesowych.

Vai a CodeWorlds