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.
Zanim przejdziemy do implementacji, zrozummy podstawowe koncepcje:
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:
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}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}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};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}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}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};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}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}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}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}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.
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}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});Minimalne uprawnienia - przydzielaj użytkownikom tylko niezbędne uprawnienia (zasada najmniejszych uprawnień)
Hierarchiczne role - projektuj role w sposób hierarchiczny, gdzie role wyższe zawierają wszystkie uprawnienia niższych ról plus dodatkowe
Rozdzielenie uwierzytelniania i autoryzacji - traktuj je jako oddzielne warstwy zabezpieczeń
Szczegółowe logi - rejestruj wszystkie próby dostępu do chronionych zasobów, szczególnie te nieudane
Regularne audyty - okresowo przeglądaj i weryfikuj przydzielone role i uprawnienia
Usuwanie nieużywanych uprawnień - systematycznie usuwaj nieużywane role i uprawnienia
Awaryjny dostęp - zapewnij mechanizm awaryjnego dostępu dla administratorów w przypadku problemów z systemem ról
Testowanie penetracyjne - regularnie testuj system pod kątem luk w zabezpieczeniach
W tym module zaimplementowaliśmy zaawansowany system ról i uprawnień dla aplikacji Next.js. Poznaliśmy:
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.