Kurs Next.js · Moduł 9: Integracje i zaawansowane funkcje

forbidden() i unauthorized() - Nowe funkcje obsługi błędów

8 min czytania
W tej lekcji10

Do sektora administracyjnego Metropolii Quantum 2150 próbują wejść dwie osoby. Pierwsza nie ma przepustki wcale, druga ma przepustkę gościa. Obie muszą zostać zatrzymane, ale każda z innym komunikatem: pierwsza powinna iść do punktu rejestracji, druga dowiedzieć się, że ten sektor nie jest dla niej. W HTTP to różnica między kodem 401 a 403. Poznasz funkcje forbidden() i unauthorized(), wprowadzone w Next.js 15.1, które upraszczają obsługę błędów autoryzacji i autentykacji.

Problem: Ręczna obsługa błędów 401/403

Wcześniej obsługa błędów autoryzacji wymagała ręcznego tworzenia obiektu Response z kodem statusu w każdym miejscu, które sprawdza sesję:

1// Stary, rozwlekły sposób
2export async function GET(request: Request) {
3  const session = await getSession();
4
5  if (!session) {
6    return new Response('Unauthorized', { status: 401 });
7  }
8
9  if (!session.user.isAdmin) {
10    return new Response('Forbidden', { status: 403 });
11  }
12
13  return Response.json(await getAdminData());
14}

Ten wzorzec działa w Route Handlerze, ale w komponencie strony nie zwrócisz przecież obiektu Response, a komunikaty rozjeżdżają się po całej aplikacji.

Rozwiązanie: forbidden() i unauthorized()

unauthorized() - Błąd 401

Funkcja unauthorized() importowana z next/navigation przerywa renderowanie segmentu, zwraca status 401 i pokazuje interfejs z pliku unauthorized.tsx:

1// app/api/profile/route.ts
2import { unauthorized } from 'next/navigation';
3import { getSession } from '@/lib/auth';
4
5export async function GET(request: Request) {
6  const session = await getSession();
7
8  if (!session) {
9    unauthorized(); // Rzuca błąd 401
10  }
11
12  return Response.json(session.user);
13}

Nie piszesz return unauthorized(), bo funkcja rzuca wyjątek i ma typ zwracany never. Dzięki temu TypeScript wie, że po warunku session na pewno istnieje.

forbidden() - Błąd 403

Funkcja forbidden() działa tak samo, ale oznacza "wiem, kim jesteś, i nie masz dostępu". Renderuje interfejs z pliku forbidden.tsx:

1// app/api/admin/users/route.ts
2import { forbidden, unauthorized } from 'next/navigation';
3import { getSession } from '@/lib/auth';
4
5export async function GET(request: Request) {
6  const session = await getSession();
7
8  if (!session) {
9    unauthorized(); // 401 - niezalogowany
10  }
11
12  if (session.user.role !== 'admin') {
13    forbidden(); // 403 - brak uprawnień
14  }
15
16  return Response.json(await getAllUsers());
17}

Kolejność sprawdzeń ma znaczenie: najpierw tożsamość (401), potem uprawnienia (403). Obie funkcje działają w Server Components, Server Actions i Route Handlers.

Włączenie funkcji

W Next.js 16 obie funkcje wciąż są eksperymentalne i dokumentacja nie zaleca ich jeszcze na produkcji. Włączasz je opcją authInterrupts:

1// next.config.js
2module.exports = {
3  experimental: {
4    authInterrupts: true,
5  },
6};

Bez tej flagi wywołanie funkcji skończy się błędem, więc dodaj ją, zanim zaczniesz eksperymentować.

Tworzenie stron błędów

unauthorized.tsx - Strona 401

Plik unauthorized.tsx w katalogu app to zwykły komponent React, który Next.js wyświetla po wywołaniu unauthorized():

1// app/unauthorized.tsx
2export default function UnauthorizedPage() {
3  return (
4    <div className="min-h-screen flex items-center justify-center bg-gray-900">
5      <div className="text-center">
6        <h1 className="text-6xl font-bold text-red-500 mb-4">401</h1>
7        <h2 className="text-2xl text-white mb-4">Brak autoryzacji</h2>
8        <p className="text-gray-400 mb-8">
9          Musisz się zalogować, aby uzyskać dostęp do tej strony.
10        </p>
11        <a
12          href="/login"
13          className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
14        >
15          Zaloguj się
16        </a>
17      </div>
18    </div>
19  );
20}

Najważniejszy jest tu link do logowania, bo użytkownik bez sesji potrzebuje drogi naprzód, a nie tylko komunikatu.

forbidden.tsx - Strona 403

Strona 403 ma inną rolę: logowanie nic nie da, więc proponujemy powrót na stronę główną:

1// app/forbidden.tsx
2export default function ForbiddenPage() {
3  return (
4    <div className="min-h-screen flex items-center justify-center bg-gray-900">
5      <div className="text-center">
6        <h1 className="text-6xl font-bold text-yellow-500 mb-4">403</h1>
7        <h2 className="text-2xl text-white mb-4">Dostęp zabroniony</h2>
8        <p className="text-gray-400 mb-8">
9          Nie masz uprawnień do wyświetlenia tej strony.
10        </p>
11        <a
12          href="/"
13          className="px-6 py-3 bg-gray-600 text-white rounded-lg hover:bg-gray-700"
14        >
15          Wróć na stronę główną
16        </a>
17      </div>
18    </div>
19  );
20}

Next.js dodaje do obu stron znacznik noindex, więc wyszukiwarki ich nie indeksują.

Zagnieżdżone strony błędów

Możesz tworzyć strony błędów specyficzne dla segmentów. Next.js wybiera najbliższy plik w górę drzewa katalogów:

1app/
2├── unauthorized.tsx          # Globalna strona 401
3├── forbidden.tsx             # Globalna strona 403
4├── admin/
5│   ├── unauthorized.tsx      # 401 dla /admin/*
6│   ├── forbidden.tsx         # 403 dla /admin/*
7│   └── page.tsx
8└── dashboard/
9    ├── forbidden.tsx         # 403 dla /dashboard/*
10    └── page.tsx

Brak unauthorized.tsx w dashboard/ oznacza, że dla tej sekcji zadziała globalna strona 401 z katalogu app.

Przykład - Admin forbidden.tsx

Strona błędu może być asynchronicznym Server Componentem i czytać sesję, żeby zwrócić się do użytkownika po imieniu:

1// app/admin/forbidden.tsx
2import { getSession } from '@/lib/auth';
3
4export default async function AdminForbiddenPage() {
5  const session = await getSession();
6
7  return (
8    <div className="min-h-screen flex items-center justify-center bg-slate-900">
9      <div className="max-w-md text-center p-8 bg-slate-800 rounded-xl">
10        <div className="text-6xl mb-4">◆</div>
11        <h1 className="text-2xl font-bold text-white mb-4">
12          Strefa administratora
13        </h1>
14        <p className="text-gray-400 mb-6">
15          Witaj {session?.user?.name}! Niestety, twoje konto nie ma uprawnień
16          administratora. Skontaktuj się z administratorem systemu, jeśli
17          uważasz, że to błąd.
18        </p>
19        <div className="flex gap-4 justify-center">
20          <a
21            href="/dashboard"
22            className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
23          >
24            Panel użytkownika
25          </a>
26          <a
27            href="/support"
28            className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
29          >
30            Kontakt z supportem
31          </a>
32        </div>
33      </div>
34    </div>
35  );
36}

Użytkownik wie, kim jest zalogowany i dokąd może pójść dalej: do swojego panelu albo do supportu.

Użycie w Server Components

W komponencie strony sprawdzenia umieszczasz na początku, przed pobraniem danych:

1// app/admin/page.tsx
2import { forbidden, unauthorized } from 'next/navigation';
3import { getSession } from '@/lib/auth';
4
5export default async function AdminPage() {
6  const session = await getSession();
7
8  if (!session) {
9    unauthorized();
10  }
11
12  if (!session.user.permissions.includes('admin:access')) {
13    forbidden();
14  }
15
16  const adminData = await getAdminDashboard();
17
18  return (
19    <div className="admin-dashboard">
20      <h1>Panel Administratora</h1>
21      <AdminStats data={adminData} />
22    </div>
23  );
24}

Jeśli użytkownik nie ma uprawnienia admin:access, funkcja getAdminDashboard w ogóle się nie wykona. Pamiętaj tylko, że forbidden() nie można wywołać w głównym layoucie aplikacji (root layout). Uważaj też na try/catch wokół tych funkcji: połknie przerwanie i strona błędu się nie pokaże, chyba że przepuścisz je przez unstable_rethrow.

Użycie w Middleware

Middleware działa przed renderowaniem, więc może odrzucić żądanie najszybciej. W Next.js 16 plik middleware.ts został przemianowany na proxy.ts, a funkcja na proxy, jednak logika pozostaje ta sama:

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  const path = request.nextUrl.pathname;
9
10  // Ścieżki wymagające logowania
11  if (path.startsWith('/dashboard')) {
12    if (!token) {
13      // Przekierowanie do strony unauthorized
14      return NextResponse.rewrite(new URL('/unauthorized', request.url));
15    }
16  }
17
18  // Ścieżki wymagające roli admin
19  if (path.startsWith('/admin')) {
20    if (!token) {
21      return NextResponse.rewrite(new URL('/unauthorized', request.url));
22    }
23
24    if (token.role !== 'admin') {
25      return NextResponse.rewrite(new URL('/forbidden', request.url));
26    }
27  }
28
29  return NextResponse.next();
30}
31
32export const config = {
33  matcher: ['/dashboard/:path*', '/admin/:path*'],
34};

NextResponse.rewrite pokazuje treść innego adresu bez zmiany URL w pasku przeglądarki. Uwaga: pliki unauthorized.tsx i forbidden.tsx nie są trasami, więc taki rewrite wymaga zwykłych stron pod adresami /unauthorized i /forbidden. Middleware sprawdza tylko token, a właściwą autoryzację i tak powtarzasz blisko danych.

Praktyczny przykład - System ról

Zamiast powtarzać te same warunki w każdym pliku, zbierz je w małe funkcje pomocnicze. Typ Permission wylicza dozwolone uprawnienia:

1// lib/auth-utils.ts
2import { forbidden, unauthorized } from 'next/navigation';
3import { getSession } from '@/lib/auth';
4
5type Permission = 'read' | 'write' | 'delete' | 'admin';
6
7export async function requireAuth() {
8  const session = await getSession();
9
10  if (!session) {
11    unauthorized();
12  }
13
14  return session;
15}
16
17export async function requirePermission(permission: Permission) {
18  const session = await requireAuth();
19
20  if (!session.user.permissions.includes(permission)) {
21    forbidden();
22  }
23
24  return session;
25}
26
27export async function requireRole(role: string) {
28  const session = await requireAuth();
29
30  if (session.user.role !== role) {
31    forbidden();
32  }
33
34  return session;
35}
36
37export async function requireAdmin() {
38  return requireRole('admin');
39}

requireAuth obsługuje 401, a requirePermission i requireRole dokładają 403. Każda zwraca sesję, więc kod wywołujący od razu ma dostęp do użytkownika.

Użycie w komponentach:

1// app/admin/users/page.tsx
2import { requireAdmin } from '@/lib/auth-utils';
3
4export default async function AdminUsersPage() {
5  const session = await requireAdmin(); // Rzuci 401 lub 403 jeśli trzeba
6
7  const users = await getAllUsers();
8
9  return <UserManagement users={users} currentAdmin={session.user} />;
10}

Jedna linia zastępuje dwa warunki. Strona jest czytelna, a zasady dostępu mieszkają w jednym pliku.

Ten sam pomocnik chroni Route Handler usuwający post:

1// app/api/posts/[id]/route.ts
2import { requirePermission } from '@/lib/auth-utils';
3
4export async function DELETE(
5  request: Request,
6  { params }: { params: Promise<{ id: string }> }
7) {
8  await requirePermission('delete'); // Rzuci 401 lub 403 jeśli trzeba
9
10  await deletePost((await params).id);
11
12  return Response.json({ success: true });
13}

Endpoint API i strona korzystają z identycznych reguł, więc nie ma ryzyka, że jedno z nich zapomni o sprawdzeniu.

Różnice między unauthorized() a forbidden()

Aspektunauthorized()forbidden()
Kod HTTP401 Unauthorized403 Forbidden
ZnaczenieBrak autentykacjiBrak autoryzacji
Kiedy używaćUżytkownik niezalogowanyUżytkownik zalogowany, ale bez uprawnień
Strona błęduunauthorized.tsxforbidden.tsx
Typowa akcjaPrzekierowanie do logowaniaInformacja o braku dostępu

Jedna pułapka: jeśli sprawdzenie odbywa się w komponencie wewnątrz Suspense, odpowiedź zaczęła się już strumieniować ze statusem 200. Użytkownik zobaczy właściwą stronę błędu, ale kod HTTP się nie zmieni. Jeśli potrzebujesz prawdziwego 401 lub 403, sprawdzaj przed strumieniowaniem, np. w proxy.

Podsumowanie

Funkcje forbidden() i unauthorized() w Next.js 15.1+:

  • Upraszczają kod - jedna linia zamiast tworzenia Response
  • Standaryzują obsługę - spójne zachowanie w całej aplikacji
  • Wspierają SSR - działają w Server Components, Server Actions i Route Handlers
  • Są konfigurowalne - własne strony błędów per segment

Moja rada: trzymaj reguły dostępu w jednym pliku pomocniczym i zawsze sprawdzaj uprawnienia blisko danych, nie tylko w middleware. W następnej lekcji poznasz after(), które wykonuje kod już po wysłaniu odpowiedzi.

Zapamiętaj: 401 mówi "przedstaw się", 403 mówi "znam cię, ale tu nie wejdziesz" - strażnicy Metropolii nigdy ich nie mylą.

Kod do tej lekcji: App.tsx
1import React, { useState } from 'react';
2
3// Stworz Article content type z polami title, content, slug
4
5interface ArticleContentType {
6  title: string;
7  content: string;
8  slug: string;
9  author?: string;
10  publishedAt?: string;
11}
12
13export default function ContentTypeBuilder() {
14  const [article, setArticle] = useState<ArticleContentType>({
15    title: '', content: '', slug: '',
16  });
17  const [articles, setArticles] = useState<ArticleContentType[]>([]);
18
19  // TODO: Zaimplementuj auto-generowanie slug z tytulu
20  // const generateSlug = (title: string) => title.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
21
22  // TODO: Zaimplementuj handleSave - dodaj artykul do listy
23
24  return (
25    <div style={{ background: '#0f0f23', minHeight: '100vh', padding: 20, color: '#e0e0e0', fontFamily: 'monospace' }}>
26      <h1 style={{ color: '#64ffda' }}>Article Content Type</h1>
27      <form style={{ maxWidth: 500 }}>
28        {/* TODO: Input title z auto-slug generation */}
29        {/* TODO: Textarea content */}
30        {/* TODO: Wyswietl wygenerowany slug */}
31        {/* TODO: Przycisk "Zapisz" */}
32      </form>
33      {/* TODO: Lista zapisanych artykulow */}
34    </div>
35  );
36}

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. Co robią nowe funkcje forbidden() i unauthorized() wprowadzone w Next.js 15?

Przydatne artykuły