Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Role i uprawnienia użytkowników

W poprzednich modułach poznaliśmy podstawy uwierzytelniania, podłączanie zewnętrznych dostawców oraz zarządzanie sesjami i tokenami. Teraz zajmiemy się kolejnym kluczowym elementem bezpieczeństwa aplikacji - systemem ról i uprawnień użytkowników, czyli autoryzacją.

Podczas gdy uwierzytelnianie odpowiada na pytanie "Kim jesteś?", autoryzacja odpowiada na pytanie "Co możesz zrobić?". Solidny system ról i uprawnień jest niezbędny w każdej aplikacji, która wymaga różnych poziomów dostępu dla różnych użytkowników.

Podstawowe koncepcje ról i uprawnień

Zanim przejdziemy do implementacji, zrozummy podstawowe koncepcje:

  1. Role - kategorie użytkowników z podobnymi poziomami dostępu (np. użytkownik, moderator, administrator)
  2. Uprawnienia - konkretne działania, które może wykonać użytkownik (np. tworzenie postów, edycja profili, usuwanie kont)
  3. RBAC (Role-Based Access Control) - kontrola dostępu oparta na rolach, gdzie uprawnienia są przypisane do ról, a role do użytkowników
  4. ABAC (Attribute-Based Access Control) - bardziej zaawansowany model, gdzie decyzje o dostępie są podejmowane na podstawie różnych atrybutów (rola, czas, lokalizacja, itp.)

Model danych dla ról i uprawnień

Zacznijmy od zdefiniowania odpowiedniego modelu danych w Prisma:

1// prisma/schema.prisma
2model User {
3  id            String    @id @default(cuid())
4  name          String?
5  email         String?   @unique
6  emailVerified DateTime?
7  image         String?
8  password      String?
9  accounts      Account[]
10  sessions      Session[]
11  role          Role      @relation(fields: [roleId], references: [id])
12  roleId        String
13  permissions   UserPermission[]
14  createdAt     DateTime  @default(now())
15  updatedAt     DateTime  @updatedAt
16}
17
18model Role {
19  id          String        @id @default(cuid())
20  name        String        @unique
21  description String?
22  users       User[]
23  permissions RolePermission[]
24  createdAt   DateTime      @default(now())
25  updatedAt   DateTime      @updatedAt
26}
27
28model Permission {
29  id          String           @id @default(cuid())
30  name        String           @unique
31  description String?
32  roles       RolePermission[]
33  users       UserPermission[]
34  createdAt   DateTime         @default(now())
35  updatedAt   DateTime         @updatedAt
36}
37
38model RolePermission {
39  id           String     @id @default(cuid())
40  role         Role       @relation(fields: [roleId], references: [id], onDelete: Cascade)
41  roleId       String
42  permission   Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
43  permissionId String
44  createdAt    DateTime   @default(now())
45  updatedAt    DateTime   @updatedAt
46
47  @@unique([roleId, permissionId])
48}
49
50model UserPermission {
51  id           String     @id @default(cuid())
52  user         User       @relation(fields: [userId], references: [id], onDelete: Cascade)
53  userId       String
54  permission   Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
55  permissionId String
56  granted      Boolean    @default(true) // true = nadane, false = odebrane
57  createdAt    DateTime   @default(now())
58  updatedAt    DateTime   @updatedAt
59
60  @@unique([userId, permissionId])
61}

Ten model wspiera:

  1. Hierarchiczne role - użytkownicy mają przypisane role
  2. Uprawnienia oparte na rolach - role mają zestawy uprawnień
  3. Indywidualne uprawnienia - poszczególni użytkownicy mogą mieć nadane lub odebrane specyficzne uprawnienia, niezależnie od ich roli

Inicjalizacja podstawowych ról i uprawnień

Stwórzmy skrypt inicjalizujący podstawowe role i uprawnienia w naszej aplikacji:

1// prisma/seed.ts
2import { PrismaClient } from '@prisma/client';
3
4const prisma = new PrismaClient();
5
6async function main() {
7  // Utworzenie podstawowych ról
8  const roles = [
9    {
10      name: 'admin',
11      description: 'Administrator z pełnym dostępem do systemu'
12    },
13    {
14      name: 'moderator',
15      description: 'Moderator z dostępem do zarządzania zawartością'
16    },
17    {
18      name: 'user',
19      description: 'Standardowy użytkownik z podstawowymi uprawnieniami'
20    },
21    {
22      name: 'guest',
23      description: 'Gość z minimalnym dostępem tylko do odczytu'
24    }
25  ];
26
27  for (const role of roles) {
28    await prisma.role.upsert({
29      where: { name: role.name },
30      update: {},
31      create: role,
32    });
33  }
34
35  // Utworzenie podstawowych uprawnień
36  const permissions = [
37    // Uprawnienia użytkowników
38    { name: 'user:read', description: 'Może widzieć profile użytkowników' },
39    { name: 'user:create', description: 'Może tworzyć nowych użytkowników' },
40    { name: 'user:update', description: 'Może aktualizować dane użytkowników' },
41    { name: 'user:delete', description: 'Może usuwać użytkowników' },
42    
43    // Uprawnienia zawartości
44    { name: 'content:read', description: 'Może czytać zawartość' },
45    { name: 'content:create', description: 'Może tworzyć nową zawartość' },
46    { name: 'content:update', description: 'Może aktualizować zawartość' },
47    { name: 'content:delete', description: 'Może usuwać zawartość' },
48    { name: 'content:publish', description: 'Może publikować zawartość' },
49    
50    // Uprawnienia administracyjne
51    { name: 'admin:access', description: 'Ma dostęp do panelu administracyjnego' },
52    { name: 'admin:settings', description: 'Może zmieniać ustawienia systemu' },
53    { name: 'admin:roles', description: 'Może zarządzać rolami i uprawnieniami' },
54  ];
55
56  for (const permission of permissions) {
57    await prisma.permission.upsert({
58      where: { name: permission.name },
59      update: {},
60      create: permission,
61    });
62  }
63
64  // Przypisanie uprawnień do ról
65  const rolePermissions = [
66    // Uprawnienia administratora
67    { roleName: 'admin', permissionNames: permissions.map(p => p.name) },
68    
69    // Uprawnienia moderatora
70    { 
71      roleName: 'moderator', 
72      permissionNames: [
73        'user:read',
74        'content:read', 'content:create', 'content:update', 'content:delete', 'content:publish',
75        'admin:access',
76      ]
77    },
78    
79    // Uprawnienia standardowego użytkownika
80    {
81      roleName: 'user',
82      permissionNames: [
83        'user:read',
84        'content:read', 'content:create', 'content:update',
85      ]
86    },
87    
88    // Uprawnienia gościa
89    {
90      roleName: 'guest',
91      permissionNames: [
92        'user:read',
93        'content:read',
94      ]
95    },
96  ];
97
98  for (const { roleName, permissionNames } of rolePermissions) {
99    const role = await prisma.role.findUnique({ where: { name: roleName } });
100    
101    if (role) {
102      for (const permName of permissionNames) {
103        const permission = await prisma.permission.findUnique({ 
104          where: { name: permName } 
105        });
106        
107        if (permission) {
108          await prisma.rolePermission.upsert({
109            where: {
110              roleId_permissionId: {
111                roleId: role.id,
112                permissionId: permission.id
113              }
114            },
115            update: {},
116            create: {
117              roleId: role.id,
118              permissionId: permission.id
119            }
120          });
121        }
122      }
123    }
124  }
125
126  // Utworzenie użytkownika administracyjnego (jeśli nie istnieje)
127  const adminRole = await prisma.role.findUnique({ where: { name: 'admin' } });
128  if (adminRole) {
129    const adminEmail = 'admin@example.com';
130    const existingAdmin = await prisma.user.findUnique({
131      where: { email: adminEmail }
132    });
133
134    if (!existingAdmin) {
135      await prisma.user.create({
136        data: {
137          name: 'Administrator',
138          email: adminEmail,
139          // W produkcji hasło powinno być zahaszowane!
140          password: '$2a$10$iqJSHD.BGr0E2IxQwYgJmeP3NvhPrXAeLSaGCj6IR/XU5QtjVu5Tm', // "secret"
141          roleId: adminRole.id,
142        }
143      });
144      console.log('Utworzono użytkownika administracyjnego');
145    }
146  }
147
148  console.log('Inicjalizacja ról i uprawnień zakończona pomyślnie');
149}
150
151main()
152  .catch((e) => {
153    console.error(e);
154    process.exit(1);
155  })
156  .finally(async () => {
157    await prisma.$disconnect();
158  });

Aby uruchomić ten skrypt, dodaj następującą konfigurację do

package.json
:

1"prisma": {
2  "seed": "ts-node prisma/seed.ts"
3},
4"scripts": {
5  "db:seed": "prisma db seed"
6}

Rozszerzenie typu sesji NextAuth

Aby mieć dostęp do informacji o roli użytkownika w sesji NextAuth, rozszerzmy typy:

1// types/next-auth.d.ts
2import { DefaultSession } from 'next-auth';
3
4declare module 'next-auth' {
5  interface Session {
6    user: {
7      id: string;
8      role: string;
9      permissions: string[];
10    } & DefaultSession['user'];
11  }
12
13  interface User {
14    role: string;
15    roleId: string;
16    permissions: string[];
17  }
18}
19
20declare module 'next-auth/jwt' {
21  interface JWT {
22    userId: string;
23    role: string;
24    permissions: string[];
25  }
26}

Implementacja konfiguracji NextAuth

Teraz zmodyfikujmy konfigurację NextAuth, aby uwzględniała role i uprawnienia:

1// app/api/auth/[...nextauth]/route.ts
2import NextAuth from 'next-auth';
3import { authOptions } from '@/lib/auth';
4
5const handler = NextAuth(authOptions);
6export { handler as GET, handler as POST };
7
8// lib/auth.ts
9import { NextAuthOptions } from 'next-auth';
10import { PrismaAdapter } from '@auth/prisma-adapter';
11import { prisma } from '@/lib/prisma';
12import CredentialsProvider from 'next-auth/providers/credentials';
13import GoogleProvider from 'next-auth/providers/google';
14import { compare } from 'bcrypt';
15
16export const authOptions: NextAuthOptions = {
17  adapter: PrismaAdapter(prisma),
18  providers: [
19    GoogleProvider({
20      clientId: process.env.GOOGLE_CLIENT_ID!,
21      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
22    }),
23    CredentialsProvider({
24      name: 'Credentials',
25      credentials: {
26        email: { label: "Email", type: "email" },
27        password: { label: "Hasło", type: "password" }
28      },
29      async authorize(credentials) {
30        if (!credentials?.email || !credentials?.password) {
31          return null;
32        }
33
34        const user = await prisma.user.findUnique({
35          where: { email: credentials.email },
36          include: {
37            role: true,
38            permissions: {
39              include: {
40                permission: true,
41              },
42            },
43          },
44        });
45
46        if (!user || !user.password) {
47          return null;
48        }
49
50        const isPasswordValid = await compare(credentials.password, user.password);
51
52        if (!isPasswordValid) {
53          return null;
54        }
55
56        // Pobierz uprawnienia roli
57        const rolePermissions = await prisma.rolePermission.findMany({
58          where: { roleId: user.roleId },
59          include: { permission: true },
60        });
61
62        // Utwórz listę uprawnień (uprawnienia roli + indywidualne uprawnienia)
63        const allPermissions = new Set<string>();
64        
65        // Dodaj uprawnienia roli
66        rolePermissions.forEach(rp => {
67          allPermissions.add(rp.permission.name);
68        });
69        
70        // Dodaj/usuń indywidualne uprawnienia użytkownika
71        user.permissions.forEach(up => {
72          if (up.granted) {
73            allPermissions.add(up.permission.name);
74          } else {
75            allPermissions.delete(up.permission.name);
76          }
77        });
78
79        return {
80          id: user.id,
81          email: user.email,
82          name: user.name,
83          image: user.image,
84          role: user.role.name,
85          roleId: user.roleId,
86          permissions: Array.from(allPermissions),
87        };
88      },
89    }),
90  ],
91  callbacks: {
92    async jwt({ token, user }) {
93      if (user) {
94        token.userId = user.id;
95        token.role = user.role;
96        token.permissions = user.permissions;
97      }
98      return token;
99    },
100    async session({ session, token }) {
101      if (session.user) {
102        session.user.id = token.userId;
103        session.user.role = token.role;
104        session.user.permissions = token.permissions;
105      }
106      return session;
107    },
108  },
109  pages: {
110    signIn: '/auth/login',
111    error: '/auth/error',
112  },
113  session: {
114    strategy: 'jwt',
115  },
116};

Narzędzia do autoryzacji

Teraz stwórzmy narzędzia do łatwego sprawdzania ról i uprawnień użytkowników:

1// lib/auth-utils.ts
2import { getServerSession } from 'next-auth/next';
3import { authOptions } from '@/lib/auth';
4import { redirect } from 'next/navigation';
5import { Session } from 'next-auth';
6
7// Funkcje pomocnicze do sprawdzania ról i uprawnień
8export function hasRole(session: Session | null, role: string | string[]): boolean {
9  if (!session?.user?.role) return false;
10  
11  if (Array.isArray(role)) {
12    return role.includes(session.user.role);
13  }
14  
15  return session.user.role === role;
16}
17
18export function hasPermission(session: Session | null, permission: string | string[]): boolean {
19  if (!session?.user?.permissions) return false;
20  
21  if (Array.isArray(permission)) {
22    return permission.some(p => session.user.permissions.includes(p));
23  }
24  
25  return session.user.permissions.includes(permission);
26}
27
28// Funkcja serwerowa - sprawdza rolę i przekierowuje, jeśli brak dostępu
29export async function requireRole(role: string | string[], redirectTo: string = '/auth/unauthorized') {
30  const session = await getServerSession(authOptions);
31  
32  if (!session) {
33    redirect('/auth/login');
34  }
35  
36  if (!hasRole(session, role)) {
37    redirect(redirectTo);
38  }
39  
40  return session;
41}
42
43// Funkcja serwerowa - sprawdza uprawnienie i przekierowuje, jeśli brak dostępu
44export async function requirePermission(permission: string | string[], redirectTo: string = '/auth/unauthorized') {
45  const session = await getServerSession(authOptions);
46  
47  if (!session) {
48    redirect('/auth/login');
49  }
50  
51  if (!hasPermission(session, permission)) {
52    redirect(redirectTo);
53  }
54  
55  return session;
56}

Implementacja komponentów ochrony dostępu

Utwórzmy teraz komponenty klienckie, które pozwolą na łatwe kontrolowanie dostępu do części interfejsu użytkownika:

1// components/RoleGuard.tsx
2'use client';
3
4import { useSession } from 'next-auth/react';
5import { ReactNode } from 'react';
6import { hasRole } from '@/lib/auth-utils';
7
8interface RoleGuardProps {
9  children: ReactNode;
10  role: string | string[];
11  fallback?: ReactNode;
12}
13
14export function RoleGuard({ children, role, fallback = null }: RoleGuardProps) {
15  const { data: session } = useSession();
16  
17  if (!hasRole(session, role)) {
18    return fallback;
19  }
20  
21  return <>{children}</>;
22}
23
24// components/PermissionGuard.tsx
25'use client';
26
27import { useSession } from 'next-auth/react';
28import { ReactNode } from 'react';
29import { hasPermission } from '@/lib/auth-utils';
30
31interface PermissionGuardProps {
32  children: ReactNode;
33  permission: string | string[];
34  fallback?: ReactNode;
35}
36
37export function PermissionGuard({ children, permission, fallback = null }: PermissionGuardProps) {
38  const { data: session } = useSession();
39  
40  if (!hasPermission(session, permission)) {
41    return fallback;
42  }
43  
44  return <>{children}</>;
45}
46
47// components/AdminOnly.tsx
48'use client';
49
50import { RoleGuard } from '@/components/RoleGuard';
51import { ReactNode } from 'react';
52
53export function AdminOnly({ children, fallback = null }: { children: ReactNode, fallback?: ReactNode }) {
54  return (
55    <RoleGuard role="admin" fallback={fallback}>
56      {children}
57    </RoleGuard>
58  );
59}

Implementacja middleware

Możemy również zaimplementować middleware do kontroli dostępu do całych ścieżek:

1// middleware.ts
2import { NextResponse } from 'next/server';
3import { 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  // Ścieżki administracyjne
10  if (request.nextUrl.pathname.startsWith('/admin')) {
11    if (!token) {
12      return NextResponse.redirect(new URL('/auth/login?callbackUrl=/admin', request.url));
13    }
14    
15    // Sprawdź, czy użytkownik ma rolę administratora
16    if (token.role !== 'admin') {
17      return NextResponse.redirect(new URL('/auth/unauthorized', request.url));
18    }
19  }
20  
21  // Ścieżki moderatorów
22  if (request.nextUrl.pathname.startsWith('/moderator')) {
23    if (!token) {
24      return NextResponse.redirect(new URL('/auth/login?callbackUrl=/moderator', request.url));
25    }
26    
27    // Sprawdź, czy użytkownik ma rolę administratora lub moderatora
28    if (token.role !== 'admin' && token.role !== 'moderator') {
29      return NextResponse.redirect(new URL('/auth/unauthorized', request.url));
30    }
31  }
32  
33  // Ścieżki chronione - dostępne tylko dla zalogowanych użytkowników
34  if (
35    request.nextUrl.pathname.startsWith('/dashboard') ||
36    request.nextUrl.pathname.startsWith('/profile') ||
37    request.nextUrl.pathname.startsWith('/settings')
38  ) {
39    if (!token) {
40      const callbackUrl = encodeURIComponent(request.nextUrl.pathname);
41      return NextResponse.redirect(new URL(`/auth/login?callbackUrl=${callbackUrl}`, request.url));
42    }
43  }
44  
45  // Zabezpieczenie API endpoints
46  if (request.nextUrl.pathname.startsWith('/api/protected')) {
47    if (!token) {
48      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
49    }
50    
51    // Można dodać dodatkowe sprawdzanie uprawnień w zależności od ścieżki API
52    if (request.nextUrl.pathname.startsWith('/api/protected/admin') && token.role !== 'admin') {
53      return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
54    }
55  }
56  
57  return NextResponse.next();
58}
59
60export const config = {
61  matcher: [
62    '/admin/:path*',
63    '/moderator/:path*',
64    '/dashboard/:path*',
65    '/profile/:path*', 
66    '/settings/:path*',
67    '/api/protected/:path*',
68  ],
69};

Przykłady użycia w aplikacji

1. Ochrona tras za pomocą funkcji serwerowych

1// app/admin/dashboard/page.tsx
2import { requireRole } from '@/lib/auth-utils';
3
4export default async function AdminDashboardPage() {
5  // Przekieruje jeśli użytkownik nie jest administratorem
6  const session = await requireRole('admin');
7  
8  return (
9    <div className="p-6">
10      <h1 className="text-2xl font-bold mb-4">Panel administracyjny</h1>
11      <p>Witaj, {session.user.name}! Masz pełny dostęp administracyjny.</p>
12      {/* Zawartość dostępna tylko dla administratorów */}
13    </div>
14  );
15}
16
17// app/content/edit/[id]/page.tsx
18import { requirePermission } from '@/lib/auth-utils';
19
20export default async function EditContentPage({ params }: { params: Promise<{ id: string }> }) {
21  // Przekieruje jeśli użytkownik nie ma uprawnienia do edycji zawartości
22  const session = await requirePermission('content:update');
23  
24  // Pobierz zawartość do edycji
25  const content = await fetchContent((await params).id);
26  
27  return (
28    <div className="p-6">
29      <h1 className="text-2xl font-bold mb-4">Edycja zawartości</h1>
30      {/* Formularz edycji */}
31    </div>
32  );
33}

2. Ochrona komponentów UI

1// components/NavBar.tsx
2'use client';
3
4import { useSession } from 'next-auth/react';
5import Link from 'next/link';
6import { AdminOnly } from '@/components/AdminOnly';
7import { RoleGuard } from '@/components/RoleGuard';
8import { PermissionGuard } from '@/components/PermissionGuard';
9
10export function NavBar() {
11  const { data: session } = useSession();
12  
13  return (
14    <nav className="bg-white shadow-sm">
15      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
16        <div className="flex justify-between h-16">
17          <div className="flex space-x-8 items-center">
18            <Link href="/" className="font-bold text-xl">MojaAplikacja</Link>
19            <Link href="/dashboard">Dashboard</Link>
20            
21            {/* Link widoczny tylko dla moderatorów i administratorów */}
22            <RoleGuard role={['admin', 'moderator']}>
23              <Link href="/moderator/content" className="text-blue-600">
24                Zarządzanie zawartością
25              </Link>
26            </RoleGuard>
27            
28            {/* Link tylko dla administratorów */}
29            <AdminOnly>
30              <Link href="/admin/dashboard" className="text-red-600">
31                Panel administratora
32              </Link>
33            </AdminOnly>
34            
35            {/* Link dla użytkowników z uprawnieniem do tworzenia zawartości */}
36            <PermissionGuard permission="content:create">
37              <Link href="/content/new" className="text-green-600">
38                Utwórz zawartość
39              </Link>
40            </PermissionGuard>
41          </div>
42          
43          <div className="flex items-center">
44            {session ? (
45              <div className="flex items-center space-x-4">
46                <span>{session.user.name}</span>
47                <span className="px-2 py-1 bg-gray-200 rounded-full text-xs">
48                  {session.user.role}
49                </span>
50              </div>
51            ) : (
52              <Link href="/auth/login">Zaloguj się</Link>
53            )}
54          </div>
55        </div>
56      </div>
57    </nav>
58  );
59}

3. Implementacja Route Handlers z autoryzacją

1// app/api/protected/users/route.ts
2import { NextResponse } from 'next/server';
3import { getServerSession } from 'next-auth/next';
4import { authOptions } from '@/lib/auth';
5import { hasPermission } from '@/lib/auth-utils';
6import { prisma } from '@/lib/prisma';
7
8export async function GET() {
9  const session = await getServerSession(authOptions);
10  
11  if (!session) {
12    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
13  }
14  
15  // Sprawdź, czy użytkownik ma uprawnienie do odczytu użytkowników
16  if (!hasPermission(session, 'user:read')) {
17    return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
18  }
19  
20  try {
21    const users = await prisma.user.findMany({
22      select: {
23        id: true,
24        name: true,
25        email: true,
26        image: true,
27        role: {
28          select: {
29            name: true,
30          },
31        },
32        createdAt: true,
33      }
34    });
35    
36    return NextResponse.json(users);
37  } catch (error) {
38    console.error('Error fetching users:', error);
39    return NextResponse.json(
40      { error: 'Failed to fetch users' },
41      { status: 500 }
42    );
43  }
44}
45
46// app/api/protected/users/[id]/route.ts
47import { NextResponse } from 'next/server';
48import { getServerSession } from 'next-auth/next';
49import { authOptions } from '@/lib/auth';
50import { hasPermission } from '@/lib/auth-utils';
51import { prisma } from '@/lib/prisma';
52
53export async function DELETE(
54  request: Request,
55  { params }: { params: Promise<{ id: string }> }
56) {
57  const session = await getServerSession(authOptions);
58  
59  if (!session) {
60    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
61  }
62  
63  // Sprawdź, czy użytkownik ma uprawnienie do usuwania użytkowników
64  if (!hasPermission(session, 'user:delete')) {
65    return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
66  }
67  
68  try {
69    // Sprawdź, czy użytkownik nie próbuje usunąć samego siebie
70    if ((await params).id === session.user.id) {
71      return NextResponse.json(
72        { error: 'Cannot delete your own account' },
73        { status: 400 }
74      );
75    }
76    
77    // Sprawdź, czy użytkownik nie próbuje usunąć administratora (jeśli sam nie jest administratorem)
78    if (session.user.role !== 'admin') {
79      const userToDelete = await prisma.user.findUnique({
80        where: { id: (await params).id },
81        include: { role: true },
82      });
83      
84      if (userToDelete?.role.name === 'admin') {
85        return NextResponse.json(
86          { error: 'Cannot delete admin accounts' },
87          { status: 403 }
88        );
89      }
90    }
91    
92    // Usuń użytkownika
93    await prisma.user.delete({
94      where: { id: (await params).id },
95    });
96    
97    return NextResponse.json({ success: true });
98  } catch (error) {
99    console.error('Error deleting user:', error);
100    return NextResponse.json(
101      { error: 'Failed to delete user' },
102      { status: 500 }
103    );
104  }
105}

Panel administracyjny do zarządzania rolami i uprawnieniami

Fragment implementacji panelu administracyjnego do zarządzania rolami i uprawnieniami:

1// app/admin/roles/page.tsx
2import { requirePermission } from '@/lib/auth-utils';
3import Link from 'next/link';
4import { prisma } from '@/lib/prisma';
5
6export default async function RolesAdminPage() {
7  await requirePermission('admin:roles');
8  
9  const roles = await prisma.role.findMany({
10    include: {
11      _count: {
12        select: {
13          users: true,
14          permissions: true,
15        },
16      },
17    },
18    orderBy: {
19      name: 'asc',
20    },
21  });
22  
23  return (
24    <div className="p-6">
25      <div className="flex justify-between items-center mb-6">
26        <h1 className="text-2xl font-bold">Zarządzanie rolami</h1>
27        <Link 
28          href="/admin/roles/new" 
29          className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700"
30        >
31          Dodaj nową rolę
32        </Link>
33      </div>
34      
35      <div className="bg-white shadow overflow-hidden rounded-md">
36        <table className="min-w-full divide-y divide-gray-200">
37          <thead className="bg-gray-50">
38            <tr>
39              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
40                Nazwa
41              </th>
42              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
43                Opis
44              </th>
45              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
46                Użytkowników
47              </th>
48              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
49                Uprawnień
50              </th>
51              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
52                Akcje
53              </th>
54            </tr>
55          </thead>
56          <tbody className="bg-white divide-y divide-gray-200">
57            {roles.map((role) => (
58              <tr key={role.id}>
59                <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
60                  {role.name}
61                </td>
62                <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
63                  {role.description}
64                </td>
65                <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
66                  {role._count.users}
67                </td>
68                <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
69                  {role._count.permissions}
70                </td>
71                <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
72                  <Link 
73                    href={`/admin/roles/${role.id}`}
74                    className="text-blue-600 hover:text-blue-900 mr-4"
75                  >
76                    Edytuj
77                  </Link>
78                  <Link 
79                    href={`/admin/roles/${role.id}/permissions`}
80                    className="text-green-600 hover:text-green-900"
81                  >
82                    Uprawnienia
83                  </Link>
84                </td>
85              </tr>
86            ))}
87          </tbody>
88        </table>
89      </div>
90    </div>
91  );
92}

Debugowanie i diagnostyka uprawnień

Przydatne może być utworzenie komponentu diagnostycznego, który pokazuje bieżące role i uprawnienia zalogowanego użytkownika:

1// components/PermissionDebugger.tsx
2'use client';
3
4import { useSession } from 'next-auth/react';
5import { useState } from 'react';
6
7export function PermissionDebugger() {
8  const { data: session, status } = useSession();
9  const [showDetails, setShowDetails] = useState(false);
10  
11  if (status !== 'authenticated') {
12    return null;
13  }
14  
15  return (
16    <div className="fixed bottom-4 right-4 p-4 bg-white border border-gray-300 rounded-lg shadow-lg max-w-md z-50">
17      <div className="flex justify-between items-center mb-2">
18        <h3 className="font-bold text-lg">Diagnostyka uprawnień</h3>
19        <button 
20          onClick={() => setShowDetails(!showDetails)}
21          className="text-blue-600 hover:text-blue-800"
22        >
23          {showDetails ? 'Ukryj szczegóły' : 'Pokaż szczegóły'}
24        </button>
25      </div>
26      
27      <div className="mb-2">
28        <span className="font-semibold">Użytkownik:</span> {session.user.name}
29      </div>
30      
31      <div className="mb-2">
32        <span className="font-semibold">Rola:</span> 
33        <span className="ml-2 px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs">
34          {session.user.role}
35        </span>
36      </div>
37      
38      {showDetails && (
39        <div>
40          <div className="font-semibold mb-1">Uprawnienia:</div>
41          <div className="max-h-40 overflow-y-auto">
42            {session.user.permissions.length > 0 ? (
43              <ul className="list-disc pl-5 space-y-1">
44                {session.user.permissions.sort().map((permission) => (
45                  <li key={permission} className="text-sm">
46                    {permission}
47                  </li>
48                ))}
49              </ul>
50            ) : (
51              <p className="text-sm text-red-600">Brak uprawnień</p>
52            )}
53          </div>
54        </div>
55      )}
56    </div>
57  );
58}

Ten komponent można użyć podczas fazy rozwoju lub dla administratorów w środowisku produkcyjnym.

Rozszerzenie RBAC o dodatkowe warunki (ABAC)

Możemy rozszerzyć nasz system autoryzacji o dodatkowe warunki, wdrażając elementy kontroli dostępu opartej na atrybutach (ABAC):

1// lib/auth-conditions.ts
2import { Session } from 'next-auth';
3
4interface AccessConditions {
5  isOwner?: (userId: string, resourceId: string) => Promise<boolean>;
6  isInSameTeam?: (userId: string, resourceId: string) => Promise<boolean>;
7  isWithinBusinessHours?: () => boolean;
8  isFromAllowedIP?: (ipAddress: string) => boolean;
9  // Można dodać więcej warunków według potrzeb
10}
11
12export async function checkAccessConditions(
13  session: Session | null, 
14  permission: string,
15  resourceId: string,
16  conditions: AccessConditions,
17  ipAddress?: string
18): Promise<boolean> {
19  // Najpierw sprawdź podstawowe uprawnienie
20  if (!session?.user?.permissions?.includes(permission)) {
21    return false;
22  }
23  
24  // Jeśli to administrator, zawsze zezwól na dostęp
25  if (session.user.role === 'admin') {
26    return true;
27  }
28  
29  // Sprawdź dodatkowe warunki
30  if (conditions.isOwner && resourceId) {
31    const isOwner = await conditions.isOwner(session.user.id, resourceId);
32    if (!isOwner) {
33      return false;
34    }
35  }
36  
37  if (conditions.isInSameTeam && resourceId) {
38    const isInSameTeam = await conditions.isInSameTeam(session.user.id, resourceId);
39    if (!isInSameTeam) {
40      return false;
41    }
42  }
43  
44  if (conditions.isWithinBusinessHours) {
45    const isWithinBusinessHours = conditions.isWithinBusinessHours();
46    if (!isWithinBusinessHours) {
47      return false;
48    }
49  }
50  
51  if (conditions.isFromAllowedIP && ipAddress) {
52    const isFromAllowedIP = conditions.isFromAllowedIP(ipAddress);
53    if (!isFromAllowedIP) {
54      return false;
55    }
56  }
57  
58  // Wszystkie warunki spełnione
59  return true;
60}
61
62// Przykładowe implementacje warunków
63export async function isContentOwner(userId: string, contentId: string): Promise<boolean> {
64  const content = await prisma.content.findUnique({
65    where: { id: contentId },
66    select: { authorId: true },
67  });
68  
69  return content?.authorId === userId;
70}
71
72export function isWithinBusinessHours(): boolean {
73  const now = new Date();
74  const hours = now.getHours();
75  const day = now.getDay(); // 0 = niedziela, 6 = sobota
76  
77  // Godziny pracy: 9:00 - 17:00, od poniedziałku do piątku
78  return day >= 1 && day <= 5 && hours >= 9 && hours < 17;
79}

Przyklad użycia:

1// app/api/protected/content/[id]/route.ts
2import { NextResponse } from 'next/server';
3import { getServerSession } from 'next-auth/next';
4import { authOptions } from '@/lib/auth';
5import { checkAccessConditions, isContentOwner } from '@/lib/auth-conditions';
6import { headers } from 'next/headers';
7
8export async function PUT(
9  request: Request,
10  { params }: { params: Promise<{ id: string }> }
11) {
12  const session = await getServerSession(authOptions);
13  const headersList = await headers();
14  const ipAddress = headersList.get('x-forwarded-for') || 'unknown';
15  
16  // Sprawdzamy uprawnienie 'content:update' oraz dodatkowe warunki
17  const hasAccess = await checkAccessConditions(
18    session,
19    'content:update',
20    (await params).id,
21    { isOwner: isContentOwner },
22    ipAddress as string
23  );
24  
25  if (!hasAccess) {
26    return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
27  }
28  
29  // Kontynuuj z aktualizacją zawartości
30  //
31}

Testowanie systemu ról i uprawnień

Przykladowy test jednostkowy dla funkcji sprawdzania uprawnień:

1// __tests__/auth-utils.test.ts
2import { hasPermission, hasRole } from '@/lib/auth-utils';
3
4describe('Auth Utils', () => {
5  describe('hasRole', () => {
6    it('should return false for null session', () => {
7      expect(hasRole(null, 'admin')).toBe(false);
8    });
9    
10    it('should return false if user has no role', () => {
11      const session = { user: {} } as any;
12      expect(hasRole(session, 'admin')).toBe(false);
13    });
14    
15    it('should return true if user has the specified role', () => {
16      const session = { user: { role: 'admin' } } as any;
17      expect(hasRole(session, 'admin')).toBe(true);
18    });
19    
20    it('should return false if user does not have the specified role', () => {
21      const session = { user: { role: 'user' } } as any;
22      expect(hasRole(session, 'admin')).toBe(false);
23    });
24    
25    it('should return true if user has one of the specified roles', () => {
26      const session = { user: { role: 'moderator' } } as any;
27      expect(hasRole(session, ['admin', 'moderator'])).toBe(true);
28    });
29  });
30  
31  describe('hasPermission', () => {
32    it('should return false for null session', () => {
33      expect(hasPermission(null, 'user:read')).toBe(false);
34    });
35    
36    it('should return false if user has no permissions', () => {
37      const session = { user: {} } as any;
38      expect(hasPermission(session, 'user:read')).toBe(false);
39    });
40    
41    it('should return true if user has the specified permission', () => {
42      const session = { user: { permissions: ['user:read', 'user:create'] } } as any;
43      expect(hasPermission(session, 'user:read')).toBe(true);
44    });
45    
46    it('should return false if user does not have the specified permission', () => {
47      const session = { user: { permissions: ['user:read'] } } as any;
48      expect(hasPermission(session, 'user:delete')).toBe(false);
49    });
50    
51    it('should return true if user has one of the specified permissions', () => {
52      const session = { user: { permissions: ['user:read', 'content:read'] } } as any;
53      expect(hasPermission(session, ['user:delete', 'content:read'])).toBe(true);
54    });
55  });
56});

Dobre praktyki i zalecenia

  1. Minimalne uprawnienia - przydzielaj użytkownikom tylko niezbędne uprawnienia (zasada najmniejszych uprawnień)

  2. Hierarchiczne role - projektuj role w sposób hierarchiczny, gdzie role wyższe zawierają wszystkie uprawnienia niższych ról plus dodatkowe

  3. Rozdzielenie uwierzytelniania i autoryzacji - traktuj je jako oddzielne warstwy zabezpieczeń

  4. Szczegółowe logi - rejestruj wszystkie próby dostępu do chronionych zasobów, szczególnie te nieudane

  5. Regularne audyty - okresowo przeglądaj i weryfikuj przydzielone role i uprawnienia

  6. Usuwanie nieużywanych uprawnień - systematycznie usuwaj nieużywane role i uprawnienia

  7. Awaryjny dostęp - zapewnij mechanizm awaryjnego dostępu dla administratorów w przypadku problemów z systemem ról

  8. Testowanie penetracyjne - regularnie testuj system pod kątem luk w zabezpieczeniach

Podsumowanie

W tym module zaimplementowaliśmy zaawansowany system ról i uprawnień dla aplikacji Next.js. Poznaliśmy:

  1. Model danych dla RBAC - jak projektować strukturę bazy danych wspierającą role i uprawnienia
  2. Integracja z NextAuth.js - jak rozszerzyć sesję o informacje o rolach i uprawnieniach
  3. Narzędzia do autoryzacji - implementacja funkcji pomocniczych do sprawdzania ról i uprawnień
  4. Zabezpieczanie różnych elementów aplikacji:
    • Ochrona tras za pomocą middleware
    • Ochrona komponentów za pomocą Guardów
    • Zabezpieczanie API endpointów
  5. Rozszerzenie o ABAC - dodanie warunkowej autoryzacji opartej na różnych atrybutach

Solidny system ról i uprawnień jest fundamentem bezpieczeństwa aplikacji, zwłaszcza tych z wieloma typami użytkowników i złożonymi wymaganiami dostępu. Dzięki przedstawionym w tym module rozwiązaniom, możesz zbudować elastyczny i bezpieczny system autoryzacji dostosowany do potrzeb Twojej aplikacji.

W następnym module omówimy jak tworzyć middleware do ochrony tras, co stanowi dodatkową warstwę zabezpieczeń w aplikacjach Next.js.

Przejdź do CodeWorlds