Kurs vibe coding z AI · Moduł 9: Discord Clone

Authentication - NextAuth.js

3 min czytania
W tej lekcji4

Każdy może dziś otworzyć panel gildii i zmienić rangi, bo aplikacja nie wie, kto siedzi po drugiej stronie ekranu. Rangi z poprzednich lekcji mówią, co komu wolno, ale najpierw trzeba ustalić, kim ktoś jest. To zadanie logowania, czyli uwierzytelniania. Własny system haseł łatwo zepsuć, dlatego dokumentacja Next.js poleca gotowe biblioteki, takie jak NextAuth.js.

Instalacja

Paczkę instalujesz jednym poleceniem:

npm install next-auth

To polecenie instaluje stabilną wersję 4 biblioteki i właśnie jej API pokazuje ta lekcja.

Logowanie przez Google

Konfiguracja trafia do Route Handlera, który obsłuży wszystkie adresy zaczynające się od /api/auth:

// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';

const handler = NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!
    })
  ]
});

export { handler as GET, handler as POST };

Folder [...nextauth] łapie każdy adres pod /api/auth: stronę logowania, wylogowanie i powrót z Google. Używamy GoogleProvider z clientId i clientSecret, które pobierasz z Google Cloud Console i trzymasz w zmiennych środowiskowych, nigdy w kodzie. Na liście dozwolonych adresów przekierowania w Google wpisujesz http://localhost:3000/api/auth/callback/google, a na produkcji ten sam adres z Twoją domeną. Ostatnia linia eksportuje tę samą funkcję jako GET i POST, bo logowanie korzysta z obu metod.

Chroniona strona

Panel ma zobaczyć tylko zalogowany pirat. W Server Component sesję pobierasz po stronie serwera:

import { getServerSession } from 'next-auth';

export default async function Dashboard() {
  const session = await getServerSession();

  if (!session) {
    redirect('/api/auth/signin');
  }

  return <div>Welcome {session.user.name}!</div>;
}

await getServerSession() zwraca sesję albo null. Gdy sesji brak, następuje redirect('/api/auth/signin'), czyli przejście do wbudowanej strony logowania NextAuth. Zalogowanego pirata strona wita imieniem z session.user.name.

W przykładzie brakuje jednego importu, bez którego redirect nie zadziała:

import { redirect } from 'next/navigation';

W pełnym projekcie przekazujesz też do getServerSession ten sam obiekt konfiguracji (authOptions), którym inicjujesz NextAuth, bo tak zaleca dokumentacja biblioteki. Sesja jest sprawdzana na serwerze, zanim cokolwiek trafi do przeglądarki, więc niezalogowany użytkownik nie zobaczy nawet fragmentu panelu.

Co zmienia się w nowszych wersjach

NextAuth.js rozwija się dziś pod nazwą Auth.js. Wersja 5 jest nadal oznaczona jako beta: konfiguracja trafia do pliku auth.ts, który eksportuje auth, handlers, signIn i signOut, a zamiast getServerSession wywołujesz jedną funkcję:

const session = await auth();

Od września 2025 roku Auth.js utrzymuje zespół Better Auth, który dla nowych projektów poleca swoją bibliotekę Better Auth, a istniejącym aplikacjom obiecuje dalsze poprawki bezpieczeństwa. W Next.js 16 plik middleware.ts, często używany do ochrony wielu stron naraz, nazywa się proxy.ts.

Rada Kapitana Redbearda: nie stawiaj jednego strażnika przy trapie. Przekierowanie w proxy jest wygodne, ale sesję sprawdzaj też tam, gdzie czytasz i zmieniasz dane, tak jak radzi dokumentacja Next.js.

Zapamiętaj: logowanie odpowiada na pytanie, kim jesteś, a rangi na pytanie, co Ci wolno. Pirate's Cove powstawał w kolejności: serwery z kategoriami i kanałami, kanały głosowe z rich presence, rangi z moderacją, a na koniec integracje i logowanie. Edytor poniżej zawiera panel rang z lekcji o uprawnieniach, czyli dokładnie to miejsce, które logowanie powinno chronić. Potem rozwiąż zadania.

Kod do tej lekcji: App.jsx
// Menedżer rang
import { useState } from 'react';

const roles = [
  { id: 1, name: 'Kapitan', color: '#e74c3c', memberCount: 3, position: 4 },
  { id: 2, name: 'Pierwszy oficer', color: '#3498db', memberCount: 5, position: 3 },
  { id: 3, name: 'Nawigator', color: '#2ecc71', memberCount: 8, position: 2 },
  { id: 4, name: 'Załoga', color: '#95a5a6', memberCount: 45, position: 1 }
];

const permissions = [
  'ADMINISTRATOR',
  'MANAGE_SERVER',
  'MANAGE_ROLES',
  'MANAGE_CHANNELS',
  'KICK_MEMBERS',
  'BAN_MEMBERS',
  'SEND_MESSAGES',
  'MANAGE_MESSAGES'
];

export default function App() {
  const [selectedRoleId, setSelectedRoleId] = useState(1);

  const selectedRole = roles.find(r => r.id === selectedRoleId);

  return (
    <div className="flex h-screen bg-gray-800">
      {/* Lista rang */}
      <div className="w-64 bg-gray-900 p-4">
        <div className="flex items-center justify-between mb-4">
          <h3 className="font-bold text-white">Rangi</h3>
          <button className="w-8 h-8 bg-green-600 hover:bg-green-700 rounded-full text-white">
            +
          </button>
        </div>

        <div className="space-y-1">
          {roles.sort((a, b) => b.position - a.position).map(role => (
            <button
              key={role.id}
              onClick={() => setSelectedRoleId(role.id)}
              className={`w-full text-left px-3 py-2 rounded flex items-center justify-between ${
                selectedRoleId === role.id ? 'bg-gray-700' : 'hover:bg-gray-800'
              }`}
            >
              <div className="flex items-center gap-2 flex-1">
                <div
                  className="w-3 h-3 rounded-full"
                  style={{ backgroundColor: role.color }}
                />
                <span className="text-white text-sm truncate">{role.name}</span>
              </div>
              <span className="text-xs text-gray-400">{role.memberCount}</span>
            </button>
          ))}
        </div>
      </div>

      {/* Edytor rangi */}
      {selectedRole && (
        <div className="flex-1 p-6 overflow-y-auto">
          <h2 className="text-2xl font-bold text-white mb-6">
            Edytuj rangę: {selectedRole.name}
          </h2>

          {/* Wygląd */}
          <div className="bg-gray-700 rounded-lg p-4 mb-6">
            <h3 className="font-bold text-white mb-3">Wygląd</h3>

            <div className="mb-4">
              <label className="block text-sm text-gray-300 mb-2">Nazwa rangi</label>
              <input
                type="text"
                key={selectedRole.id}
                defaultValue={selectedRole.name}
                className="w-full bg-gray-900 text-white px-3 py-2 rounded"
              />
            </div>

            <div className="mb-4">
              <label className="block text-sm text-gray-300 mb-2">Kolor rangi</label>
              <div className="flex items-center gap-3">
                <input
                  type="color"
                  key={selectedRole.id}
                  defaultValue={selectedRole.color}
                  className="w-16 h-10 rounded cursor-pointer"
                />
                <span className="text-gray-400 text-sm">{selectedRole.color}</span>
              </div>
            </div>

            <label className="flex items-center gap-2 text-sm text-gray-300 mb-2">
              <input type="checkbox" className="w-4 h-4" />
              Wyświetlaj członków tej rangi osobno
            </label>

            <label className="flex items-center gap-2 text-sm text-gray-300">
              <input type="checkbox" className="w-4 h-4" />
              Pozwól każdemu wspominać tę rangę przez @
            </label>
          </div>

          {/* Uprawnienia */}
          <div className="bg-gray-700 rounded-lg p-4">
            <h3 className="font-bold text-white mb-3">Uprawnienia</h3>

            <div className="space-y-2">
              {permissions.map(permission => (
                <label
                  key={permission}
                  className="flex items-center gap-3 text-sm text-gray-300 hover:bg-gray-600 p-2 rounded cursor-pointer"
                >
                  <input type="checkbox" className="w-4 h-4" />
                  <span>{permission.split('_').join(' ')}</span>
                </label>
              ))}
            </div>
          </div>

          {/* Akcje */}
          <div className="mt-6 flex gap-3">
            <button className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded font-semibold">
              Zapisz zmiany
            </button>
            <button className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded font-semibold">
              Usuń rangę
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Jakiego providera użyto w konfiguracji NextAuth z przykładu?

  2. 2. Co dzieje się w Dashboard, gdy getServerSession() zwróci brak sesji?

Zadania praktyczne w grze

  • Układanie w poziomie

    Ułóż dostęp do nazwy zalogowanego użytkownika: session.user.name

  • Klikanie w kolejności

    Klikaj, aby zbudować pobranie sesji po stronie serwera: await getServerSession()

  • Układanie w pionie

    Ułóż kolejność budowy Discord clone w module:

Przydatne artykuły