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.
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:
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/nextjsPo 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.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}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.
Clerk dostarcza gotowe komponenty do najczęstszych operacji związanych z uwierzytelnianiem.
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.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}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}Jedną z mocnych stron Clerk jest wbudowana obsługa organizacji (teamów), co jest szczególnie przydatne w aplikacjach B2B.
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}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}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}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}Clerk oferuje rozbudowane opcje dostosowywania wyglądu. Można to zrobić na trzy sposoby:
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}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}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";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:
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}Clerk oferuje wbudowane wsparcie dla 2FA. Możesz włączyć tę funkcję w panelu administracyjnym Clerk.
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}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}Choć Clerk oferuje wiele korzyści, warto być świadomym kilku ograniczeń:
Przy podejmowaniu decyzji warto rozważyć następujące czynniki:
Wybierz Clerk, gdy:
Wybierz Auth.js, gdy:
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.