From fbf58d4e44640142c832f3095a250de6c30383ca Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Tue, 5 May 2026 20:26:54 +0200 Subject: [PATCH] fix: admin-navigatie zichtbaar voor ADMIN-rol gebruikers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - requireAdmin() checkt nu de database i.p.v. session.isAdmin (was altijd undefined) - loginAction stelt session.isAdmin in op basis van UserRole in de DB - registerAction stelt session.isAdmin = false expliciet in - NavBar toont 'Admin'-link conditioneel als roles.includes('ADMIN') - UserMenu ROLE_LABELS uitgebreid met ADMIN → 'Admin' - Tests aangepast: prismaUserRole.findFirst mock toegevoegd Co-Authored-By: Claude Sonnet 4.6 --- __tests__/actions/auth.test.ts | 8 +++++++- __tests__/lib/auth-guard.test.ts | 4 ++++ actions/auth.ts | 5 +++++ components/shared/nav-bar.tsx | 1 + components/shared/user-menu.tsx | 1 + lib/auth-guard.ts | 9 ++++++++- 6 files changed, 26 insertions(+), 2 deletions(-) diff --git a/__tests__/actions/auth.test.ts b/__tests__/actions/auth.test.ts index a5a9be7..7c8dd86 100644 --- a/__tests__/actions/auth.test.ts +++ b/__tests__/actions/auth.test.ts @@ -7,6 +7,7 @@ const { sessionSaveMock, requireSessionMock, prismaUserUpdateMock, + prismaUserRoleFindFirstMock, } = vi.hoisted(() => ({ redirectMock: vi.fn((path: string) => { throw new Error(`REDIRECT:${path}`) }), verifyUserMock: vi.fn(), @@ -14,6 +15,7 @@ const { sessionSaveMock: vi.fn(), requireSessionMock: vi.fn(), prismaUserUpdateMock: vi.fn(), + prismaUserRoleFindFirstMock: vi.fn().mockResolvedValue(null), })) vi.mock('next/navigation', () => ({ redirect: redirectMock })) @@ -36,7 +38,10 @@ vi.mock('@/lib/auth', () => ({ })) vi.mock('@/lib/auth-guard', () => ({ requireSession: requireSessionMock })) vi.mock('@/lib/prisma', () => ({ - prisma: { user: { update: prismaUserUpdateMock } }, + prisma: { + user: { update: prismaUserUpdateMock }, + userRole: { findFirst: prismaUserRoleFindFirstMock }, + }, })) vi.mock('@/lib/rate-limit', () => ({ checkRateLimit: vi.fn().mockReturnValue(true) })) @@ -60,6 +65,7 @@ beforeEach(() => { sessionSaveMock.mockReset() requireSessionMock.mockReset() prismaUserUpdateMock.mockReset() + prismaUserRoleFindFirstMock.mockResolvedValue(null) }) describe('loginAction UA-redirect', () => { diff --git a/__tests__/lib/auth-guard.test.ts b/__tests__/lib/auth-guard.test.ts index b162921..ebfa9a5 100644 --- a/__tests__/lib/auth-guard.test.ts +++ b/__tests__/lib/auth-guard.test.ts @@ -3,10 +3,14 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' const getSessionMock = vi.fn() const isPairedSessionExpiredMock = vi.fn() const redirectMock = vi.fn(() => { throw new Error('REDIRECT_CALLED') }) +const prismaUserRoleFindFirstMock = vi.fn() vi.mock('@/lib/auth', () => ({ getSession: getSessionMock })) vi.mock('@/lib/auth/pairing', () => ({ isPairedSessionExpired: isPairedSessionExpiredMock })) vi.mock('next/navigation', () => ({ redirect: redirectMock })) +vi.mock('@/lib/prisma', () => ({ + prisma: { userRole: { findFirst: prismaUserRoleFindFirstMock } }, +})) describe('requireSession', () => { beforeEach(() => { diff --git a/actions/auth.ts b/actions/auth.ts index d40d188..a08c502 100644 --- a/actions/auth.ts +++ b/actions/auth.ts @@ -47,6 +47,7 @@ export async function registerAction(_prevState: unknown, formData: FormData) { const session = await getIronSession(await cookies(), sessionOptions) session.userId = result.user!.id session.isDemo = false + session.isAdmin = false await session.save() redirect('/dashboard') @@ -72,9 +73,13 @@ export async function loginAction(_prevState: unknown, formData: FormData) { return { error: 'Onjuiste gebruikersnaam of wachtwoord' } } + const adminRole = await prisma.userRole.findFirst({ + where: { user_id: user.id, role: 'ADMIN' }, + }) const session = await getIronSession(await cookies(), sessionOptions) session.userId = user.id session.isDemo = user.is_demo + session.isAdmin = !!adminRole await session.save() // PBI-11 / ST-1135: telefoon-UA's krijgen de mobile-shell. diff --git a/components/shared/nav-bar.tsx b/components/shared/nav-bar.tsx index 1749134..bcdba58 100644 --- a/components/shared/nav-bar.tsx +++ b/components/shared/nav-bar.tsx @@ -142,6 +142,7 @@ export function NavBar({ {navLink('/insights', 'Insights', pathname.startsWith('/insights'))} {navLink('/ideas', 'Ideeën', pathname.startsWith('/ideas'))} {navLink('/todos', "Todo's", pathname.startsWith('/todos'))} + {roles.includes('ADMIN') && navLink('/admin', 'Admin', pathname.startsWith('/admin'))} diff --git a/components/shared/user-menu.tsx b/components/shared/user-menu.tsx index b628476..12e9b54 100644 --- a/components/shared/user-menu.tsx +++ b/components/shared/user-menu.tsx @@ -20,6 +20,7 @@ const ROLE_LABELS: Record = { PRODUCT_OWNER: 'Product Owner', SCRUM_MASTER: 'Scrum Master', DEVELOPER: 'Developer', + ADMIN: 'Admin', } interface UserMenuProps { diff --git a/lib/auth-guard.ts b/lib/auth-guard.ts index e82a568..b36b1af 100644 --- a/lib/auth-guard.ts +++ b/lib/auth-guard.ts @@ -1,6 +1,7 @@ import { redirect } from 'next/navigation' import { getSession } from '@/lib/auth' import { isPairedSessionExpired } from '@/lib/auth/pairing' +import { prisma } from '@/lib/prisma' /** * Layout-side auth guard. Returns the session when valid; otherwise redirects @@ -25,7 +26,13 @@ export async function requireSession() { export async function requireAdmin() { const session = await getSession() - if (!session.userId || !session.isAdmin) { + if (!session.userId) { + redirect('/dashboard') + } + const adminRole = await prisma.userRole.findFirst({ + where: { user_id: session.userId, role: 'ADMIN' }, + }) + if (!adminRole) { redirect('/dashboard') } return session